/*
 * Project: MoleCuilder
 * Description: creates and alters molecular systems
 * Copyright (C)  2013 University of Bonn. All rights reserved.
 * Copyright (C)  2013 Frederik Heber. All rights reserved.
 * 
 *
 *   This file is part of MoleCuilder.
 *
 *    MoleCuilder 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.
 *
 *    MoleCuilder 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.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with MoleCuilder.  If not, see .
 */
/*
 * SaturatedFragment.cpp
 *
 *  Created on: Mar 3, 2013
 *      Author: heber
 */
// include config.h
#ifdef HAVE_CONFIG_H
#include 
#endif
#include "CodePatterns/MemDebug.hpp"
#include "SaturatedFragment.hpp"
#include 
#include 
#include "CodePatterns/Assert.hpp"
#include "CodePatterns/Log.hpp"
#include "LinearAlgebra/Exceptions.hpp"
#include "LinearAlgebra/Plane.hpp"
#include "LinearAlgebra/RealSpaceMatrix.hpp"
#include "LinearAlgebra/Vector.hpp"
#include "Atom/atom.hpp"
#include "Bond/bond.hpp"
#include "config.hpp"
#include "Descriptors/AtomIdDescriptor.hpp"
#include "Fragmentation/Exporters/HydrogenPool.hpp"
#include "Fragmentation/HydrogenSaturation_enum.hpp"
#include "Graph/BondGraph.hpp"
#include "World.hpp"
SaturatedFragment::SaturatedFragment(
    const KeySet &_set,
    KeySetsInUse_t &_container,
    HydrogenPool &_hydrogens,
    const enum HydrogenTreatment _treatment,
    const enum HydrogenSaturation _saturation) :
  container(_container),
  set(_set),
  hydrogens(_hydrogens),
  FullMolecule(set),
  treatment(_treatment),
  saturation(_saturation)
{
  // add to in-use container
  ASSERT( container.find(set) == container.end(),
      "SaturatedFragment::SaturatedFragment() - the set "
      +toString(set)+" is already marked as in use.");
  container.insert(set);
  // prepare saturation hydrogens
  saturate();
}
SaturatedFragment::~SaturatedFragment()
{
  // release all saturation hydrogens if present
  for (KeySet::iterator iter = SaturationHydrogens.begin();
      !SaturationHydrogens.empty();
      iter = SaturationHydrogens.begin()) {
    hydrogens.releaseHydrogen(*iter);
    SaturationHydrogens.erase(iter);
  }
  // remove ourselves from in-use container
  KeySetsInUse_t::iterator iter = container.find(set);
  ASSERT( container.find(set) != container.end(),
      "SaturatedFragment::SaturatedFragment() - the set "
      +toString(set)+" is not marked as in use.");
  container.erase(iter);
}
void SaturatedFragment::saturate()
{
  // gather all atoms in a vector
  std::vector atoms;
  for (KeySet::const_iterator iter = FullMolecule.begin();
      iter != FullMolecule.end();
      ++iter) {
    atom * const Walker = World::getInstance().getAtom(AtomById(*iter));
    ASSERT( Walker != NULL,
        "SaturatedFragment::OutputConfig() - id "
        +toString(*iter)+" is unknown to World.");
    atoms.push_back(Walker);
  }
//  bool LonelyFlag = false;
  for (std::vector::const_iterator iter = atoms.begin();
      iter != atoms.end();
      ++iter) {
    atom * const Walker = *iter;
    // go through all bonds
    const BondList& ListOfBonds = Walker->getListOfBonds();
    for (BondList::const_iterator BondRunner = ListOfBonds.begin();
        BondRunner != ListOfBonds.end();
        ++BondRunner) {
      atom * const OtherWalker = (*BondRunner)->GetOtherAtom(Walker);
      // if in set
      if (set.find(OtherWalker->getId()) != set.end()) {
        LOG(4, "DEBUG: Walker " << *Walker << " is bound to " << *OtherWalker << ".");
//        if (OtherWalker->getId() > Walker->getId()) { // add bond (Nr check is for adding only one of both variants: ab, ba)
////          std::stringstream output;
////            output << "ACCEPT: Adding Bond: "
//          output << Leaf->AddBond((*iter), OtherWalker, (*BondRunner)->getDegree());
////            LOG(3, output.str());
//          //NumBonds[(*iter)->getNr()]++;
//        } else {
////            LOG(3, "REJECY: Not adding bond, labels in wrong order.");
//        }
//        LonelyFlag = false;
      } else {
        LOG(4, "DEBUG: Walker " << *Walker << " is bound to "
            << *OtherWalker << ", who is not in this fragment molecule.");
        if (saturation == DoSaturate) {
//          LOG(3, "ACCEPT: Adding Hydrogen to " << (*iter)->Name << " and a bond in between.");
          if (!AddHydrogenReplacementAtom(
              (*BondRunner),
              Walker,
              OtherWalker,
              World::getInstance().getConfig()->IsAngstroem == 1))
            exit(1);
        }
//        } else if ((treatment == ExcludeHydrogen) && (OtherWalker->getElementNo() == (atomicNumber_t)1)) {
//          // just copy the atom if it's a hydrogen
//          atom * const OtherWalker = Leaf->AddCopyAtom(OtherWalker);
//          Leaf->AddBond((*iter), OtherWalker, (*BondRunner)->getDegree());
//        }
        //NumBonds[(*iter)->getNr()] += Binder->getDegree();
      }
    }
  }
}
bool SaturatedFragment::OutputConfig(
    std::ostream &out,
    const ParserTypes _type) const
{
  // gather all atoms in a vector
  std::vector atoms;
  for (KeySet::const_iterator iter = FullMolecule.begin();
      iter != FullMolecule.end();
      ++iter) {
    atom * const Walker = World::getInstance().getAtom(AtomById(*iter));
    ASSERT( Walker != NULL,
        "SaturatedFragment::OutputConfig() - id "
        +toString(*iter)+" is unknown to World.");
    atoms.push_back(Walker);
  }
  // TODO: ScanForPeriodicCorrection() is missing so far!
  // note however that this is not straight-forward for the following reasons:
  // - we do not copy all atoms anymore, hence we are forced to shift the real
  //   atoms hither and back again
  // - we use a long-range potential that supports periodic boundary conditions.
  //   Hence, there we would like the original configuration (split through the
  //   the periodic boundaries). Otherwise, we would have to shift (and probably
  //   interpolate) the potential with OBCs applying.
  // list atoms in fragment for debugging
  {
    std::stringstream output;
    output << "INFO: Contained atoms: ";
    for (std::vector::const_iterator iter = atoms.begin();
        iter != atoms.end(); ++iter) {
      output << (*iter)->getName() << " ";
    }
    LOG(3, output.str());
  }
  // store to stream via FragmentParser
  const bool intermediateResult =
      FormatParserStorage::getInstance().save(
          out,
          FormatParserStorage::getInstance().getSuffixFromType(_type),
          atoms);
  return intermediateResult;
}
atom * const SaturatedFragment::getHydrogenReplacement(atom * const replacement)
{
  atom * const _atom = hydrogens.leaseHydrogen();    // new atom
  _atom->setAtomicVelocity(replacement->getAtomicVelocity()); // copy velocity
  _atom->setFixedIon(replacement->getFixedIon());
  // if we replace hydrogen, we mark it as our father, otherwise we are just an added hydrogen with no father
  _atom->father = replacement;
  SaturationHydrogens.insert(_atom->getId());
  return _atom;
}
bool SaturatedFragment::AddHydrogenReplacementAtom(
    bond::ptr TopBond,
    atom *Origin,
    atom *Replacement,
    bool IsAngstroem)
{
//  Info info(__func__);
  bool AllWentWell = true;    // flag gathering the boolean return value of molecule::AddAtom and other functions, as return value on exit
  double bondlength;  // bond length of the bond to be replaced/cut
  double bondangle;  // bond angle of the bond to be replaced/cut
  double BondRescale;   // rescale value for the hydrogen bond length
  bond::ptr FirstBond;
  bond::ptr SecondBond; // Other bonds in double bond case to determine "other" plane
  atom *FirstOtherAtom = NULL, *SecondOtherAtom = NULL, *ThirdOtherAtom = NULL; // pointer to hydrogen atoms to be added
  double b,l,d,f,g, alpha, factors[NDIM];    // hold temporary values in triple bond case for coordination determination
  Vector Orthovector1, Orthovector2;  // temporary vectors in coordination construction
  Vector InBondvector;    // vector in direction of *Bond
  const RealSpaceMatrix &matrix =  World::getInstance().getDomain().getM();
  bond::ptr Binder;
  // create vector in direction of bond
  InBondvector = Replacement->getPosition() - Origin->getPosition();
  bondlength = InBondvector.Norm();
   // is greater than typical bond distance? Then we have to correct periodically
   // the problem is not the H being out of the box, but InBondvector have the wrong direction
   // due to Replacement or Origin being on the wrong side!
  const BondGraph * const BG = World::getInstance().getBondGraph();
  const range MinMaxBondDistance(
      BG->getMinMaxDistance(Origin,Replacement));
  if (!MinMaxBondDistance.isInRange(bondlength)) {
//    LOG(4, "InBondvector is: " << InBondvector << ".");
    Orthovector1.Zero();
    for (int i=NDIM;i--;) {
      l = Replacement->at(i) - Origin->at(i);
      if (fabs(l) > MinMaxBondDistance.last) { // is component greater than bond distance (check against min not useful here)
        Orthovector1[i] = (l < 0) ? -1. : +1.;
      } // (signs are correct, was tested!)
    }
    Orthovector1 *= matrix;
    InBondvector -= Orthovector1; // subtract just the additional translation
    bondlength = InBondvector.Norm();
//    LOG(4, "INFO: Corrected InBondvector is now: " << InBondvector << ".");
  } // periodic correction finished
  InBondvector.Normalize();
  // get typical bond length and store as scale factor for later
  ASSERT(Origin->getType() != NULL,
      "SaturatedFragment::AddHydrogenReplacementAtom() - element of Origin is not given.");
  BondRescale = Origin->getType()->getHBondDistance(TopBond->getDegree()-1);
  if (BondRescale == -1) {
    ELOG(1, "There is no typical hydrogen bond distance in replacing bond (" << Origin->getName() << "<->" << Replacement->getName() << ") of degree " << TopBond->getDegree() << "!");
    return false;
    BondRescale = bondlength;
  } else {
    if (!IsAngstroem)
      BondRescale /= (1.*AtomicLengthToAngstroem);
  }
  // discern single, double and triple bonds
  switch(TopBond->getDegree()) {
    case 1:
      // check whether replacement has been an excluded hydrogen
      if (Replacement->getType()->getAtomicNumber() == HydrogenPool::HYDROGEN) { // neither rescale nor replace if it's already hydrogen
        FirstOtherAtom = Replacement;
        BondRescale = bondlength;
      } else {
        FirstOtherAtom = getHydrogenReplacement(Replacement);
        InBondvector *= BondRescale;   // rescale the distance vector to Hydrogen bond length
        FirstOtherAtom->setPosition(Origin->getPosition() + InBondvector); // set coordination to origin and add distance vector to replacement atom
      }
      FullMolecule.insert(FirstOtherAtom->getId());
//      LOG(4, "INFO: Added " << *FirstOtherAtom << " at: " << FirstOtherAtom->x << ".");
      break;
    case 2:
      {
        // determine two other bonds (warning if there are more than two other) plus valence sanity check
        const BondList& ListOfBonds = Origin->getListOfBonds();
        for (BondList::const_iterator Runner = ListOfBonds.begin();
            Runner != ListOfBonds.end();
            ++Runner) {
          if ((*Runner) != TopBond) {
            if (FirstBond == NULL) {
              FirstBond = (*Runner);
              FirstOtherAtom = (*Runner)->GetOtherAtom(Origin);
            } else if (SecondBond == NULL) {
              SecondBond = (*Runner);
              SecondOtherAtom = (*Runner)->GetOtherAtom(Origin);
            } else {
              ELOG(2, "Detected more than four bonds for atom " << Origin->getName());
            }
          }
        }
      }
      if (SecondOtherAtom == NULL) {  // then we have an atom with valence four, but only 3 bonds: one to replace and one which is TopBond (third is FirstBond)
        SecondBond = TopBond;
        SecondOtherAtom = Replacement;
      }
      if (FirstOtherAtom != NULL) { // then we just have this double bond and the plane does not matter at all
//        LOG(3, "Regarding the double bond (" << Origin->Name << "<->" << Replacement->Name << ") to be constructed: Taking " << FirstOtherAtom->Name << " and " << SecondOtherAtom->Name << " along with " << Origin->Name << " to determine orthogonal plane.");
        // determine the plane of these two with the *origin
        try {
          Orthovector1 = Plane(Origin->getPosition(), FirstOtherAtom->getPosition(), SecondOtherAtom->getPosition()).getNormal();
        }
        catch(LinearDependenceException &excp){
          LOG(0, boost::diagnostic_information(excp));
          // TODO: figure out what to do with the Orthovector in this case
          AllWentWell = false;
        }
      } else {
        Orthovector1.GetOneNormalVector(InBondvector);
      }
      //LOG(3, "INFO: Orthovector1: " << Orthovector1 << ".");
      // orthogonal vector and bond vector between origin and replacement form the new plane
      Orthovector1.MakeNormalTo(InBondvector);
      Orthovector1.Normalize();
      //LOG(3, "ReScaleCheck: " << Orthovector1.Norm() << " and " << InBondvector.Norm() << ".");
      // create the two Hydrogens ...
      FirstOtherAtom = getHydrogenReplacement(Replacement);
      SecondOtherAtom = getHydrogenReplacement(Replacement);
      FullMolecule.insert(FirstOtherAtom->getId());
      FullMolecule.insert(SecondOtherAtom->getId());
      bondangle = Origin->getType()->getHBondAngle(1);
      if (bondangle == -1) {
        ELOG(1, "There is no typical hydrogen bond angle in replacing bond (" << Origin->getName() << "<->" << Replacement->getName() << ") of degree " << TopBond->getDegree() << "!");
        return false;
        bondangle = 0;
      }
      bondangle *= M_PI/180./2.;
//      LOG(3, "INFO: ReScaleCheck: InBondvector " << InBondvector << ", " << Orthovector1 << ".");
//      LOG(3, "Half the bond angle is " << bondangle << ", sin and cos of it: " << sin(bondangle) << ", " << cos(bondangle));
      FirstOtherAtom->Zero();
      SecondOtherAtom->Zero();
      for(int i=NDIM;i--;) { // rotate by half the bond angle in both directions (InBondvector is bondangle = 0 direction)
        FirstOtherAtom->set(i, InBondvector[i] * cos(bondangle) + Orthovector1[i] * (sin(bondangle)));
        SecondOtherAtom->set(i, InBondvector[i] * cos(bondangle) + Orthovector1[i] * (-sin(bondangle)));
      }
      FirstOtherAtom->Scale(BondRescale);  // rescale by correct BondDistance
      SecondOtherAtom->Scale(BondRescale);
      //LOG(3, "ReScaleCheck: " << FirstOtherAtom->x.Norm() << " and " << SecondOtherAtom->x.Norm() << ".");
      *FirstOtherAtom += Origin->getPosition();
      *SecondOtherAtom += Origin->getPosition();
      // ... and add to molecule
//      LOG(4, "INFO: Added " << *FirstOtherAtom << " at: " << FirstOtherAtom->x << ".");
//      LOG(4, "INFO: Added " << *SecondOtherAtom << " at: " << SecondOtherAtom->x << ".");
      break;
    case 3:
      // take the "usual" tetraoidal angle and add the three Hydrogen in direction of the bond (height of the tetraoid)
      FirstOtherAtom = getHydrogenReplacement(Replacement);
      SecondOtherAtom = getHydrogenReplacement(Replacement);
      ThirdOtherAtom = getHydrogenReplacement(Replacement);
      FullMolecule.insert(FirstOtherAtom->getId());
      FullMolecule.insert(SecondOtherAtom->getId());
      FullMolecule.insert(ThirdOtherAtom->getId());
      // we need to vectors orthonormal the InBondvector
      AllWentWell = AllWentWell && Orthovector1.GetOneNormalVector(InBondvector);
//      LOG(3, "INFO: Orthovector1: " << Orthovector1 << ".");
      try{
        Orthovector2 = Plane(InBondvector, Orthovector1,0).getNormal();
      }
      catch(LinearDependenceException &excp) {
        LOG(0, boost::diagnostic_information(excp));
        AllWentWell = false;
      }
//      LOG(3, "INFO: Orthovector2: " << Orthovector2 << ".")
      // create correct coordination for the three atoms
      alpha = (Origin->getType()->getHBondAngle(2))/180.*M_PI/2.;  // retrieve triple bond angle from database
      l = BondRescale;        // desired bond length
      b = 2.*l*sin(alpha);    // base length of isosceles triangle
      d = l*sqrt(cos(alpha)*cos(alpha) - sin(alpha)*sin(alpha)/3.);   // length for InBondvector
      f = b/sqrt(3.);   // length for Orthvector1
      g = b/2.;         // length for Orthvector2
//      LOG(3, "Bond length and half-angle: " << l << ", " << alpha << "\t (b,d,f,g) = " << b << ", " << d << ", " << f << ", " << g << ", ");
//      LOG(3, "The three Bond lengths: " << sqrt(d*d+f*f) << ", " << sqrt(d*d+(-0.5*f)*(-0.5*f)+g*g) << ", "  << sqrt(d*d+(-0.5*f)*(-0.5*f)+g*g));
      factors[0] = d;
      factors[1] = f;
      factors[2] = 0.;
      FirstOtherAtom->LinearCombinationOfVectors(InBondvector, Orthovector1, Orthovector2, factors);
      factors[1] = -0.5*f;
      factors[2] = g;
      SecondOtherAtom->LinearCombinationOfVectors(InBondvector, Orthovector1, Orthovector2, factors);
      factors[2] = -g;
      ThirdOtherAtom->LinearCombinationOfVectors(InBondvector, Orthovector1, Orthovector2, factors);
      // rescale each to correct BondDistance
//      FirstOtherAtom->x.Scale(&BondRescale);
//      SecondOtherAtom->x.Scale(&BondRescale);
//      ThirdOtherAtom->x.Scale(&BondRescale);
      // and relative to *origin atom
      *FirstOtherAtom += Origin->getPosition();
      *SecondOtherAtom += Origin->getPosition();
      *ThirdOtherAtom += Origin->getPosition();
      // ... and add to molecule
//      LOG(4, "INFO: Added " << *FirstOtherAtom << " at: " << FirstOtherAtom->x << ".");
//      LOG(4, "INFO: Added " << *SecondOtherAtom << " at: " << SecondOtherAtom->x << ".");
//      LOG(4, "INFO: Added " << *ThirdOtherAtom << " at: " << ThirdOtherAtom->x << ".");
      break;
    default:
      ELOG(1, "BondDegree does not state single, double or triple bond!");
      AllWentWell = false;
      break;
  }
  return AllWentWell;
};