/* Distributed under the Apache License, Version 2.0.
   See accompanying NOTICE file for details.*/

#include "PulsePainterPlot.h"

#include <QThread>
#include <QPainter>
#include <QVector>

//-----------------------------------------------------------------------------
class PulsePainterPlot::Private
{
public:
  Private(PulsePainterPlot* q) : q{q} {
  }

  void scheduleUpdate();
  void update();

  PulsePainterPlot* const q;

  QVector<double> m_times;
  QVector<double> m_values;
  double m_userMinY = +std::numeric_limits<double>::infinity();
  double m_userMaxY = -std::numeric_limits<double>::infinity();
  double m_dataMinY = +std::numeric_limits<double>::infinity();
  double m_dataMaxY = -std::numeric_limits<double>::infinity();

  double m_userMinX = +std::numeric_limits<double>::infinity();
  double m_userMaxX = -std::numeric_limits<double>::infinity();

  double m_padding = 0.0;

  int m_maxSize = 100;
  bool m_updateScheduled = false;
  int m_index = 0;

  UpdateMode m_updateMode = UpdateMode::Timeline;
  ScaleMode m_scaleMode = ScaleMode::Adaptive;

  bool m_xZeroEnabled = false;
  bool m_yZeroEnabled = false;
};

//-----------------------------------------------------------------------------
void PulsePainterPlot::Private::scheduleUpdate()
{
  if (!m_updateScheduled)
  {
    m_updateScheduled = true;
    QMetaObject::invokeMethod(q, [this]{ update(); }, Qt::QueuedConnection);
  }
}

//-----------------------------------------------------------------------------
void PulsePainterPlot::Private::update()
{
  m_dataMinY = +std::numeric_limits<double>::infinity();
  m_dataMaxY = -std::numeric_limits<double>::infinity();

  auto const size = m_values.size();
  if (size > 2)
  {
    for (int i = 0; i < size; i++)
    {
      auto const v = m_values[i];
      m_dataMinY = qMin(m_dataMinY, v);
      m_dataMaxY = qMax(m_dataMaxY, v);
    }
    if (m_dataMinY == m_dataMaxY)
    {
      m_dataMinY -= 0.5;
      m_dataMaxY += 0.5;
    }

    if (m_padding > 0.0)
    {
      auto const d = m_dataMaxY - m_dataMinY;
      auto const p = m_padding * d;
      m_dataMinY -= p;
      m_dataMaxY += p;
    }
  }

  m_updateScheduled = false;
  q->update();
}

//-----------------------------------------------------------------------------
PulsePainterPlot::PulsePainterPlot(QWidget* parent)
  : QWidget{parent}, d{new Private{this}}
{
}

//-----------------------------------------------------------------------------
PulsePainterPlot::~PulsePainterPlot()
{
}

//-----------------------------------------------------------------------------
void PulsePainterPlot::setMaxPoints(int count)
{
  d->m_maxSize = count;
  while (d->m_times.size() > count)
    d->m_times.pop_front();
  while (d->m_values.size() > count)
    d->m_values.pop_front();
}

//-----------------------------------------------------------------------------
void PulsePainterPlot::clear()
{
  if (QThread::currentThread() != this->thread())
  {
    QMetaObject::invokeMethod(this, &PulsePainterPlot::clear,
                              Qt::QueuedConnection);
    return;
  }

  d->m_times.clear();
  d->m_values.clear();
  if (d->m_updateMode == UpdateMode::InPlace)
  {
    d->m_values.resize(d->m_maxSize);
    d->m_values.fill(0.0);
    d->m_times.resize(d->m_maxSize);
    for (int i = 0; i < d->m_times.size(); ++i)
    {
      d->m_times[i] = i;
    }
    d->m_index = 0;
  }
}

//-----------------------------------------------------------------------------
void PulsePainterPlot::setYRange(double min, double max)
{
  d->m_userMinY = min;
  d->m_userMaxY = max;
}

QPair<double, double> PulsePainterPlot::getYRange()
{
  return { d->m_userMinY, d->m_userMaxY };
}

void PulsePainterPlot::setXRange(double min, double max)
{
  d->m_userMinX = min;
  d->m_userMaxX = max;
}

void PulsePainterPlot::setScaleMode(ScaleMode mode)
{
  d->m_scaleMode = mode;
}

void PulsePainterPlot::setUpdateMode(UpdateMode mode)
{
  d->m_updateMode = mode;
  clear();
}

void PulsePainterPlot::enableXZero(bool doDraw)
{
  d->m_xZeroEnabled = doDraw;
}

void PulsePainterPlot::enableYZero(bool doDraw)
{
  d->m_yZeroEnabled = doDraw;
}

//-----------------------------------------------------------------------------
void PulsePainterPlot::append(double time, double value)
{
  if (QThread::currentThread() != this->thread())
  {
    QMetaObject::invokeMethod(
      this, [time, value, this]{ append(time, value); },
      Qt::QueuedConnection);
    return;
  }

  switch (d->m_updateMode)
  {
  case UpdateMode::Timeline:
  {
    d->m_times.push_back(time);
    d->m_values.push_back(value);

    double t = time;
    if (d->m_values.size() < d->m_maxSize)
    {
      for (int i = d->m_values.size(); i <= d->m_maxSize; ++i)
      {
        t -= 1. / 50.;
        d->m_values.push_back(value);
        d->m_times.push_front(t);
      }
    }

    d->m_values.pop_front();
    d->m_times.pop_front();

  }
  break;
  case UpdateMode::InPlace:
    d->m_values[d->m_index] = value;
    d->m_index = (d->m_index + 1) % d->m_maxSize;
    d->m_values[d->m_index] = 0.0;
    break;
  case UpdateMode::RawData:
    d->m_times.push_back(time);
    d->m_values.push_back(value);
    if (d->m_times.size() > d->m_maxSize)
    {
      d->m_times.pop_front();
      d->m_values.pop_front();
    }
    break;
  }

  d->scheduleUpdate();
}

//-----------------------------------------------------------------------------
void PulsePainterPlot::paintEvent(QPaintEvent*)
{
  auto const size = d->m_values.size();
  if (size > 2)
  {
    QPainter p{this};
    auto const& c = palette().color(QPalette::WindowText);

    auto const w = width();
    auto const h = height();

    double minX, minY;
    double maxX, maxY;
    double xs, ys;

    switch (d->m_scaleMode)
    {
    case ScaleMode::Adaptive:
    case ScaleMode::Expanding:
    {

      minX = d->m_times.first();
      maxX = d->m_times.last();
      minY = qMin(d->m_userMinY, d->m_dataMinY);
      maxY = qMax(d->m_userMaxY, d->m_dataMaxY);

      auto const xRange = maxX - minX;
      auto const yRange = maxY - minY;
      xs = static_cast<double>(w) / xRange;
      ys = static_cast<double>(h) / yRange;
      if (d->m_scaleMode == ScaleMode::Expanding)
      {
        d->m_userMinY = minY;
        d->m_userMaxY = maxY;
      }
      break;
    }

    case ScaleMode::Fixed:
    {
      minX = d->m_userMinX;
      maxX = d->m_userMaxX;
      minY = d->m_userMinY;
      maxY = d->m_userMaxY;

      auto const xRange = maxX - minX;
      auto const yRange = maxY - minY;
      xs = static_cast<double>(w) / xRange;
      ys = static_cast<double>(h) / yRange;
      break;
    }
    }

    auto points = QPolygonF{};
    points.reserve(d->m_maxSize);

    p.setRenderHint(QPainter::Antialiasing);
    p.setRenderHint(QPainter::Antialiasing);
    p.setPen({ c, 2.0f });

    int split = (d->m_updateMode == UpdateMode::InPlace) ? d->m_index : size;

    for (int i = 0; i < split; i++)
    {
      auto const t = d->m_times[i];
      auto const v = d->m_values[i];
      auto const x = static_cast<qreal>((t - minX) * xs);
      auto const y = static_cast<qreal>((v - minY) * ys);

      points.append({ x, h - y });
    }
    p.drawPolyline(points);
    points.clear();

    // if updateInPlace is false we will fall through here, the previous
    // loop draws all the points
    for (int i = split + 1; i < size; i++)
    {
      auto const t = d->m_times[i];
      auto const v = d->m_values[i];
      auto const x = static_cast<qreal>((t - minX) * xs);
      auto const y = static_cast<qreal>((v - minY) * ys);

      points.append({ x, h - y });
    }
    p.drawPolyline(points);

    p.setPen({ c, 1.0f });
    if (d->m_xZeroEnabled)
    {
        points.clear();
        points.append({ -minX * xs, static_cast<qreal>(h) });
        points.append({ -minX * xs, 0 });
        p.drawPolyline(points);
    }

    if (d->m_yZeroEnabled)
    {
        points.clear();
        points.append({ 0, h + minY * ys});
        points.append({ static_cast<qreal>(w), h + minY * ys});
        p.drawPolyline(points);
    }
  }
}

