/**********************************************************************
Copyright (C) 2007 by Maxim Fedorovsky, University of Fribourg (Switzerland).

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 version 2 of the License.

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

#include <numeric>

// No diagnoalization yet. Perhaps for 2.2 -GRH
// #include <eigen/matrix.h>

#include <openbabel/mol.h>
#include <openbabel/obconversion.h>
#include <openbabel/obmolecformat.h>
#include <openbabel/math/matrix3x3.h>

#define BOHR2ANGSTROM 0.5291772083
#define HARTREE2INVCM 219474.631371
#define AMU2AU 1822.88848
#define REDDIPSTR2INT 974.864

using namespace std;

namespace OpenBabel
{
  //! \brief Class for parsing Gaussian formatted checkpoint files
  class FCHKFormat : public OBMoleculeFormat
  {
    public :
      /* Register this format type ID */
      FCHKFormat()
      {
        OBConversion::RegisterFormat("fchk", this,
                                     "chemical/x-gaussian-checkpoint");
        OBConversion::RegisterFormat("fch", this,
                                     "chemical/x-gaussian-checkpoint");
        OBConversion::RegisterFormat("fck", this,
                                     "chemical/x-gaussian-checkpoint");
      }

      virtual const char * Description()
      {
        return "Gaussian formatted checkpoint file format\n \
                  Read options e.g. -as\n\
                    s  Single bonds only\n\
                    b  No bond perception\n\n";
        // Vibrational analysis not yet supported in OB-2.1.
        //                    v  Do not perform the vibrational analysis\n\n";
      };

      virtual const char * GetMIMEType()
      {
        return "chemical/x-gaussian-checkpoint";
      };

      virtual unsigned int Flags()
      {
        return NOTWRITABLE;
      };

      virtual bool ReadMolecule(OBBase *, OBConversion *);

    private :
      bool static read_int(const char * const, int * const);
      template<class T>  static bool read_numbers(const char * const,
                                                  vector<T> &);
      template <class T> static bool read_section(const char * const,
                                                  vector<T> &,
                                                  const unsigned int,
                                                  bool * const,
                                                  const char * const,
                                                  const unsigned int);
      bool static validate_section(const char * const,
                                   const int,
                                   const char * const,
                                   const unsigned int);
      bool static validate_number(const int,
                                  const char * const,
                                  const unsigned int);
      void static construct_mol(OBMol * const,
                                OBConversion * const,
                                const unsigned int,
                                const vector<int> &,
                                const vector<double> &,
                                const int,
                                const vector<int> &,
                                const vector<int> &);
//       void static vibana(OBMol * const,
//                          const vector<double> &,
//                          const vector<double> &);
//       bool static generate_tr_rot(OBMol * const,
//                                   double * const,
//                                   unsigned int * const);
//       void static rotmatrix_axis(const double * const, const double,
//                                  matrix3x3 &);
//       double static cosine(const double * const, const double * const);
//       void static normalize_set(double * const,
//                                 const unsigned int, const unsigned int);
//       void static orthogonalize_set(double * const,
//                                     const unsigned int, const unsigned int,
//                                     const bool);
//       void static rests(const unsigned int, unsigned int * const,
//                         unsigned int * const, const unsigned int);

//       void static retrieve_hessian_indices(const unsigned int,
//                                            unsigned int * const,
//                                            unsigned int * const,
//                                            unsigned int * const,
//                                            unsigned int * const);
  };

  /* Make an instance of the format class */
  FCHKFormat theFCHKFormat;


  /*!
  **\brief Extract the data.
  **
  **The data are :
  **  comment
  **  charge
  **  multiplicity
  **  geometry
  **  connection table
  */
  bool FCHKFormat::ReadMolecule(OBBase * pOb, OBConversion * pConv)
  {
    OBMol * const pmol = dynamic_cast<OBMol*>(pOb);

    if (!pmol)
      return false;

    /* input stream */
    istream * const pifs = pConv->GetInStream();

    /* for logging errors */
    stringstream  error_msg;

    /* help variables */
    char buff[BUFF_SIZE];
    unsigned int lineno = 0;
    int  intvar;
    bool finished;
    bool atomnos_found = false;
    bool coords_found = false;
    bool nbond_found = false;
    bool ibond_found = false;
    bool hessian_found = false;
    bool dipder_found = false;

    /* variables for storing the read data */
    int  Natoms = -1, MxBond = -1;
    vector<int>    atomnos, NBond, IBond;
    vector<double> coords, hessian, dipder;

    /*** start reading ***/
    pmol->BeginModify();

    /* first line : comment */
    if (!pifs->getline(buff, BUFF_SIZE))
    {
      obErrorLog.ThrowError("FCHKFormat::ReadMolecule()",
                            "Failed to read the comment.",
                            obError);
      return false;
    }

    ++lineno;
    pmol->SetTitle(buff);

    /* extract all the information here */
    while (pifs->getline(buff, BUFF_SIZE))
    {
      ++lineno;

      /* Number of atoms */
      if (buff == strstr(buff, "Number of atoms"))
      {
        if (!FCHKFormat::read_int(buff, &Natoms))
        {
          error_msg << "Could not read the number of atoms from line #"
                    << lineno << ".";

          obErrorLog.ThrowError("FCHKFormat::ReadMolecule()",
                                error_msg.str(),
                                obError);
          return false;
        }
        if (0 >= Natoms)
        {
          error_msg << "Invalid number of atoms : " << Natoms << ".";
          obErrorLog.ThrowError("FCHKFormat::ReadMolecule()",
                                error_msg.str(),
                                obError);
          return false;
        }
        continue;
      }

      /* Dipole Moment */
      if (buff == strstr(buff, "Dipole Moment"))
      {
        if (!pifs->getline(buff, BUFF_SIZE)) {
          error_msg << "Could not read the dipole moment after line #"
                    << lineno << ".";
          obErrorLog.ThrowError("FCHKFormat::ReadMolecule()",
                                error_msg.str(),
                                obError);
          return false;
        }
        ++lineno;
        vector<double> moments;
        if (!FCHKFormat::read_numbers(buff, moments) || moments.size() != 3) {
          error_msg << "Could not read the dipole moment from line #"
                    << lineno << ".";
          obErrorLog.ThrowError("FCHKFormat::ReadMolecule()",
                                error_msg.str(),
                                obError);
          return false;
        }
        
        OBVectorData *dipoleMoment = new OBVectorData;
        dipoleMoment->SetAttribute("Dipole Moment");
        dipoleMoment->SetData(moments[0], moments[1], moments[2]);
        dipoleMoment->SetOrigin(fileformatInput);
        pmol->SetData(dipoleMoment);
        continue;
      }

      /* Charge */
      if (buff == strstr(buff, "Charge"))
      {
        if (!FCHKFormat::read_int(buff, &intvar))
        {
          error_msg << "Could not read the charge from line #"
                    << lineno << ".";
          obErrorLog.ThrowError("FCHKFormat::ReadMolecule()",
                                error_msg.str(),
                                obError);
          return false;
        }

        pmol->SetTotalCharge(intvar);
        continue;
      }

      /* Multiplicity */
      if (buff == strstr(buff, "Multiplicity"))
      {
        if (!FCHKFormat::read_int(buff, &intvar))
        {
          error_msg << "Could not read the multiplicity from line #"
                    << lineno << ".";
          obErrorLog.ThrowError("FCHKFormat::ReadMolecule()",
                                error_msg.str(),
                                obError);
          return false;
        }

        pmol->SetTotalSpinMultiplicity(intvar);
        continue;
      }

      /* start of the "Atomic numbers" section */
      if (buff == strstr(buff, "Atomic numbers"))
      {
        if (!FCHKFormat::validate_number(Natoms, "number of atoms", lineno))
          return false;

        if (!FCHKFormat::validate_section(buff,
                                          Natoms,
                                          "number of atomic numbers",
                                          lineno))
          return false;

        atomnos_found = true;
        continue;
      }

      /* start of the "Current cartesian coordinates" section */
      if (buff == strstr(buff, "Current cartesian coordinates"))
      {
        if (!FCHKFormat::validate_number(Natoms, "number of atoms", lineno))
          return false;

        if (!FCHKFormat::validate_section(buff,
                                          3 * Natoms,
                                          "number of coordinates",
                                          lineno))
          return false;

        coords_found = true;
        continue;
      }

      /* MxBond - largest number of bonds to an atom */
      if (buff == strstr(buff, "MxBond"))
      {
        if (!FCHKFormat::read_int(buff, &MxBond))
        {
          error_msg << "Could not read the number of bonds to each atom"
                    << " (MxBond) from line #" << lineno << ".";

          obErrorLog.ThrowError("FCHKFormat::ReadMolecule()",
                                error_msg.str(),
                                obError);
          return false;
        }
        if (0 >= MxBond)
        {
          error_msg << "Invalid number of bonds to each atom : "
                    << MxBond << ".";
          obErrorLog.ThrowError("FCHKFormat::ReadMolecule()",
                                error_msg.str(),
                                obError);
          return false;
        }
        continue;
      }

      /* start of the "NBond" section */
      if (buff == strstr(buff, "NBond"))
      {
        if (!FCHKFormat::validate_number(Natoms, "number of atoms", lineno))
          return false;

        if (!FCHKFormat::validate_number(MxBond,
                                         "number of bonds to each atom",
                                         lineno))
          return false;

        if (!FCHKFormat::validate_section(buff,
                                          Natoms,
                                          "number of bonds to each atom",
                                          lineno))
          return false;

        nbond_found = true;
        continue;
      }

      /* start of the "IBond" section */
      if (buff == strstr(buff, "IBond"))
      {
        if (!FCHKFormat::validate_number(Natoms, "number of atoms", lineno))
          return false;

        if (!FCHKFormat::validate_number(MxBond,
                                         "number of bonds to each atom",
                                         lineno))
          return false;

        if (!FCHKFormat::validate_section(buff,
                                          MxBond * Natoms,
                                          "number of bonds",
                                          lineno))
          return false;

        ibond_found = true;
        continue;
      }

      /* start of the "Cartesian Force Constants" section */
      if (buff == strstr(buff, "Cartesian Force Constants"))
      {
        if (!FCHKFormat::validate_number(Natoms, "number of atoms", lineno))
          return false;

        if (!FCHKFormat::validate_section(buff,
                                          (3 * Natoms) * (3 * Natoms + 1) / 2,
                                          "number of force constants",
                                          lineno))
          return false;

        hessian_found = true;
        continue;
      }

      /* start of the "Dipole Derivatives" section */
      if (buff == strstr(buff, "Dipole Derivatives"))
      {
        if (!FCHKFormat::validate_number(Natoms, "number of atoms", lineno))
          return false;

        if (!FCHKFormat::validate_section(buff,
                                          9 * Natoms,
                                          "number of dipole derivatives",
                                          lineno))
          return false;

        dipder_found = true;
        continue;
      }

      /* reading the atomic numbers */
      if (atomnos_found && Natoms > (int)atomnos.size())
      {
        if (!FCHKFormat::read_section(buff, atomnos, Natoms, &finished,
                                      "atomic numbers", lineno))
          return false;

        atomnos_found = !finished;
        continue;
      }

      /* reading the coordinates */
      if (coords_found && 3 * Natoms > (int)coords.size())
      {
        if (!FCHKFormat::read_section(buff, coords, 3 * Natoms, &finished,
                                      "coordinates", lineno))
          return false;

        coords_found = !finished;
        continue;
      }

      /* reading the numbers of bonds to each atom */
      if (nbond_found && Natoms > (int)NBond.size())
      {
        if (!FCHKFormat::read_section(buff, NBond, Natoms, &finished,
                                      "number of bonds to each atom", lineno))
          return false;

        nbond_found = !finished;
        continue;
      }

      /* reading the list of atoms bound to each atom */
      if (ibond_found && MxBond * Natoms > (int)IBond.size())
      {
        if (!FCHKFormat::read_section(buff, IBond, MxBond * Natoms, &finished,
                                      "atom bonds", lineno))
          return false;

        ibond_found = !finished;
        continue;
      }

      /* reading the hessian */
      if (hessian_found &&
          (3 * Natoms) * (3 * Natoms + 1) / 2 > (int)hessian.size())
      {
        if (!FCHKFormat::read_section(buff, hessian,
                                      (3 * Natoms) * (3 * Natoms + 1) / 2,
                                      &finished,
                                      "hessian", lineno))
          return false;

        hessian_found = !finished;
        continue;
      }

      /* reading the dipole derivatives */
      if (dipder_found && 9 * Natoms > (int)dipder.size())
      {
        if (!FCHKFormat::read_section(buff, dipder,
                                      9 * Natoms,
                                      &finished,
                                      "dipole derivatives", lineno))
          return false;

        dipder_found = !finished;
        continue;
      }

    } /* while */

    /*** validating ***/
    if (0 >= Natoms)
    {
      error_msg << "Number of atoms could not be read.";
      obErrorLog.ThrowError("FCHKFormat::ReadMolecule()",
                            error_msg.str(),
                            obError);
      return false;
    }

    if (atomnos.empty())
    {
      error_msg << "\"Atomic numbers\" section was not found.";
      obErrorLog.ThrowError("FCHKFormat::ReadMolecule()",
                            error_msg.str(),
                            obError);
      return false;
    }

    if (coords.empty())
    {
      error_msg << "\"Current cartesian coordinates\" section was not found.";
      obErrorLog.ThrowError("FCHKFormat::ReadMolecule()",
                            error_msg.str(),
                            obError);
      return false;
    }

    /* connectivity : if MxBond is set, all the sections must be supplied */
    if (-1 != MxBond)
    {
      if (NBond.empty() || IBond.empty())
      {
        error_msg << "If MxBond is set, then the \"NBond\" and \"IBond\""
                  << " sections *must* be supplied.";
        obErrorLog.ThrowError("FCHKFormat::ReadMolecule()",
                              error_msg.str(),
                              obError);
        return false;
      }

      /* not nbonds > MxBond or <= 0
         no atom numbers < 0 or > Natoms */
      if (NBond.end() != find_if(NBond.begin(),
                                 NBond.end(),
                                 bind2nd(less_equal<int>(), 0)) ||
          NBond.end() != find_if(NBond.begin(),
                                 NBond.end(),
                                 bind2nd(greater<int>(), MxBond)) ||
          IBond.end() != find_if(IBond.begin(),
                                 IBond.end(),
                                 bind2nd(less<int>(), 0)) ||
          IBond.end() != find_if(IBond.begin(),
                                 IBond.end(),
                                 bind2nd(greater<int>(), Natoms)))
      {
        error_msg << "Invalid connectivity : check the \"NBond\" and/or"
                  << " \"IBond\" section(s).";
        obErrorLog.ThrowError("FCHKFormat::ReadMolecule()",
                              error_msg.str(),
                              obError);
        return false;
      }
    }

    /*** finalizing ***/
    FCHKFormat::construct_mol(pmol, pConv, Natoms, atomnos, coords,
                              MxBond, NBond, IBond);

    //    if (!hessian.empty() && !pConv->IsOption("v", OBConversion::INOPTIONS))
    //    {
    //      FCHKFormat::vibana(pmol, hessian, dipder);
    //    }

    /*** finished ***/
    pmol->EndModify();

    return true;
  }

  /*!
  **\brief Convert the last token in a string to integer
  **
  **Return false if the conversion failed.
  **The read integer is stored in the num argument.
  */
  bool FCHKFormat::read_int(const char * const line, int * const num)
  {
    vector<string>  vs;
    char * endptr;

    tokenize(vs, line);
    *num = strtol(vs.back().c_str(), &endptr, 10);

    return endptr != vs.back().c_str();
  }

  /*!
  **\brief Read integers or doubles into a vector from a string
  **\param line string
  **\param v    vector to read in
  */
  template<class T>
  bool FCHKFormat::read_numbers(const char * const line, vector<T> & v)
  {
    vector<string>  vs;
    tokenize(vs, line);

    if (0 < vs.size())
    {
      char * endptr;
      T num;

      for (vector<string>::const_iterator it=vs.begin(); vs.end() != it; ++it)
      {
        if (typeid(double) == typeid(T))
          num = static_cast<T>(strtod((*it).c_str(), &endptr));
        else
          num = static_cast<T>(strtol((*it).c_str(), &endptr, 10));

        /* quit if the value cannot be read */
        if (endptr == (*it).c_str())
          return false;

        v.push_back(num);
      }
    }

    return true;
  }

  /*!
  **\brief Validate a section
  **
  **Try to convert the last token of a string to integer and compare it to a
  **given number. If they are not equal report an error and return false.
  **Otherwise return true.
  */
  bool FCHKFormat::validate_section(const char * const line,
                                    const int nreq,
                                    const char * const desc,
                                    const unsigned int lineno)
  {
    int intvar;
    stringstream error_msg;

    if (!FCHKFormat::read_int(line, &intvar))
    {
      error_msg << "Could not read the " << desc
                << " from line #" << lineno << ".";
      obErrorLog.ThrowError("FCHKFormat::ReadMolecule()",
                            error_msg.str(),
                            obError);
      return false;
    }
    if (intvar != nreq)
    {
      error_msg << desc << " must be exactly " << nreq
                << ", found " << intvar << ".";
      obErrorLog.ThrowError("FCHKFormat::ReadMolecule()",
                            error_msg.str(),
                            obError);
      return false;
    }

    return true;
  }

  /*!
  **\brief Validate a number
  **
  **Compare it with a given value and report an error unless they are equal.
  */
  bool FCHKFormat::validate_number(const int num,
                                   const char * const desc,
                                   const unsigned int lineno)
  {
    stringstream error_msg;

    if (-1 == num)
    {
      error_msg << desc   << " must be already read before line #"
                << lineno << ".";
      obErrorLog.ThrowError("FCHKFormat::ReadMolecule()",
                            error_msg.str(),
                            obError);
      return false;
    }
    return true;
  }

  /*!
  **\brief Read numbers in a section
  **\param line     line
  **\param v        vector to read in
  **\param nreq     maximum number of elements in v
  **\param finished whether the reading is finished
  **\param desc     name of the section
  **\param lineno   line number
  */
  template <class T>
  bool FCHKFormat::read_section(const char * const line,
                                vector<T> & v,
                                const unsigned int nreq,
                                bool * const finished,
                                const char * const desc,
                                const unsigned int lineno)
  {
    stringstream error_msg;

    *finished = false;

    if(!FCHKFormat::read_numbers(line, v))
    {
      error_msg << "Expecting " << desc << " in line #" << lineno << ".";
      obErrorLog.ThrowError("FCHKFormat::ReadMolecule()",
                            error_msg.str(),
                            obError);
      return false;
    }
    if (nreq <= v.size())
    {
      *finished = true;
      if (nreq < v.size())
      {
        error_msg << "Ignoring the superfluous " << desc
                  << "in line #" << lineno << ".";
        obErrorLog.ThrowError("FCHKFormat::ReadMolecule()",
                              error_msg.str(),
                              obWarning);
      }
    }

    return true;
  }

  /*!
   **\brief Construct the molecule
   **\param pmol    molecule
   **\param Natoms  number of atoms
   **\param atomnos atomic numbers
   **\param coords  coordinates
   **\param MxBond  largest number of bonds to an atom (usually 4)
   **\param NBond   number of bonds to each atom
   **\param IBond   list of atoms bounded to each atom
   */
  void FCHKFormat::construct_mol(OBMol * const pmol,
                                 OBConversion * const pConv,
                                 const unsigned int Natoms,
                                 const vector<int> & atomnos,
                                 const vector<double> & coords,
                                 const int MxBond,
                                 const vector<int> & NBond,
                                 const vector<int> & IBond)
  {
    pmol->ReserveAtoms(Natoms);

    OBAtom * atom;
    for (unsigned int a = 0; Natoms > a; ++a)
    {
      atom = pmol->NewAtom();

      atom->SetAtomicNum(atomnos[a]);
      atom->SetVector(coords[0 + 3 * a] * BOHR2ANGSTROM,
                      coords[1 + 3 * a] * BOHR2ANGSTROM,
                      coords[2 + 3 * a] * BOHR2ANGSTROM);
    }

    /* unless suppressed */
    if (!pConv->IsOption("b", OBConversion::INOPTIONS))
    {
      /* if the connectivity is provided */
      if (-1 != MxBond)
      {
        /* atoms */
        for (unsigned int a = 0; Natoms > a; ++a)
        {
          /* bonds for an atom */
          for (unsigned int i = 0; (unsigned int)NBond[a] > i; ++i)
          {
            pmol->AddBond(1 + a, IBond[MxBond*a + i], 1);
          }
        }
      }
      else
      {
        pmol->ConnectTheDots();
      }
    }

    if (!pConv->IsOption("s", OBConversion::INOPTIONS) &&
        !pConv->IsOption("b", OBConversion::INOPTIONS))
      pmol->PerceiveBondOrders();
  }

} /* namespace OpenBabel */

