#include "knobgadget.h"
using namespace Xybrid::UI;

#include "util/strings.h"
#include "ui/gadgets/layoutgadget.h"
#include "nodelib/basics.h"

#include "config/uiconfig.h"
using namespace Xybrid::Config;

#include <cmath>

#include <QDebug>
#include <QPainter>
#include <QRadialGradient>
#include <QGraphicsSceneMouseEvent>

template KnobGadget* KnobGadget::bind(double&);
template KnobGadget* KnobGadget::bind(float&);
template KnobGadget* KnobGadget::bind(int8_t&);
template KnobGadget* KnobGadget::bind(uint8_t&);
template KnobGadget* KnobGadget::bind(int16_t&);
template KnobGadget* KnobGadget::bind(uint16_t&);
template KnobGadget* KnobGadget::bind(int32_t&);
template KnobGadget* KnobGadget::bind(uint32_t&);

template KnobGadget* KnobGadget::bind(std::atomic<double>&);
template KnobGadget* KnobGadget::bind(std::atomic<float>&);
template KnobGadget* KnobGadget::bind(std::atomic<int8_t>&);
template KnobGadget* KnobGadget::bind(std::atomic<uint8_t>&);
template KnobGadget* KnobGadget::bind(std::atomic<int16_t>&);
template KnobGadget* KnobGadget::bind(std::atomic<uint16_t>&);
template KnobGadget* KnobGadget::bind(std::atomic<int32_t>&);
template KnobGadget* KnobGadget::bind(std::atomic<uint32_t>&);

double KnobGadget::get() {
    double v = fGet();
    if (v != lastVal) {
        lastVal = v;
        dirty = true;
    }
    return v;
}

KnobGadget::KnobGadget(QGraphicsItem* parent) : Gadget(parent) {
    static QFont f("Arcon Rounded", 7);

    label = new QGraphicsSimpleTextItem(this);
    valLabel = new QGraphicsSimpleTextItem(this);
    label->setBrush(QColor(255, 255, 255));
    valLabel->setBrush(QColor(255, 255, 255));
    label->setFont(f);
    valLabel->setFont(f);
    //label->setScale(0.8);
    //valLabel->setScale(0.8);
}

QRectF KnobGadget::boundingRect() const {
    return QRectF(0, 0, size, size);
}

QRectF KnobGadget::layoutBoundingRect() const {
    constexpr qreal lh = 14;
    return boundingRect().adjusted(0, -lh, 0, lh);
}

QPainterPath KnobGadget::shape() const {
    QPainterPath p;
    p.addEllipse(boundingRect());
    return p;
}

void KnobGadget::paint(QPainter* painter, const QStyleOptionGraphicsItem*, QWidget*) {
    auto val = get();
    auto proportion = (val - min) / (max - min);

    auto r = boundingRect();
    auto ir = r - QMarginsF(2, 2, 2, 2);

    if (dirty) {
        dirty = false;

        valLabel->setText(fText(val));

        const qreal pad = 2;

        auto ls = label->boundingRect().size() * label->scale();
        label->setPos(r.center().x() - ls.width() / 2.0, r.height() + pad);
        auto vls = valLabel->boundingRect().size() * valLabel->scale();
        valLabel->setPos(r.center().x() - vls.width() / 2.0, -vls.height() - pad);
    }

    QColor outline = QColor(31, 31, 31);
    if (highlighted) outline = QColor(127, 127, 255);
    QColor needle = QColor(127, 127, 127);

    // base circle
    painter->setPen(Qt::NoPen);
    painter->setBrush(outline);
    painter->drawEllipse(r);

    // range
    painter->setBrush(QColor(63, 63, 63));
    painter->drawPie(ir, 250*16, -320*16);

    // fill
    painter->setBrush(QColor(95, 95, 95));
    painter->drawPie(ir, 250*16, static_cast<int>(-320.0*16*proportion));

    // needle
    QRadialGradient gr = QRadialGradient(ir.center(), ir.width() / 2.0, ir.center());
    gr.setColorAt(0.0, QColor(0, 0, 0, 0));
    gr.setColorAt(1.0, needle);

    painter->setPen(QPen(gr, 1.25));
    painter->setBrush(Qt::NoBrush);
    painter->drawPie(ir, 250*16 + static_cast<int>(-320.0*16*proportion), 0);
}

namespace { // interaction tracking vars
    int acc = 0;
    int wAcc = 0;
    double trackVal;

    inline double stepRound(double v, double s) { return std::round(v / s) * s; }

    int accumulate(int& acc, int step) {
        int sgn = acc < 0 ? -1 : 1;
        int rm = std::abs(acc) % step;
        int diff = (std::abs(acc)-rm)*sgn / step;
        acc = rm*sgn;
        return diff;
    }
}

void KnobGadget::hoverEnterEvent(QGraphicsSceneHoverEvent*) {
    wAcc = 0; //reset wheel accumulator on hovering over a new knob
}

void KnobGadget::mousePressEvent(QGraphicsSceneMouseEvent* e) {
    if (e->button() == Qt::LeftButton) {
        highlighted = true;
        trackVal = get();
        auto mod = e->modifiers();
        if (mod.testFlag(Qt::AltModifier)) ; // no rounding until alt released
        else if (mod.testFlag(Qt::ShiftModifier) && subStep > 0.0)
            trackVal = stepRound(trackVal, subStep);
        else if (step > 0.0)
            trackVal = stepRound(trackVal, step);
        fSet(trackVal);
    } else if (e->button() == Qt::RightButton) {
        fSet(defaultVal);
        e->accept();
    }
    update();
}

void KnobGadget::mouseReleaseEvent(QGraphicsSceneMouseEvent* e) {
    if (e->button() == Qt::LeftButton) {
        highlighted = false;
    }
    update();
}

void KnobGadget::mouseMoveEvent(QGraphicsSceneMouseEvent* e) {
    if (highlighted) {
        auto curPos = e->pos().toPoint();
        auto lastPos = e->lastPos().toPoint();
        if (UIConfig::verticalKnobs) acc -= curPos.y() - lastPos.y();
        else acc += curPos.x() - lastPos.x();

        auto d = accumulate(acc, stepPx);

        auto mod = e->modifiers();
        if (mod.testFlag(Qt::AltModifier)) trackVal = std::clamp(trackVal, min, max); // soft release; just keep track within range
        else if (mod.testFlag(Qt::ShiftModifier) && subStep > 0) {
            trackVal = stepRound(trackVal + subStep*d, subStep);
            fSet(std::clamp(trackVal, min, max));
        } else if (step > 0) {
            trackVal = stepRound(trackVal + step*d, step);
            fSet(std::clamp(trackVal, min, max));
        }
    }
    update();
}

void KnobGadget::wheelEvent(QGraphicsSceneWheelEvent* e) {
    e->accept(); // never pass through

    wAcc += e->delta();
    auto d = accumulate(wAcc, 120);
    if (d == 0) return; // don't need to update anything if incomplete accumulation
    if (UIConfig::invertScrollWheel) d *= -1;

    auto mod = e->modifiers();
    if (highlighted) { // wheel while dragging
        /*if (mod.testFlag(Qt::ShiftModifier) && subStep > 0) {
            trackVal = stepRound(trackVal + subStep*d, subStep);
            fSet(std::clamp(trackVal, min, max));
        } else if (step > 0) {
            trackVal = stepRound(trackVal + step*d, step);
            fSet(std::clamp(trackVal, min, max));
        }*/
        return;
    }

    if (mod.testFlag(Qt::ShiftModifier) && subStep > 0)
        fSet(std::clamp(stepRound(get()+subStep*d, subStep), min, max));
    else if (step > 0)
        fSet(std::clamp(stepRound(get()+step*d, step), min, max));
    update();
}

void KnobGadget::contextMenuEvent(QGraphicsSceneContextMenuEvent* e) {
    e->accept();
}

QString KnobGadget::textPercent(double d) { return qs("%1%").arg(d*100); }
QString KnobGadget::textOffset(double d) { return (d > 0 ? qs("+%1") : qs("%1")).arg(d); }
QString KnobGadget::textFrequency(double d) { return qs("%1Hz").arg(d); }

QString KnobGadget::textGain(double d) { return (d > 0 ? qs("+%1dB") : qs("%1dB")).arg(d); }
QString KnobGadget::textBalance(double d) { return (d > 0 ? qs("+%1%") : qs("%1%")).arg(d*100); }

void KnobGadget::autoCreate(LayoutGadget* l, NodeLib::ADSR& adsr) {
    KnobGadget* k [[maybe_unused]];

    k = (new KnobGadget(l))->setRange(0.0, 5.0, 0.01)->bind(adsr.a)->setLabel(qs("Attack"));
    k = (new KnobGadget(l))->setRange(0.0, 5.0, 0.01)->bind(adsr.d)->setLabel(qs("Decay"));
    k = KnobGadget::autoPercent(l, adsr.s)->setLabel(qs("Sustain"));
    k = (new KnobGadget(l))->setRange(0.0, 5.0, 0.01)->bind(adsr.r)->setLabel(qs("Release"));
}
