// clang-format off
/*----------------------------------------------------------------------
  PuReMD - Purdue ReaxFF Molecular Dynamics Program

  Copyright (2010) Purdue University
  Hasan Metin Aktulga, hmaktulga@lbl.gov
  Joseph Fogarty, jcfogart@mail.usf.edu
  Sagar Pandit, pandit@usf.edu
  Ananth Y Grama, ayg@cs.purdue.edu

  Please cite the related publication:
  H. M. Aktulga, J. C. Fogarty, S. A. Pandit, A. Y. Grama,
  "Parallel Reactive Molecular Dynamics: Numerical Methods and
  Algorithmic Techniques", Parallel Computing, in press.

  This program is free software; you can redistribute it and/or
  modify it under the terms of the GNU General Public License as
  published by the Free Software Foundation; either version 2 of
  the License, or (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  See the GNU General Public License for more details:
  <https://www.gnu.org/licenses/>.
  ----------------------------------------------------------------------*/

#include "reaxff_api.h"

#include "pair.h"

#include <cmath>

#define MIN_SINE 1e-10

namespace ReaxFF {
  double Calculate_Omega(rvec dvec_ij, double r_ij, rvec dvec_jk, double r_jk,
                         rvec dvec_kl, double r_kl, rvec dvec_li, double r_li,
                         three_body_interaction_data *p_ijk,
                         three_body_interaction_data *p_jkl,
                         rvec dcos_omega_di, rvec dcos_omega_dj,
                         rvec dcos_omega_dk, rvec dcos_omega_dl)
  {
    double unnorm_cos_omega, unnorm_sin_omega, omega;
    double sin_ijk, cos_ijk, sin_jkl, cos_jkl;
    double htra, htrb, htrc, hthd, hthe, hnra, hnrc, hnhd, hnhe;
    double arg, poem, tel;
    rvec cross_jk_kl;

    sin_ijk = sin(p_ijk->theta);
    cos_ijk = cos(p_ijk->theta);
    sin_jkl = sin(p_jkl->theta);
    cos_jkl = cos(p_jkl->theta);

    /* omega */
    unnorm_cos_omega = -rvec_Dot(dvec_ij, dvec_jk) * rvec_Dot(dvec_jk, dvec_kl) +
      SQR(r_jk) *  rvec_Dot(dvec_ij, dvec_kl);

    rvec_Cross(cross_jk_kl, dvec_jk, dvec_kl);
    unnorm_sin_omega = -r_jk * rvec_Dot(dvec_ij, cross_jk_kl);

    omega = atan2(unnorm_sin_omega, unnorm_cos_omega);

    htra = r_ij + cos_ijk * (r_kl * cos_jkl - r_jk);
    htrb = r_jk - r_ij * cos_ijk - r_kl * cos_jkl;
    htrc = r_kl + cos_jkl * (r_ij * cos_ijk - r_jk);
    hthd = r_ij * sin_ijk * (r_jk - r_kl * cos_jkl);
    hthe = r_kl * sin_jkl * (r_jk - r_ij * cos_ijk);
    hnra = r_kl * sin_ijk * sin_jkl;
    hnrc = r_ij * sin_ijk * sin_jkl;
    hnhd = r_ij * r_kl * cos_ijk * sin_jkl;
    hnhe = r_ij * r_kl * sin_ijk * cos_jkl;

    poem = 2.0 * r_ij * r_kl * sin_ijk * sin_jkl;
    if (poem < 1e-20) poem = 1e-20;

    tel  = SQR(r_ij) + SQR(r_jk) + SQR(r_kl) - SQR(r_li) -
      2.0 * (r_ij * r_jk * cos_ijk - r_ij * r_kl * cos_ijk * cos_jkl +
              r_jk * r_kl * cos_jkl);

    arg  = tel / poem;
    if (arg >  1.0) arg =  1.0;
    if (arg < -1.0) arg = -1.0;

    if (sin_ijk >= 0 && sin_ijk <= MIN_SINE) sin_ijk = MIN_SINE;
    else if (sin_ijk <= 0 && sin_ijk >= -MIN_SINE) sin_ijk = -MIN_SINE;
    if (sin_jkl >= 0 && sin_jkl <= MIN_SINE) sin_jkl = MIN_SINE;
    else if (sin_jkl <= 0 && sin_jkl >= -MIN_SINE) sin_jkl = -MIN_SINE;

    // dcos_omega_di
    rvec_ScaledSum(dcos_omega_di, (htra-arg*hnra)/r_ij, dvec_ij, -1., dvec_li);
    rvec_ScaledAdd(dcos_omega_di,-(hthd-arg*hnhd)/sin_ijk, p_ijk->dcos_dk);
    rvec_Scale(dcos_omega_di, 2.0 / poem, dcos_omega_di);

    // dcos_omega_dj
    rvec_ScaledSum(dcos_omega_dj,-(htra-arg*hnra)/r_ij, dvec_ij,
                    -htrb / r_jk, dvec_jk);
    rvec_ScaledAdd(dcos_omega_dj,-(hthd-arg*hnhd)/sin_ijk, p_ijk->dcos_dj);
    rvec_ScaledAdd(dcos_omega_dj,-(hthe-arg*hnhe)/sin_jkl, p_jkl->dcos_di);
    rvec_Scale(dcos_omega_dj, 2.0 / poem, dcos_omega_dj);

    // dcos_omega_dk
    rvec_ScaledSum(dcos_omega_dk,-(htrc-arg*hnrc)/r_kl, dvec_kl,
                    htrb / r_jk, dvec_jk);
    rvec_ScaledAdd(dcos_omega_dk,-(hthd-arg*hnhd)/sin_ijk, p_ijk->dcos_di);
    rvec_ScaledAdd(dcos_omega_dk,-(hthe-arg*hnhe)/sin_jkl, p_jkl->dcos_dj);
    rvec_Scale(dcos_omega_dk, 2.0 / poem, dcos_omega_dk);

    // dcos_omega_dl
    rvec_ScaledSum(dcos_omega_dl, (htrc-arg*hnrc)/r_kl, dvec_kl, 1., dvec_li);
    rvec_ScaledAdd(dcos_omega_dl,-(hthe-arg*hnhe)/sin_jkl, p_jkl->dcos_dk);
    rvec_Scale(dcos_omega_dl, 2.0 / poem, dcos_omega_dl);

    return omega;
  }

  void Torsion_Angles(reax_system *system, control_params *control,
                      simulation_data *data, storage *workspace,
                      reax_list **lists)
  {
    int i, j, k, l, pi, pj, pk, pl, pij, plk, natoms;
    int type_i, type_j, type_k, type_l;
    int start_j, end_j;
    int start_pj, end_pj, start_pk, end_pk;
    int num_frb_intrs = 0;

    double Delta_j, Delta_k;
    double r_ij, r_jk, r_kl, r_li;
    double BOA_ij, BOA_jk, BOA_kl;

    double exp_tor2_ij, exp_tor2_jk, exp_tor2_kl;
    double exp_tor1, exp_tor3_DjDk, exp_tor4_DjDk, exp_tor34_inv;
    double exp_cot2_jk, exp_cot2_ij, exp_cot2_kl;
    double fn10, f11_DjDk, dfn11, fn12;
    double theta_ijk, theta_jkl;
    double sin_ijk, sin_jkl;
    double cos_ijk, cos_jkl;
    double tan_ijk_i, tan_jkl_i;
    double omega, cos_omega, cos2omega, cos3omega;
    rvec dcos_omega_di, dcos_omega_dj, dcos_omega_dk, dcos_omega_dl;
    double CV, cmn, CEtors1, CEtors2, CEtors3, CEtors4;
    double CEtors5, CEtors6, CEtors7, CEtors8, CEtors9;
    double Cconj, CEconj1, CEconj2, CEconj3;
    double CEconj4, CEconj5, CEconj6;
    double e_tor, e_con;
    rvec dvec_li;
    four_body_header *fbh;
    four_body_parameters *fbp;
    bond_data *pbond_ij, *pbond_jk, *pbond_kl;
    bond_order_data *bo_ij, *bo_jk, *bo_kl;
    three_body_interaction_data *p_ijk, *p_jkl;
    double p_tor2 = system->reax_param.gp.l[23];
    double p_tor3 = system->reax_param.gp.l[24];
    double p_tor4 = system->reax_param.gp.l[25];
    double p_cot2 = system->reax_param.gp.l[27];
    reax_list *bonds = (*lists) + BONDS;
    reax_list *thb_intrs = (*lists) + THREE_BODIES;

    // Virial tallying variables
    double delil[3], deljl[3], delkl[3];
    double eng_tmp, fi_tmp[3], fj_tmp[3], fk_tmp[3];

    natoms = system->n;

    for (j = 0; j < natoms; ++j) {
      type_j = system->my_atoms[j].type;
      Delta_j = workspace->Delta_boc[j];
      start_j = Start_Index(j, bonds);
      end_j = End_Index(j, bonds);

      for (pk = start_j; pk < end_j; ++pk) {
        pbond_jk = &(bonds->select.bond_list[pk]);
        k = pbond_jk->nbr;
        bo_jk = &(pbond_jk->bo_data);
        BOA_jk = bo_jk->BO - control->thb_cut;

        if (system->my_atoms[j].orig_id > system->my_atoms[k].orig_id)
          continue;
        if (system->my_atoms[j].orig_id == system->my_atoms[k].orig_id) {
          if (system->my_atoms[k].x[2] <  system->my_atoms[j].x[2]) continue;
          if (system->my_atoms[k].x[2] == system->my_atoms[j].x[2] &&
              system->my_atoms[k].x[1] <  system->my_atoms[j].x[1]) continue;
          if (system->my_atoms[k].x[2] == system->my_atoms[j].x[2] &&
              system->my_atoms[k].x[1] == system->my_atoms[j].x[1] &&
              system->my_atoms[k].x[0] <  system->my_atoms[j].x[0]) continue;
        }

        if (bo_jk->BO > control->thb_cut/*0*/ && Num_Entries(pk, thb_intrs)) {
          pj = pbond_jk->sym_index; // pj points to j on k's list

          if (Num_Entries(pj, thb_intrs)) {
            type_k = system->my_atoms[k].type;
            Delta_k = workspace->Delta_boc[k];
            r_jk = pbond_jk->d;

            start_pk = Start_Index(pk, thb_intrs);
            end_pk = End_Index(pk, thb_intrs);
            start_pj = Start_Index(pj, thb_intrs);
            end_pj = End_Index(pj, thb_intrs);

            exp_tor2_jk = exp(-p_tor2 * BOA_jk);
            exp_cot2_jk = exp(-p_cot2 * SQR(BOA_jk - 1.5));
            exp_tor3_DjDk = exp(-p_tor3 * (Delta_j + Delta_k));
            exp_tor4_DjDk = exp(p_tor4  * (Delta_j + Delta_k));
            exp_tor34_inv = 1.0 / (1.0 + exp_tor3_DjDk + exp_tor4_DjDk);
            f11_DjDk = (2.0 + exp_tor3_DjDk) * exp_tor34_inv;

            for (pi = start_pk; pi < end_pk; ++pi) {
              p_ijk = &(thb_intrs->select.three_body_list[pi]);
              pij = p_ijk->pthb; // pij is pointer to i on j's bond_list
              pbond_ij = &(bonds->select.bond_list[pij]);
              bo_ij = &(pbond_ij->bo_data);

              if (bo_ij->BO > control->thb_cut/*0*/) {
                i = p_ijk->thb;
                type_i = system->my_atoms[i].type;
                r_ij = pbond_ij->d;
                BOA_ij = bo_ij->BO - control->thb_cut;

                theta_ijk = p_ijk->theta;
                sin_ijk = sin(theta_ijk);
                cos_ijk = cos(theta_ijk);
                //tan_ijk_i = 1. / tan(theta_ijk);
                if (sin_ijk >= 0 && sin_ijk <= MIN_SINE)
                  tan_ijk_i = cos_ijk / MIN_SINE;
                else if (sin_ijk <= 0 && sin_ijk >= -MIN_SINE)
                  tan_ijk_i = cos_ijk / -MIN_SINE;
                else tan_ijk_i = cos_ijk / sin_ijk;

                exp_tor2_ij = exp(-p_tor2 * BOA_ij);
                exp_cot2_ij = exp(-p_cot2 * SQR(BOA_ij -1.5));

                for (pl = start_pj; pl < end_pj; ++pl) {
                  p_jkl = &(thb_intrs->select.three_body_list[pl]);
                  l = p_jkl->thb;
                  plk = p_jkl->pthb; //pointer to l on k's bond_list!
                  pbond_kl = &(bonds->select.bond_list[plk]);
                  bo_kl = &(pbond_kl->bo_data);
                  type_l = system->my_atoms[l].type;
                  fbh = &(system->reax_param.fbp[type_i][type_j]
                          [type_k][type_l]);
                  fbp = &(system->reax_param.fbp[type_i][type_j]
                          [type_k][type_l].prm[0]);

                  if (i != l && fbh->cnt &&
                       bo_kl->BO > control->thb_cut/*0*/ &&
                       bo_ij->BO * bo_jk->BO * bo_kl->BO > control->thb_cut/*0*/) {
                    ++num_frb_intrs;
                    r_kl = pbond_kl->d;
                    BOA_kl = bo_kl->BO - control->thb_cut;

                    theta_jkl = p_jkl->theta;
                    sin_jkl = sin(theta_jkl);
                    cos_jkl = cos(theta_jkl);
                    //tan_jkl_i = 1. / tan(theta_jkl);
                    if (sin_jkl >= 0 && sin_jkl <= MIN_SINE)
                      tan_jkl_i = cos_jkl / MIN_SINE;
                    else if (sin_jkl <= 0 && sin_jkl >= -MIN_SINE)
                      tan_jkl_i = cos_jkl / -MIN_SINE;
                    else tan_jkl_i = cos_jkl /sin_jkl;

                    rvec_ScaledSum(dvec_li, 1., system->my_atoms[i].x,
                                    -1., system->my_atoms[l].x);
                    r_li = rvec_Norm(dvec_li);


                    /* omega and its derivative */
                    omega = Calculate_Omega(pbond_ij->dvec, r_ij,
                                             pbond_jk->dvec, r_jk,
                                             pbond_kl->dvec, r_kl,
                                             dvec_li, r_li,
                                             p_ijk, p_jkl,
                                             dcos_omega_di, dcos_omega_dj,
                                             dcos_omega_dk, dcos_omega_dl);

                    cos_omega = cos(omega);
                    cos2omega = cos(2. * omega);
                    cos3omega = cos(3. * omega);
                    /* end omega calculations */

                    /* torsion energy */
                    exp_tor1 = exp(fbp->p_tor1 *
                                    SQR(2.0 - bo_jk->BO_pi - f11_DjDk));
                    exp_tor2_kl = exp(-p_tor2 * BOA_kl);
                    exp_cot2_kl = exp(-p_cot2 * SQR(BOA_kl - 1.5));
                    fn10 = (1.0 - exp_tor2_ij) * (1.0 - exp_tor2_jk) *
                      (1.0 - exp_tor2_kl);

                    CV = 0.5 * (fbp->V1 * (1.0 + cos_omega) +
                                 fbp->V2 * exp_tor1 * (1.0 - cos2omega) +
                                 fbp->V3 * (1.0 + cos3omega));

                    data->my_en.e_tor += e_tor = fn10 * sin_ijk * sin_jkl * CV;

                    dfn11 = (-p_tor3 * exp_tor3_DjDk +
                             (p_tor3 * exp_tor3_DjDk - p_tor4 * exp_tor4_DjDk) *
                             (2.0 + exp_tor3_DjDk) * exp_tor34_inv) *
                      exp_tor34_inv;

                    CEtors1 = sin_ijk * sin_jkl * CV;

                    CEtors2 = -fn10 * 2.0 * fbp->p_tor1 * fbp->V2 * exp_tor1 *
                      (2.0 - bo_jk->BO_pi - f11_DjDk) * (1.0 - SQR(cos_omega)) *
                      sin_ijk * sin_jkl;
                    CEtors3 = CEtors2 * dfn11;

                    CEtors4 = CEtors1 * p_tor2 * exp_tor2_ij *
                      (1.0 - exp_tor2_jk) * (1.0 - exp_tor2_kl);
                    CEtors5 = CEtors1 * p_tor2 *
                      (1.0 - exp_tor2_ij) * exp_tor2_jk * (1.0 - exp_tor2_kl);
                    CEtors6 = CEtors1 * p_tor2 *
                      (1.0 - exp_tor2_ij) * (1.0 - exp_tor2_jk) * exp_tor2_kl;

                    cmn = -fn10 * CV;
                    CEtors7 = cmn * sin_jkl * tan_ijk_i;
                    CEtors8 = cmn * sin_ijk * tan_jkl_i;

                    CEtors9 = fn10 * sin_ijk * sin_jkl *
                      (0.5 * fbp->V1 - 2.0 * fbp->V2 * exp_tor1 * cos_omega +
                       1.5 * fbp->V3 * (cos2omega + 2.0 * SQR(cos_omega)));
                    /* end  of torsion energy */

                    /* 4-body conjugation energy */
                    fn12 = exp_cot2_ij * exp_cot2_jk * exp_cot2_kl;
                    data->my_en.e_con += e_con =
                      fbp->p_cot1 * fn12 *
                      (1.0 + (SQR(cos_omega) - 1.0) * sin_ijk * sin_jkl);

                    Cconj = -2.0 * fn12 * fbp->p_cot1 * p_cot2 *
                      (1.0 + (SQR(cos_omega) - 1.0) * sin_ijk * sin_jkl);

                    CEconj1 = Cconj * (BOA_ij - 1.5e0);
                    CEconj2 = Cconj * (BOA_jk - 1.5e0);
                    CEconj3 = Cconj * (BOA_kl - 1.5e0);

                    CEconj4 = -fbp->p_cot1 * fn12 *
                      (SQR(cos_omega) - 1.0) * sin_jkl * tan_ijk_i;
                    CEconj5 = -fbp->p_cot1 * fn12 *
                      (SQR(cos_omega) - 1.0) * sin_ijk * tan_jkl_i;
                    CEconj6 = 2.0 * fbp->p_cot1 * fn12 *
                      cos_omega * sin_ijk * sin_jkl;
                    /* end 4-body conjugation energy */

                    /* forces */
                    bo_jk->Cdbopi += CEtors2;
                    workspace->CdDelta[j] += CEtors3;
                    workspace->CdDelta[k] += CEtors3;
                    bo_ij->Cdbo += (CEtors4 + CEconj1);
                    bo_jk->Cdbo += (CEtors5 + CEconj2);
                    bo_kl->Cdbo += (CEtors6 + CEconj3);

                    /* dcos_theta_ijk */
                    rvec_ScaledAdd(workspace->f[i], CEtors7 + CEconj4, p_ijk->dcos_dk);
                    rvec_ScaledAdd(workspace->f[j], CEtors7 + CEconj4, p_ijk->dcos_dj);
                    rvec_ScaledAdd(workspace->f[k], CEtors7 + CEconj4, p_ijk->dcos_di);

                    /* dcos_theta_jkl */
                    rvec_ScaledAdd(workspace->f[j], CEtors8 + CEconj5, p_jkl->dcos_di);
                    rvec_ScaledAdd(workspace->f[k], CEtors8 + CEconj5, p_jkl->dcos_dj);
                    rvec_ScaledAdd(workspace->f[l], CEtors8 + CEconj5, p_jkl->dcos_dk);

                    /* dcos_omega */
                    rvec_ScaledAdd(workspace->f[i], CEtors9 + CEconj6, dcos_omega_di);
                    rvec_ScaledAdd(workspace->f[j], CEtors9 + CEconj6, dcos_omega_dj);
                    rvec_ScaledAdd(workspace->f[k], CEtors9 + CEconj6, dcos_omega_dk);
                    rvec_ScaledAdd(workspace->f[l], CEtors9 + CEconj6, dcos_omega_dl);

                    /* tally into per-atom virials */
                    if (system->pair_ptr->evflag) {

                      // acquire vectors
                      rvec_ScaledSum(delil, 1., system->my_atoms[l].x, -1., system->my_atoms[i].x);
                      rvec_ScaledSum(deljl, 1., system->my_atoms[l].x, -1., system->my_atoms[j].x);
                      rvec_ScaledSum(delkl, 1., system->my_atoms[l].x, -1., system->my_atoms[k].x);
                      // dcos_theta_ijk
                      rvec_Scale(fi_tmp, CEtors7 + CEconj4, p_ijk->dcos_dk);
                      rvec_Scale(fj_tmp, CEtors7 + CEconj4, p_ijk->dcos_dj);
                      rvec_Scale(fk_tmp, CEtors7 + CEconj4, p_ijk->dcos_di);

                      // dcos_theta_jkl
                      rvec_ScaledAdd(fj_tmp, CEtors8 + CEconj5, p_jkl->dcos_di);
                      rvec_ScaledAdd(fk_tmp, CEtors8 + CEconj5, p_jkl->dcos_dj);

                      // dcos_omega
                      rvec_ScaledAdd(fi_tmp, CEtors9 + CEconj6, dcos_omega_di);
                      rvec_ScaledAdd(fj_tmp, CEtors9 + CEconj6, dcos_omega_dj);
                      rvec_ScaledAdd(fk_tmp, CEtors9 + CEconj6, dcos_omega_dk);

                      // tally
                      eng_tmp = e_tor + e_con;
                      if (system->pair_ptr->eflag_either)
                        system->pair_ptr->ev_tally(j,k,natoms,1,eng_tmp,0.0,0.0,0.0,0.0,0.0);
                      if (system->pair_ptr->vflag_either)
                        system->pair_ptr->v_tally4(i,j,k,l,fi_tmp,fj_tmp,fk_tmp,delil,deljl,delkl);
                    }
                  } // pl check ends
                } // pl loop ends
              } // pi check ends
            } // pi loop ends
          } // k-j neighbor check ends
        } // j-k neighbor check ends
      } // pk loop ends
    } // j loop
  }
}
