text: #include "spline-widget.hpp"
  #include "compat/math.hpp"
  #include "compat/macros.hpp"
  
  #include <algorithm>
  
  #include <QPainter>
  #include <QPainterPath>
  #include <QPixmap>
  #include <QString>
  #include <QToolTip>
  #include <QtEvents>
  #include <QPainterPath>
  
  #include <QDebug>
  
  #if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
  #   define OTR_OBSOLETE_QT_WORKAROUND
  #endif
  
  namespace spline_detail {
  
  spline_widget::spline_widget(QWidget *parent) : QWidget(parent)
  {
      setMouseTracking(true);
      //setFocusPolicy(Qt::ClickFocus);
      setCursor(Qt::ArrowCursor);
  }
  
  spline_widget::~spline_widget()
  {
      if (connection)
      {
          QObject::disconnect(connection);
          connection = {};
      }
  }
  
  void spline_widget::set_config(base_spline* spl)
  {
      if (connection)
      {
          QObject::disconnect(connection);
          connection = {};
      }
  
      config = spl;
  
      if (spl)
      {
          std::shared_ptr<base_settings> s = spl->get_settings();
          connection = connect(&*s, &base_settings::recomputed,
                               this, [this] { reload_spline(); },
                               Qt::QueuedConnection);
      }
  
      reload_spline();
  }
  
  QColor spline_widget::colorBezier() const
  {
      return spline_color;
  }
  
  void spline_widget::setColorBezier(QColor const& color)
  {
      spline_color = color;
      repaint();
  }
  
  void spline_widget::force_redraw()
  {
      background_img = {};
      repaint();
  }
  
  void spline_widget::set_preview_only(bool val)
  {
      preview_only = val;
  }
  
  bool spline_widget::is_preview_only() const
  {
      return preview_only;
  }
  
  void spline_widget::drawBackground()
  {
      QPainter painter(&background_img);
  
      painter.fillRect(rect(), widget_bg_color);
  
      {
          QColor bg_color(112, 154, 209);
          if (!isEnabled() && !preview_only)
              bg_color = QColor(176,176,180);
          painter.fillRect(pixel_bounds, bg_color);
      }
  
      QFont font;
      font.setPointSize(8);
      font.setStyleHint(QFont::Monospace, QFont::PreferAntialias);
      painter.setFont(font);
      const QFontMetricsF metrics(font);
  
      QColor color__(176, 190, 209, 127);
  
      if (!isEnabled())
          color__ = QColor(70, 90, 100, 96);
  
      const QPen pen(color__, 1, Qt::SolidLine, Qt::FlatCap);
  
      const int ystep = (int)std::ceil(y_step_), xstep = (int)std::ceil(x_step_);
      const double maxx = config->max_input();
      const double maxy = config->max_output();
  #ifndef OTR_OBSOLETE_QT_WORKAROUND
      double space_width = metrics.horizontalAdvance(' ');
  #else
      double space_width = metrics.boundingRect(' ').right();
  #endif
      painter.setPen(palette().text().color());
  
      // vertical grid
      for (int i = 0; i <= maxy; i += ystep)
      {
          const double y = pixel_bounds.height() - i * c.y() + pixel_bounds.y();
          drawLine(painter,
                   QPointF(pixel_bounds.x(), y),
                   QPointF(pixel_bounds.x() + pixel_bounds.width(), y),
                   pen);
          QString text = QString::number(i);
          QRectF rect = metrics.boundingRect(text);
  #ifndef OTR_OBSOLETE_QT_WORKAROUND
          double advance = metrics.horizontalAdvance(text);
  #else
          double advance = rect.right();
  #endif
          painter.drawText(QPointF(pixel_bounds.x() - advance - space_width,
                                   y - rect.height()/2 - rect.top()),
                           text);
      }
  
      // horizontal grid
      for (int i = 0; i <= maxx; i += xstep)
      {
          const double x = pixel_bounds.x() + i * c.x();
          drawLine(painter,
                   QPointF(x, pixel_bounds.y()),
                   QPointF(x, pixel_bounds.y() + pixel_bounds.height()),
                   pen);
  
          const QString text = QString::number(i);
          QRectF rect = metrics.boundingRect(text);
  #ifndef OTR_OBSOLETE_QT_WORKAROUND
          double advance = metrics.horizontalAdvance(text);
  #else
          double advance = metrics.boundingRect(text).right();
  #endif
          painter.drawText(QPointF(x - advance/2 - rect.left(),
                                   pixel_bounds.bottom() + metrics.lineSpacing()),
                           text);
      }
  }
  
  void spline_widget::drawFunction()
  {
      QPainter painter(&spline_img);
      painter.setRenderHint(QPainter::Antialiasing, true);
  
      const points_t& points = config->get_points();
  
      if (moving_control_point_idx >= 0 &&
          moving_control_point_idx < points.size())
      {
          const QPen pen(Qt::white, 1, Qt::SolidLine, Qt::FlatCap);
          QPointF prev = point_to_pixel({});
          for (const auto& point : points)
          {
              const QPointF tmp = point_to_pixel(point);
              drawLine(painter, prev, tmp, pen);
              prev = tmp;
          }
      }
  
      const QColor color_ = progn(
          if (!isEnabled() && !preview_only)
          {
              QColor color(spline_color);
              const int avg = int(float(color.red() + color.green() + color.blue())/3);
              return QColor(int(float(color.red() + avg)*.5f),
                            int(float(color.green() + avg)*.5f),
                            int(float(color.blue() + avg)*.5f),
                            96);
          }
          else
          {
              QColor color(spline_color);
              color.setAlphaF(color.alphaF()*.9);
              return color;
          }
      );
  
      painter.setPen(QPen(color_, 2, Qt::SolidLine, Qt::FlatCap));
  
      const double dpr = screen_dpi();
      const double line_length_pixels = std::fmax(1, dpr);
      const double step = std::fmax(.1, line_length_pixels / c.x());
      const double maxx = config->max_input();
  
  #define USE_CUBIC_SPLINE
  #if defined USE_CUBIC_SPLINE
      QPainterPath path;
  
      path.moveTo(point_to_pixel({}));
  
      const double max_x_pixel = point_to_pixel({maxx, 0}).x();
  
      auto clamp = [=](const QPointF& val) {
          return val.x() <= max_x_pixel
                 ? val
                 : QPointF{max_x_pixel, val.y()};
      };
  
      const auto fn = [&] (double k) {
        const auto next_1 = config->get_value_no_save(k + step*1);
        const auto next_2 = config->get_value_no_save(k + step*2);
        const auto next_3 = config->get_value_no_save(k + step*3);
  
        QPointF b(clamp(point_to_pixel({k + step*1, next_1}))),
            c(clamp(point_to_pixel({k + step*2, next_2}))),
            d(clamp(point_to_pixel({k + step*3, next_3})));
  
        path.cubicTo(b, c, d);
      };
  
      for (double k = 0; k < maxx; k += step*3) // NOLINT
          fn(k);
  
      fn(maxx);
  
      painter.drawPath(path);
  #else
      QPointF prev = point_to_pixel({});
      for (double i = 0; i < maxx; i += step) // NOLINT
      {
          const auto val = config->get_value_no_save(i);
          const QPointF cur = point_to_pixel({i, val});
          painter.drawLine(prev, cur);
          prev = cur;
      }
      {
          const double maxx = config->max_input();
          const double maxy = config->get_value_no_save(maxx);
          painter.drawLine(prev, point_to_pixel({ maxx, maxy }));
      }
  #endif
  
      const QRect r1(pixel_bounds.left(), 0, width() - pixel_bounds.left(), pixel_bounds.top()),
                  r2(pixel_bounds.right(), 0, width() - pixel_bounds.right(), pixel_bounds.bottom());
  
      // prevent topward artifacts the lazy way
      painter.fillRect(r1, widget_bg_color);
      // same for rightward artifacts
      painter.fillRect(r2, widget_bg_color);
  
      const int alpha = !isEnabled() ? 64 : 120;
      if (!preview_only)
      {
          for (auto const& point : points)
          {
              drawPoint(painter,
                        point_to_pixel(point),
                        QColor(200, 200, 210, alpha),
                        isEnabled() ? QColor(50, 100, 120, 200) : QColor(200, 200, 200, 96));
          }
      }
  }
  
  QSize spline_widget::minimumSizeHint() const
  {
      const double dpi = screen_dpi();
      return { iround(800 * dpi), iround(250 * dpi) };
  }
  
  void spline_widget::paintEvent(QPaintEvent *e)
  {
      if (!config)
          return;
  
      QPainter p(this);
  
      const double dpr = devicePixelRatioF();
      const int W = iround(width() * dpr);
      const int H = iround(height() * dpr);
  
      if (background_img.size() != QSize(W, H))
      {
          update_range();
  
          background_img = { W, H };
          background_img.setDevicePixelRatio(dpr);
          drawBackground();
          draw_function = true;
      }
  
      if (draw_function)
      {
          draw_function = false;
          spline_img = background_img;
          drawFunction();
      }
  
      p.drawPixmap(e->rect(), spline_img);
  
      // If the Tracker is active, the 'Last Point' it requested is recorded.
      // Show that point on the graph, with some lines to assist.
      // This new feature is very handy for tweaking the curves!
      QPointF last;
      if (config->get_last_value(last) && isEnabled())
          drawPoint(p, point_to_pixel(last), QColor(255, 0, 0, 120));
  }
  
  void spline_widget::drawPoint(QPainter& painter, const QPointF& pos, const QColor& colBG, const QColor& border)
  {
      painter.save();
      painter.setPen(QPen(border, 1, Qt::SolidLine, Qt::PenCapStyle::FlatCap));
      painter.setBrush(colBG);
      painter.drawEllipse(QRectF{pos.x() - point_size_in_pixels,
                                 pos.y() - point_size_in_pixels,
                                 point_size_in_pixels*2, point_size_in_pixels*2});
      painter.restore();
  }
  
  void spline_widget::drawLine(QPainter& painter, const QPointF& start, const QPointF& end, const QPen& pen)
  {
      painter.save();
      painter.setPen(pen);
      painter.setBrush(Qt::NoBrush);
      painter.drawLine(start, end);
      painter.restore();
  }
  
  void spline_widget::mousePressEvent(QMouseEvent *e)
  {
      if (!config || !isEnabled() || !is_in_bounds(e->localPos()) || preview_only)
          return;
  
      const double min_dist = min_pt_distance();
  
      moving_control_point_idx = -1;
  
      const points_t& points = config->get_points();
  
      if (e->button() == Qt::LeftButton)
      {
          bool is_touching_point = false;
  
          for (int i = 0; i < points.size(); i++)
          {
              if (point_within_pixel(points[i], e->localPos()))
              {
                  is_touching_point = true;
                  moving_control_point_idx = i;
                  break;
              }
          }
  
          if (!is_touching_point)
          {
              bool too_close = false;
              const QPointF pos = pixel_to_point(e->localPos());
  
              for (QPointF const& point : points)
              {
                  const double x = std::fabs(point.x() - pos.x());
                  if (min_dist > x)
                  {
                      too_close = true;
                      break;
                  }
              }
  
              if (!too_close)
              {
                  config->add_point(pixel_to_point(e->localPos()));
                  show_tooltip(e->pos());
              }
          }
  
          draw_function = true;
      }
  
      if (e->button() == Qt::RightButton)
      {
          if (config)
          {
              for (int i = 0; i < points.size(); i++)
              {
                  if (point_within_pixel(points[i], e->localPos()))
                  {
                      config->remove_point(i);
                      draw_function = true;
                      break;
                  }
              }
          }
      }
  
      if (draw_function)
          repaint();
  }
  
  void spline_widget::mouseMoveEvent(QMouseEvent *e)
  {
      if (preview_only && config)
      {
          show_tooltip(e->pos());
          return;
      }
  
      if (!config || !isEnabled() || !isActiveWindow())
      {
          QToolTip::hideText();
          return;
      }
  
      const int i = moving_control_point_idx;
      const points_t& points = config->get_points();
      const int sz = points.size();
  
      if (i >= 0 && i < sz)
      {
          const double min_dist = min_pt_distance();
          QPointF new_pt = pixel_to_point(e->localPos());
  
          const bool has_prev = i > 0, has_next = i + 1 < points.size();
  
          auto check_next = [&] {
              return points[i+1].x() - new_pt.x() >= min_dist;
          };
  
          auto check_prev = [&] {
              return new_pt.x() - points[i-1].x() >= min_dist;
          };
  
          if (has_prev && !check_prev())
          {
              new_pt.rx() = points[i-1].x() + min_dist + 1e-4;
          }
  
          if (has_next && !check_next())
          {
              new_pt.rx() = points[i+1].x() - min_dist - 1e-4;
          }
  
          setCursor(Qt::ClosedHandCursor);
          show_tooltip(point_to_pixel(new_pt).toPoint(), new_pt);
  
          if ((!has_prev || check_prev()) && (!has_next || check_next()))
          {
              config->move_point(i, new_pt);
              draw_function = true;
              repaint();
          }
      }
      else if (sz)
      {
          int i;
          bool is_on_point = is_on_pt(e->localPos(), &i);
  
          if (is_on_point)
          {
              setCursor(Qt::CrossCursor);
              show_tooltip(e->pos(), points[i]);
          }
          else
          {
              setCursor(Qt::ArrowCursor);
              if (is_in_bounds(e->localPos()))
                  show_tooltip(e->pos());
              else
                  QToolTip::hideText();
          }
      }
  }
  
  void spline_widget::mouseReleaseEvent(QMouseEvent *e)
  {
      if (!config || !isEnabled() || !isActiveWindow() || preview_only)
          return;
  
      const bool redraw = moving_control_point_idx != -1;
      moving_control_point_idx = -1;
  
      if (e->button() == Qt::LeftButton)
      {
          {
              if (is_on_pt(e->localPos(), nullptr))
                  setCursor(Qt::CrossCursor);
              else
                  setCursor(Qt::ArrowCursor);
          }
  
          if (is_in_bounds(e->localPos()))
              show_tooltip(e->pos());
          else
              QToolTip::hideText();
      }
  
      if (redraw)
      {
          draw_function = true;
          repaint();
      }
  }
  
  void spline_widget::reload_spline()
  {
      // don't recompute here as the value's about to be recomputed in the callee
      background_img = {};
      update();
  }
  
  double spline_widget::min_pt_distance() const
  {
      double pt = 3*point_size_in_pixels / c.x();
      pt = snap(pt, snap_x);
      return pt;
  }
  
  void spline_widget::show_tooltip(const QPoint& pos, const QPointF& value_)
  {
      const QPointF value = value_.isNull() ? pixel_to_point(pos) : value_;
  
      double x = value.x(), y = value.y();
  
      if (preview_only)
          y = config->get_value_no_save(x);
  
      const int x_ = iround(x), y_ = iround(y);
  
      if (std::fabs(x_ - x) < 1e-3)
          x = x_;
      if (std::fabs(y_ - y) < 1e-3)
          y = y_;
  
      // the native OSX style doesn't look right otherwise
  #if defined __APPLE__
      constexpr int off_x = 0, off_y = 0;
  #else
      constexpr int off_x = 25, off_y = 15;
  #endif
  
      const QPoint pix(pos.x() + off_x, pos.y() + off_y);
  
      QToolTip::showText(mapToGlobal(pix),
                         QString{"value: %1x%2"}.arg(x, 0, 'f', 2).arg(y, 0, 'f', 2),
                         this,
                         rect(),
                         0);
  }
  
  bool spline_widget::is_in_bounds(const QPointF& pos) const
  {
      const int grace = (int)std::ceil(point_size_in_pixels * 3);
      const int bottom_grace = (int)std::ceil(point_size_in_pixels * 1.5);
      return (pos.x() + grace        > pixel_bounds.left() &&
              pos.x() - grace        < pixel_bounds.right() &&
              pos.y() + grace        > pixel_bounds.top() &&
              pos.y() - bottom_grace < pixel_bounds.bottom());
  }
  
  void spline_widget::update_range()
  {
      if (!config)
          return;
  
      const int w = width(), h = height();
      const int mwl = 40, mhl = 20;
      const int mwr = 15, mhr = 35;
  
      pixel_bounds = { mwl, mhl, (w - mwl - mwr), (h - mhl - mhr) };
      c = {
          pixel_bounds.width() / std::fmax(1, config->max_input()),
          pixel_bounds.height() / std::fmax(1, config->max_output())
      };
  
      draw_function = true;
      background_img = {};
      spline_img = {};
  }
  
  bool spline_widget::point_within_pixel(const QPointF& pt, const QPointF& pixel)
  {
      const QPointF tmp = pixel - point_to_pixel(pt);
      return QPointF::dotProduct(tmp, tmp) < point_size_in_pixels * point_size_in_pixels;
  }
  
  void spline_widget::focusOutEvent(QFocusEvent* e)
  {
      if (moving_control_point_idx != -1)
          QToolTip::hideText();
      moving_control_point_idx = -1;
      draw_function = true;
      lower();
      setCursor(Qt::ArrowCursor);
      e->accept();
  }
  
  double spline_widget::snap(double x, double snap_value)
  {
      if (snap_value > 0)
      {
          constexpr int c = 1000;
          x += snap_value * .5;
          x -= std::fmod(x, snap_value);
          // truncate after few decimal places to reduce rounding errors.
          // round upward.
          x = int(x * c + .5/c) / double(c);
      }
  
      return x;
  }
  
  QPointF spline_widget::pixel_to_point(const QPointF& point)
  {
      double x = (point.x() - pixel_bounds.x()) / c.x();
      double y = (pixel_bounds.height() - point.y() + pixel_bounds.y()) / c.y();
  
      if (snap_x > 0)
          x = snap(x, snap_x);
      if (snap_y > 0)
          y = snap(y, snap_y);
  
      x = std::clamp(x, 0., config->max_input());
      y = std::clamp(y, 0., config->max_output());
  
      return { x, y };
  }
  
  QPointF spline_widget::point_to_pixel(const QPointF& point)
  {
      return {
          pixel_bounds.x() + point.x() * c.x(),
          pixel_bounds.y() + pixel_bounds.height() - point.y() * c.y()
      };
  }
  
  void spline_widget::resizeEvent(QResizeEvent *)
  {
      reload_spline();
  }
  
  bool spline_widget::is_on_pt(const QPointF& pos, int* pt)
  {
      if (!config)
      {
          if (pt)
              *pt = -1;
          return false;
      }
  
      const points_t& points = config->get_points();
  
      for (int i = 0; i < points.size(); i++)
      {
          if (point_within_pixel(points[i], pos))
          {
              if (pt)
                  *pt = i;
              return true;
          }
      }
  
      if (pt)
          *pt = -1;
      return false;
  }
  
  void spline_widget::changeEvent(QEvent* e)
  {
      switch (e->type())
      {
      case QEvent::EnabledChange:
          background_img = {}; spline_img = {};
          update();
          break;
      default:
          break;
      }
  }
  } // ns spline_detail
  