/* ----------------------------------------------------------------------
   LAMMPS - Large-scale Atomic/Molecular Massively Parallel Simulator
   https://www.lammps.org/, Sandia National Laboratories
   LAMMPS development team: developers@lammps.org

   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 "fix_wall_table.h"
#include "atom.h"
#include "comm.h"
#include "error.h"
#include "memory.h"
#include "table_file_reader.h"
#include "tokenizer.h"

#include <cstring>

using namespace LAMMPS_NS;

enum { NONE, LINEAR, SPLINE };

static constexpr double BIGNUM = 1.0e300;

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

FixWallTable::FixWallTable(LAMMPS *lmp, int narg, char **arg) :
    FixWall(lmp, narg, arg), tables(nullptr)
{
  dynamic_group_allow = 1;
  tabstyle = NONE;
  if (strcmp(arg[3], "linear") == 0)
    tabstyle = LINEAR;
  else if (strcmp(arg[3], "spline") == 0)
    tabstyle = SPLINE;
  else
    error->all(FLERR, "Unknown table style {} in fix {}", arg[3], style);

  tablength = utils::inumeric(FLERR, arg[4], false, lmp);
  if (tablength < 2) error->all(FLERR, "Illegal number of fix {} table entries", style);
}

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

void FixWallTable::post_constructor()
{
  memory->sfree(tables);
  tables = (Table *) memory->smalloc(nwall * sizeof(Table), "wall:tables");

  for (int m = 0; m < nwall; ++m) {
    Table &tb = tables[m];
    null_table(tb);

    if (comm->me == 0) read_table(tb, fstr[m], kstr[m]);
    bcast_table(tb);

    // error check on table parameters
    if (tb.ninput <= 1) error->all(FLERR, "Invalid fix {} table length: {}", style, tb.ninput);
    tb.lo = tb.rfile[0];
    tb.hi = tb.rfile[tb.ninput - 1];
    if (tb.lo >= tb.hi) error->all(FLERR, "Fix {} table distance values do not increase", style);
    if (cutoff[m] > tb.hi)
      error->all(FLERR, "Fix {} wall cutoff {} is larger than table outer cutoff {}", style,
                 cutoff[m], tb.hi);

    // spline read-in data and compute r,e,f arrays within table
    spline_table(tb);
    compute_table(tb);
  }
}

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

FixWallTable::~FixWallTable()
{
  for (int m = 0; m < nwall; m++) free_table(tables[m]);
  memory->sfree(tables);
}

/* ----------------------------------------------------------------------
   compute the potential energy offset so it can be shifted to zero at the cutoff
------------------------------------------------------------------------- */

void FixWallTable::precompute(int m)
{
  double u, mdu;

  // always use linear interpolation to determine the offset, since we may be at the
  // outer table cutoff and spline interpolation would cause out-of-bounds array accesses
  int savedtabstyle = tabstyle;
  tabstyle = LINEAR;
  uf_lookup(m, cutoff[m], u, mdu);
  offset[m] = u;
  tabstyle = savedtabstyle;
}

/* ----------------------------------------------------------------------
   interaction of all particles in group with a wall
   m = index of wall coeffs
   which = xlo,xhi,ylo,yhi,zlo,zhi
   error if any particle is on or behind wall
------------------------------------------------------------------------- */

void FixWallTable::wall_particle(int m, int which, double coord)
{
  double delta, fwall, vn, u, mdu;

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

  int dim = which / 2;
  int side = which % 2;
  if (side == 0) side = -1;

  int onflag = 0;

  for (int i = 0; i < nlocal; i++)
    if (mask[i] & groupbit) {
      if (side < 0)
        delta = x[i][dim] - coord;
      else
        delta = coord - x[i][dim];
      if (delta >= cutoff[m]) continue;
      if (delta <= 0.0) {
        onflag = 1;
        continue;
      }

      uf_lookup(m, delta, u, mdu);
      fwall = side * mdu;
      f[i][dim] -= fwall;
      ewall[0] += u - offset[m];
      ewall[m + 1] += fwall;

      if (evflag) {
        if (side < 0)
          vn = -fwall * delta;
        else
          vn = fwall * delta;
        v_tally(dim, i, vn);
      }
    }

  if (onflag) error->one(FLERR, "Particle on or inside fix {} surface", style);
}

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

void FixWallTable::null_table(Table &tb)
{
  tb.rfile = tb.efile = tb.ffile = nullptr;
  tb.e2file = tb.f2file = nullptr;
  tb.r = tb.e = tb.de = nullptr;
  tb.f = tb.df = tb.e2 = tb.f2 = nullptr;
}

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

void FixWallTable::free_table(Table &tb)
{
  memory->destroy(tb.rfile);
  memory->destroy(tb.efile);
  memory->destroy(tb.ffile);
  memory->destroy(tb.e2file);
  memory->destroy(tb.f2file);

  memory->destroy(tb.r);
  memory->destroy(tb.e);
  memory->destroy(tb.de);
  memory->destroy(tb.f);
  memory->destroy(tb.df);
  memory->destroy(tb.e2);
  memory->destroy(tb.f2);
}

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

void FixWallTable::read_table(Table &tb, const std::string &tabfile, const std::string &keyword)
{
  TableFileReader reader(lmp, tabfile, "fix wall/table");

  char *line = reader.find_section_start(keyword);
  if (!line) error->one(FLERR, "Did not find keyword {} in table file", keyword);

  // read args on 2nd line of section
  // allocate table arrays for file values

  line = reader.next_line();
  param_extract(tb, line);
  memory->create(tb.rfile, tb.ninput, "wall:rfile");
  memory->create(tb.efile, tb.ninput, "wall:efile");
  memory->create(tb.ffile, tb.ninput, "wall:ffile");

  // read r,e,f table values from file

  reader.skip_line();
  for (int i = 0; i < tb.ninput; i++) {
    line = reader.next_line();
    if (!line)
      error->one(FLERR, "Data missing when parsing wall table '{}' line {} of {}.", keyword, i + 1,
                 tb.ninput);
    try {
      ValueTokenizer values(line);
      values.next_int();
      tb.rfile[i] = values.next_double();
      tb.efile[i] = values.next_double();
      tb.ffile[i] = values.next_double();
    } catch (TokenizerException &e) {
      error->one(FLERR, "Error parsing wall table '{}' line {} of {}. {}\nLine was: {}", keyword,
                 i + 1, tb.ninput, e.what(), line);
    }
  }

  // warn if force != dE/dr at any point that is not an inflection point
  // check via secant approximation to dE/dr
  // skip two end points since do not have surrounding secants
  // inflection point is where curvature changes sign

  double r, e, f, rprev, rnext, eprev, enext, fleft, fright;

  int ferror = 0;
  for (int i = 1; i < tb.ninput - 1; i++) {
    r = tb.rfile[i];
    rprev = tb.rfile[i - 1];
    rnext = tb.rfile[i + 1];
    e = tb.efile[i];
    eprev = tb.efile[i - 1];
    enext = tb.efile[i + 1];
    f = tb.ffile[i];
    fleft = -(e - eprev) / (r - rprev);
    fright = -(enext - e) / (rnext - r);
    if (f < fleft && f < fright) ferror++;
    if (f > fleft && f > fright) ferror++;
  }

  if (ferror)
    error->warning(FLERR,
                   "{} of {} force values in table are inconsistent with -dE/dr.\n"
                   "WARNING:  Should only be flagged at inflection points",
                   ferror, tb.ninput);
}

/* ----------------------------------------------------------------------
   build spline representation of e,f over entire range of read-in table
   this function sets these values in e2file,f2file
------------------------------------------------------------------------- */

void FixWallTable::spline_table(Table &tb)
{
  memory->create(tb.e2file, tb.ninput, "wall:e2file");
  memory->create(tb.f2file, tb.ninput, "wall:f2file");

  double ep0 = -tb.ffile[0];
  double epn = -tb.ffile[tb.ninput - 1];
  spline(tb.rfile, tb.efile, tb.ninput, ep0, epn, tb.e2file);

  if (tb.fpflag == 0) {
    tb.fplo = (tb.ffile[1] - tb.ffile[0]) / (tb.rfile[1] - tb.rfile[0]);
    tb.fphi = (tb.ffile[tb.ninput - 1] - tb.ffile[tb.ninput - 2]) /
        (tb.rfile[tb.ninput - 1] - tb.rfile[tb.ninput - 2]);
  }

  double fp0 = tb.fplo;
  double fpn = tb.fphi;
  spline(tb.rfile, tb.ffile, tb.ninput, fp0, fpn, tb.f2file);
}

/* ----------------------------------------------------------------------
   compute r,e,f vectors from splined values
------------------------------------------------------------------------- */

void FixWallTable::compute_table(Table &tb)
{
  // delta = table spacing for N-1 bins
  int tlm1 = tablength - 1;

  tb.delta = (tb.hi - tb.lo) / tlm1;
  tb.invdelta = 1.0 / tb.delta;
  tb.deltasq6 = tb.delta * tb.delta / 6.0;

  // N-1 evenly spaced bins in r from min to max
  // r,e,f = value at lower edge of bin
  // de,df values = delta values of e,f
  // r,e,f are N in length so de,df arrays can compute difference

  memory->create(tb.r, tablength, "wall:r");
  memory->create(tb.e, tablength, "wall:e");
  memory->create(tb.de, tablength, "wall:de");
  memory->create(tb.f, tablength, "wall:f");
  memory->create(tb.df, tablength, "wall:df");
  memory->create(tb.e2, tablength, "wall:e2");
  memory->create(tb.f2, tablength, "wall:f2");

  double a;
  for (int i = 0; i < tablength; i++) {
    a = tb.lo + i * tb.delta;
    tb.r[i] = a;
    tb.e[i] = splint(tb.rfile, tb.efile, tb.e2file, tb.ninput, a);
    tb.f[i] = splint(tb.rfile, tb.ffile, tb.f2file, tb.ninput, a);
  }

  for (int i = 0; i < tlm1; i++) {
    tb.de[i] = tb.e[i + 1] - tb.e[i];
    tb.df[i] = tb.f[i + 1] - tb.f[i];
  }
  // get final elements from linear extrapolation
  tb.de[tlm1] = 2.0 * tb.de[tlm1 - 1] - tb.de[tlm1 - 2];
  tb.df[tlm1] = 2.0 * tb.df[tlm1 - 1] - tb.df[tlm1 - 2];

  double ep0 = -tb.f[0];
  double epn = -tb.f[tlm1];
  spline(tb.r, tb.e, tablength, ep0, epn, tb.e2);
  spline(tb.r, tb.f, tablength, tb.fplo, tb.fphi, tb.f2);
}

/* ----------------------------------------------------------------------
   extract attributes from parameter line in table section
   format of line: N value FP fplo fphi
   N is required, other params are optional
------------------------------------------------------------------------- */

void FixWallTable::param_extract(Table &tb, char *line)
{
  tb.ninput = 0;
  tb.fpflag = 0;

  try {
    ValueTokenizer values(line);

    while (values.has_next()) {
      std::string word = values.next_string();

      if (word == "N") {
        tb.ninput = values.next_int();
      } else if (word == "FP") {
        tb.fpflag = 1;
        tb.fplo = values.next_double();
        tb.fphi = values.next_double();
      } else {
        error->one(FLERR, "Invalid keyword {} in fix {} table parameters", word, style);
      }
    }
  } catch (TokenizerException &e) {
    error->one(FLERR, e.what());
  }

  if (tb.ninput == 0) error->one(FLERR, "Fix {} parameters did not set N", style);
}

/* ----------------------------------------------------------------------
   broadcast read-in table info from proc 0 to other procs
   this function communicates these values in Table:
     ninput,rfile,efile,ffile,fpflag,fplo,fphi
------------------------------------------------------------------------- */

void FixWallTable::bcast_table(Table &tb)
{
  MPI_Bcast(&tb.ninput, 1, MPI_INT, 0, world);

  if (comm->me > 0) {
    memory->create(tb.rfile, tb.ninput, "wall:rfile");
    memory->create(tb.efile, tb.ninput, "wall:efile");
    memory->create(tb.ffile, tb.ninput, "wall:ffile");
  }

  MPI_Bcast(tb.rfile, tb.ninput, MPI_DOUBLE, 0, world);
  MPI_Bcast(tb.efile, tb.ninput, MPI_DOUBLE, 0, world);
  MPI_Bcast(tb.ffile, tb.ninput, MPI_DOUBLE, 0, world);

  MPI_Bcast(&tb.fpflag, 1, MPI_INT, 0, world);
  if (tb.fpflag) {
    MPI_Bcast(&tb.fplo, 1, MPI_DOUBLE, 0, world);
    MPI_Bcast(&tb.fphi, 1, MPI_DOUBLE, 0, world);
  }
}

/* ----------------------------------------------------------------------
   spline and splint routines modified from Numerical Recipes
------------------------------------------------------------------------- */

void FixWallTable::spline(double *x, double *y, int n, double yp1, double ypn, double *y2)
{
  int i, k;
  double p, qn, sig, un;
  auto *u = new double[n];

  if (yp1 > BIGNUM)
    y2[0] = u[0] = 0.0;
  else {
    y2[0] = -0.5;
    u[0] = (3.0 / (x[1] - x[0])) * ((y[1] - y[0]) / (x[1] - x[0]) - yp1);
  }
  for (i = 1; i < n - 1; i++) {
    sig = (x[i] - x[i - 1]) / (x[i + 1] - x[i - 1]);
    p = sig * y2[i - 1] + 2.0;
    y2[i] = (sig - 1.0) / p;
    u[i] = (y[i + 1] - y[i]) / (x[i + 1] - x[i]) - (y[i] - y[i - 1]) / (x[i] - x[i - 1]);
    u[i] = (6.0 * u[i] / (x[i + 1] - x[i - 1]) - sig * u[i - 1]) / p;
  }
  if (ypn > BIGNUM)
    qn = un = 0.0;
  else {
    qn = 0.5;
    un = (3.0 / (x[n - 1] - x[n - 2])) * (ypn - (y[n - 1] - y[n - 2]) / (x[n - 1] - x[n - 2]));
  }
  y2[n - 1] = (un - qn * u[n - 2]) / (qn * y2[n - 2] + 1.0);
  for (k = n - 2; k >= 0; k--) y2[k] = y2[k] * y2[k + 1] + u[k];

  delete[] u;
}

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

double FixWallTable::splint(double *xa, double *ya, double *y2a, int n, double x)
{
  int klo, khi, k;
  double h, b, a, y;

  klo = 0;
  khi = n - 1;
  while (khi - klo > 1) {
    k = (khi + klo) >> 1;
    if (xa[k] > x)
      khi = k;
    else
      klo = k;
  }
  h = xa[khi] - xa[klo];
  a = (xa[khi] - x) / h;
  b = (x - xa[klo]) / h;
  y = a * ya[klo] + b * ya[khi] +
      ((a * a * a - a) * y2a[klo] + (b * b * b - b) * y2a[khi]) * (h * h) / 6.0;
  return y;
}

/* ----------------------------------------------------------------------
   calculate potential u and force f at distance x
   ensure x is between wall min/max, exit with error if not
------------------------------------------------------------------------- */

void FixWallTable::uf_lookup(int m, double x, double &u, double &f)
{
  double fraction, a, b;
  const Table &tb = tables[m];
  const int itable = static_cast<int>((x - tb.lo) * tb.invdelta);
  if (itable < 0)
    error->one(FLERR, "Particle / wall distance < table inner cutoff: {:.8}", x);
  else if (itable >= tablength)
    error->one(FLERR, "Particle / wall distance > table outer cutoff: {:.8}", x);

  if (tabstyle == LINEAR) {
    fraction = (x - tb.r[itable]) * tb.invdelta;
    u = tb.e[itable] + fraction * tb.de[itable];
    f = tb.f[itable] + fraction * tb.df[itable];
  } else if (tabstyle == SPLINE) {
    fraction = (x - tb.r[itable]) * tb.invdelta;

    b = (x - tb.r[itable]) * tb.invdelta;
    a = 1.0 - b;
    u = a * tb.e[itable] + b * tb.e[itable + 1] +
        ((a * a * a - a) * tb.e2[itable] + (b * b * b - b) * tb.e2[itable + 1]) * tb.deltasq6;
    f = a * tb.f[itable] + b * tb.f[itable + 1] +
        ((a * a * a - a) * tb.f2[itable] + (b * b * b - b) * tb.f2[itable + 1]) * tb.deltasq6;
  }
}
