#include "InkEngine.h"
#include "FbCapture.h"
#include "epfb.h"

#include <QImage>
#include <QPainter>
#include <QLineF>
#include <QFont>
#include <QThread>
#include <QtGlobal>
#include <cmath>

InkEngine::InkEngine(QObject *parent) : QObject(parent)
{
    m_timer.start();
    bool ok = false;
    const int v = qEnvironmentVariableIntValue("CHATTER_FULL_SM", &ok);
    if (ok) m_fullSm = v;
    const int g = qEnvironmentVariableIntValue("CHATTER_FLASH_GRAY", &ok);
    if (ok) m_flashGray = qBound(0, g, 255);

    // De-ghost once the user settles, not on every scroll-lift.
    m_deghostTimer.setSingleShot(true);
    connect(&m_deghostTimer, &QTimer::timeout, this, [this] { renderAll(true); });
}

void InkEngine::pushRect(const QRect &r)
{
    if (!r.isValid())
        return;
    if (void *fb = epfb_instance())
        epfb_swapBuffers(fb, {r.left(), r.top(), r.right(), r.bottom()}, 0, 0, 0);
}

// Manual de-ghost: drive the whole panel to black with a full update, then a
// full update to the real content. The black full-update cycles every particle
// (incl. the color ones that leave the red residue). Uses only our reliable
// QRect swapBuffers — NOT ghostControl(), whose region-swap corrupts internal
// state we don't maintain. Only called on scroll-end / clear (rare), so the
// ~0.4 s flash is acceptable.
void InkEngine::fullRefresh()
{
    if (!FbCapture::ready() || m_canvas.isNull())
        return;
    QImage fb = FbCapture::framebuffer();
    const QRect full(0, 0, m_screenW, m_screenH);
    const EpRect er{full.left(), full.top(), full.right(), full.bottom()};
    void *inst = epfb_instance();

    { QPainter p(&fb); p.fillRect(full, QColor(m_flashGray, m_flashGray, m_flashGray)); }
    if (inst) epfb_swapBuffers(inst, er, 0, m_fullSm, 0);
    QThread::msleep(220);

    { QPainter p(&fb);
      p.drawImage(full.topLeft(), m_canvas, full.translated(0, m_panY));
      for (const ButtonDef &b : m_buttons) paintButton(p, b.rect, b.text, false); }
    if (inst) epfb_swapBuffers(inst, er, 0, m_fullSm, 0);
}

// ---- virtual canvas ------------------------------------------------------

void InkEngine::ensureCanvas()
{
    if (!m_canvas.isNull() || !FbCapture::ready())
        return;
    const QImage fb = FbCapture::framebuffer();
    m_screenW = fb.width();
    m_screenH = fb.height();
    m_canvas = QImage(m_screenW, m_screenH * 2, QImage::Format_RGB32);
    m_canvas.fill(Qt::white);
}

void InkEngine::growIfNeeded(int canvasBottomY)
{
    if (m_canvas.isNull() || canvasBottomY < m_canvas.height() - m_screenH / 2)
        return;
    QImage bigger(m_screenW, m_canvas.height() + m_screenH, QImage::Format_RGB32);
    bigger.fill(Qt::white);
    QPainter p(&bigger);
    p.drawImage(0, 0, m_canvas);
    p.end();
    m_canvas = bigger;
}

// Copy a screen-space rect from the canvas window into the framebuffer, keep the
// buttons on top, and refresh just that rect.
void InkEngine::blitRegion(const QRect &screenRect, bool full)
{
    if (!FbCapture::ready() || m_canvas.isNull())
        return;
    if (full) { // full-screen de-ghosting refresh (draws content itself)
        fullRefresh();
        return;
    }
    const QRect sr = screenRect.intersected(QRect(0, 0, m_screenW, m_screenH));
    if (sr.isEmpty())
        return;
    QImage fb = FbCapture::framebuffer();
    QPainter p(&fb);
    p.drawImage(sr.topLeft(), m_canvas, sr.translated(0, m_panY)); // canvas -> screen
    for (const ButtonDef &b : m_buttons)
        if (b.rect.intersects(sr))
            paintButton(p, b.rect, b.text, false);
    p.end();
    pushRect(sr);
}

void InkEngine::renderAll(bool full) { blitRegion(QRect(0, 0, m_screenW, m_screenH), full); }

void InkEngine::panBy(qreal dyScreen)
{
    ensureCanvas();
    if (m_canvas.isNull())
        return;
    const int maxPan = qMax(0, m_canvas.height() - m_screenH);
    const int np = qBound(0, m_panY - qRound(dyScreen), maxPan); // natural scroll
    if (np == m_panY)
        return;
    m_deghostTimer.stop(); // still moving — postpone the de-ghost
    m_scrolled = true;
    m_panY = np;
    renderAll(false); // fast during the drag
}

// Scrolling stopped: de-ghost once after a short settle (debounced), so rapid
// scrolling doesn't flash on every lift.
void InkEngine::panEnd() { m_deghostTimer.start(1000); }

// ---- buttons -------------------------------------------------------------

void InkEngine::registerButton(int x, int y, int w, int h, const QString &text)
{
    const QRect r(x, y, w, h);
    m_buttons.append({r, text});
    m_uiMask = m_uiMask.united(QRegion(r));
}

int InkEngine::buttonAt(const QPointF &p) const
{
    for (int i = 0; i < m_buttons.size(); ++i)
        if (m_buttons[i].rect.contains(p.toPoint()))
            return i;
    return -1;
}

void InkEngine::paintButton(QPainter &p, const QRect &rect, const QString &text, bool pressed) const
{
    p.setRenderHint(QPainter::Antialiasing, true);
    const QRectF box(rect.x() + 1.5, rect.y() + 1.5, rect.width() - 3.0, rect.height() - 3.0);
    p.setPen(QPen(Qt::black, 3));
    p.setBrush(pressed ? QColor(0x9e, 0x9e, 0x9e) : QColor(Qt::white));
    p.drawRoundedRect(box, 8, 8);
    p.setPen(Qt::black);
    QFont f = p.font();
    f.setPixelSize(38);
    p.setFont(f);
    p.drawText(rect.translated(0, -2), Qt::AlignCenter, text); // keep in sync with Main.qml offset
}

void InkEngine::flashButton(int x, int y, int w, int h, const QString &text, bool pressed)
{
    if (!FbCapture::ready())
        return;
    QImage fb = FbCapture::framebuffer();
    QPainter p(&fb);
    paintButton(p, QRect(x, y, w, h), text, pressed);
    p.end();
    pushRect(QRect(x - 2, y - 2, w + 4, h + 4));
}

void InkEngine::activateButton(const QString &text)
{
    if (text == QLatin1String("Clear"))
        clearPage(); // also repaints the buttons (reverting any pressed state)
    else if (text == QLatin1String("Back"))
        emit backRequested(); // wired to AppControl::returnToStandard
}

// ---- finger-wipe erase ---------------------------------------------------

void InkEngine::eraseStart(qreal x, qreal y)
{
    ensureCanvas();
    m_erasePress = QPointF(x, y + m_panY); // canvas coords
    m_eraseLast = m_erasePress;
    m_erasing = false; // wait for real movement (tap-safe)
}

void InkEngine::eraseMove(qreal x, qreal y)
{
    if (!FbCapture::ready())
        return;
    ensureCanvas();
    if (m_canvas.isNull())
        return;
    const QPointF cp(x, y + m_panY); // canvas coords
    if (!m_erasing) {
        // Ignore tap jitter; only a deliberate wipe (moved > ~1.5 mm) erases.
        if (QLineF(m_erasePress, cp).length() < 16.0)
            return;
        m_erasing = true;
        m_eraseLast = m_erasePress; // erase the whole wipe, from first contact
    }
    constexpr qreal w = 124.0; // finger-wipe width (~12 mm @ 264 PPI)
    QPainter painter(&m_canvas);
    painter.setPen(QPen(Qt::white, w, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
    painter.drawLine(m_eraseLast, cp);
    painter.end();
    const QRect dirty = QRectF(m_eraseLast, cp).normalized()
                            .adjusted(-w - 2, -w - 2, w + 2, w + 2)
                            .toRect();
    m_eraseLast = cp;
    blitRegion(dirty.translated(0, -m_panY));
}

void InkEngine::eraseEnd() { m_erasing = false; }

// ---- ink -----------------------------------------------------------------

void InkEngine::strokeStart(QPointF p)
{
    m_btnDown = buttonAt(p); // screen coords
    if (m_btnDown >= 0) {
        const ButtonDef &b = m_buttons[m_btnDown];
        flashButton(b.rect.x(), b.rect.y(), b.rect.width(), b.rect.height(), b.text, true);
        m_drawing = false;
        return;
    }
    ensureCanvas();
    m_last = p + QPointF(0, m_panY); // canvas coords
    m_drawing = true;
    m_lastT = m_timer.elapsed();
}

void InkEngine::strokeMove(QPointF p, qreal pressure, qreal tiltX, qreal tiltY, bool eraser)
{
    if (!FbCapture::ready())
        return;

    // In a stylus button-tap: if the pen slides off the button, cancel it.
    if (m_btnDown >= 0) {
        if (buttonAt(p) != m_btnDown) {
            const ButtonDef &b = m_buttons[m_btnDown];
            flashButton(b.rect.x(), b.rect.y(), b.rect.width(), b.rect.height(), b.text, false);
            m_btnDown = -1;
        }
        return; // never ink during a button tap
    }

    ensureCanvas();
    if (m_canvas.isNull())
        return;
    const QPointF cp = p + QPointF(0, m_panY); // canvas coords
    if (!m_drawing) {
        m_last = cp;
        m_drawing = true;
        m_lastT = m_timer.elapsed();
    }

    QPainter painter(&m_canvas);
    painter.setRenderHint(QPainter::Antialiasing, false);

    const qreal span = m_maxWidth - m_minWidth;
    qreal extent; // half-width used to size the dirty rect
    if (eraser) {
        extent = 40.0;
        painter.setPen(QPen(Qt::white, 40.0, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
        painter.drawLine(m_last, cp);
    } else if (m_penType == 21) {
        // reMarkable Calligraphy is a DYNAMIC model (per reMarkable docs +
        // observed behavior): thicker on downstrokes, with more pressure, when
        // slow, and when the stylus is tilted — not a fixed geometric nib.
        const qreal len = QLineF(m_last, cp).length();
        const qreal dyN = len > 1e-3 ? (cp.y() - m_last.y()) / len : 0.0; // +1 down, -1 up
        const qreal dirF = 0.5 + 0.5 * dyN;                              // 0 (up) .. 1 (down)

        const qint64 now = m_timer.elapsed();
        const qreal dt = qMax<qint64>(1, now - m_lastT);
        m_lastT = now;
        const qreal speed = len / dt;                              // px / ms
        const qreal speedF = 1.0 - qBound(0.0, speed / 3.0, 1.0);  // slow -> 1

        const qreal tiltMag = qBound(0.0, std::hypot(tiltX, tiltY), 1.0);

        const qreal f = 0.25 * dirF + 0.50 * pressure + 0.12 * speedF + 0.13 * tiltMag;
        const qreal w = m_minWidth + span * qBound(0.0, f, 1.0);
        painter.setPen(QPen(m_color, w, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
        painter.drawLine(m_last, cp);
        extent = w;
    } else {
        const qreal w = m_minWidth + span * qBound(0.0, 0.5 + 0.5 * pressure, 1.0);
        painter.setPen(QPen(m_color, w, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
        painter.drawLine(m_last, cp);
        extent = w;
    }
    painter.end();

    const QRect dirty = QRectF(m_last, cp).normalized()
                            .adjusted(-extent - 2, -extent - 2, extent + 2, extent + 2)
                            .toRect();
    growIfNeeded(qMax(m_last.y(), cp.y()) + extent + 2); // may reallocate the canvas
    m_last = cp;
    blitRegion(dirty.translated(0, -m_panY));
}

void InkEngine::strokeEnd()
{
    if (m_btnDown >= 0) {
        const QString text = m_buttons[m_btnDown].text;
        m_btnDown = -1;
        activateButton(text); // Clear repaints (reverts); Back leaves it pressed
        return;
    }
    m_drawing = false;
}

void InkEngine::clearPage()
{
    if (!FbCapture::ready())
        return;
    ensureCanvas();
    m_deghostTimer.stop();
    if (!m_canvas.isNull())
        m_canvas.fill(Qt::white);
    m_panY = 0;
    // Two Clear methods: if the user scrolled, de-ghost (black flash); otherwise
    // (the common fill-one-screen-then-clear case) use the gentle fast clear.
    renderAll(m_scrolled);
    m_scrolled = false;
}
