#include <graph/server/gui/chart/chart.h>
#include <graph/server/gui/mainwindow.h>
#include <QPainter>
#include <iostream>
#include <iterator>
#include <limits>
#include <string>

#define MIN(a, b) (a < b ? a : b)
#define MAX(a, b) (a > b ? a : b)

namespace Graph::GUI {
ChartWidget::ChartWidget(QWidget* parent, const chart_spec_t& spec) : QWidget(parent), spec(spec) {
  setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
  setMinimumSize(QSize(600, 350));

  for (const QString& name : spec.series) {
    series.insert(std::pair<std::string, series_t>(name.toStdString(), series_t()));
    std::cout << "create a series named " << name.toStdString() << std::endl;
  }

  // context menu
  setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu);
  connect(this, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
    ContextMenu menu(nullptr, this);
    menu.exec(mapToGlobal(pos));
  });
}

ChartWidget::~ChartWidget() {}

void ChartWidget::addData(const char* seriesName, const point_t* points, unsigned long count) {
  QMutexLocker locker(&mutex);

  if (!this->series.contains(std::string(seriesName)))
    return;

  series_t& series = this->series.at(std::string(seriesName));

  series.minX = std::numeric_limits<double>::max();
  series.minY = std::numeric_limits<double>::max();
  series.maxX = std::numeric_limits<double>::min();
  series.maxY = std::numeric_limits<double>::min();

  if (!spec.keep_all) {
    if (count >= spec.keep_limit) {
      series.points.clear();
    } else if (series.points.size() + count > spec.keep_limit) {
      std::list<point_t>::iterator end = std::next(series.points.begin(), series.points.size() - (spec.keep_limit - count));
      series.points.erase(series.points.begin(), end);
    }
  }

  for (const point_t& point : series.points) {
    if (spec.axis.x.auto_limit) {
      series.minX = MIN(series.minX, point.x);
      series.maxX = MAX(series.maxX, point.x);
    }
    if (spec.axis.y.auto_limit) {
      series.minY = MIN(series.minY, point.y);
      series.maxY = MAX(series.maxY, point.y);
    }
  }

  for (unsigned long i = spec.keep_all ? 0 : (count < spec.keep_limit ? 0 : count - spec.keep_limit); i < count; i++) {
    if (decimateState >= spec.decimate)
      decimateState = 0;
    if (decimateState++ % spec.decimate != 0)
      continue;

    series.points.push_back(points[i]);

    if (spec.axis.x.auto_limit) {
      series.minX = MIN(series.minX, points[i].x);
      series.maxX = MAX(series.maxX, points[i].x);
    }
    if (spec.axis.y.auto_limit) {
      series.minY = MIN(series.minY, points[i].y);
      series.maxY = MAX(series.maxY, points[i].y);
    }
  }

  update();
}

void ChartWidget::clear() {
  QMutexLocker locker(&mutex);
  for (auto& series : this->series) {
    series.second.points.clear();
    series.second.minX = std::numeric_limits<double>::max();
    series.second.maxX = std::numeric_limits<double>::min();
    series.second.minY = std::numeric_limits<double>::max();
    series.second.maxY = std::numeric_limits<double>::min();
  }
}

QSize ChartWidget::sizeHint() const {
  return QSize(600, 350);
}

QPointF ChartWidget::pointToCoord(const point_t& point) {
  return QPointF((point.x - spec.axis.x.min) * scaleX + marginX, height() - (point.y - spec.axis.y.min) * scaleY - marginY);
}

QColor interpolateColor(const QColor& a, const QColor& b, float ratio) {
  return QColor(255 * (b.redF() * ratio + a.redF() * (1 - ratio)), 255 * (b.greenF() * ratio + a.greenF() * (1 - ratio)),
                255 * (b.blueF() * ratio + a.blueF() * (1 - ratio)), 255 * (b.alphaF() * ratio + a.alphaF() * (1 - ratio)));
}

void ChartWidget::paintEvent(QPaintEvent* e) {
  QMutexLocker locker(&mutex);

  // axis ranges
  if (spec.axis.x.auto_limit) {
    spec.axis.x.min = std::numeric_limits<double>::max();
    spec.axis.x.max = std::numeric_limits<double>::min();
  }
  if (spec.axis.y.auto_limit) {
    spec.axis.y.min = std::numeric_limits<double>::max();
    spec.axis.y.max = std::numeric_limits<double>::min();
  }

  for (const auto& series : this->series) {
    if (spec.axis.x.auto_limit) {
      spec.axis.x.min = MIN(spec.axis.x.min, series.second.minX);
      spec.axis.x.max = MAX(spec.axis.x.max, series.second.maxX);
    }
    if (spec.axis.y.auto_limit) {
      spec.axis.y.min = MIN(spec.axis.y.min, series.second.minY);
      spec.axis.y.max = MAX(spec.axis.y.max, series.second.maxY);
    }
  }

  QPainter painter(this);

  // background
  painter.fillRect(QRectF(0, 0, width(), height()), Qt::lightGray);
  painter.fillRect(QRectF(marginX, marginY, width() - 2 * marginX, height() - 2 * marginY), Qt::white);

  // title
  painter.setPen(Qt::black);
  painter.drawText(QRect(0, 0, width(), marginY), Qt::AlignCenter, spec.title);

  // limits
  // y max-mid-min
  painter.save();
  painter.rotate(-90);
  painter.translate(-height(), 0);

  painter.drawText(QRect(marginY, marginX / 2, marginY, marginX / 2), Qt::AlignCenter, QString::number(spec.axis.y.min, 'f', 2));
  painter.drawText(QRect(height() / 2 - marginY / 2, marginX / 2, marginY, marginX / 2), Qt::AlignCenter,
                   QString::number((spec.axis.y.min + spec.axis.y.max) / 2, 'f', 2));
  painter.drawText(QRect(height() - marginY * 2, marginX / 2, marginY, marginX / 2), Qt::AlignCenter, QString::number(spec.axis.y.max, 'f', 2));

  painter.restore();

  // x min-mid-max
  painter.drawText(QRect(marginX, height() - marginY, marginX, marginY / 2), Qt::AlignCenter, QString::number(spec.axis.x.min, 'f', 2));
  painter.drawText(QRect(width() / 2 - marginX / 2, height() - marginY, marginX, marginY / 2), Qt::AlignCenter,
                   QString::number((spec.axis.x.min + spec.axis.x.max) / 2, 'f', 2));
  painter.drawText(QRect(width() - marginX * 2, height() - marginY, marginX, marginY / 2), Qt::AlignCenter, QString::number(spec.axis.x.max, 'f', 2));

  // axis labels
  painter.drawText(QRect(0, height() - marginY / 2, width(), marginY / 2), Qt::AlignCenter, spec.axis.x.label);
  painter.save();
  painter.rotate(-90);
  painter.translate(-height(), 0);
  painter.drawText(QRect(0, 0, height(), marginX / 2), Qt::AlignCenter, spec.axis.y.label);
  painter.restore();

  // grid
  if (spec.grid) {
    painter.setPen(Qt::lightGray);

    int innerHeight = height() - 2 * marginY;
    painter.drawLine(marginX, marginY + innerHeight / 4, width() - marginX, marginY + innerHeight / 4);
    painter.drawLine(marginX, marginY + innerHeight / 2, width() - marginX, marginY + innerHeight / 2);
    painter.drawLine(marginX, marginY + 3 * innerHeight / 4, width() - marginX, marginY + 3 * innerHeight / 4);

    int innerWidth = width() - 2 * marginX;
    painter.drawLine(marginX + innerWidth / 4, marginY, marginX + innerWidth / 4, height() - marginY);
    painter.drawLine(marginX + innerWidth / 2, marginY, marginX + innerWidth / 2, height() - marginY);
    painter.drawLine(marginX + 3 * innerWidth / 4, marginY, marginX + 3 * innerWidth / 4, height() - marginY);
  }

  // points
  scaleX = (width() - marginX * 2) / (spec.axis.x.max - spec.axis.x.min);
  scaleY = (height() - marginY * 2) / (spec.axis.y.max - spec.axis.y.min);

  painter.setPen(QPen(Qt::black, 1));

  int i = 0;
  for (const auto& series : this->series) {
    QPolygonF polygon;
    for (const point_t point : series.second.points) {
      // TODO: only paint visible points
      //       (+ 1-1 before and after)
      polygon << pointToCoord(point);
    }

    painter.setBrush(interpolateColor(Qt::darkBlue, Qt::green, i / (float)this->series.size()));
    painter.setPen(interpolateColor(Qt::darkBlue, Qt::green, i / (float)this->series.size()));

    // line
    painter.drawPolyline(polygon);

    // legend
    if (spec.legend) {
      painter.fillRect(QRect(marginX + 5, marginY + 5 + i * 15, 10, 10), painter.brush());
      painter.drawText(QRect(marginX + 17, marginY + 2 + i * 15, width() - 2 * marginX, 15), QString::fromStdString(series.first));
    }

    i++;
  }
}

ContextMenu::ContextMenu(QWidget* parent, ChartWidget* chartWidget) : QMenu(parent) {
  remove = new QAction(tr("&Remove"), this);
  connect(remove, &QAction::triggered, this, [chartWidget]() { MainWindow::getInstance()->removeChart(chartWidget); });
  addAction(remove);

  clear = new QAction(tr("&Clear"), this);
  connect(clear, &QAction::triggered, this, [chartWidget]() { chartWidget->clear(); });
  addAction(clear);

  // edit = new QAction(tr("&Edit"), this);
  // connect(edit, &QAction::triggered, this, []() {
  //   // TODO: implement chart editing
  // });
  // addAction(edit);
}

ContextMenu::~ContextMenu() {}
} // namespace Graph::GUI