/*
 * Copyright (C) 2008 Emweb bvba, Kessel-Lo, Belgium.
 *
 * See the LICENSE file for terms of use.
 */

#include "ChartConfig.h"
#include "PanelList.h"

#include <iostream>
#include <boost/date_time/gregorian/greg_year.hpp>

#include <Wt/WAbstractItemModel>
#include <Wt/WApplication>
#include <Wt/WCheckBox>
#include <Wt/WComboBox>
#include <Wt/WDoubleValidator>
#include <Wt/WDate>
#include <Wt/WEnvironment>
#include <Wt/WIntValidator>
#include <Wt/WLineEdit>
#include <Wt/WLocale>
#include <Wt/WPanel>
#include <Wt/WPushButton>
#include <Wt/WStandardItemModel>
#include <Wt/WTable>
#include <Wt/WText>
#include <Wt/WPainterPath>

#include <Wt/Chart/WCartesianChart>

using namespace Wt;
using namespace Wt::Chart;

namespace {
  void addHeader(WTable *t, const char *value) {
    t->elementAt(0, t->columnCount())->addWidget(new WText(value));
  }

  void addEntry(WAbstractItemModel *model, const char *value) {
    model->insertRows(model->rowCount(), 1);
    model->setData(model->rowCount()-1, 0, boost::any(std::string(value)));
  }

  bool getDouble(WLineEdit *edit, double& value) {
    try {
      value = WLocale::currentLocale().toDouble(edit->text());
      return true;
    } catch (...) {
      return false;
    }
  }

  int seriesIndexOf(WCartesianChart* chart, int modelColumn) {
    for (unsigned i = 0; i < chart->series().size(); ++i)
      if (chart->series()[i].modelColumn() == modelColumn)
	return i;
    
    return -1;
  }
}

ChartConfig::ChartConfig(WCartesianChart *chart, WContainerWidget *parent)
  : WContainerWidget(parent),
    chart_(chart),
    fill_(MinimumValueFill)
{
  chart_->setLegendStyle(chart_->legendFont(), WPen(black),
			 WBrush(WColor(0xFF, 0xFA, 0xE5)));
  chart->initLayout();

  PanelList *list = new PanelList(this);

  WIntValidator *sizeValidator = new WIntValidator(200, 2000, this);
  sizeValidator->setMandatory(true);

  WDoubleValidator *anyNumberValidator = new WDoubleValidator(this);
  anyNumberValidator->setMandatory(true);

  WDoubleValidator *angleValidator = new WDoubleValidator(-90, 90, this);
  angleValidator->setMandatory(true);

  // ---- Chart properties ----

  WStandardItemModel *orientation = new WStandardItemModel(0, 1, this);
  addEntry(orientation, "Vertical");
  addEntry(orientation, "Horizontal");

  WStandardItemModel *legendLocation = new WStandardItemModel(0, 1, this);
  addEntry(legendLocation, "Outside");
  addEntry(legendLocation, "Inside");

  WStandardItemModel *legendSide = new WStandardItemModel(0, 1, this);
  addEntry(legendSide, "Top");
  addEntry(legendSide, "Right");
  addEntry(legendSide, "Bottom");
  addEntry(legendSide, "Left");

  WStandardItemModel *legendAlignment = new WStandardItemModel(0, 1, this);
  addEntry(legendAlignment, "AlignLeft");
  addEntry(legendAlignment, "AlignCenter");
  addEntry(legendAlignment, "AlignRight");
  addEntry(legendAlignment, "AlignTop");
  addEntry(legendAlignment, "AlignMiddle");
  addEntry(legendAlignment, "AlignBottom");

  WTable *chartConfig = new WTable();
  chartConfig->setMargin(WLength::Auto, Left | Right);

  int row = 0;
  chartConfig->elementAt(row, 0)->addWidget(new WText("Title:"));
  titleEdit_ = new WLineEdit(chartConfig->elementAt(row, 1));
  connectSignals(titleEdit_);
  ++row;

  chartConfig->elementAt(row, 0)->addWidget(new WText("Width:"));
  chartWidthEdit_ = new WLineEdit(chartConfig->elementAt(row, 1));
  chartWidthEdit_
    ->setText(WLocale::currentLocale().toString(chart_->width().value()));
  chartWidthEdit_->setValidator(sizeValidator);
  chartWidthEdit_->setMaxLength(4);
  connectSignals(chartWidthEdit_);
  ++row;

  chartConfig->elementAt(row, 0)->addWidget(new WText("Height:"));
  chartHeightEdit_ = new WLineEdit(chartConfig->elementAt(row, 1));
  chartHeightEdit_
    ->setText(WLocale::currentLocale().toString(chart_->height().value()));
  chartHeightEdit_->setValidator(sizeValidator);
  chartHeightEdit_->setMaxLength(4);
  connectSignals(chartHeightEdit_);
  ++row;

  chartConfig->elementAt(row, 0)->addWidget(new WText("Orientation:"));
  chartOrientationEdit_ = new WComboBox(chartConfig->elementAt(row, 1));
  chartOrientationEdit_->setModel(orientation);
  connectSignals(chartOrientationEdit_);
  ++row;

  chartConfig->elementAt(row, 0)->addWidget(new WText("Legend location:"));
  legendLocationEdit_ = new WComboBox(chartConfig->elementAt(row, 1));
  legendLocationEdit_->setModel(legendLocation);
  connectSignals(legendLocationEdit_);
  ++row;

  chartConfig->elementAt(row, 0)->addWidget(new WText("Legend side:"));
  legendSideEdit_ = new WComboBox(chartConfig->elementAt(row, 1));
  legendSideEdit_->setModel(legendSide);
  legendSideEdit_->setCurrentIndex(1);
  connectSignals(legendSideEdit_);
  ++row;

  chartConfig->elementAt(row, 0)->addWidget(new WText("Legend alignment:"));
  legendAlignmentEdit_ = new WComboBox(chartConfig->elementAt(row, 1));
  legendAlignmentEdit_->setModel(legendAlignment);
  legendAlignmentEdit_->setCurrentIndex(4);
  connectSignals(legendAlignmentEdit_);
  ++row;

  for (int i = 0; i < chartConfig->rowCount(); ++i) {
    chartConfig->elementAt(i, 0)->setStyleClass("tdhead");
    chartConfig->elementAt(i, 1)->setStyleClass("tddata");
  }

  WPanel *p = list->addWidget("Chart properties", chartConfig);
  p->setMargin(WLength::Auto, Left | Right);
  p->resize(880, WLength::Auto);
  p->setMargin(20, Top | Bottom);

  if (chart_->isLegendEnabled())
    chart_->setPlotAreaPadding(200, Right);

  // ---- Series properties ----

  WStandardItemModel *types = new WStandardItemModel(0, 1, this);
  addEntry(types, "Points");
  addEntry(types, "Line");
  addEntry(types, "Curve");
  addEntry(types, "Bar");
  addEntry(types, "Line Area");
  addEntry(types, "Curve Area");
  addEntry(types, "Stacked Bar");
  addEntry(types, "Stacked Line Area");
  addEntry(types, "Stacked Curve Area");

  WStandardItemModel *markers = new WStandardItemModel(0, 1, this);
  addEntry(markers, "None");
  addEntry(markers, "Square");
  addEntry(markers, "Circle");
  addEntry(markers, "Cross");
  addEntry(markers, "X cross");
  addEntry(markers, "Triangle");
  addEntry(markers, "Diamond");

  WStandardItemModel *axes = new WStandardItemModel(0, 1, this);
  addEntry(axes, "1st Y axis");
  addEntry(axes, "2nd Y axis");

  WStandardItemModel *labels = new WStandardItemModel(0, 1, this);
  addEntry(labels, "None");
  addEntry(labels, "X");
  addEntry(labels, "Y");
  addEntry(labels, "X: Y");

  WTable *seriesConfig = new WTable();
  seriesConfig->setMargin(WLength::Auto, Left | Right);

  ::addHeader(seriesConfig, "Name");
  ::addHeader(seriesConfig, "Enabled");
  ::addHeader(seriesConfig, "Type");
  ::addHeader(seriesConfig, "Marker");
  ::addHeader(seriesConfig, "Y axis");
  ::addHeader(seriesConfig, "Legend");
  ::addHeader(seriesConfig, "Shadow");
  ::addHeader(seriesConfig, "Value labels");

  seriesConfig->rowAt(0)->setStyleClass("trhead");

  for (int j = 1; j < chart->model()->columnCount(); ++j) {
    SeriesControl sc;

    new WText(asString(chart->model()->headerData(j)),
	      seriesConfig->elementAt(j, 0));

    sc.enabledEdit = new WCheckBox(seriesConfig->elementAt(j, 1));
    connectSignals(sc.enabledEdit);

    sc.typeEdit = new WComboBox(seriesConfig->elementAt(j, 2));
    sc.typeEdit->setModel(types);
    connectSignals(sc.typeEdit);

    sc.markerEdit = new WComboBox(seriesConfig->elementAt(j, 3));
    sc.markerEdit->setModel(markers);
    connectSignals(sc.markerEdit);

    sc.axisEdit = new WComboBox(seriesConfig->elementAt(j, 4));
    sc.axisEdit->setModel(axes);
    connectSignals(sc.axisEdit);

    sc.legendEdit = new WCheckBox(seriesConfig->elementAt(j, 5));
    connectSignals(sc.legendEdit);

    sc.shadowEdit = new WCheckBox(seriesConfig->elementAt(j, 6));
    connectSignals(sc.shadowEdit);

    sc.labelsEdit = new WComboBox(seriesConfig->elementAt(j, 7));
    sc.labelsEdit->setModel(labels);
    connectSignals(sc.labelsEdit);

    int si = seriesIndexOf(chart, j);

    if (si != -1) {
      sc.enabledEdit->setChecked();
      const WDataSeries& s = chart_->series(j);
      switch (s.type()) {
      case PointSeries:
	sc.typeEdit->setCurrentIndex(0); break;
      case LineSeries:
	sc.typeEdit->setCurrentIndex(s.fillRange() != NoFill ?
				     (s.isStacked() ? 7 : 4) : 1); break;
      case CurveSeries:
	sc.typeEdit->setCurrentIndex(s.fillRange() != NoFill ?
				     (s.isStacked() ? 8 : 5) : 2); break;
      case BarSeries:
	sc.typeEdit->setCurrentIndex(s.isStacked() ? 6 : 3);
      }

      sc.markerEdit->setCurrentIndex((int)s.marker());
      sc.legendEdit->setChecked(s.isLegendEnabled());
      sc.shadowEdit->setChecked(s.shadow() != WShadow());
    }

    seriesControls_.push_back(sc);

    seriesConfig->rowAt(j)->setStyleClass("trdata");
  }

  p = list->addWidget("Series properties", seriesConfig);
  p->expand();
  p->setMargin(WLength::Auto, Left | Right);
  p->resize(880, WLength::Auto);
  p->setMargin(20, Top | Bottom);

  // ---- Axis properties ----

  WStandardItemModel *yScales = new WStandardItemModel(0, 1, this);
  addEntry(yScales, "Linear scale");
  addEntry(yScales, "Log scale");

  WStandardItemModel *xScales = new WStandardItemModel(0, 1, this);
  addEntry(xScales, "Categories");
  addEntry(xScales, "Linear scale");
  addEntry(xScales, "Log scale");
  addEntry(xScales, "Date scale");

  WTable *axisConfig = new WTable();
  axisConfig->setMargin(WLength::Auto, Left | Right);

  ::addHeader(axisConfig, "Axis");
  ::addHeader(axisConfig, "Visible");
  ::addHeader(axisConfig, "Scale");
  ::addHeader(axisConfig, "Automatic");
  ::addHeader(axisConfig, "Minimum");
  ::addHeader(axisConfig, "Maximum");
  ::addHeader(axisConfig, "Gridlines");
  ::addHeader(axisConfig, "Label angle");

  axisConfig->rowAt(0)->setStyleClass("trhead");

  for (int i = 0; i < 3; ++i) {
    const char *axisName[] = { "X axis", "1st Y axis", "2nd Y axis" };
    int j = i + 1;

    const WAxis& axis = chart_->axis(static_cast<Axis>(i));
    AxisControl sc;

    new WText(WString(axisName[i], UTF8), axisConfig->elementAt(j, 0));

    sc.visibleEdit = new WCheckBox(axisConfig->elementAt(j, 1));
    sc.visibleEdit->setChecked(axis.isVisible());
    connectSignals(sc.visibleEdit);

    sc.scaleEdit = new WComboBox(axisConfig->elementAt(j, 2));
    if (axis.scale() == CategoryScale)
      sc.scaleEdit->addItem("Category scale");
    else {
      if (axis.id() == XAxis) {
	sc.scaleEdit->setModel(xScales);
	sc.scaleEdit->setCurrentIndex(axis.scale());
      } else {
	sc.scaleEdit->setModel(yScales);
	sc.scaleEdit->setCurrentIndex(axis.scale() - 1);
      }
    }
    connectSignals(sc.scaleEdit);

    bool autoValues = axis.autoLimits() == (MinimumValue | MaximumValue);

    sc.minimumEdit = new WLineEdit(axisConfig->elementAt(j, 4));
    sc.minimumEdit->setText(WLocale::currentLocale()
                            .toString(axis.minimum()));
    sc.minimumEdit->setValidator(anyNumberValidator);
    sc.minimumEdit->setEnabled(!autoValues);
    connectSignals(sc.minimumEdit);

    sc.maximumEdit = new WLineEdit(axisConfig->elementAt(j, 5));
    sc.maximumEdit->setText(WLocale::currentLocale()
                            .toString(axis.maximum()));
    sc.maximumEdit->setValidator(anyNumberValidator);
    sc.maximumEdit->setEnabled(!autoValues);
    connectSignals(sc.maximumEdit);

    sc.autoEdit = new WCheckBox(axisConfig->elementAt(j, 3));
    sc.autoEdit->setChecked(autoValues);
    connectSignals(sc.autoEdit);
    sc.autoEdit->checked().connect(sc.maximumEdit, &WLineEdit::disable);
    sc.autoEdit->unChecked().connect(sc.maximumEdit, &WLineEdit::enable);
    sc.autoEdit->checked().connect(sc.minimumEdit, &WLineEdit::disable);
    sc.autoEdit->unChecked().connect(sc.minimumEdit, &WLineEdit::enable);

    sc.gridLinesEdit = new WCheckBox(axisConfig->elementAt(j, 6));
    connectSignals(sc.gridLinesEdit);

    sc.labelAngleEdit = new WLineEdit(axisConfig->elementAt(j, 7));
    sc.labelAngleEdit->setText("0");
    sc.labelAngleEdit->setValidator(angleValidator);
    connectSignals(sc.labelAngleEdit);

    axisConfig->rowAt(j)->setStyleClass("trdata");

    axisControls_.push_back(sc);
  }

  p = list->addWidget("Axis properties", axisConfig);
  p->setMargin(WLength::Auto, Left | Right);
  p->resize(880, WLength::Auto);
  p->setMargin(20, Top | Bottom);

  /*
   * If we do not have JavaScript, then add a button to reflect changes to
   * the chart.
   */
  if (!WApplication::instance()->environment().javaScript()) {
    WPushButton *b = new WPushButton(this);
    b->setText("Update chart");
    b->setInline(false); // so we can add margin to center horizontally
    b->setMargin(WLength::Auto, Left | Right);
    b->clicked().connect(this, &ChartConfig::update);
  }
}

void ChartConfig::setValueFill(FillRangeType fill)
{
  fill_ = fill;
}

void ChartConfig::update()
{
  bool haveLegend = false;
  std::vector<WDataSeries> series;

  for (int i = 1; i < chart_->model()->columnCount(); ++i) {
    SeriesControl& sc = seriesControls_[i-1];

    if (sc.enabledEdit->isChecked()) {
      WDataSeries s(i);

      switch (sc.typeEdit->currentIndex()) {
      case 0:
	s.setType(PointSeries);
	if (sc.markerEdit->currentIndex() == 0)
	  sc.markerEdit->setCurrentIndex(1);
	break;
      case 1:
	s.setType(LineSeries);
	sc.markerEdit->setCurrentIndex(0);
	break;
      case 2:
	s.setType(CurveSeries);
	sc.markerEdit->setCurrentIndex(0);
	break;
      case 3:
	s.setType(BarSeries);
	sc.markerEdit->setCurrentIndex(0);
	break;
      case 4:
	s.setType(LineSeries);
	s.setFillRange(fill_);
	sc.markerEdit->setCurrentIndex(0);
	break;
      case 5:
	s.setType(CurveSeries);
	s.setFillRange(fill_);
	sc.markerEdit->setCurrentIndex(0);
	break;
      case 6:
	s.setType(BarSeries);
	s.setStacked(true);
	sc.markerEdit->setCurrentIndex(0);
	break;
      case 7:
	s.setType(LineSeries);
	s.setFillRange(fill_);
	s.setStacked(true);
	sc.markerEdit->setCurrentIndex(0);
	break;
      case 8:
	s.setType(CurveSeries);
	s.setFillRange(fill_);
	s.setStacked(true);
	sc.markerEdit->setCurrentIndex(0);
      }

      //set WPainterPath to draw a diamond
      if(sc.markerEdit->currentIndex() == CustomMarker){
	WPainterPath pp = WPainterPath();
	pp.moveTo(-6, 0);
	pp.lineTo(0, 6);
	pp.lineTo(6, 0);
	pp.lineTo(0, -6);
	pp.lineTo(-6, 0);
	s.setCustomMarker(pp);
      }

      s.setMarker(static_cast<MarkerType>(sc.markerEdit->currentIndex()));

      if (sc.axisEdit->currentIndex() == 1) {
	s.bindToAxis(Y2Axis);
      }

      if (sc.legendEdit->isChecked()) {
	s.setLegendEnabled(true);
	haveLegend = true;
      } else
	s.setLegendEnabled(false);

      if (sc.shadowEdit->isChecked()) {
	s.setShadow(WShadow(3, 3, WColor(0, 0, 0, 127), 3));
      } else
	s.setShadow(WShadow());

      switch (sc.labelsEdit->currentIndex()) {
      case 1:
	s.setLabelsEnabled(XAxis);
	break;
      case 2:
	s.setLabelsEnabled(YAxis);
	break;
      case 3:
	s.setLabelsEnabled(XAxis);	
	s.setLabelsEnabled(YAxis);
	break;
      }

      series.push_back(s);
    }
  }

  chart_->setSeries(series);

  for (int i = 0; i < 3; ++i) {
    AxisControl& sc = axisControls_[i];
    WAxis& axis = chart_->axis(static_cast<Axis>(i));

    axis.setVisible(sc.visibleEdit->isChecked());

    if (sc.scaleEdit->count() != 1) {
      int k = sc.scaleEdit->currentIndex();
      if (axis.id() != XAxis)
	k += 1;
      else {
	if (k == 0)
	  chart_->setType(CategoryChart);
	else
	  chart_->setType(ScatterPlot);
      }

      switch (k) {
      case 1:
	axis.setScale(LinearScale); break;
      case 2:
	axis.setScale(LogScale); break;
      case 3:
	axis.setScale(DateScale); break;
      }
    }

    if (sc.autoEdit->isChecked())
      axis.setAutoLimits(MinimumValue | MaximumValue);
    else {
      if (validate(sc.minimumEdit) && validate(sc.maximumEdit)) {
          double min, max;
          getDouble(sc.minimumEdit, min);
          getDouble(sc.maximumEdit, max);

          if (axis.scale() == LogScale)
              if (min <= 0)
                  min = 0.0001;

          if (axis.scale() == DateScale){
              //the number of julian days until year 1986
              WDate dMin = WDate(1900,1,1);
              double gregDaysMin = (double)dMin.toJulianDay();
              //the number of julian days until year 1988
              WDate dMax = WDate(3000,1,1);
              double gregDaysMax = (double)dMax.toJulianDay();

              bool greg_year_validation =
                      (min > gregDaysMin &&
                       min < gregDaysMax &&
                       max > gregDaysMin &&
                       max < gregDaysMax);

              if(!greg_year_validation){
                  min = gregDaysMin;
                  max = gregDaysMax;
              }
          }

          axis.setRange(min, max);
      }

    }

    if (validate(sc.labelAngleEdit)) {
      double angle;
      getDouble(sc.labelAngleEdit, angle);
      axis.setLabelAngle(angle);
    }

    axis.setGridLinesEnabled(sc.gridLinesEdit->isChecked());
  }

  chart_->setTitle(titleEdit_->text());

  if (validate(chartWidthEdit_) && validate(chartHeightEdit_)) {
    double width, height;
    getDouble(chartWidthEdit_, width);
    getDouble(chartHeightEdit_, height);
    chart_->resize(width, height);
  }

  switch (chartOrientationEdit_->currentIndex()) {
  case 0:
    chart_->setOrientation(Vertical); break;
  case 1:
    chart_->setOrientation(Horizontal); break;
  }

  chart_->setLegendEnabled(haveLegend);

  if (haveLegend) {
    LegendLocation location = LegendOutside;
    Side side = Right;
    AlignmentFlag alignment = AlignMiddle;

    switch (legendLocationEdit_->currentIndex()) {
    case 0: location = LegendOutside; break;
    case 1: location = LegendInside; break;
    }

    switch (legendSideEdit_->currentIndex()) {
    case 0: side = Top; break;
    case 1: side = Right; break;
    case 2: side = Bottom; break;
    case 3: side = Left; break;
    }

    if (side == Left || side == Right) {
      if (legendAlignmentEdit_->currentIndex() < 3)
	legendAlignmentEdit_->setCurrentIndex(4);
    } else {
      if (legendAlignmentEdit_->currentIndex() >= 3)
	legendAlignmentEdit_->setCurrentIndex(2);
    }

    switch (legendAlignmentEdit_->currentIndex()) {
    case 0: alignment = AlignLeft; break;
    case 1: alignment = AlignCenter; break;
    case 2: alignment = AlignRight; break;
    case 3: alignment = AlignTop; break;
    case 4: alignment = AlignMiddle; break;
    case 5: alignment = AlignBottom; break;
    }

    chart_->setLegendLocation(location, side, alignment);

    chart_->setLegendColumns((side == Top || side == Bottom ) ? 2 : 1,
			     WLength(100));
  }

  for (unsigned i = 0; i < 4; ++i) {
    Side sides[] = { Top, Right, Bottom, Left };

    bool legendRoom =
      haveLegend
      && chart_->legendLocation() == LegendOutside
      && chart_->legendSide() == sides[i];

    int padding;

    if (i % 2 == 0)
      padding = legendRoom ? 80 : 40;
    else
      padding = legendRoom ? 200 : 80;

    chart_->setPlotAreaPadding(padding, sides[i]);
  }
}

bool ChartConfig::validate(WFormWidget *w)
{
  bool valid = w->validate() == WValidator::Valid;

  if (!WApplication::instance()->environment().javaScript()) {
    w->setStyleClass(valid ? "" : "Wt-invalid");
    w->setToolTip(valid ? "" : "Invalid value");
  }

  return valid;
}

void ChartConfig::connectSignals(WFormWidget *w)
{
  w->changed().connect(this, &ChartConfig::update);
  if (dynamic_cast<WLineEdit *>(w))
    w->enterPressed().connect(this, &ChartConfig::update);
}
