//
// This file is part of MorphoGraphX - http://www.MorphoGraphX.org
// Copyright (C) 2012-2015 Richard S. Smith and collaborators.
//
// If you use MorphoGraphX in your work, please cite:
//   http://dx.doi.org/10.7554/eLife.05864
//
// MorphoGraphX is free software, and is licensed under under the terms of the
// GNU General (GPL) Public License version 2.0, http://www.gnu.org/licenses.
//
#ifndef PCAnalysis_HPP
#define PCAnalysis_HPP

#include <Process.hpp>

#include <Information.hpp>

#include <QRegExp>
#include <QString>
#include <QStringList>

namespace mgx
{

  /**
   * \class PCAnalysis PCAnalysis.hpp <PCAnalysis.hpp>
   *
   * Perform a PCA on each labeled regions of the segmented stack, and create
   * shapes in the mesh reflecting the main components.
   *
   * \ingroup MiscellaneousProcess
   */
  class mgxBase_EXPORT PCAnalysis : public Process
  {
  public:
    PCAnalysis(const Process& process) : Process(process)
    {
	  setName("Stack/Shape Analysis/PCAnalysis");
	  setDesc("Compute the principle components of the image. If the threshold is -1, then all \n"
	  "the values are used, as is. If 'Draw Result' is set to true, the current mesh \n"
	  "will be erased and replaced with shapes representing the cells fit. 'Splan Correction' \n"
	  "can be either a shape, a single value of a vector of 3 values, corresponding to the \n"
	  "correction to apply for the eigen-values on all three directions.");
	  setIcon(QIcon(":/images/PCAnalysis.png"));

	  addParm("Output","File to write the output to","output.csv");
	  addParm("Span Correction","Span correction can be made using pre-computed formula for regular shapes, or a percentage of the PC.","Ellipsoid", 
        QStringList() << "Ellipsoid" << "Cuboid" << "Elliptical Cylinder" << "Maximum Span");
	  addParm("Draw Result","Shape used to display the result","No", QStringList() << "No" << "Ellipsoids" << "Cuboids" << "Cylinder");
	  addParm("Threshold","If the stack is not labeled, a single volume is considered with all voxels of intensity greater than this threshold.","100");
	  addParm("Shape Details","How finely to draw the shape (for cylinders or ellipsoids)","3");
	}

    bool run()
    {
      if(!checkState().stack())
        return false;
      Stack* stk = currentStack();
      Store* store = stk->currentStore();
      QString fn = parm("Output");
      QString correction = parm("Span Correction").trimmed();
      Point3f correctingFactor;
      if(correction == "Ellipsoid")
        correctingFactor = sqrt(5);
      else if(correction == "Cuboid")
        correctingFactor = sqrt(3);
      else if(correction == "Elliptical Cylinder")
        correctingFactor = Point3f(sqrt(3), sqrt(4), sqrt(4));
      else if(correction == "Maximum Span")
        correctingFactor = 0;
      else if(correction.endsWith('%')) {
        QString cor = correction.left(correction.size() - 1);
        bool ok;
        float cr = cor.toFloat(&ok);
        if(ok) {
          correctingFactor = -cr / 100.f;
        } else {
          setErrorMessage(QString("Error, percentage value is not valid: '%1'").arg(cor));
          return false;
        }
      } else {
        bool ok;
        float cr = correction.toFloat(&ok);
        if(ok)
          correctingFactor = cr;
        else {
          QStringList vs = correction.split(QRegExp("[ ,-;:-]"));
          if(vs.size() == 3) {
            correctingFactor.x() = vs[0].toFloat(&ok);
            if(!ok) {
              setErrorMessage(QString("Invalid x value for correction factor: '%1'.").arg(vs[0]));
              return false;
            }
            correctingFactor.y() = vs[1].toFloat(&ok);
            if(!ok) {
              setErrorMessage(QString("Invalid y value for correction factor: '%1'.").arg(vs[1]));
              return false;
            }
            correctingFactor.z() = vs[2].toFloat(&ok);
            if(!ok) {
              setErrorMessage(QString("Invalid z value for correction factor: '%1'.").arg(vs[2]));
              return false;
            }
          } else {
            setErrorMessage(QString("Invalid correction string '%1', expected one of 'Ellipsoid'"
               ", 'Cuboid', 'Elliptical Cylinder', Maximum Span', a percentage, a single "
                                    "value or three values"));
            return false;
          }
        }
      }
      bool draw_result = false;
      Mesh* m = mesh(stk->id());
      bool res
        = run(stk, store, m, fn, parm("Threshold").toInt(), parm("Draw Result"), correctingFactor,
            parm("Shape Details").toInt(), draw_result);
      if(res and draw_result) {

        m->setShowSurface();
        m->setShowLabel("Label");
      }
      return res;
    }

    /**
     * @brief operator ()
     * @param stack Stack to act on
     * @param store Store containing the data to process
     * @param filename File to save the result to
     * @param treshold Threshold for volume detection, used if the store is not labeled
     * @param shape Shape used to draw the cells, "No" or empty string for no drawing.
     * @param correctingFactor Vector giving the correction factor to apply for eigen values
     * if the x coordinate is 0, then maximum span is used instead, and negative values
     * (between 0 and 1) correspond to extracting the percentile of the span.
     * @param number of elements used to discretize the shape (if any)
     * @param draw_result Return whether the mesh has been replaced by cell shapes or not
     * @return
     */
    bool run(Stack* stack, Store* store, Mesh* m, QString filename, int treshold, QString shape,
                    Point3f correctingFactor, int slices, bool& draw_result);
  };

  /**
   * \class PCAnalysis PCAnalysis.hpp <PCAnalysis.hpp>
   *
   * Perform a PCA on each labeled regions of the segmented stack, and create
   * shapes in the mesh reflecting the main components.
   *
   * \ingroup MiscellaneousProcess
   */
  class mgxBase_EXPORT PCAnalysis2D : public Process
  {
  public:
    PCAnalysis2D(const Process& process) : Process(process)
    {
	    setName("Mesh/Cell Axis/Shape Analysis/Compute Shape Analysis 2D");
	    setDesc("Compute the principle components of the image. If the threshold is -1, then all \n"
	    "the values are used, as is. If 'Draw Result' is set to true, the current mesh \n"
	    "will be erased and replaced with shapes representing the cells fit. 'Span Correction' \n"
	    "can be either a shape, a single value of a vector of 3 values, corresponding to the b\n"
	    "correction to apply for the eigen-values on all three directions.");
	    setIcon(QIcon(":/images/PDG.png"));

	    addParm("Output","File to write the output to","output.csv");
	    addParm("Axis Line Scale","Amount to scale the axis lines","1.0");
	    addParm("Axis Line Width","","2.0");
      addParm("Span Correction","Span correction can be made using pre-computed formula for regular shapes, or a percentage of the PC.",
          "Cuboid", QStringList() << "Ellipsoid" << "Cuboid");  // 0
	  }

    bool run()
    {
      Mesh* m = currentMesh();
      IntFloatAttr outputHeatMap;
      QString outp = "None";

      Point3f correcting_factor;
      if(parm("Span Correction") == "Ellipsoid")
        correcting_factor = sqrt(5);
      else if(parm("Span Correction") == "Cuboid")
        correcting_factor = sqrt(3);

      return run(m, parm("Output"), parm("Axis Line Scale").toDouble(), parm("Axis Line Width").toDouble(), outp, 
          correcting_factor, outputHeatMap, true);
    }

    bool run(Mesh* m, QString filename, double axisLineScale, double axisLineWidth, QString outp, 
        Point3f correcting_factor, IntFloatAttr &outputHeatMap, bool runDisplay);
  };

  /**
   * \class DisplayPCA ProcessPCA.hpp <MeshProcessPCA.hpp>
   */
  class mgxBase_EXPORT DisplayPCAOld : public Process
  {
  public:
    enum ParmNames { pDisplayHeatMap, pScaleHeatMap, pRangeHeatLow, pRangeHeatHigh, pDisplayAxis,
      pAxisColor, pAxisWidth, pAxisScale, pAxisOffset, pAspectRatioThreshold, pCustomShear, pNumParms};

    DisplayPCAOld(const Process& process) : Process(process)
    {
    setName("Mesh/Cell Axis/Shape Analysis/Display Shape Axis");
    setDesc("Display the principle growth directions");
    setIcon(QIcon(":/images/PDG.png"));

    addParm("Heatmap","Display as a color map PCA values in max or min direction, or max*min, or max/min.","Product Max*Min", 
        QStringList() << "None" << "Max" << "Min" << "Ratio Max/Min" << "Product Max*Min" << "Anisotropy Max/(Max+Min)" << "CustomX" << "CustomY");
    addParm("ScaleHeat","Scale heat map","Auto", QStringList() << "None" << "Auto" << "Manual");
    addParm("Heat min","High bound heat map","1");
    addParm("Heat max","Low bound heat map","3");
    addParm("Show Axis","Draw pca directions as vectors.","Both", 
        QStringList() << "Both" << "Max" << "Min" << "CustomX" << "CustomY" << "BothCustom" << "None"); // 4
    addParm("Axis Color","Color used to draw pca","white", QColor::colorNames());
    addParm("Line Width","Line Width","2.0");
    addParm("Line Scale","Length of the vectors = Scale * Strain.","1.0");
    addParm("Line Offset","Draw the vector ends a bit tilted up for proper display on surfaces.","0.0");
    addParm("Threshold","Minimal value of aspect ratio (= PCAMax/PCAMin) required for drawing PCA.","0.0");
    addParm("Create Attr Maps","Create Attr Maps","No",QStringList() << booleanChoice());
  }

    bool initialize( QWidget* parent);

    // Process the parameters
    void processParms();

    bool run()
    {
      if(!checkState().mesh(MESH_NON_EMPTY))
        return false;
      return run(currentMesh());
    }

    bool run(Mesh* mesh)
    {
      processParms();
      return run(mesh, DisplayHeatMap, scaleHeatMap, RangeHeat, DisplayAxis,
        AxisColor, AxisWidth, AxisScale, AxisOffset, AspectRatioThreshold, CustomShear, CreateAttrs);
    }

    bool run(Mesh* mesh, const QString displayHeatMap, const QString scaleHeatMap, const Point2d &rangeHeat,
      const QString displayPCA, const QColor& axisColor, float axisLineWidth,
      float scaleAxisLength, float axisOffset, float aspectRatioThreshold, bool customShear, bool createAttrMaps);

  private:
    QString DisplayHeatMap;
    QString scaleHeatMap;
    Point2d RangeHeat;
    QString DisplayAxis;
    QColor AxisColor;
    float AxisWidth;
    float AxisScale;
    float AxisOffset;
    float AspectRatioThreshold;
    bool CustomShear;
    bool CreateAttrs;
  };
  /**
   * \class DisplayPCA ProcessPCA.hpp <MeshProcessPCA.hpp>
   */
  class mgxBase_EXPORT DisplayPCA : public Process
  {
  public:
    enum ParmNames { pDisplayHeatMap,	pScaleHeatMap, pRangeHeatLow, pRangeHeatHigh, pDisplayAxis,
		  pAxisColor, pAxisWidth, pAxisScale, pAxisOffset, pAspectRatioThreshold, pCustomShear, pNumParms};

    DisplayPCA(const Process& process) : Process(process)
    {
	  setName("Mesh/Cell Axis/Experimental/Shape Analysis/Display Shape Axis New");
	  setDesc("Display the principle growth directions");
	  setIcon(QIcon(":/images/PDG.png"));

	  addParm("Heatmap","Display as a color map PCA values in max or min direction, or max*min, or max/min.","Product Max*Min", 
        QStringList() << "None" << "Max" << "Min" << "Ratio Max/Min" << "Product Max*Min" << "Anisotropy Max/(Max+Min)" << "CustomX" << "CustomY");
	  addParm("ScaleHeat","Scale heat map","Auto", QStringList() << "None" << "Auto" << "Manual");
	  addParm("Heat min","High bound heat map","1");
	  addParm("Heat max","Low bound heat map","3");
	  addParm("Show Axis","Draw pca directions as vectors.","Both", 
        QStringList() << "Both" << "Max" << "Min" << "CustomX" << "CustomY" << "BothCustom" << "None");	// 4
	  addParm("Axis Color","Color used to draw pca","white", QColor::colorNames());
	  addParm("Line Width","Line Width","2.0");
	  addParm("Line Scale","Length of the vectors = Scale * Strain.","1.0");
	  addParm("Line Offset","Draw the vector ends a bit tilted up for proper display on surfaces.","0.0");
	  addParm("Threshold","Minimal value of aspect ratio (= PCAMax/PCAMin) required for drawing PCA.","0.0");
    addParm("Create Attr Maps","Create Attr Maps","No",QStringList() << booleanChoice());
	}

		// Process the parameters
    void processParms();

    bool run()
    {
      if(!checkState().mesh(MESH_NON_EMPTY))
        return false;
      return run(currentMesh());
    }

    bool run(Mesh* mesh)
    {
      DisplayHeatMap = parm("Heatmap");
      scaleHeatMap = parm("ScaleHeat");
      RangeHeat = Point2d(parm("Heat min").toDouble(), parm("Heat max").toDouble());
      DisplayAxis = parm("Show Axis");
      AxisColor = parm("Axis Color");
      AxisWidth = parm("Line Width").toFloat();
      AxisScale = parm("Line Scale").toFloat();
      AxisOffset = parm("Line Offset").toFloat();
      AspectRatioThreshold = parm("Threshold").toFloat();
      CreateAttrs = stringToBool(parm("Create Attr Maps"));
      return run(mesh, DisplayHeatMap, scaleHeatMap, RangeHeat, DisplayAxis,
			  AxisColor, AxisWidth, AxisScale, AxisOffset, AspectRatioThreshold, CustomShear, CreateAttrs);
    }

    bool run(Mesh* mesh, const QString displayHeatMap, const QString scaleHeatMap, const Point2d &rangeHeat,
		  const QString displayPCA, const QColor& axisColor, float axisLineWidth,
      float scaleAxisLength, float axisOffset, float aspectRatioThreshold, bool customShear, bool createAttrMaps);

  private:
    QString DisplayHeatMap;
		QString scaleHeatMap;
		Point2d RangeHeat;
		QString DisplayAxis;
		QColor AxisColor;
		float AxisWidth;
		float AxisScale;
		float AxisOffset;
		float AspectRatioThreshold;
		bool CustomShear;
    bool CreateAttrs;
  };
  ///@}

  /**
   * \class PCAnalysis PCAnalysis.hpp <PCAnalysis.hpp>
   *
   * Perform a PCA on each labeled regions of the segmented stack, and create
   * shapes in the mesh reflecting the main components.
   *
   * \ingroup MiscellaneousProcess
   */
  class mgxBase_EXPORT PCAnalysis3D : public Process
  {
  public:
    PCAnalysis3D(const Process& process) : Process(process)
    {
	    setName("Mesh/Cell Axis 3D/Shape Analysis/Compute Shape Analysis 3D");
	    setDesc("Compute the PCA of the cell shapes");
	    setIcon(QIcon(":/images/PDG.png"));

      addParm("Input","Input","Mesh Triangles", QStringList() << "Stack Voxels" << "Mesh Triangles");  // 0
	    addParm("Span Correction","Span correction can be made using pre-computed formula for regular shapes, or a percentage of the PC.",
              "Ellipsoid", QStringList() << "Ellipsoid" << "Cuboid" << "Elliptical Cylinder" << "Maximum Span");	// 0
      
      //addParm("Weight","Weight","Area", QStringList() << "none" << "Area" << "Signal" << "Area*Signal");  // 0
      //addParm("Normalize Directions","Normalize Directions","No", booleanChoice());  // 0
	  }

    bool run()
    {
      QString correction = parm("Span Correction").trimmed();
      Point3d correctingFactor;
      if(correction == "Ellipsoid")
        correctingFactor = sqrt(5);
      else if(correction == "Cuboid")
        correctingFactor = sqrt(3);
      else if(correction == "Elliptical Cylinder")
        correctingFactor = Point3d(sqrt(3), sqrt(4), sqrt(4));
      else if(correction == "Maximum Span")
        correctingFactor = 0;
      else if(correction.endsWith('%')) {
        QString cor = correction.left(correction.size() - 1);
        bool ok;
        float cr = cor.toFloat(&ok);
        if(ok) {
          correctingFactor = -cr / 100.f;
        } else {
          setErrorMessage(QString("Error, percentage value is not valid: '%1'").arg(cor));
          return false;
        }
      } else {
        bool ok;
        float cr = correction.toFloat(&ok);
        if(ok)
          correctingFactor = cr;
        else {
          QStringList vs = correction.split(QRegExp("[ ,-;:-]"));
          if(vs.size() == 3) {
            correctingFactor.x() = vs[0].toDouble(&ok);
            if(!ok) {
              setErrorMessage(QString("Invalid x value for correction factor: '%1'.").arg(vs[0]));
              return false;
            }
            correctingFactor.y() = vs[1].toDouble(&ok);
            if(!ok) {
              setErrorMessage(QString("Invalid y value for correction factor: '%1'.").arg(vs[1]));
              return false;
            }
            correctingFactor.z() = vs[2].toDouble(&ok);
            if(!ok) {
              setErrorMessage(QString("Invalid z value for correction factor: '%1'.").arg(vs[2]));
              return false;
            }
          } else {
            setErrorMessage(QString("Invalid correction string '%1', expected one of 'Ellipsoid'"
               ", 'Cuboid', 'Elliptical Cylinder', Maximum Span', a percentage, a single "
                                    "value or three values"));
            return false;
          }
        }
      }
      bool draw_result = false;
      Mesh* m = currentMesh();
      bool res = run(m, correctingFactor, parm("Input"));
      if(res and draw_result) {
        m->setShowSurface();
        m->setShowLabel("Label");
      }
      return res;
    }

    bool run(Mesh* m, Point3d correctingFactor, QString inputMode);

  };

  /**
   * \class DisplayPCA ProcessPCA.hpp <MeshProcessPCA.hpp>
   */
  class mgxBase_EXPORT DisplayPCA3D : public Process
  {
  public:
    enum ParmNames { pDisplayHeatMap, pScaleHeatMap, pRangeHeatLow, pRangeHeatHigh, pDisplayAxis,
      pAxisColor, pAxisWidth, pAxisScale, pAxisOffset, pAspectRatioThreshold, pNumParms};

    DisplayPCA3D(const Process& process) : Process(process)
    {
	    setName("Mesh/Cell Axis 3D/Shape Analysis/Display Shape Axis 3D");
	    setDesc("Display the PCA based shape axes directions");
	    setIcon(QIcon(":/images/PDG.png"));

	    addParm("Heatmap","Display shape values as a heat map. Product = Max*Mid*Min","Product", 
              QStringList() << "None" << "Max" << "Mid" << "Min" << "Elongation (Max/Mid)" << "Flatness (Mid/Min)" << "Max/Min" 
                            << "Product" << "Shape Anisotropy" << "Custom X" << "Custom Y" << "Custom Z");
	    addParm("ScaleHeat","Scale heat map","Auto", QStringList() << "None" << "Auto" << "Manual");
	    addParm("Heat min","High bound heat map","1");
	    addParm("Heat max","Low bound heat map","3");
	    addParm("Show Axis","Draw pca directions as vectors.","All", QStringList() 
          << "All" << "Max" << "Mid" << "Min" << "Custom All" << "Custom X" << "Custom Y" << "Custom Z" << "None");	// 4
	    addParm("Axis Color","Color used to draw pca","white", QColor::colorNames());
	    addParm("Line Width","Line Width","2.0");
	    addParm("Line Scale","Length of the vectors = Scale * Strain.","1.0");
	    addParm("Line Offset","Draw the vector ends a bit tilted up for proper display on surfaces.","0.0");
	    addParm("Threshold","Minimal value of aspect ratio (= PCAMax/PCAMin) required for drawing PCA.","0.0");
      addParm("Create Attr Maps","Create Attribute Maps for all heat map options","No", booleanChoice());
	  }

    // Process the parameters
    void processParms();

    bool run()
    {
      if(!checkState().mesh(MESH_NON_EMPTY))
        return false;
      return run(currentMesh());
    }

    bool run(Mesh* mesh)
    {
      processParms();
      return run(mesh, DisplayHeatMap, ScaleHeatMap, RangeHeat, DisplayAxis,
        AxisColor, AxisWidth, AxisScale, AxisOffset, AspectRatioThreshold, attrMaps);
    }

    bool run(Mesh* mesh, const QString displayHeatMap, const QString scaleHeatMap, const Point2d &rangeHeat,
      const QString displayPCA, const QColor& axisColor, float axisLineWidth,
      float scaleAxisLength, float axisOffset, float aspectRatioThreshold, bool attrMaps);

  private:
    QString DisplayHeatMap;
    QString ScaleHeatMap;
    Point2d RangeHeat;
    QString DisplayAxis;
    QColor AxisColor;
    float AxisWidth;
    float AxisScale;
    float AxisOffset;
    float AspectRatioThreshold;
    bool attrMaps;
  };
}

#endif
