/* -*- c++ -*- ----------------------------------------------------------
   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.
------------------------------------------------------------------------- */

/* ----------------------------------------------------------------------
   This class contains a series of tools for DEM contacts
   Multiple models can be defined and used to calculate forces
   and torques based on contact geometry

   Contributing authors:
   Dan Bolintineanu (SNL), Joel Clemmer (SNL)
----------------------------------------------------------------------- */

#include "granular_model.h"

#include "comm.h"
#include "error.h"
#include "force.h"
#include "gran_sub_mod.h"
#include "math_extra.h"
#include "memory.h"

#include "style_gran_sub_mod.h"    // IWYU pragma: keep

#include <cmath>
#include <cstring>
#include <utility>

using namespace LAMMPS_NS;
using namespace Granular_NS;
using namespace MathExtra;

/* ----------------------------------------------------------------------
   one instance per GranSubMod style in style_gran_sub_mod.h
------------------------------------------------------------------------- */

template <typename T> static GranSubMod *gran_sub_mod_creator(GranularModel *gm, LAMMPS *lmp)
{
  return new T(gm, lmp);
}

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

GranularModel::GranularModel(LAMMPS *lmp) : Pointers(lmp)
{
  limit_damping = 0;
  beyond_contact = 0;
  nondefault_history_transfer = 0;
  classic_model = 0;
  contact_type = PAIR;

  normal_model = nullptr;
  damping_model = nullptr;
  tangential_model = nullptr;
  rolling_model = nullptr;
  twisting_model = nullptr;
  heat_model = nullptr;

  calculate_svector = 0;
  nsvector = 0;
  svector = nullptr;

  for (int i = 0; i < NSUBMODELS; i++) sub_models[i] = nullptr;
  transfer_history_factor = nullptr;

  // extract info from GranSubMod classes listed in style_gran_sub_mod.h

  nclass = 0;

#define GRAN_SUB_MOD_CLASS
#define GranSubModStyle(key, Class, type) nclass++;
#include "style_gran_sub_mod.h"    // IWYU pragma: keep
#undef GranSubModStyle
#undef GRAN_SUB_MOD_CLASS

  gran_sub_mod_class = new GranSubModCreator[nclass];
  gran_sub_mod_names = new char *[nclass];
  gran_sub_mod_types = new int[nclass];
  nclass = 0;

#define GRAN_SUB_MOD_CLASS
#define GranSubModStyle(key, Class, type)                    \
  gran_sub_mod_class[nclass] = &gran_sub_mod_creator<Class>; \
  gran_sub_mod_names[nclass] = (char *) #key;                \
  gran_sub_mod_types[nclass++] = type;
#include "style_gran_sub_mod.h"    // IWYU pragma: keep
#undef GranSubModStyle
#undef GRAN_SUB_MOD_CLASS
}

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

GranularModel::~GranularModel()
{
  delete[] transfer_history_factor;
  delete[] gran_sub_mod_class;
  delete[] gran_sub_mod_names;
  delete[] gran_sub_mod_types;
  delete[] svector;

  for (int i = 0; i < NSUBMODELS; i++) delete sub_models[i];
}

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

int GranularModel::add_sub_model(char **arg, int iarg, int narg, SubModelType model_type)
{
  if (iarg >= narg) error->all(FLERR, "Must specify granular sub model name");

  std::string model_name = std::string(arg[iarg++]);

  construct_sub_model(model_name, model_type);

  int num_coeffs = sub_models[model_type]->num_coeffs;
  if (iarg + num_coeffs > narg)
    error->all(FLERR, "Insufficient arguments provided for {} model", model_name);

  for (int i = 0; i < num_coeffs; i++) {
    // A few parameters (e.g. kt for tangential mindlin) allow null
    if (strcmp(arg[iarg + i], "NULL") == 0)
      sub_models[model_type]->coeffs[i] = -1;
    else
      sub_models[model_type]->coeffs[i] = utils::numeric(FLERR, arg[iarg + i], false, lmp);
  }

  sub_models[model_type]->coeffs_to_local();

  return iarg + num_coeffs;
}

// clang-format off

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

void GranularModel::construct_sub_model(std::string model_name, SubModelType model_type)
{
  int i;
  for (i = 0; i < nclass; i++) {
    if (gran_sub_mod_types[i] == model_type) {
      if (strcmp(gran_sub_mod_names[i], model_name.c_str()) == 0) {
        GranSubModCreator &gran_sub_mod_creator = gran_sub_mod_class[i];
        delete sub_models[model_type];
        sub_models[model_type] = gran_sub_mod_creator(this, lmp);
        break;
      }
    }
  }

  if (i == nclass)
    error->all(FLERR, "Illegal model type {}", model_name);

  sub_models[model_type]->name.assign(model_name);
  sub_models[model_type]->allocate_coeffs();

  // Assign specific sub model pointer
  if (model_type == NORMAL) normal_model = dynamic_cast<GranSubModNormal *> (sub_models[model_type]);
  if (model_type == DAMPING) damping_model = dynamic_cast<GranSubModDamping *> (sub_models[model_type]);
  if (model_type == TANGENTIAL) tangential_model = dynamic_cast<GranSubModTangential *> (sub_models[model_type]);
  if (model_type == ROLLING) rolling_model = dynamic_cast<GranSubModRolling *> (sub_models[model_type]);
  if (model_type == TWISTING) twisting_model = dynamic_cast<GranSubModTwisting *> (sub_models[model_type]);
  if (model_type == HEAT) heat_model = dynamic_cast<GranSubModHeat *> (sub_models[model_type]);
}

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

int GranularModel::define_classic_model(char **arg, int iarg, int narg)
{
  double kn, kt, gamman, gammat, xmu;

  classic_model = 1;

  if (iarg + 6 >= narg)
    error->all(FLERR,"Insufficient arguments provided for classic gran model command");

  kn = utils::numeric(FLERR,arg[iarg + 1],false,lmp);
  if (strcmp(arg[iarg + 2],"NULL") == 0) kt = kn * 2.0 / 7.0;
  else kt = utils::numeric(FLERR,arg[iarg + 2],false,lmp);

  gamman = utils::numeric(FLERR,arg[iarg + 3],false,lmp);
  if (strcmp(arg[iarg + 4],"NULL") == 0) gammat = 0.5 * gamman;
  else gammat = utils::numeric(FLERR,arg[iarg + 4],false,lmp);

  xmu = utils::numeric(FLERR,arg[iarg + 5],false,lmp);
  int dampflag = utils::inumeric(FLERR,arg[iarg + 6],false,lmp);
  if (dampflag == 0) gammat = 0.0;

  if (kn < 0.0 || kt < 0.0 || gamman < 0.0 || gammat < 0.0 ||
      xmu < 0.0 || xmu > 10000.0 || dampflag < 0 || dampflag > 1)
    error->all(FLERR,"Illegal classic gran model command");

  if (strcmp(arg[iarg],"hooke") == 0) {
    construct_sub_model("hooke", NORMAL);
    construct_sub_model("linear_nohistory", TANGENTIAL);
    construct_sub_model("mass_velocity", DAMPING);
  } else if (strcmp(arg[iarg],"hooke/history") == 0) {
    construct_sub_model("hooke", NORMAL);
    construct_sub_model("linear_history_classic", TANGENTIAL);
    construct_sub_model("mass_velocity", DAMPING);
  } else if (strcmp(arg[iarg],"hertz/history") == 0) {
    // convert Kn and Kt from pressure units to force/distance^2 if Hertzian
    kn /= force->nktv2p;
    kt /= force->nktv2p;
    construct_sub_model("hertz", NORMAL);
    construct_sub_model("mindlin_classic", TANGENTIAL);
    construct_sub_model("viscoelastic", DAMPING);
  } else error->all(FLERR,"Invalid classic gran model");

  // ensure additional models are undefined
  construct_sub_model("none", ROLLING);
  construct_sub_model("none", TWISTING);
  construct_sub_model("none", HEAT);

  // manually parse coeffs
  normal_model->coeffs[0] = kn;
  normal_model->coeffs[1] = gamman;

  if (tangential_model->num_coeffs == 2) {
    tangential_model->coeffs[0] = gammat / gamman;
    tangential_model->coeffs[1] = xmu;
  } else {
    tangential_model->coeffs[0] = kt;
    tangential_model->coeffs[1] = gammat / gamman;
    tangential_model->coeffs[2] = xmu;
  }

  normal_model->coeffs_to_local();
  tangential_model->coeffs_to_local();
  damping_model->coeffs_to_local();

  iarg += 7;
  return iarg;
}

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

void GranularModel::init()
{
  for (int i = 0; i < NSUBMODELS; i++)
    if (!sub_models[i]) construct_sub_model("none", (SubModelType) i);

  // Must have valid normal, damping, and tangential models
  if (normal_model->name == "none") error->all(FLERR, "Must specify normal granular model");
  if (normal_model->name == "mdr") {
     if (damping_model->name != "none")
       error->all(FLERR, "MDR require 'none' damping model. To damp, specify a coefficient of restitution < 1.");
  } else {
    if (damping_model->name == "none") error->all(FLERR, "Must specify damping granular model");
  }
  if (tangential_model->name == "none") error->all(FLERR, "Must specify tangential granular model");

  // Twisting, rolling, and heat are optional
  twisting_defined = rolling_defined = heat_defined = 1;
  if (twisting_model->name == "none") twisting_defined = 0;
  if (rolling_model->name == "none") rolling_defined = 0;
  if (heat_model->name == "none") heat_defined = 0;

  int size_cumulative;
  size_history = 0;
  contact_radius_flag = 0;
  for (int i = 0; i < NSUBMODELS; i++) {
    if (sub_models[i]->nondefault_history_transfer)
      nondefault_history_transfer = 1;
    if (sub_models[i]->beyond_contact)
      beyond_contact = 1;
    size_history += sub_models[i]->size_history;
    if (!sub_models[i]->allow_cohesion && normal_model->get_cohesive_flag())
      error->all(FLERR,"Cannot use {} model with a cohesive normal model, {}",
                 sub_models[i]->name, normal_model->name);
    if (sub_models[i]->contact_radius_flag) contact_radius_flag = 1;
  }

  if (limit_damping && normal_model->get_cohesive_flag())
    error->all(FLERR,"Cannot limit damping with a cohesive normal model, {}", normal_model->name);

  if (nondefault_history_transfer) {
    transfer_history_factor = new double[size_history];

    int j;
    for (int i = 0; i < size_history; i++) {
      // Find which sub model owns this history value
      size_cumulative = 0;
      for (j = 0; j < NSUBMODELS; j++) {
        if ((size_cumulative + sub_models[j]->size_history) > i) break;
        size_cumulative += sub_models[j]->size_history;
      }

      // Check if sub model has nondefault transfers, if so copy its array
      transfer_history_factor[i] = -1;
      if (j != NSUBMODELS) {
        if (sub_models[j]->nondefault_history_transfer) {
          transfer_history_factor[i] = sub_models[j]->transfer_history_factor[i - size_cumulative];
        }
      }
    }
  }

  for (int i = 0; i < NSUBMODELS; i++) sub_models[i]->init();

  nsvector = 0;
  int index_svector = 0;
  for (int i = 0; i < NSUBMODELS; i++) {
    if (sub_models[i]->nsvector != 0) {
      sub_models[i]->index_svector = index_svector;
      nsvector += sub_models[i]->nsvector;
      index_svector += sub_models[i]->nsvector;
    }
  }

  if (nsvector != 0) {
    delete[] svector;
    svector = new double[nsvector];
  }
}

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

int GranularModel::mix_coeffs(GranularModel *g1, GranularModel *g2)
{
  for (int i = 0; i < NSUBMODELS; i++) {
    if (g1->sub_models[i]->name != g2->sub_models[i]->name) return i;

    construct_sub_model(g1->sub_models[i]->name, (SubModelType) i);
    sub_models[i]->mix_coeffs(g1->sub_models[i]->coeffs, g2->sub_models[i]->coeffs);
  }

  limit_damping = MAX(g1->limit_damping, g2->limit_damping);

  return -1;
}

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

void GranularModel::write_restart(FILE *fp)
{
  int num_char, num_coeffs;

  for (int i = 0; i < NSUBMODELS; i++) {
    num_char = sub_models[i]->name.length();
    num_coeffs = sub_models[i]->num_coeffs;
    fwrite(&num_char, sizeof(int), 1, fp);
    fwrite(sub_models[i]->name.data(), sizeof(char), num_char, fp);
    fwrite(&num_coeffs, sizeof(int), 1, fp);
    fwrite(sub_models[i]->coeffs, sizeof(double), num_coeffs, fp);
  }

  fwrite(&limit_damping, sizeof(int), 1, fp);
}

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

void GranularModel::read_restart(FILE *fp)
{
  int num_char, num_coeff;

  for (int i = 0; i < NSUBMODELS; i++) {
    if (comm->me == 0)
      utils::sfread(FLERR, &num_char, sizeof(int), 1, fp, nullptr, error);
    MPI_Bcast(&num_char, 1, MPI_INT, 0, world);

    std::string model_name(num_char, ' ');
    if (comm->me == 0)
      utils::sfread(FLERR, const_cast<char*>(model_name.data()), sizeof(char),num_char, fp, nullptr, error);
    MPI_Bcast(const_cast<char*>(model_name.data()), num_char, MPI_CHAR, 0, world);
    construct_sub_model(std::move(model_name), (SubModelType) i);

    if (comm->me == 0)
      utils::sfread(FLERR, &num_coeff, sizeof(int), 1, fp, nullptr, error);
    MPI_Bcast(&num_coeff, 1, MPI_INT, 0, world);
    if (num_coeff != sub_models[i]->num_coeffs)
      error->all(FLERR, "Invalid granular model written to restart file");

    if (comm->me == 0)
      utils::sfread(FLERR, sub_models[i]->coeffs, sizeof(double), num_coeff, fp, nullptr, error);
    MPI_Bcast(sub_models[i]->coeffs, num_coeff, MPI_DOUBLE, 0, world);
    sub_models[i]->coeffs_to_local();
  }

  if (comm->me == 0)
    utils::sfread(FLERR, &limit_damping, sizeof(int), 1, fp, nullptr, error);
  MPI_Bcast(&limit_damping, 1, MPI_INT, 0, world);
}

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

bool GranularModel::check_contact()
{
  if (contact_type == WALL) {
    // Used by fix_wall_gran.cpp
    //   radj = radius of wall
    //   dx already provided
    rsq = lensq3(dx);
    radsum = radi;
    if (radj == 0) Reff = radi;
    else Reff = radi * radj / (radi + radj);
  } else if (contact_type == WALLREGION) {
    // Used by fix_wall_gran_region.cpp
    //   radj = radius of wall
    //   dx and r already provided
    rsq = r * r;
    radsum = radi;
    if (radj == 0) Reff = radi;
    else Reff = radi * radj / (radi + radj);
  } else {
    sub3(xi, xj, dx);
    rsq = lensq3(dx);
    radsum = radi + radj;
    Reff = radi * radj / radsum;
  }

  touch = normal_model->touch();
  return touch;
}

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

void GranularModel::calculate_forces()
{
  // Standard geometric quantities

  if (contact_type != WALLREGION) r = sqrt(rsq);
  rinv = 1.0 / r;
  delta = radsum - r;
  dR = delta * Reff;
  scale3(rinv, dx, nx);

  // relative translational velocity
  sub3(vi, vj, vr);

  // normal component
  vnnr = dot3(vr, nx);
  scale3(vnnr, nx, vn);

  // tangential component
  sub3(vr, vn, vt);

  // relative rotational velocity
  scaleadd3(radi, omegai, radj, omegaj, wr);

  // relative tangential velocities
  double temp[3];
  cross3(wr, nx, temp);
  sub3(vt, temp, vtr);
  vrel = len3(vtr);

  // calculate forces/torques
  double Fdamp, dist_to_contact;
  if (contact_radius_flag)
    contact_radius = normal_model->calculate_contact_radius();
  Fnormal = normal_model->calculate_forces();

  Fdamp = damping_model->calculate_forces();
  Fntot = Fnormal + Fdamp;
  if (limit_damping && Fntot < 0.0) Fntot = 0.0;

  normal_model->set_fncrit(); // Needed for tangential, rolling, twisting
  tangential_model->calculate_forces();

  // sum normal + tangential contributions

  scale3(Fntot, nx, forces);
  add3(forces, fs, forces);

  // May need to eventually rethink tris..
  cross3(nx, fs, torquesi);
  scale3(-1, torquesi);

  if (contact_type == PAIR) {
    copy3(torquesi, torquesj);

    // Classic pair styles don't scale, but classic option is currently only used by walls
    dist_to_contact = radi - 0.5 * delta;
    scale3(dist_to_contact, torquesi);
    dist_to_contact = radj - 0.5 * delta;
    scale3(dist_to_contact, torquesj);
  } else {
    scale3(radi, torquesi);
  }

  // Extra modes

  if (rolling_defined || twisting_defined)
    sub3(omegai, omegaj, relrot);

  if (rolling_defined) {
    // rolling velocity, see eq. 31 of Wang et al, Particuology v 23, p 49 (2015)
    // this is different from the Marshall papers, which use the Bagi/Kuhn formulation
    // for rolling velocity (see Wang et al for why the latter is wrong)
    vrl[0] = Reff * (relrot[1] * nx[2] - relrot[2] * nx[1]);
    vrl[1] = Reff * (relrot[2] * nx[0] - relrot[0] * nx[2]);
    vrl[2] = Reff * (relrot[0] * nx[1] - relrot[1] * nx[0]);

    rolling_model->calculate_forces();

    double torroll[3];
    cross3(nx, fr, torroll);
    scale3(Reff, torroll);
    add3(torquesi, torroll, torquesi);
    if (contact_type == PAIR) sub3(torquesj, torroll, torquesj);
  }

  if (twisting_defined) {
    // omega_T (eq 29 of Marshall)
    magtwist = dot3(relrot, nx);

    twisting_model->calculate_forces();

    double tortwist[3];
    scale3(magtortwist, nx, tortwist);
    add3(torquesi, tortwist, torquesi);
    if (contact_type == PAIR) sub3(torquesj, tortwist, torquesj);
  }

  if (heat_defined)
    dq = heat_model->calculate_heat();
}

/* ----------------------------------------------------------------------
   compute pull-off distance (beyond contact) for a given radius and atom type
   use temporary variables since this does not use a specific contact geometry
------------------------------------------------------------------------- */

double GranularModel::pulloff_distance(double radi, double radj)
{
  return normal_model->pulloff_distance(radi, radj);
}
