

#include <math.h>
#include <array>
#include <QtGlobal>

#include "toonzqt/doublefield.h"
#include "toonzqt/dvdialog.h"
#include "tunit.h"

#include <QDoubleValidator>
#include <QHBoxLayout>
#include <QFocusEvent>
#include <QSlider>

using namespace DVGui;

//=============================================================================
// DoubleValueLineEdit
//-----------------------------------------------------------------------------

void DoubleValueLineEdit::focusOutEvent(QFocusEvent *e) {
  double value = getValue();
  double minValue, maxValue;
  getRange(minValue, maxValue);

  bool isOutOfRange;
  // Absorb rounding errors
  MeasuredDoubleLineEdit *lineEdit =
      qobject_cast<MeasuredDoubleLineEdit *>(this);
  if (lineEdit) {
    int decimal  = lineEdit->getDecimals();
    isOutOfRange = (value < minValue - std::pow(0.1, decimal + 1) ||
                    value > maxValue + std::pow(0.1, decimal + 1));
  } else {
    isOutOfRange = (value < minValue || value > maxValue);
  }

  if (isOutOfRange) {
    setValue(tcrop(value, minValue, maxValue));
    emit editingFinished();
  }

  QLineEdit::focusOutEvent(e);
  m_isTyping = false;
}

//-----------------------------------------------------------------------------

void DoubleValueLineEdit::mousePressEvent(QMouseEvent *e) {
  if (e->buttons() == Qt::MiddleButton) {
    m_xMouse           = e->x();
    m_mouseDragEditing = true;
    e->accept();
  } else {
    QLineEdit::mousePressEvent(e);
    if (!m_isTyping) {  // only the first click will select all
      selectAll();
      m_isTyping = true;
    }
  }
}

//-----------------------------------------------------------------------------

void DoubleValueLineEdit::mouseMoveEvent(QMouseEvent *e) {
  if (m_mouseDragEditing && (e->buttons() & Qt::MiddleButton)) {
    setValue(getValue() + ((e->x() - m_xMouse) / 2));
    m_xMouse = e->x();
    emit valueChanged();
    e->accept();
  } else {
    QLineEdit::mouseMoveEvent(e);
  }
}

//-----------------------------------------------------------------------------

void DoubleValueLineEdit::mouseReleaseEvent(QMouseEvent *e) {
  if (m_mouseDragEditing && e->buttons() == Qt::NoButton) {
    m_mouseDragEditing = false;
    clearFocus();
    e->accept();
  } else {
    QLineEdit::mouseReleaseEvent(e);
  }
}

//-----------------------------------------------------------------------------

void DoubleValueLineEdit::keyPressEvent(QKeyEvent *event) {
  // Allow users to type comma as decimal separator by converting it to a dot.
  if (event->text() == ",") {
    QKeyEvent dotEvent(QEvent::KeyPress, Qt::Key_Period, Qt::NoModifier, ".");
    QLineEdit::keyPressEvent(&dotEvent);
  } else if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) {
    // Validate value before emitting signal to prevent crashes
    double value = getValue();
    double minValue, maxValue;
    getRange(minValue, maxValue);

    // Clamp value to valid range before processing
    if (!std::isfinite(value) || value < minValue || value > maxValue) {
      setValue(std::clamp(value, minValue, maxValue));
    }
    emit editingFinished();
    clearFocus();
  } else {
    QLineEdit::keyPressEvent(event);
  }
}
//=============================================================================
// DoubleValueField
//-----------------------------------------------------------------------------

DoubleValueField::DoubleValueField(QWidget *parent,
                                   DoubleValueLineEdit *lineEdit)
    : QWidget(parent)
    , m_lineEdit(lineEdit)
    , m_slider(0)
    , m_roller(0)
    , m_isLinearSlider(true) {
  assert(m_lineEdit);

  QWidget *field = new QWidget(this);
  m_roller       = new RollerField(field);
  m_slider       = new QSlider(Qt::Horizontal, this);

  field->setMaximumWidth(100);

  //---- layout
  QHBoxLayout *layout = new QHBoxLayout(this);
  layout->setContentsMargins(0, 0, 0, 0);
  layout->setSpacing(5);
  {
    QVBoxLayout *vLayout = new QVBoxLayout(field);
    vLayout->setContentsMargins(0, 0, 0, 0);
    vLayout->setSpacing(0);
    {
      vLayout->addWidget(m_lineEdit);
      vLayout->addWidget(m_roller);
    }
    layout->addWidget(field);
    layout->addWidget(m_slider);
  }
  setLayout(layout);

  //---- signal/slot connections
  bool ret = true;
  ret      = ret && connect(m_lineEdit, SIGNAL(valueChanged()),
                            SLOT(onLineEditValueChanged()));
  ret      = ret && connect(m_roller, SIGNAL(valueChanged(bool)),
                            SLOT(onRollerValueChanged(bool)));
  ret      = ret && connect(m_slider, SIGNAL(valueChanged(int)),
                            SLOT(onSliderChanged(int)));
  ret      = ret &&
        connect(m_slider, SIGNAL(sliderReleased()), SLOT(onSliderReleased()));
  ret = ret && connect(m_lineEdit, SIGNAL(editingFinished()), this,
                       SIGNAL(valueEditedByHand()));
  ret = ret && connect(m_slider, SIGNAL(sliderReleased()), this,
                       SIGNAL(valueEditedByHand()));
  assert(ret);

  m_spaceWidget = new QWidget();
  m_spaceWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
  layout->addWidget(m_spaceWidget, 1, Qt::AlignLeft);

  setRange(-100.0, 100.0);

  double value = getValue();
  m_roller->setValue(value);
  m_slider->setValue(value2pos(value));
}

//-----------------------------------------------------------------------------

double DoubleValueField::pos2value(int x) const {
  const int decimal = m_lineEdit->getDecimals();
  if (m_isLinearSlider) return static_cast<double>(x) * std::pow(0.1, decimal);

  // nonlinear slider case - use constexpr where is possible
  static constexpr std::array<double, 4> thresholds{0.5, 0.75, 0.9, 1.0};
  const double rangeSize =
      static_cast<double>(m_slider->maximum() - m_slider->minimum());

  // Safety check for zero range
  if (std::abs(rangeSize) < std::numeric_limits<double>::epsilon()) {
    return static_cast<double>(x) * std::pow(0.1, decimal);
  }

  const double posRatio =
      static_cast<double>(x - m_slider->minimum()) / rangeSize;

  double t = 0.0;
  if (posRatio <= thresholds[0]) {
    t = 0.04 * posRatio;
  } else if (posRatio <= thresholds[1]) {
    t = -0.02 + 0.08 * posRatio;
  } else if (posRatio <= thresholds[2]) {
    t = -0.26 + 0.4 * posRatio;
  } else {
    t = -8.0 + 9.0 * posRatio;
  }

  const double sliderValue =
      std::round(static_cast<double>(m_slider->minimum()) + rangeSize * t);
  return sliderValue * std::pow(0.1, decimal);
}

//-----------------------------------------------------------------------------

int DoubleValueField::value2pos(double v) const {
  int decimal        = m_lineEdit->getDecimals();
  double sliderValue = round(v * std::pow(10., decimal));
  if (m_isLinearSlider) return (int)sliderValue;

  // nonlinear slider case
  double rangeSize  = (double)(m_slider->maximum() - m_slider->minimum());
  double valueRatio = (sliderValue - (double)m_slider->minimum()) / rangeSize;
  double t;
  if (valueRatio <= 0.02)
    t = valueRatio / 0.04;
  else if (valueRatio <= 0.04)
    t = (valueRatio + 0.02) / 0.08;
  else if (valueRatio <= 0.1)
    t = (valueRatio + 0.26) / 0.4;
  else
    t = (valueRatio + 8.0) / 9.0;
  return m_slider->minimum() + (int)(t * rangeSize);
}

//-----------------------------------------------------------------------------

void DoubleValueField::getRange(double &minValue, double &maxValue) {
  m_lineEdit->getRange(minValue, maxValue);
}

//-----------------------------------------------------------------------------

void DoubleValueField::setRange(double minValue, double maxValue) {
  m_lineEdit->setRange(minValue, maxValue);

  m_roller->setRange(minValue, maxValue);

  int decimal   = m_lineEdit->getDecimals();
  int sliderMax = (int)round(maxValue * std::pow(10., decimal));
  int sliderMin = (int)round(minValue * std::pow(10., decimal));

  m_slider->setRange(sliderMin, sliderMax);

  enableSlider(maxValue - minValue < 10000);
}

//-----------------------------------------------------------------------------

void DoubleValueField::setValue(double value) {
  if (!std::isfinite(value) ||
      qFuzzyCompare(m_lineEdit->getValue() + 1.0, value + 1.0))
    return;
  double minValue, maxValue;
  getRange(minValue, maxValue);
  // Safer clamping
  if (value < minValue) value = minValue;
  if (value > maxValue) value = maxValue;
  bool signalsBlocked = m_slider->blockSignals(true);
  m_lineEdit->setValue(value);
  m_roller->setValue(value);
  int newSliderPos = value2pos(value);
  if (m_slider->value() != newSliderPos) {
    m_slider->setValue(newSliderPos);
    // Force repaint()... sometimes it doesn't update and update doesn't seem to
    // solve the problem!!! try use update() for now
    m_slider->update();
  }
  m_slider->blockSignals(signalsBlocked);
}

//-----------------------------------------------------------------------------

double DoubleValueField::getValue() { return (m_lineEdit->getValue()); }

//-----------------------------------------------------------------------------

void DoubleValueField::setValues(double value, double minValue,
                                 double maxValue) {
  setRange(minValue, maxValue);
  setValue(value);
}

//-----------------------------------------------------------------------------

void DoubleValueField::enableSlider(bool enable) {
  m_slider->setEnabled(enable);
  m_spaceWidget->setEnabled(!enable);
  if (enable) {
    m_slider->show();
    m_spaceWidget->hide();
  } else {
    m_slider->hide();
    m_spaceWidget->show();
  }
}

//-----------------------------------------------------------------------------

bool DoubleValueField::isSliderEnabled() { return m_slider->isEnabled(); }

//-----------------------------------------------------------------------------

void DoubleValueField::enableRoller(bool enable) {
  m_roller->setEnabled(enable);
  if (enable)
    m_roller->show();
  else
    m_roller->hide();
}

//-----------------------------------------------------------------------------

bool DoubleValueField::isRollerEnabled() { return m_roller->isEnabled(); }

//-----------------------------------------------------------------------------

void DoubleValueField::onSliderChanged(int sliderPos) {
  double val = pos2value(sliderPos);

  // Control necessary to prevent the change signal from being emitted more than
  // once.
  if (qFuzzyCompare(m_lineEdit->getValue() + 1.0, val + 1.0) ||
      (qFuzzyCompare(m_roller->getValue() + 1.0, val + 1.0) &&
       m_roller->isVisible()))
    return;
  m_lineEdit->setValue(val);
  m_roller->setValue(val);
  // Make the cursor on the first digit, so if the string to display is longer
  // than the field the digits that are truncated are the last ones and not the
  // first (they should be the ones after the decimal point).
  m_lineEdit->setCursorPosition(0);

  emit valueChanged(true);
}

//-----------------------------------------------------------------------------

void DoubleValueField::onLineEditValueChanged() {
  double value = m_lineEdit->getValue();
  int decimal  = m_lineEdit->getDecimals();

  // Control necessary to prevent the change signal from being emitted more than
  // once.
  if ((qFuzzyCompare(pos2value(m_slider->value()) + 1.0, value + 1.0) &&
       m_slider->isVisible()) ||
      (qFuzzyCompare(m_roller->getValue() + 1.0, value + 1.0) &&
       m_roller->isVisible()))
    return;
  m_slider->setValue(value2pos(value));
  m_roller->setValue(value);
  emit valueChanged(false);
}

//-----------------------------------------------------------------------------

void DoubleValueField::onRollerValueChanged(bool isDragging) {
  double value = m_roller->getValue();

  if (qFuzzyCompare(value + 1.0, m_lineEdit->getValue() + 1.0)) {
    assert(qFuzzyCompare(pos2value(m_slider->value()) + 1.0, value + 1.0) ||
           !m_slider->isVisible());
    // If isDragging is false, it's right that the change notification is
    // emitted.
    if (!isDragging) emit valueChanged(isDragging);
    return;
  }
  m_slider->setValue(value2pos(value));
  m_lineEdit->setValue(value);

  // Make the cursor on the first digit, so if the string to display
  // is longer than the field the digits that are truncated are the
  // last ones and not the first (they should be the ones after the decimal
  // point).
  m_lineEdit->setCursorPosition(0);

  emit valueChanged(isDragging);
}

//=============================================================================
// DoubleLineEdit
//-----------------------------------------------------------------------------

DoubleLineEdit::DoubleLineEdit(QWidget *parent, double value)
    : DoubleValueLineEdit(parent) {
  m_validator =
      new QDoubleValidator(-(std::numeric_limits<double>::max)(),
                           (std::numeric_limits<double>::max)(), 5, this);
  m_validator->setLocale(QLocale::c());

  setValidator(m_validator);
  // Limit maximum characters to prevent extreme values
  setMaxLength(9);

  setValue(value);

  bool ret = connect(this, SIGNAL(editingFinished()), SIGNAL(valueChanged()));
  assert(ret);
}

//-----------------------------------------------------------------------------

void DoubleLineEdit::setValue(double value) {
  if (!std::isfinite(value)) return;
  double minValue, maxValue;
  getRange(minValue, maxValue);
  value = std::clamp(value, minValue, maxValue);
  QString str;
  str.setNum(value);
  int pos = 0;
  if (m_validator->validate(str, pos) != QValidator::Acceptable) {
    // If it fails, check for too many decimal places and truncate if needed
    int decimals   = getDecimals();
    int decimalPos = str.indexOf('.');
    if (decimals != -1 && decimalPos != -1 &&
        (str.length() - decimalPos - 1) > decimals) {
      double factor = std::pow(10.0, getDecimals());
      value         = std::round(value * factor) / factor;
      str.setNum(value);
    }
  }
  setText(str);

  // Ensures only decimals are truncated, not leading digits.
  setCursorPosition(0);
}

//-----------------------------------------------------------------------------

double DoubleLineEdit::getValue() { return QLocale::c().toDouble(text()); }

//-----------------------------------------------------------------------------

void DoubleLineEdit::setRange(double minValue, double maxValue) {
  m_validator->setRange(minValue, maxValue, m_validator->decimals());
}

//-----------------------------------------------------------------------------

void DoubleLineEdit::getRange(double &minValue, double &maxValue) {
  minValue = m_validator->bottom();
  maxValue = m_validator->top();
}

//-----------------------------------------------------------------------------

void DoubleLineEdit::setDecimals(int decimals) {
  m_validator->setDecimals(decimals);
}

//-----------------------------------------------------------------------------

int DoubleLineEdit::getDecimals() { return m_validator->decimals(); }

//=============================================================================
// DoubleField
//-----------------------------------------------------------------------------

DoubleField::DoubleField(QWidget *parent, bool isRollerHide, int decimals)
    : DoubleValueField(parent, new DoubleLineEdit(0)) {
  if (isRollerHide) enableRoller(false);

  DoubleLineEdit *lineEdit = qobject_cast<DoubleLineEdit *>(m_lineEdit);
  assert(lineEdit);
  lineEdit->setDecimals(decimals);

  // Set step for roller too
  if (!isRollerHide) m_roller->setStep(std::pow(0.1, decimals));
}

//=============================================================================
// MeasuredDoubleLineEdit
//-----------------------------------------------------------------------------

MeasuredDoubleLineEdit::MeasuredDoubleLineEdit(QWidget *parent)
    : DoubleValueLineEdit(parent)
    , m_minValue(-(std::numeric_limits<double>::max)())
    , m_maxValue((std::numeric_limits<double>::max)())
    , m_modified(false)
    , m_errorHighlighting(0)
    , m_errorHighlightingTimerId(0)
    , m_decimals(7) {
  setObjectName("ValueLineEdit");
  // Limit character input to prevent extreme values
  setMaxLength(9);
  m_value = new TMeasuredValue("length");
  valueToText();
  bool ret =
      connect(this, SIGNAL(editingFinished()), this, SLOT(onEditingFinished()));
  ret = ret && connect(this, SIGNAL(textChanged(const QString &)), this,
                       SLOT(onTextChanged(const QString &)));
  assert(ret);
}

//-----------------------------------------------------------------------------

MeasuredDoubleLineEdit::~MeasuredDoubleLineEdit() { delete m_value; }

//-----------------------------------------------------------------------------

void MeasuredDoubleLineEdit::valueToText() {
  bool oldModified = m_modified;
  setText(QString::fromStdWString(m_value->toWideString(m_decimals)));
  setCursorPosition(0);
  m_modified = oldModified;
}

//-----------------------------------------------------------------------------

void MeasuredDoubleLineEdit::setValue(double value) {
  m_value->setValue(TMeasuredValue::MainUnit, value);
  valueToText();
  m_modified = false;
}

//-----------------------------------------------------------------------------

double MeasuredDoubleLineEdit::getValue() {
  return m_value->getValue(TMeasuredValue::MainUnit);
}

//-----------------------------------------------------------------------------

void MeasuredDoubleLineEdit::setRange(double minValue, double maxValue) {
  m_minValue = minValue;
  m_maxValue = maxValue;
}

//-----------------------------------------------------------------------------

void MeasuredDoubleLineEdit::getRange(double &minValue, double &maxValue) {
  minValue = m_minValue;
  maxValue = m_maxValue;
}

//-----------------------------------------------------------------------------

void MeasuredDoubleLineEdit::setMeasure(std::string name) {
  delete m_value;
  m_value = new TMeasuredValue(name != "" ? name : "dummy");
  valueToText();
}

//-----------------------------------------------------------------------------

void MeasuredDoubleLineEdit::setDecimals(int decimals) {
  m_decimals = decimals;
  valueToText();
}

//-----------------------------------------------------------------------------

int MeasuredDoubleLineEdit::getDecimals() { return m_decimals; }

//-----------------------------------------------------------------------------

void MeasuredDoubleLineEdit::onTextChanged(const QString &) {
  m_modified = true;
}

//-----------------------------------------------------------------------------

void MeasuredDoubleLineEdit::onEditingFinished() {
  if (!m_modified) return;
  m_modified = false;

  double oldValue       = getValue();
  QString oldStyleSheet = styleSheet();

  int err = -10;
  m_value->setValue(text().toStdWString(), &err);

  bool outOfRange = false;
  if (!err) {
    double v   = getValue();
    outOfRange = m_minValue > v || m_maxValue < v;
  }

  if (err) {
    m_errorHighlighting = 1;
    if (m_errorHighlightingTimerId == 0)
      m_errorHighlightingTimerId = startTimer(40);
  } else {
    if (m_errorHighlightingTimerId != 0) killTimer(m_errorHighlightingTimerId);
    m_errorHighlightingTimerId = 0;
    m_errorHighlighting        = 0;
    setStyleSheet("");
  }

  double newValue = getValue();
  if (m_minValue > newValue || m_maxValue < newValue) {
    m_value->setValue(TMeasuredValue::MainUnit, oldValue);
    valueToText();
    emit valueChanged();
    return;
  }
  valueToText();
  emit valueChanged();
}

//-----------------------------------------------------------------------------

void MeasuredDoubleLineEdit::timerEvent(QTimerEvent *) {
  if (m_errorHighlighting < 0.01) {
    if (m_errorHighlightingTimerId != 0) killTimer(m_errorHighlightingTimerId);
    m_errorHighlightingTimerId = 0;
    m_errorHighlighting        = 0;
    setStyleSheet("");
  } else {
    int v               = 255 - (int)(m_errorHighlighting * 255);
    m_errorHighlighting = m_errorHighlighting * 0.8;
    int c               = 255 << 16 | v << 8 | v;
    setStyleSheet(QString("#ValueLineEdit {background-color:#%1}")
                      .arg(c, 6, 16, QLatin1Char('0')));
  }
}

//-----------------------------------------------------------------------------

void MeasuredDoubleLineEdit::mousePressEvent(QMouseEvent *e) {
  if ((e->buttons() == Qt::MiddleButton) || m_labelClicked) {
    m_xMouse           = e->x();
    m_mouseDragEditing = true;
    e->accept();
  } else {
    QLineEdit::mousePressEvent(e);
    if (!m_isTyping) {  // only the first click will select all
      selectAll();
      m_isTyping = true;
    }
  }
}

//-----------------------------------------------------------------------------

void MeasuredDoubleLineEdit::mouseMoveEvent(QMouseEvent *e) {
  if ((m_mouseDragEditing && (e->buttons() & Qt::MiddleButton)) ||
      m_labelClicked) {
    int precision = getDecimals();
    m_value->modifyValue((e->x() - m_xMouse) / 2, precision);
    m_xMouse = e->x();
    valueToText();
    m_modified = false;
    e->accept();
  } else {
    QLineEdit::mouseMoveEvent(e);
  }
}

//-----------------------------------------------------------------------------

void MeasuredDoubleLineEdit::mouseReleaseEvent(QMouseEvent *e) {
  if ((m_mouseDragEditing && e->buttons() == Qt::NoButton) || m_labelClicked) {
    m_xMouse   = -1;
    m_modified = true;
    onEditingFinished();
    clearFocus();
    m_mouseDragEditing = false;
    m_labelClicked     = false;
    e->accept();
  } else {
    QLineEdit::mouseReleaseEvent(e);
  }
}

void MeasuredDoubleLineEdit::receiveMousePress(QMouseEvent *e) {
  m_labelClicked = true;
  mousePressEvent(e);
}

void MeasuredDoubleLineEdit::receiveMouseMove(QMouseEvent *e) {
  mouseMoveEvent(e);
}

void MeasuredDoubleLineEdit::receiveMouseRelease(QMouseEvent *e) {
  mouseReleaseEvent(e);
  m_labelClicked = false;
}

//=============================================================================
// MeasuredDoubleField
//-----------------------------------------------------------------------------

MeasuredDoubleField::MeasuredDoubleField(QWidget *parent, bool isRollerHide)
    : DoubleValueField(parent, new MeasuredDoubleLineEdit(0)) {
  m_lineEdit->setMaximumWidth(100);
  if (isRollerHide) enableRoller(false);
}

//-----------------------------------------------------------------------------

void MeasuredDoubleField::setMeasure(std::string measureName) {
  MeasuredDoubleLineEdit *lineEdit =
      qobject_cast<MeasuredDoubleLineEdit *>(m_lineEdit);
  assert(lineEdit);
  lineEdit->setMeasure(measureName);
}

//-----------------------------------------------------------------------------

void MeasuredDoubleField::setDecimals(int decimals) {
  MeasuredDoubleLineEdit *lineEdit =
      qobject_cast<MeasuredDoubleLineEdit *>(m_lineEdit);
  if (lineEdit) lineEdit->setDecimals(decimals);
  // Set step for roller too
  if (isRollerEnabled())
    m_roller->setStep(std::pow(0.1, std::max(decimals - 1, 1)));
}
