/*
 * coot-utils/reduce.hh
 *
 * Copyright 2016 by Medical Research Council
 * Author: Paul Emsley
 *
 * This file is part of Coot
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation; either version 3 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
 * Lesser General Public License for more details.
 *
 * You should have received a copies of the GNU General Public License and
 * the GNU Lesser General Public License along with this program; if not,
 * write to the Free Software Foundation, Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA, 02110-1301, USA.
 * See http://www.gnu.org/licenses/
 *
 */

#ifndef REDUCE_HH
#define REDUCE_HH

#include "clipper/core/coords.h"
#include "mmdb2/mmdb_manager.h"

#include "geometry/protein-geometry.hh" // needed to get H-bond types when checking HIS protonatino
                                        // (and spin searching OHs).

namespace coot {

   class reduce {

      class torsion_info_t {
      public:
	 torsion_info_t(const std::string &at_name_1_in, // CA 
			const std::string &at_name_2_in, // CB
			const std::string &at_name_3_in, // CG1 (i.e. other way round)
			double bond_length_in,
			double angle_deg_in,
			double torsion_deg_in) {
	    bond_length = bond_length_in;
	    at_name_1 = at_name_1_in;
	    at_name_2 = at_name_2_in;
	    at_name_3 = at_name_3_in;
	    angle_deg = angle_deg_in;
	    torsion_deg = torsion_deg_in;
	 }
	 // can throw a std::runtime_error
	 torsion_info_t(const std::string &first_neighb,
			const std::vector<std::string> &second_neighb_vec,
			const std::map<std::string, std::vector<std::string> > &third_neighbour_map,
			double bl,
			double angle_in,
			double torsion_in) {
	    if (second_neighb_vec.size() > 0) {
	       std::string second = second_neighb_vec[0];
	       std::map<std::string, std::vector<std::string> >::const_iterator it;
	       it = third_neighbour_map.find(second);
	       if (it != third_neighbour_map.end()) {
		  std::vector<std::string> thirds = it->second;
		  if (thirds.size() > 0) {
		     const std::string &third = thirds[0];
		     at_name_1 = third;
		     at_name_2 = second;
		     at_name_3 = first_neighb;
		     angle_deg = angle_in;
		     torsion_deg = torsion_in;
		     bond_length = bl;
		  }
	       }
	    }
	 }
	 std::string at_name_1; // CA (not sure if this atom ordering is sensible)
	 std::string at_name_2; // CB
	 std::string at_name_3; // CG1
	 double bond_length;
	 double angle_deg;
	 double torsion_deg;
      };

      class atom_with_attached_Hs {
      public:
	 enum hydrogen_t { METHYL, HYDROXYL, SULFHYDRYL };

	 atom_with_attached_Hs(mmdb::Atom *at_in, hydrogen_t type_in,
			       const std::vector<mmdb::Atom *> &hydrogens_in) :
	    at(at_in), type(type_in), hydrogens(hydrogens_in) {}
	 mmdb::Atom* at;
	 hydrogen_t type;
	 std::vector<mmdb::Atom *> hydrogens;
      };

      class atoms_with_spinnable_Hs {
      public:

	 std::map<std::string, std::vector<atom_with_attached_Hs> > typed_atoms;
	 std::vector<std::vector<atom_with_attached_Hs> > cliques;

      public:
	 atoms_with_spinnable_Hs() { typed_atoms[""].reserve(200); }

	 void add(mmdb::Atom* at, atom_with_attached_Hs::hydrogen_t type,
		  const std::vector<mmdb::Atom *> &attahed_hydrogen_atoms);
	 void add(mmdb::Atom* at, atom_with_attached_Hs::hydrogen_t type,
		  mmdb::Atom *attahed_hydrogen_atom);
	 void cliquize();
	 std::vector<std::vector<atom_with_attached_Hs> > get_cliques() const {
	    return cliques;
	 }
	 void resolve_clashing_cliques() {
	    for (std::size_t i=0; i<cliques.size(); i++)
	       resolve_clashing_clique(cliques[i]);
	 }
	 // Move the H atoms of the clique if there are classes.
	 // (clashes may still remain after having done so, of course).
	 void resolve_clashing_clique(const std::vector<atom_with_attached_Hs> &clique);
      };

      clipper::Coord_orth position_by_bond_length_angle_torsion(mmdb::Atom *at_1,  // CA
								mmdb::Atom *at_2,  // CB
								mmdb::Atom *at_3,  // CG
								double bl,
								double angle_rad,
								double torsion_rad) const;
      clipper::Coord_orth position_by_bisection(mmdb::Atom *at_1, // for Hs on PHE etc
						mmdb::Atom *at_2,
						mmdb::Atom *at_3,
						double bl) const;
      std::pair<clipper::Coord_orth, clipper::Coord_orth>
      position_pair_by_bisection(mmdb::Atom *at_1,  // CA
				 mmdb::Atom *at_2,  // CB
				 mmdb::Atom *at_3,  // CG
				 double bl,
				 double angle_separation // how far apart are the two H atoms?
				 ) const;
      clipper::Coord_orth position_by_tetrahedron(mmdb::Atom *at_central,
						  mmdb::Atom *at_n_1,
						  mmdb::Atom *at_n_2,
						  mmdb::Atom *at_n_3,
						  double bl) const;
      void add_methyl_Hs(const std::string &at_name_1,
			 const std::string &at_name_2,
			 const std::string &at_name_3,
			 torsion_info_t torsion_1,
			 mmdb::Residue *residue_p);
      void add_methyl_Hs(const std::string &at_name_1,
			 const std::string &at_name_2,
			 const std::string &at_name_3,
			 torsion_info_t torsion_1, torsion_info_t torsion_2,
			 mmdb::Residue *residue_p);
      void add_2_sp3_hydrogens(const std::string &H_at_name_1,
			       const std::string &H_at_name_2,
			       const std::string &at_name_1,
			       const std::string &at_name_2,
			       const std::string &at_name_3,
			       double bond_length,
			       double angle_between_Hs, // in degrees
			       mmdb::Residue *residue_p,
			       bool choose_only_farthest_position=false);
      void add_2_sp3_hydrogens(const std::string &H_at_name_1,
			       const std::string &H_at_name_2,
			       const std::string &first_neighb,
			       const std::vector<std::string> &second_neighb_vec,
			       double bond_length,
			       double angle_between_Hs, // in degrees
			       mmdb::Residue *residue_p,
			       bool choose_only_farthest_position=false);
      void add_tetrahedral_hydrogen(const std::string &H_at_name,
				    const std::string &central_name,
				    const std::string &neighb_at_name_1,
				    const std::string &neighb_at_name_2,
				    const std::string &neighb_at_name_3,
				    double bond_length,
				    mmdb::Residue *residue_p);
      void add_tetrahedral_hydrogen(const std::string &H_at_name,
				    const std::string &first_neighb,
				    const std::vector<std::string> &second_neighb_vec,
				    double bl, mmdb::Residue *residue_p);
      
      void add_aromatic_hydrogen(const std::string &H_at_name,
				 const std::string &neighb_at_name_1,
				 const std::string &neighb_at_name_2, // add to this
				 const std::string &neighb_at_name_3,
				 double bl, mmdb::Residue *residue_p);
      void add_amino_hydrogens(const std::string &H_at_name_1,
			       const std::string &H_at_name_2,
			       const std::string &at_name_1,
			       const std::string &at_name_2,
			       const std::string &at_name_3,
			       double bl_amino, // angle is 120, torsions are 180 and 0
			       mmdb::Residue *residue_p);
      void add_amino_hydrogens(const std::string &H_at_name_1,
			       const std::string &H_at_name_2,
			       const std::string &first_neighb,
			       const std::vector<std::string> &second_neighb_vec,
			       const std::map<std::string, std::vector<std::string> > &third_neighb_vec,
			       double bl_amino,
			       mmdb::Residue *residue_p);

      void add_guanidinium_hydrogens(mmdb::Residue *residue_p);
      void add_trp_indole_hydrogens(mmdb::Residue *residue_p, double bl_HN, double bl_arom);
      void add_trp_indole_hydrogen(const std::string &H_name,
				   const std::string &at_name_1,
				   const std::string &at_name_2,
				   const std::string &at_name_3,
				   double bl,
				   mmdb::Residue *residue_p);

      atoms_with_spinnable_Hs spinables;

      // this will need a spin-search
      void add_OH_H(const std::string &H_name,
		    const std::string &at_name_1,
		    const std::string &at_name_2,
		    const std::string &at_name_3,
		    double bl,
		    double angle,      // deg
		    double tor_inital, // deg
		    mmdb::Residue *residue_p);
      // this will need a spin-search
      std::vector<mmdb::Atom *> add_SH_H(const std::string &H_name,
					 const std::string &at_name_1,
					 const std::string &at_name_2,
					 const std::string &at_name_3,
					 double bl,
					 double angle,      // deg
					 double tor_inital, // deg
					 mmdb::Residue *residue_p);
      // both of the above wrap this:
      std::vector<mmdb::Atom *> add_xH_H(const std::string &H_name,
					 const std::string &at_name_1,
					 const std::string &at_name_2,
					 const std::string &at_name_3,
					 double bl,
					 double angle,      // deg
					 double tor_inital, // deg
					 mmdb::Residue *residue_p);

      void add_OH_H(const std::string &H_at_name,
		    const std::string &first_neighb,
		    const std::vector<std::string> &second_neighb_vec,
		    const std::map<std::string, std::vector<std::string> > &third_neighb_vec,
		    double bond_length,
		    double ang_deg,
		    double torsion_deg,
		    mmdb::Residue *residue_p);
      
      
      void add_his_ring_C_Hs(mmdb::Residue *residue_p, double bl_arom);
      std::vector<mmdb::Atom *> add_his_ring_H(const std::string &H_name,
					       const std::string &at_name_1,
					       const std::string &at_name_2,
					       const std::string &at_name_3,
					       double bl,
					       mmdb::Residue *residue_p);

      void add_his_ring_H(const std::string &H_at_name,
			  const std::string &first_neigh,
			  const std::vector<std::string> &second_neighb_vec,
			  double bl,
			  mmdb::Residue *residue_p);

      void add_aromatic_hydrogen(const std::string &H_at_name,
				 const std::string &first_neigh,
				 const std::vector<std::string> &second_neighb_vec,
				 double bl,
				 mmdb::Residue *residue_p);

      mmdb::Manager *mol;
      int imol; // for dictionary lookups.
      protein_geometry *geom_p;
      void add_riding_hydrogens(double bl_aliph,
                                double bl_arom,
                                double bl_amino,
                                double bl_oh,
                                double bl_sh); // non-spin-search
      bool add_riding_hydrogens(mmdb::Residue *residue_p, mmdb::Residue *residue_prev_p,
                                double bl_aliph,
                                double bl_arom,
                                double bl_amino,
                                double bl_oh,
                                double bl_sh);
      void add_main_chain_hydrogens(mmdb::Residue *residue_p, mmdb::Residue *residue_prev_p,
				    bool is_gly=false);
      void add_main_chain_HA(mmdb::Residue *residue_p);
      void add_main_chain_H(mmdb::Residue *residue_p, mmdb::Residue *residue_prev_p);
      // like above but for ligands (both second neighbours come from ligand residue)
      void add_amino_single_H(const std::string &H_at_name,
			      const std::string &first_neighb,
			      const std::vector<std::string> &second_neighb_vec,
			      double bl,
			      mmdb::Residue *residue_p);
      // add H to second atom by bisection
      void add_amino_single_H(const std::string &H_at_name,
			      const std::string &at_name_1,
			      const std::string &at_name_2,
			      const std::string &at_name_3,
			      double bl,
			      mmdb::Residue *residue_p);

      mmdb::Atom *add_hydrogen_atom(std::string atom_name, clipper::Coord_orth &pos,
				    mmdb::realtype bf,
				    const std::string &altconf,
				    mmdb::Residue *residue_p);
      // score hypotheses and convert to the best scoring one.
      void find_best_his_protonation_orientation(mmdb::Residue *residue_p);
      void delete_atom_by_name(const std::string &at_name, mmdb::Residue *residue_p);
      void hydrogen_placement_by_dictionary(mmdb::Residue *residue_p,
                                            double bl_aliph,
                                            double bl_arom,
                                            double bl_amino,
                                            double bl_oh,
                                            double bl_sh);
      void hydrogen_placement_by_dictionary(const dictionary_residue_restraints_t &rest,
					    mmdb::Residue *residue_p,
                                            double bl_aliph,
                                            double bl_arom,
                                            double bl_amino,
                                            double bl_oh,
                                            double bl_sh);
      // return a list of names of placed atoms
      std::vector<std::string>
      place_hydrogen_by_connected_atom_energy_type(unsigned int iat,
						   unsigned int iat_neighb,
						   const dictionary_residue_restraints_t &rest,
						   mmdb::Residue *residue_p,
                                                   double bl_aliph,
                                                   double bl_arom,
                                                   double bl_amino,
                                                   double bl_oh,
                                                   double bl_sh);
      // which calls
      std::vector<std::string>
      place_hydrogen_by_connected_atom_energy_type(const std::string &energy_type,
						   unsigned int iat,
						   unsigned int iat_neighb,
						   const dictionary_residue_restraints_t &rest,
						   mmdb::Residue *residue_p,
                                                   double bl_aliph,
                                                   double bl_arom,
                                                   double bl_amino,
                                                   double bl_oh,
                                                   double bl_sh);
      void place_hydrogen_by_connected_2nd_neighbours(unsigned int iat,
						      unsigned int iat_neighb,
						      const dictionary_residue_restraints_t &rest,
						      mmdb::Residue *residue_p);

      // atoms that are connected to the second neighbour that aren't the first neighbour.
      //
      std::map<std::string, std::vector<std::string> > 
      third_neighbour_map(const std::string &first_neighb,
			  const std::vector<std::string> &second_neighb_vec,
			  const dictionary_residue_restraints_t &rest) const;

      std::string get_other_H_name(const std::string &first_neighb,
				   const std::string &H_at_name,
				   const dictionary_residue_restraints_t &dict) const;
      std::vector<std::string> get_other_H_names(const std::string &first_neighb,
						 const std::string &H_at_name,
						 const dictionary_residue_restraints_t &dict) const;

      bool is_ss_bonded(mmdb::Residue *residue_p) const;
      // No SSBOND interogation yet, so...
      bool hack_ss_bond_test(mmdb::Residue *residue_p, mmdb::Model *model_p) const;

      // as a rule, we don't want to add hydrogen to atoms that are linked
      //
      bool is_linked(const std::string &atom_name, mmdb::Residue *residue_p) const;

      bool verbose_output;

      void switch_his_protonation(mmdb::Residue *residue_p, mmdb::Atom *current_H_atom, double bl_arom);

     void setup_default_bond_lengths(double *bl_aliph_p,
				     double *bl_arom_p,
				     double *bl_amino_p,
				     double *bl_oh_p,
				     double *bl_sh_p,
				     bool go_nuclear);

   public:
      reduce(mmdb::Manager *mol_in, int imol_in) {
	 mol = mol_in;
	 imol = imol_in;
         verbose_output = true;
         geom_p = 0;
      }
      void add_hydrogen_atoms(bool go_nuclear); // changes mol

     void add_riding_hydrogens_to_residue(mmdb::Residue *residue_p,
					  mmdb::Residue *residue_prev_p, // possibly undefined
					  double bl_aliph,
					  double bl_arom,
					  double bl_amino,
					  double bl_oh,
					  double bl_sh);
     void add_hydrogens_to_residue(mmdb::Residue *residue_p,
				   mmdb::Residue *residue_prev_p, // possibly undefined
				   bool go_nuclear_flag);
      void delete_hydrogen_atoms();
      void add_geometry(protein_geometry *geom_p_in) { geom_p = geom_p_in; }
      // change HE2 to HD1 and vice versa
      void switch_his_protonation(mmdb::Residue *residue_p, mmdb::Atom *current_H_atom);
      void set_verbose_output(bool flag) { verbose_output = flag; }
      
   };

}

#endif // REDUCE_HH
