#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
#pragma GCC diagnostic ignored "-Wpedantic"
#pragma GCC diagnostic ignored "-Wlong-long"

#include <openbabel/generic.h>
#include <openbabel/obconversion.h>
#include <openbabel/obmolecformat.h>
#include <openbabel/obiter.h>
#include <openbabel/elements.h>
#include <openbabel/atom.h>
#include <openbabel/bond.h>
#include <openbabel/mol.h>
#include <openbabel/math/matrix3x3.h>
#include <openbabel/math/vector3.h>

#include <visu_tools.h>
#include <visu_basic.h>
#include <renderingMethods/elementAtomic.h>
#include <coreTools/toolColor.h>
#include <coreTools/toolFileFormat.h>
#include <coreTools/toolMatrix.h>
#include <visu_elements.h>
#include <visu_dataloadable.h>
#include <visu_dataatomic.h>
#include <visu_pairset.h>
#include <dumpModules/fileDump.h>
#include <extraFunctions/fragProp.h>

#pragma GCC diagnostic pop

using namespace OpenBabel;

extern "C"
{
  gboolean obloaderInit();
  const char* obloaderGet_description();
  const char* obloaderGet_authors();
  const char* obloaderGet_icon();
}

#define OPENBABEL_DESCRIPTION _("<span size=\"smaller\">" \
				"This plug-in wraps the <b>OpenBabel</b>\n" \
				"library (visit the home page at URL\n" \
				"<span color=\"blue\"><u>http://www.openbabel.org</u></span>)</span>.")
#define OPENBABEL_AUTHORS     _("Caliste Damien:\n   wrapper.")

/* Local variables. */
static gchar *iconPath;
static VisuDataLoader *obLoader = NULL;

/* Local methods. */
static gboolean saveOpenBabelFile(VisuDumpData *format, const char* filename,
				  VisuData *dataObj, GError **error);
static gboolean loadOpenBabelFile(VisuDataLoader *self, VisuDataLoadable *data,
                                  guint type, guint nSet,
                                  GCancellable *cancel, GError **error);

VisuDataLoader* visu_data_loader_babel_getStatic(void)
{
  return obLoader;
}

/* Required methods for a loadable module. */
gboolean obloaderInit()
{
  const gchar *type[] = {(char*)0};
  const gchar *typeOut[] = {(char*)"*.cif", (char*)"*.cml", (char*)"*.dmol", (char*)0};
  VisuDumpData *dump;

  DBG_fprintf(stderr, "OpenBabel: loading plug-in 'openbabel'...\n");

  obLoader = visu_data_loader_new(_("OpenBabel known formats"), type,
                                  FALSE, loadOpenBabelFile, 75);
  tool_file_format_addPropertyBoolean(TOOL_FILE_FORMAT(obLoader),
                                      "add-hydrogens", _("Add implicit hydrogens"),
                                      TRUE);
  visu_data_atomic_class_addLoader(obLoader);

  iconPath = g_build_filename(V_SIM_PIXMAPS_DIR, "openbabel.png", NULL);

  /* Declare the output using OpenBabel. */
  dump = visu_dump_data_new(_("OpenBabel output formats"), typeOut, saveOpenBabelFile);
  g_object_set(G_OBJECT(dump), "ignore-type", TRUE, NULL);

  return TRUE;
}

const char* obloaderGet_description()
{
  return OPENBABEL_DESCRIPTION;
}

const char* obloaderGet_authors()
{
  return OPENBABEL_AUTHORS;
}

const char* obloaderGet_icon()
{
  return (char*)iconPath;
}

static gboolean loadOpenBabelFile(VisuDataLoader *self, VisuDataLoadable *data,
                                  guint type, guint nSet _U_,
                                  GCancellable *cancel _U_, GError **error)
{
  OpenBabel::OBMol mol;
  OpenBabel::OBFormat *pFormat, *xyzFormat;
  bool res;
  std::ifstream fin(visu_data_loadable_getFilename(data, type));
  std::istream* pIn = &fin;
  OpenBabel::OBConversion conv(pIn, NULL);
  VisuDataLoaderIter *iter;
  int i, j;
  VisuElement *ele1, *ele2;
  OpenBabel::OBUnitCell *uc;
  double rprimdFull[9], rprimd[3][3];
  double eleRGB[3];
  float xyz[3], xyz0[3], red[3], rgba[4], length, lengthMin, lengthMax;
  VisuPairSet *pairs;
  VisuPairLink *link;
  gchar *infoUTF8;
  double vect[3], radius;
  gboolean newEle;
  VisuBox *box;
  VisuElementAtomic *renderer;
  VisuNodeValues *vals, *hc;
  GValue hv = G_VALUE_INIT;

  g_return_val_if_fail(error && *error == (GError*)0, FALSE);

  /* Try to guess the file format. */
  DBG_fprintf(stderr, "Open Babel: try to guess the file format of '%s'.\n",
              visu_data_loadable_getFilename(data, type));
  pFormat = conv.FormatFromExt(visu_data_loadable_getFilename(data, type));
  if (!pFormat)
    {
      *error = g_error_new(VISU_DATA_LOADABLE_ERROR, DATA_LOADABLE_ERROR_FORMAT,
			   _("'%s' doesn't match any file format."),
                           visu_data_loadable_getFilename(data, type));
      return FALSE;
    }
  if ( pFormat->Flags() & NOTREADABLE )
    {
      *error = g_error_new(VISU_DATA_LOADABLE_ERROR, DATA_LOADABLE_ERROR_FORMAT,
			   _("format of '%s' is not a readable one."),
                           visu_data_loadable_getFilename(data, type));
      return FALSE;
    }
  /* Exclude the xyz file format since V_Sim handles it natively. */
  xyzFormat = conv.FindFormat("xyz");
  if (xyzFormat == pFormat)
    {
      DBG_fprintf(stderr, "OpenBabel: skip XYZ format.\n");
      return FALSE;
    }

  DBG_fprintf(stderr, " | set format %p.\n", (gpointer)pFormat);
  DBG_fprintf(stderr, " | format description\n%s\n", pFormat->Description());
  conv.SetInFormat(pFormat);

  /* Read the file. */
  DBG_fprintf(stderr, "Open Babel: read the file.\n");
  res = conv.Read(&mol);
  fin.close();
  DBG_fprintf(stderr, " | read OK.\n");
  if (!res)
    {
      *error = g_error_new(VISU_DATA_LOADABLE_ERROR, DATA_LOADABLE_ERROR_FORMAT,
			   _("The given file doesn't match the format '%s'."),
			   tool_file_format_getName(TOOL_FILE_FORMAT(self)));
      return FALSE;
    }

  if (tool_file_format_getPropertyBoolean(TOOL_FILE_FORMAT(self), "add-hydrogens"))
    mol.AddHydrogens();

  /* Store if the file is periodic or not. */
  uc = (OBUnitCell*)mol.GetData(OBGenericDataType::UnitCell);
  if (uc)
    {
      DBG_fprintf(stderr, "OpenBabel: file has periodic conditions.\n");
      uc->GetCellMatrix().GetArray(rprimdFull);
      for (i = 0; i < 3; i++)
        {
          for (j = 0; j < 3; j++)
            rprimd[i][j] = rprimdFull[i * 3 + j];
          DBG_fprintf(stderr, " | ( %g , %g , %g )\n",
                      rprimd[0][i], rprimd[1][i], rprimd[2][i]);
        }
      box = visu_box_new_full(rprimd, VISU_BOX_PERIODIC);
      if (visu_box_getGeometry(box, VISU_BOX_DXX) == G_MAXFLOAT)
        {
          *error = g_error_new(VISU_DATA_LOADABLE_ERROR, DATA_LOADABLE_ERROR_FORMAT,
                               _("Wrong OpenBabel format, basis-set is degenerated.\n"));
          g_object_unref(box);
          return TRUE;
        }
      visu_boxed_setBox(VISU_BOXED(data), VISU_BOXED(box));
      g_object_unref(box);
      uc->GetOffset().Get(vect);
      uc->FillUnitCell(&mol);
    }
  else
    {
      DBG_fprintf(stderr, "OpenBabel: file has no periodic conditions.\n");
      for (i = 0; i < 3; i++)
	vect[i] = 0.;
      box = (VisuBox*)0;
    }

  DBG_fprintf(stderr, "OpenBabel: first pass to find Elements.\n");
  iter = visu_data_loader_iter_new();
  for (OpenBabel::OBMolAtomIter a(mol); a; ++a)
    {
      ele1 = visu_element_retrieveFromName(OBElements::GetSymbol(a->GetAtomicNum()), &newEle);
      visu_data_loader_iter_addNode(iter, ele1);
      if (newEle)
	{
          renderer = visu_element_atomic_getFromPool(ele1);
	  // We use the standard OB colours.
	  OBElements::GetRGB(a->GetAtomicNum(), eleRGB, eleRGB + 1, eleRGB + 2);
	  rgba[0] = eleRGB[0];
	  rgba[1] = eleRGB[1];
	  rgba[2] = eleRGB[2];
	  rgba[3] = 1.f;
          visu_element_renderer_setColor(VISU_ELEMENT_RENDERER(renderer),
                                         tool_color_addFloatRGBA(rgba, (int*)0));
	  // We use standard radius for element.
	  radius = OBElements::GetCovalentRad(a->GetAtomicNum());
	  visu_element_atomic_setRadius(renderer, radius);
	}
    }
  visu_data_loader_iter_allocate(iter, VISU_NODE_ARRAY(data));
  visu_data_loader_iter_unref(iter);

  vals = (VisuNodeValues*)0;
  hc = (VisuNodeValues*)0;
  g_value_init(&hv, G_TYPE_INT);
  /* Stores coordinates. */
  FOR_ATOMS_OF_MOL(a,&mol)
    {
      VisuNode *node;
      xyz0[0] = xyz[0] = (float)a->GetX() + vect[0];
      xyz0[1] = xyz[1] = (float)a->GetY() + vect[1];
      xyz0[2] = xyz[2] = (float)a->GetZ() + vect[2];
      if (uc)
        {
          visu_box_convertFullToCell(box, xyz, xyz0);
          visu_box_convertXYZtoBoxCoordinates(box, red, xyz);
          if (red[0] > 1 - 1e-6 ||
              red[1] > 1 - 1e-6 ||
              red[2] > 1 - 1e-6)
            continue;
        }
      ele1 = visu_element_lookup(OBElements::GetSymbol(a->GetAtomicNum()));
      node = visu_data_addNodeFromElement(VISU_DATA(data), ele1, xyz, FALSE);
      OpenBabel::OBResidue *residue = a->GetResidue();
      if (residue)
        {
          VisuNodeFragment frag;
          if (!vals)
            {
              vals = VISU_NODE_VALUES
                (visu_node_values_frag_new(VISU_NODE_ARRAY(data),
                                           _("Fragment")));
              visu_data_addNodeProperties(VISU_DATA(data), vals);
            }
          frag.label = (char*)residue->GetName().c_str();
          frag.id    = residue->GetNum();
          visu_node_values_frag_setAt(VISU_NODE_VALUES_FRAG(vals), node, &frag);
        }
      if (a->GetImplicitHCount())
        {
          if (!hc)
            {
              hc = visu_node_values_new(VISU_NODE_ARRAY(data),
                                        _("Implicit H"), G_TYPE_INT, 1);
              visu_data_addNodeProperties(VISU_DATA(data), hc);
            }
          g_value_set_int(&hv, a->GetImplicitHCount());
          visu_node_values_setAt(hc, node, &hv);
        }
    }

  if (!uc)
    box = visu_data_setTightBox(VISU_DATA(data));

  /* Set the bonds, if any. */
  pairs = visu_pair_set_new();
  visu_pair_set_setModel(pairs, VISU_DATA(data));
  FOR_BONDS_OF_MOL(b, &mol)
    {
      ele1 = visu_element_lookup(OBElements::GetSymbol(b->GetBeginAtom()->GetAtomicNum()));
      ele2 = visu_element_lookup(OBElements::GetSymbol(b->GetEndAtom()->GetAtomicNum()));
      link = visu_pair_getNthLink(visu_pair_set_get(pairs, ele1, ele2), 0);
      lengthMin = visu_pair_link_getDistance(link, VISU_DISTANCE_MIN);
      lengthMax = visu_pair_link_getDistance(link, VISU_DISTANCE_MAX);
      length = (float)b->GetLength();
      if (lengthMax < length)
	visu_pair_link_setDistance(link, length, VISU_DISTANCE_MAX);
      if (lengthMin > length || lengthMin == 0.)
	visu_pair_link_setDistance(link, length, VISU_DISTANCE_MIN);
    }
  g_object_unref(pairs);

  /* Set the commentary. */
  if (mol.GetTitle())
    {
      infoUTF8 = g_locale_to_utf8(mol.GetTitle(), -1, NULL, NULL, NULL);
      if (infoUTF8)
	{
	  visu_data_setDescription(VISU_DATA(data), infoUTF8);
	  g_free(infoUTF8);
	}
      else
	{
	  g_warning("Can't convert '%s' to UTF8.\n", mol.GetTitle());
	}
    }

  return TRUE;
}

static gboolean saveOpenBabelFile(VisuDumpData *format _U_, const char* filename,
				  VisuData *dataObj, GError **error)
{
  std::ofstream fout(filename);
  std::ostream* pOut = &fout;
  OpenBabel::OBConversion conv(NULL, pOut);
  OpenBabel::OBFormat *pFormat;
  OpenBabel::OBMol *mol;
  OpenBabel::OBAtom *atom;
  OpenBabel::OBResidue *residue;
  OpenBabel::OBUnitCell *cell;
  OpenBabel::vector3 a(0.,0.,0.), b(0.,0.,0.), c(0.,0.,0.);
  VisuNodeArrayIter iter;
  float coord[3];
  bool res;
  const gchar *comment;
  double matrix[3][3];
  VisuNodeValuesFrag *frags;
  const VisuNodeFragment *frag;

  /* Try to guess the file format. */
  DBG_fprintf(stderr, "Open Babel: try to guess the fileformat of '%s'.\n", filename);
  pFormat = conv.FormatFromExt(filename);
  if (!pFormat)
    {
      *error = g_error_new(VISU_DUMP_ERROR, DUMP_ERROR_FILE,
			   _("'%s' doesn't match any file format."), filename);
      fout.close();
      return FALSE;
    }
  if ( pFormat->Flags() & NOTWRITABLE )
    {
      *error = g_error_new(VISU_DUMP_ERROR, DUMP_ERROR_FILE,
			   _("format of '%s' is not a readable one."), filename);
      fout.close();
      return FALSE;
    }
  DBG_fprintf(stderr, " | set format %p.\n", (gpointer)pFormat);
  DBG_fprintf(stderr, " | format description\n%s\n", pFormat->Description());
  conv.SetOutFormat(pFormat);

  /* Create a new OpenBabel object. */
  mol = new OpenBabel::OBMol;

  comment = visu_data_getDescription(dataObj);
  if (comment)
    mol->SetTitle(comment);

  if (visu_box_getBoundary(visu_boxed_getBox(VISU_BOXED(dataObj))) != VISU_BOX_FREE)
    {
      cell = new OpenBabel::OBUnitCell;
      visu_box_getCellMatrix(visu_boxed_getBox(VISU_BOXED(dataObj)), matrix);
      a.Set(matrix[0]);
      b.Set(matrix[1]);
      c.Set(matrix[2]);
      cell->SetData(a, b, c);
      mol->SetData(cell);
    }

  frags = VISU_NODE_VALUES_FRAG(visu_data_getNodeProperties(dataObj, _("Fragment")));
  visu_node_array_iter_new(VISU_NODE_ARRAY(dataObj), &iter);
  mol->ReserveAtoms(iter.nAllStoredNodes);
  for (visu_node_array_iterStart(VISU_NODE_ARRAY(dataObj), &iter); iter.node;
       visu_node_array_iterNext(VISU_NODE_ARRAY(dataObj), &iter))
    {
      atom = mol->NewAtom();
      atom->SetAtomicNum(OBElements::GetAtomicNum(iter.element->name));
      visu_data_getNodePosition(dataObj, iter.node, coord);
      atom->SetVector((double)coord[0], (double)coord[1], (double)coord[2]);
      if (frags && (frag = visu_node_values_frag_getAt(frags, iter.node)))
        {
          residue = NULL;
          FOR_RESIDUES_OF_MOL(r, mol)
            if (r->GetName() == frag->label && guint(r->GetNum()) == frag->id)
              {
                residue = &(*r);
                break;
              }
          if (!residue)
            {
              residue = mol->NewResidue();
              residue->SetName(frag->label);
              residue->SetNum(frag->id);
            }
          residue->AddAtom(atom);
        }
    }

  DBG_fprintf(stderr, "Open Babel: calling write routine.\n");
  res = conv.Write(mol);

  DBG_fprintf(stderr, "Open Babel: free memory.\n");
  delete mol;
  fout.close();

  if (!res)
    {
      *error = g_error_new(VISU_DUMP_ERROR, DUMP_ERROR_FILE,
			   _("Unable to write the file."));
      return FALSE;
    }

  DBG_fprintf(stderr, "Open Babel: write succeed.\n");
  return TRUE;
}
