/*
 * Mol2 atom typing
 *
 * (c) Schrodinger, Inc.
 */

#include "Mol2Typing.h"
#include "AtomInfo.h"
#include "ObjectMolecule.h"

/**
 * atm: Atom index of a carbon atom with geom=3
 *
 * Return: True if atm has 3 neighbors which are all nitrogens with geom=3
 */
static bool isGuanidiniumCarbon(ObjectMolecule * obj, int atm) {
  int neighbor_count = 0;
  int charge = 0;

  for (auto const& item : AtomNeighbors(obj, atm)) {
    AtomInfoType const* neighbor = obj->AtomInfo.data() + item.atm;
    if (neighbor->protons != cAN_N || neighbor->geom != 3)
      return false;
    ++neighbor_count;
    charge += neighbor->formalCharge;
  }

  return neighbor_count == 3 && charge > 0;
}

/**
 * TODO: bond order 4 seems to be no guarantee for a ring, which is
 * required for aromaticity
 *
 * Return: always false, since PyMOL doesn't know enough about aromaticity
 */
static bool isAromaticAtom(ObjectMolecule * obj, int atm) {
#if 0
  for (auto const& neighbor : AtomNeighbors(obj, atm)) {
    if (obj->Bond[neighbor.bond].order == 4)
      return true;
  }
#endif

  return false;
}

/**
 * atm: Atom index of an oxygen atom
 *
 * Return: True if atom is part of a carboxylate or phosphate group
 */
static bool isCarboxylateOrPhosphateOxygen(ObjectMolecule * obj, int atm) {
  int o_count = 0, other_count = 0;

  auto const neighbors = AtomNeighbors(obj, atm);

  // must have only one neighbor
  if (neighbors.size() != 1)
    return false;

  // get that one neighbor as center of the acidic group
  atm = neighbors[0].atm;

  // check center atom
  AtomInfoType * ai = obj->AtomInfo + atm;
  if (!(ai->protons == cAN_C && ai->geom == 3) &&
      !(ai->protons == cAN_P && ai->geom == 4))
    return false;

  // iterate over neighbors of center atom
  for (auto const& item : AtomNeighbors(obj, atm)) {
    AtomInfoType const* neighbor = obj->AtomInfo.data() + item.atm;
    if (neighbor->protons == cAN_O)
      ++o_count;
    else
      ++other_count;
  }

  // carboxylate
  if (ai->protons == cAN_C)
    return (o_count == 2 && other_count == 1);

  // phosphate
  return (o_count == 4 && other_count == 0);
}

/**
 * atm: Atom index of a sulfur atom
 *
 * Return: Number of bound Oxygens if bound to two non-Oxygen atoms. Otherwise 0.
 */
static int sulfurCountOxygenNeighbors(ObjectMolecule * obj, int atm) {
  int o_count = 0, other_count = 0;

  for (auto const& item : AtomNeighbors(obj, atm)) {
    AtomInfoType const* neighbor = obj->AtomInfo.data() + item.atm;
    if (neighbor->protons == cAN_O)
      ++o_count;
    else
      ++other_count;
  }

  return (other_count == 2) ? o_count : 0;
}

/**
 * Get the Tripos Mol2 atom type
 *
 * Pre-condition: ObjectMoleculeVerifyChemistry
 */
const char * getMOL2Type(ObjectMolecule * obj, int atm) {
  auto G = obj->G;
  auto ai = obj->AtomInfo + atm;

  switch (ai->protons) {
    case cAN_C:
      switch (ai->geom) {
        case cAtomInfoLinear:
          return "C.1";
        case cAtomInfoPlanar:
          if (isAromaticAtom(obj, atm))
            return "C.ar";
          if (isGuanidiniumCarbon(obj, atm))
            return "C.cat";
          return "C.2";
        case cAtomInfoTetrahedral:
          return "C.3";
      }
      break;

    case cAN_N:
      switch (ai->geom) {
        case cAtomInfoLinear:
          return "N.1";
        case cAtomInfoPlanar:
          if ((ai->flags & cAtomFlag_polymer)
              && ai->name == G->lex_const.N)
            return "N.am";
          if (isAromaticAtom(obj, atm))
            return "N.ar";
          if (ai->valence == 2 && ai->formalCharge == 0)
            return "N.2";
          return "N.pl3";
        case cAtomInfoTetrahedral:
          return (ai->formalCharge == 1) ? "N.4" : "N.3";
      }
      break;

    case cAN_O:
      if (isCarboxylateOrPhosphateOxygen(obj, atm))
        return "O.co2";
      switch (ai->geom) {
        case cAtomInfoPlanar:       return "O.2";
        case cAtomInfoTetrahedral:  return "O.3";
      }
      break;

    case cAN_S:
      switch (sulfurCountOxygenNeighbors(obj, atm)) {
        case 1: return "S.o";
        case 2: return "S.o2";
      }
      switch (ai->geom) {
        case cAtomInfoPlanar:       return "S.2";
        case cAtomInfoTetrahedral:  return "S.3";
      }
      break;

    case cAN_P:
      if (ai->geom == 4)
        return "P.3";
      break;

    case cAN_Cr:
      if (ai->geom == 4)
        return "Cr.th";
      return "Cr.oh";

    case cAN_Co:
      return "Co.oh";
  }

  if (ai->protons >= 0 && ai->protons < ElementTableSize)
    return ElementTable[ai->protons].symbol;

  return "Du";
}
