#include "fpcrystal.h"

#include <string.h>

#include <visu_basic.h>
#include <coreTools/toolPhysic.h>

#include <CrystalFp.h>

extern "C"
{
  gboolean crystalfpInit();
  const char* crystalfpGet_description();
  const char* crystalfpGet_authors();
  const char* crystalfpGet_icon();
}

#define CRYSTALFP_DESCRIPTION _("<span size=\"smaller\">" \
				"a wrapper around CrystalFp</span>.")
#define CRYSTALFP_AUTHORS     _("Caliste Damien:\n   wrapper.")

/* Local variables. */
static gchar *iconPath;

/* Local methods. */

/* Required methods for a loadable module. */
gboolean crystalfpInit()
{
  DBG_fprintf(stderr, "Crystalfp: loading plug-in 'crystalfp'...\n");

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

  return TRUE;
}

const char* crystalfpGet_description()
{
  return CRYSTALFP_DESCRIPTION;
}

const char* crystalfpGet_authors()
{
  return CRYSTALFP_AUTHORS;
}

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

/**
 * VisuCrystalfpClass:
 * @parent: the parent class;
 *
 * A short way to identify #_VisuCrystalfpClass structure.
 *
 * Since: 3.8
 */
/**
 * VisuCrystalfp:
 *
 * An opaque structure.
 *
 * Since: 3.8
 */
/**
 * VisuCrystalfpPrivate:
 *
 * Private fields for #VisuCrystalfp objects.
 *
 * Since: 3.8
 */
struct _VisuCrystalfpPrivate
{
  gboolean dispose_has_run;

  gboolean fp_are_ready;
  VisuCrystalfpDistanceMethod distMeth;

  GHashTable *structures;
  cfp::CrystalFp *cfp;
};

/* Local routines. */
static void visu_crystalfp_dispose (GObject* obj);
static void visu_crystalfp_finalize(GObject* obj);

G_DEFINE_TYPE(VisuCrystalfp, visu_crystalfp, G_TYPE_OBJECT)

static void visu_crystalfp_class_init(VisuCrystalfpClass *klass)
{
  DBG_fprintf(stderr, "Visu Crystalfp: creating the class of the object.\n");
  /* DBG_fprintf(stderr, "                - adding new signals ;\n"); */

  /* Connect the overloading methods. */
  G_OBJECT_CLASS(klass)->dispose      = visu_crystalfp_dispose;
  G_OBJECT_CLASS(klass)->finalize     = visu_crystalfp_finalize;

  g_type_class_add_private(klass, sizeof(VisuCrystalfpPrivate));
}

static void visu_crystalfp_init(VisuCrystalfp *obj)
{
  DBG_fprintf(stderr, "Visu Crystalfp: initializing a new object (%p).\n",
	      (gpointer)obj);
  
  obj->priv = G_TYPE_INSTANCE_GET_PRIVATE(obj, VISU_TYPE_CRYSTALFP,
                                          VisuCrystalfpPrivate);
  obj->priv->dispose_has_run = FALSE;
  obj->priv->fp_are_ready = FALSE;
  obj->priv->cfp = new cfp::CrystalFp(2);
  obj->priv->cfp->setFingerprintMethod(VISU_CRYSTALFP_PER_ELEMENT_DIFFRACTION);
  obj->priv->cfp->setDistanceMethod(VISU_CRYSTALFP_DISTANCE_COSINE);
  obj->priv->structures = g_hash_table_new_full(g_direct_hash, g_direct_equal,
                                                g_object_unref, NULL);
  obj->priv->distMeth = VISU_CRYSTALFP_DISTANCE_COSINE;
}

/* This method can be called several times.
   It should unref all of its reference to
   GObjects. */
static void visu_crystalfp_dispose(GObject* obj)
{
  VisuCrystalfp *fp;

  DBG_fprintf(stderr, "Visu Crystalfp: dispose object %p.\n", (gpointer)obj);

  fp = VISU_CRYSTALFP(obj);
  if (fp->priv->dispose_has_run)
    return;
  fp->priv->dispose_has_run = TRUE;

  /* Chain up to the parent class */
  G_OBJECT_CLASS(visu_crystalfp_parent_class)->dispose(obj);
}
/* This method is called once only. */
static void visu_crystalfp_finalize(GObject* obj)
{
  VisuCrystalfpPrivate *fp;

  g_return_if_fail(obj);

  DBG_fprintf(stderr, "Visu Crystalfp: finalize object %p.\n", (gpointer)obj);

  fp = VISU_CRYSTALFP(obj)->priv;
  delete(fp->cfp);
  g_hash_table_destroy(fp->structures);

  /* Chain up to the parent class */
  DBG_fprintf(stderr, "Visu Crystalfp: chain to parent.\n");
  G_OBJECT_CLASS(visu_crystalfp_parent_class)->finalize(obj);
  DBG_fprintf(stderr, "Visu Crystalfp: freeing ... OK.\n");
}

/**
 * visu_crystalfp_new:
 *
 *
 * Since: 3.8
 *
 * Returns: a pointer to the VisuGlExt it created or
 * NULL otherwise.
 */
VisuCrystalfp* visu_crystalfp_new(void)
{
  VisuCrystalfp *fp;
  
  fp = VISU_CRYSTALFP(g_object_new(VISU_TYPE_CRYSTALFP, NULL));
  return fp;
}

static void computeFingerprints(VisuCrystalfp *fp)
{
  g_return_if_fail(VISU_IS_CRYSTALFP(fp));

  if (fp->priv->cfp->getCutoffDistance() == 0.f)
    fp->priv->cfp->setCutoffDistance(fp->priv->cfp->computeCutoffDistance());

  fp->priv->cfp->computeFingerprints();
  fp->priv->fp_are_ready = TRUE;
}

/**
 * visu_crystalfp_addStructure:
 * @data: a #VisuData object.
 *
 *
 * Since: 3.8
 */
void visu_crystalfp_addStructure(VisuCrystalfp *fp, VisuData *data)
{
  VisuNodeArrayIter iter;
  guint i;
  float *aCoords;
  unsigned int *aZ, *zEle;
  float cell[16];
  double matrix[3][3];
  gdouble energy;
  guint id;

  g_return_if_fail(VISU_IS_CRYSTALFP(fp));

  visu_node_array_iter_new(VISU_NODE_ARRAY(data), &iter);
  zEle    = (unsigned int*)g_malloc0(sizeof(unsigned int) * iter.nElements);
  for (visu_node_array_iterStart(VISU_NODE_ARRAY(data), &iter);
       iter.node;
       visu_node_array_iterNextElement(VISU_NODE_ARRAY(data), &iter, TRUE))
    if (!tool_physic_getZFromSymbol((int*)zEle + iter.iElement, (float*)0, (gchar*)iter.element->name))
      g_warning("No such element '%s'.", iter.element->name);
  aCoords = (float*)g_malloc(sizeof(float) * 3 * iter.nAllStoredNodes);
  aZ      = (unsigned int*)g_malloc0(sizeof(unsigned int) * iter.nAllStoredNodes);
  for (visu_node_array_iterStart(VISU_NODE_ARRAY(data), &iter), i = 0;
       iter.node;
       visu_node_array_iterNext(VISU_NODE_ARRAY(data), &iter), i += 1)
    {
      visu_data_getNodePosition(data, iter.node, aCoords + 3 * i);
      aZ[i] = zEle[iter.iElement];
    }
  g_free(zEle);
  visu_box_getCellMatrix(visu_boxed_getBox(VISU_BOXED(data)), matrix);
  cell[ 0] = matrix[0][0];
  cell[ 1] = matrix[0][1];
  cell[ 2] = matrix[0][2];
  cell[ 3] = 0.f;
  cell[ 4] = matrix[1][0];
  cell[ 5] = matrix[1][1];
  cell[ 6] = matrix[1][2];
  cell[ 7] = 0.f;
  cell[ 8] = matrix[2][0];
  cell[ 9] = matrix[2][1];
  cell[10] = matrix[2][2];
  cell[11] = 0.f;
  cell[12] = 0.f;
  cell[13] = 0.f;
  cell[14] = 0.f;
  cell[15] = 1.f;
  g_object_get(G_OBJECT(data), "totalEnergy", &energy, NULL);

  id = g_hash_table_size(fp->priv->structures) + 1;
  fp->priv->cfp->addStructure(id, iter.nAllStoredNodes, aCoords, aZ, cell,
                              (energy != G_MAXFLOAT), energy, FALSE);
  g_free(aCoords);
  g_free(aZ);
  if (visu_box_getBoundary(visu_boxed_getBox(VISU_BOXED(data))) == VISU_BOX_FREE)
    fp->priv->cfp->setNanoclusterStructureType();

  g_object_ref(data);
  g_hash_table_insert(fp->priv->structures, data, GINT_TO_POINTER(id));

  fp->priv->fp_are_ready = FALSE;
}

float visu_crystalfp_computeCutoffDistance(VisuCrystalfp *fp, float margin)
{
  g_return_val_if_fail(VISU_IS_CRYSTALFP(fp), -1.f);
 
  if (margin > 0.f)
    return fp->priv->cfp->computeCutoffDistance(margin);
  else
    return fp->priv->cfp->computeCutoffDistance();
}

/**
 * visu_crystalfp_getFingerprint:
 * @fp: 
 * @data: 
 *
 * 
 *
 * Since: 3.8
 *
 * Returns: (transfer full) (element-type float):
 **/
GArray* visu_crystalfp_getFingerprint(VisuCrystalfp *fp, VisuData *data)
{
  size_t id;
  guint nSections, secLength;
  GArray *arr;

  g_return_val_if_fail(VISU_IS_CRYSTALFP(fp), (GArray*)0);

  id = GPOINTER_TO_INT(g_hash_table_lookup(fp->priv->structures, data));
  if (!id)
    {
      visu_crystalfp_addStructure(fp, data);
      id = GPOINTER_TO_INT(g_hash_table_lookup(fp->priv->structures, data));
    }

  if (!fp->priv->fp_are_ready)
    computeFingerprints(fp);

  nSections = fp->priv->cfp->getFingerprintNumSections();
  secLength = fp->priv->cfp->getFingerprintSectionLen();
  arr = g_array_sized_new(FALSE, FALSE, sizeof(gfloat), nSections * secLength);
  g_array_set_size(arr, nSections * secLength);
  memcpy(arr->data, fp->priv->cfp->getFingerprint(id - 1),
         sizeof(gfloat) * nSections * secLength);
  return arr;
}

static void computeDistances(VisuCrystalfp *fp, VisuCrystalfpDistanceMethod meth)
{
  g_return_if_fail(VISU_IS_CRYSTALFP(fp));

  if (!fp->priv->fp_are_ready)
    computeFingerprints(fp);

  if (fp->priv->distMeth != meth)
    {
      fp->priv->cfp->setDistanceMethod(meth);
      fp->priv->distMeth = meth;
      fp->priv->cfp->computeDistanceMatrix();
    }
  if (!fp->priv->cfp->hasDistanceMatrix())
    fp->priv->cfp->computeDistanceMatrix();
}

float visu_crystalfp_getDistance(VisuCrystalfp *fp, VisuData *data1, VisuData *data2,
                                 VisuCrystalfpDistanceMethod meth)
{
  size_t id1, id2;

  g_return_val_if_fail(VISU_IS_CRYSTALFP(fp), -1.f);

  id1 = GPOINTER_TO_INT(g_hash_table_lookup(fp->priv->structures, data1));
  id2 = GPOINTER_TO_INT(g_hash_table_lookup(fp->priv->structures, data2));
  if (!id1 || !id2)
    {
      g_warning("Unregistered structures.");
      return -1.f;
    }

  computeDistances(fp, meth);
  
  return fp->priv->cfp->getDistance(id1 - 1, id2 - 1);
}

#include <CrystalFpScatterplot.h>

/**
 * visu_crystalfp_get2DPlot:
 * @fp: 
 *
 * 
 *
 * Returns: (transfer full) (element-type VisuCrystalfp2DPlot):
 **/
GArray* visu_crystalfp_get2DPlot(VisuCrystalfp *fp,
                                 guint retries, guint maxIter,
                                 float min_energy, float timestep)
{
  cfp::CrystalFpScatterplot plot;
  size_t s;
  guint i, j;
  float *coords, *vals, *vals_p, *vals_s, energy;
  GArray *arr;
  VisuCrystalfp2DPlot plt;

  g_return_val_if_fail(VISU_IS_CRYSTALFP(fp), (GArray*)0);

  computeDistances(fp, fp->priv->distMeth);

  s = plot.initScatterplot(fp->priv->cfp);
  coords = (float*)g_malloc(sizeof(float) * s * 2);
  vals =   (float*)g_malloc(sizeof(float) * s);
  vals_p = (float*)g_malloc(sizeof(float) * s);
  vals_s = (float*)g_malloc(sizeof(float) * s);

  for (i = 0; i < retries; i++)
    {
      for (j = 0; j < maxIter; j++)
        {
          energy = plot.stepScatterplot(timestep);
          fprintf(stderr, "%g\n", energy);
          // plot.getPoints(coords);
          // plot.getValues(vals, cfp::CrystalFpScatterplot::VAL_TOTAL_ENERGY);
          if (energy < min_energy)
            break;
        }
      plot.perturbPositions();
      // plot.getPoints(coords);
      // plot.getValues(vals, cfp::CrystalFpScatterplot::VAL_TOTAL_ENERGY);
    }
  plot.getPoints(coords);
  plot.getValues(vals, cfp::CrystalFpScatterplot::VAL_TOTAL_ENERGY);
  plot.getValues(vals_p, cfp::CrystalFpScatterplot::VAL_PER_ATOM_ENERGY);
  plot.getValues(vals_s, cfp::CrystalFpScatterplot::VAL_STRESS);

  arr = g_array_sized_new(FALSE, FALSE, sizeof(VisuCrystalfp2DPlot), s);
  for (i = 0; i < s; i++)
    {
      plt.x = coords[i * 2 + 0];
      plt.y = coords[i * 2 + 1];
      plt.totalEnergy = vals[i];
      plt.perAtEnergy = vals_p[i];
      plt.stress      = vals_s[i];
      g_array_insert_vals(arr, i, &plt, 1);
    }
  
  g_free(coords);
  g_free(vals);
  g_free(vals_p);
  g_free(vals_s);
  return arr;
}
