/* ----------------------------------------------------------------------
   LAMMPS - Large-scale Atomic/Molecular Massively Parallel Simulator
   http://lammps.sandia.gov, Sandia National Laboratories
   Steve Plimpton, sjplimp@sandia.gov

   Copyright (2003) Sandia Corporation.  Under the terms of Contract
   DE-AC04-94AL85000 with Sandia Corporation, the U.S. Government retains
   certain rights in this software.  This software is distributed under
   the GNU General Public License.

   See the README file in the top-level LAMMPS directory.
------------------------------------------------------------------------- */

#include "change_box.h"
#include <mpi.h>
#include <cmath>
#include <cstring>
#include "atom.h"
#include "modify.h"
#include "fix.h"
#include "domain.h"
#include "lattice.h"
#include "comm.h"
#include "irregular.h"
#include "output.h"
#include "group.h"
#include "error.h"
#include "force.h"
#include "utils.h"
#include "fmt/format.h"

using namespace LAMMPS_NS;

enum{XYZ=0,TILT,BOUNDARY,ORTHO,TRICLINIC,SET,REMAP};
enum{FINAL=0,DELTA,SCALE};
enum{X=0,Y,Z,YZ,XZ,XY};

/* ---------------------------------------------------------------------- */

ChangeBox::ChangeBox(LAMMPS *lmp) : Pointers(lmp) {}

/* ---------------------------------------------------------------------- */

void ChangeBox::command(int narg, char **arg)
{
  int i;

  if (domain->box_exist == 0)
    error->all(FLERR,"Change_box command before simulation box is defined");
  if (narg < 2) error->all(FLERR,"Illegal change_box command");

  if (comm->me == 0 && screen) fprintf(screen,"Changing box ...\n");

  // group

  int igroup = group->find(arg[0]);
  if (igroup == -1) error->all(FLERR,"Could not find change_box group ID");
  int groupbit = group->bitmask[igroup];

  // parse operation arguments
  // allocate ops to max possible length
  // volume option does not increment nops

  int dimension = domain->dimension;

  ops = new Operation[narg-1];
  memset(ops,0,(narg-1)*sizeof(Operation));
  nops = 0;

  int iarg = 1;
  while (iarg < narg) {
    if (strcmp(arg[iarg],"x") == 0 || strcmp(arg[iarg],"y") == 0 ||
        strcmp(arg[iarg],"z") == 0) {
      if (iarg+2 > narg) error->all(FLERR,"Illegal change_box command");
      ops[nops].style = XYZ;
      if (strcmp(arg[iarg],"x") == 0) ops[nops].dim = X;
      else if (strcmp(arg[iarg],"y") == 0) ops[nops].dim = Y;
      else if (strcmp(arg[iarg],"z") == 0) ops[nops].dim = Z;

      if (dimension == 2 && ops[nops].dim == Z)
        error->all(FLERR,"Cannot change_box in z dimension for 2d simulation");

      if (strcmp(arg[iarg+1],"final") == 0) {
        if (iarg+4 > narg) error->all(FLERR,"Illegal change_box command");
        ops[nops].flavor = FINAL;
        ops[nops].flo = force->numeric(FLERR,arg[iarg+2]);
        ops[nops].fhi = force->numeric(FLERR,arg[iarg+3]);
        ops[nops].vdim1 = ops[nops].vdim2 = -1;
        nops++;
        iarg += 4;
      } else if (strcmp(arg[iarg+1],"delta") == 0) {
        if (iarg+4 > narg) error->all(FLERR,"Illegal change_box command");
        ops[nops].flavor = DELTA;
        ops[nops].dlo = force->numeric(FLERR,arg[iarg+2]);
        ops[nops].dhi = force->numeric(FLERR,arg[iarg+3]);
        ops[nops].vdim1 = ops[nops].vdim2 = -1;
        nops++;
        iarg += 4;
      } else if (strcmp(arg[iarg+1],"scale") == 0) {
        if (iarg+3 > narg) error->all(FLERR,"Illegal change_box command");
        ops[nops].flavor = SCALE;
        ops[nops].scale = force->numeric(FLERR,arg[iarg+2]);
        ops[nops].vdim1 = ops[nops].vdim2 = -1;
        nops++;
        iarg += 3;
      } else if (strcmp(arg[iarg+1],"volume") == 0) {
        if (nops == 0 || ops[nops-1].style != XYZ ||
            ops[nops].dim == ops[nops-1].dim)
          error->all(FLERR,"Change_box volume used incorrectly");
        if (ops[nops-1].vdim2 >= 0)
          error->all(FLERR,"Change_box volume used incorrectly");
        else if (ops[nops-1].vdim1 >= 0) ops[nops-1].vdim2 = ops[nops].dim;
        else ops[nops-1].vdim1 = ops[nops].dim;
        iarg += 2;

      } else error->all(FLERR,"Illegal change_box command");

    } else if (strcmp(arg[iarg],"xy") == 0 || strcmp(arg[iarg],"xz") == 0 ||
        strcmp(arg[iarg],"yz") == 0) {
      if (iarg+2 > narg) error->all(FLERR,"Illegal change_box command");
      ops[nops].style = TILT;
      if (strcmp(arg[iarg],"xy") == 0) ops[nops].dim = XY;
      else if (strcmp(arg[iarg],"xz") == 0) ops[nops].dim = XZ;
      else if (strcmp(arg[iarg],"yz") == 0) ops[nops].dim = YZ;

      if (dimension == 2 && (ops[nops].dim == XZ || ops[nops].dim == YZ))
        error->all(FLERR,"Cannot change_box in xz or yz for 2d simulation");

      if (strcmp(arg[iarg+1],"final") == 0) {
        if (iarg+3 > narg) error->all(FLERR,"Illegal change_box command");
        ops[nops].flavor = FINAL;
        ops[nops].ftilt = force->numeric(FLERR,arg[iarg+2]);
        nops++;
        iarg += 3;
      } else if (strcmp(arg[iarg+1],"delta") == 0) {
        if (iarg+3 > narg) error->all(FLERR,"Illegal change_box command");
        ops[nops].flavor = DELTA;
        ops[nops].dtilt = force->numeric(FLERR,arg[iarg+2]);
        nops++;
        iarg += 3;
      } else error->all(FLERR,"Illegal change_box command");

    } else if (strcmp(arg[iarg],"boundary") == 0) {
      if (iarg+4 > narg) error->all(FLERR,"Illegal change_box command");
      ops[nops].style = BOUNDARY;
      ops[nops].boundindex = iarg+1;
      nops++;
      iarg += 4;

    } else if (strcmp(arg[iarg],"ortho") == 0) {
      ops[nops].style = ORTHO;
      nops++;
      iarg += 1;

    } else if (strcmp(arg[iarg],"triclinic") == 0) {
      ops[nops].style = TRICLINIC;
      nops++;
      iarg += 1;

    } else if (strcmp(arg[iarg],"set") == 0) {
      if (iarg+1 > narg) error->all(FLERR,"Illegal change_box command");
      ops[nops].style = SET;
      nops++;
      iarg += 1;

    } else if (strcmp(arg[iarg],"remap") == 0) {
      ops[nops].style = REMAP;
      nops++;
      iarg += 1;

    } else break;
  }

  if (nops == 0) error->all(FLERR,"Illegal change_box command");

  // move_atoms = 1 if need to move atoms to new procs after box changes
  // anything other than ORTHO or TRICLINIC may cause atom movement

  int move_atoms = 0;
  for (int m = 0; m < nops; m++) {
    if (ops[m].style != ORTHO || ops[m].style != TRICLINIC) move_atoms = 1;
  }

  // error if moving atoms and there is stored per-atom restart state
  // disallowed b/c restart per-atom fix info will not move with atoms

  if (move_atoms && modify->nfix_restart_peratom)
    error->all(FLERR,"Change_box parameter not allowed after "
               "reading restart file with per-atom info");

  // read options from end of input line

  options(narg-iarg,&arg[iarg]);

  // compute scale factors if FINAL,DELTA used since they have distance units

  int flag = 0;
  for (int i = 0; i < nops; i++)
    if (ops[i].style == FINAL || ops[i].style == DELTA) flag = 1;

  if (flag && scaleflag) {
    scale[0] = domain->lattice->xlattice;
    scale[1] = domain->lattice->ylattice;
    scale[2] = domain->lattice->zlattice;
  }
  else scale[0] = scale[1] = scale[2] = 1.0;

  // perform sequence of operations
  // first insure atoms are in current box & update box via shrink-wrap
  // no exchange() since doesn't matter if atoms are assigned to correct procs
  // save current box state so can remap atoms from it, if requested

  if (domain->triclinic) domain->x2lamda(atom->nlocal);
  domain->pbc();
  domain->reset_box();
  if (domain->triclinic) domain->lamda2x(atom->nlocal);
  save_box_state();

  for (int m = 0; m < nops; m++) {
    if (ops[m].style == XYZ) {
      double volume;
      if (domain->dimension == 2) volume = domain->xprd * domain->yprd;
      else volume = domain->xprd * domain->yprd * domain->zprd;

      if (ops[m].flavor == FINAL) {
        domain->boxlo[ops[m].dim] = scale[ops[m].dim]*ops[m].flo;
        domain->boxhi[ops[m].dim] = scale[ops[m].dim]*ops[m].fhi;
        if (ops[m].vdim1 >= 0)
          volume_preserve(ops[m].vdim1,ops[m].vdim2,volume);
        domain->set_initial_box();
        domain->set_global_box();
        domain->set_local_box();
        domain->print_box("  ");

      } else if (ops[m].flavor == DELTA) {
        domain->boxlo[ops[m].dim] += scale[ops[m].dim]*ops[m].dlo;
        domain->boxhi[ops[m].dim] += scale[ops[m].dim]*ops[m].dhi;
        if (ops[m].vdim1 >= 0)
          volume_preserve(ops[m].vdim1,ops[m].vdim2,volume);
        domain->set_initial_box();
        domain->set_global_box();
        domain->set_local_box();
        domain->print_box("  ");

      } else if (ops[m].flavor == SCALE) {
        double mid = 0.5 *
          (domain->boxlo[ops[m].dim] + domain->boxhi[ops[m].dim]);
        double delta = domain->boxlo[ops[m].dim] - mid;
        domain->boxlo[ops[m].dim] = mid + ops[m].scale*delta;
        delta = domain->boxhi[ops[m].dim] - mid;
        domain->boxhi[ops[m].dim] = mid + ops[m].scale*delta;
        if (ops[m].vdim1 >= 0)
          volume_preserve(ops[m].vdim1,ops[m].vdim2,volume);
        domain->set_initial_box();
        domain->set_global_box();
        domain->set_local_box();
        domain->print_box("  ");
      }

    } else if (ops[m].style == TILT) {
      if (domain->triclinic == 0)
        error->all(FLERR,"Cannot change box tilt factors for orthogonal box");

      if (ops[m].flavor == FINAL) {
        if (ops[m].dim == XY) domain->xy = scale[X]*ops[m].ftilt;
        else if (ops[m].dim == XZ) domain->xz = scale[X]*ops[m].ftilt;
        else if (ops[m].dim == YZ) domain->yz = scale[Y]*ops[m].ftilt;
        domain->set_initial_box();
        domain->set_global_box();
        domain->set_local_box();
        domain->print_box("  ");

      } else if (ops[m].flavor == DELTA) {
        if (ops[m].dim == XY) domain->xy += scale[X]*ops[m].dtilt;
        else if (ops[m].dim == XZ) domain->xz += scale[X]*ops[m].dtilt;
        else if (ops[m].dim == YZ) domain->yz += scale[Y]*ops[m].dtilt;
        domain->set_initial_box();
        domain->set_global_box();
        domain->set_local_box();
        domain->print_box("  ");
      }

    } else if (ops[m].style == BOUNDARY) {
      domain->set_boundary(3,&arg[ops[m].boundindex],1);
      if (domain->dimension == 2 && domain->zperiodic == 0)
        error->all(FLERR,
                   "Cannot change box z boundary to "
                   "non-periodic for a 2d simulation");
      domain->set_initial_box();
      domain->set_global_box();
      domain->set_local_box();

    } else if (ops[m].style == ORTHO) {
      if (domain->xy != 0.0 || domain->yz != 0.0 || domain->xz != 0.0)
        error->all(FLERR,
                   "Cannot change box to orthogonal when tilt is non-zero");
      if (output->ndump)
        error->all(FLERR,
                   "Cannot change box ortho/triclinic with dumps defined");
      for (int i = 0; i < modify->nfix; i++)
        if (modify->fix[i]->no_change_box)
          error->all(FLERR,
                     "Cannot change box ortho/triclinic with "
                     "certain fixes defined");
      domain->triclinic = 0;
      domain->set_initial_box();
      domain->set_global_box();
      domain->set_local_box();
      domain->print_box("  ");

    } else if (ops[m].style == TRICLINIC) {
      if (output->ndump)
        error->all(FLERR,
                   "Cannot change box ortho/triclinic with dumps defined");
      for (int i = 0; i < modify->nfix; i++)
        if (modify->fix[i]->no_change_box)
          error->all(FLERR,
                     "Cannot change box ortho/triclinic with "
                     "certain fixes defined");
      domain->triclinic = 1;
      domain->set_lamda_box();
      domain->set_initial_box();
      domain->set_global_box();
      domain->set_local_box();
      domain->print_box("  ");

    } else if (ops[m].style == SET) {
      save_box_state();

    } else if (ops[m].style == REMAP) {

      if (modify->check_rigid_group_overlap(groupbit))
        error->warning(FLERR,"Attempting to remap atoms in rigid bodies");

      // convert atoms to lamda coords, using last box state
      // convert atoms back to box coords, using current box state
      // save current box state

      double **x = atom->x;
      int *mask = atom->mask;
      int nlocal = atom->nlocal;

      for (i = 0; i < nlocal; i++)
        if (mask[i] & groupbit)
          domain->x2lamda(x[i],x[i],boxlo,h_inv);

      for (i = 0; i < nlocal; i++)
        if (mask[i] & groupbit)
          domain->lamda2x(x[i],x[i]);

      save_box_state();
    }
  }

  // clean up

  delete [] ops;

  // apply shrink-wrap boundary conditions

  if (domain->nonperiodic == 2) {
    if (domain->triclinic) domain->x2lamda(atom->nlocal);
    domain->reset_box();
    if (domain->triclinic) domain->lamda2x(atom->nlocal);
  }

  // done if don't need to move atoms

  if (!move_atoms) return;

  // move atoms back inside simulation box and to new processors
  // use remap() instead of pbc()
  //   in case box moved a long distance relative to atoms
  // use irregular() in case box moved a long distance relative to atoms

  double **x = atom->x;
  imageint *image = atom->image;
  int nlocal = atom->nlocal;
  for (i = 0; i < nlocal; i++) domain->remap(x[i],image[i]);

  if (domain->triclinic) domain->x2lamda(atom->nlocal);
  domain->reset_box();
  Irregular *irregular = new Irregular(lmp);
  irregular->migrate_atoms(1);
  delete irregular;
  if (domain->triclinic) domain->lamda2x(atom->nlocal);

  // check if any atoms were lost

  bigint natoms;
  bigint nblocal = atom->nlocal;
  MPI_Allreduce(&nblocal,&natoms,1,MPI_LMP_BIGINT,MPI_SUM,world);
  if (natoms != atom->natoms && comm->me == 0)
    error->warning(FLERR,fmt::format("Lost atoms via change_box: "
                                     "original {} current {}",
                                     atom->natoms,natoms));
}

/* ----------------------------------------------------------------------
   parse optional parameters
------------------------------------------------------------------------- */

void ChangeBox::options(int narg, char **arg)
{
  if (narg < 0) error->all(FLERR,"Illegal change_box command");

  scaleflag = 1;

  int iarg = 0;
  while (iarg < narg) {
    if (strcmp(arg[iarg],"units") == 0) {
      if (iarg+2 > narg) error->all(FLERR,"Illegal change_box command");
      if (strcmp(arg[iarg+1],"box") == 0) scaleflag = 0;
      else if (strcmp(arg[iarg+1],"lattice") == 0) scaleflag = 1;
      else error->all(FLERR,"Illegal change_box command");
      iarg += 2;
    } else error->all(FLERR,"Illegal change_box command");
  }
}

/* ----------------------------------------------------------------------
   save current box state for converting atoms to lamda coords
------------------------------------------------------------------------- */

void ChangeBox::save_box_state()
{
  boxlo[0] = domain->boxlo[0];
  boxlo[1] = domain->boxlo[1];
  boxlo[2] = domain->boxlo[2];

  for (int i = 0; i < 6; i++)
    h_inv[i] = domain->h_inv[i];
}

/* ----------------------------------------------------------------------
   oldvol = box volume before dim3 changed
   newvol = box volume after dim3 changed
   reset box lengths of dim1/2 to preserve old volume
------------------------------------------------------------------------- */

void ChangeBox::volume_preserve(int dim1, int dim2, double oldvol)
{
  // invoke set_initial_box()
  // in case change by caller to dim3 was invalid or on shrink-wrapped dim

  domain->set_initial_box();

  // calculate newvol using boxlo/hi since xyz prd are not yet reset

  double newvol;
  if (domain->dimension == 2) {
    newvol = domain->boxhi[0] - domain->boxlo[0];
    newvol *= domain->boxhi[1] - domain->boxlo[1];
  } else {
    newvol = domain->boxhi[0] - domain->boxlo[0];
    newvol *= domain->boxhi[1] - domain->boxlo[1];
    newvol *= domain->boxhi[2] - domain->boxlo[2];
  }

  double scale = oldvol/newvol;
  double mid,delta;

  // change dim1 only

  if (dim2 < 0) {
    mid = 0.5 * (domain->boxlo[dim1] + domain->boxhi[dim1]);
    delta = domain->boxlo[dim1] - mid;
    domain->boxlo[dim1] = mid + scale*delta;
    delta = domain->boxhi[dim1] - mid;
    domain->boxhi[dim1] = mid + scale*delta;

  // change dim1 and dim2, keeping their relative aspect ratio constant
  // both are scaled by sqrt(scale)

  } else {
    mid = 0.5 * (domain->boxlo[dim1] + domain->boxhi[dim1]);
    delta = domain->boxlo[dim1] - mid;
    domain->boxlo[dim1] = mid + sqrt(scale)*delta;
    delta = domain->boxhi[dim1] - mid;
    domain->boxhi[dim1] = mid + sqrt(scale)*delta;
    mid = 0.5 * (domain->boxlo[dim2] + domain->boxhi[dim2]);
    delta = domain->boxlo[dim2] - mid;
    domain->boxlo[dim2] = mid + sqrt(scale)*delta;
    delta = domain->boxhi[dim2] - mid;
    domain->boxhi[dim2] = mid + sqrt(scale)*delta;
  }
}
