#include "ColorPicker.h"
#include "ui_ColorPicker.h"

#include <QCursor>
#include <QMouseEvent>
#include <QPaintEvent>
#include <QPainter>
#include <QPainterPath>
#include <QPixmap>
#include <QScreen>

#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
#define MyRealType double
#define MyFloatType float
#else
#include <QDesktopWidget>
#define MyRealType qreal
#define MyFloatType qreal
#endif

using namespace ColorPickerHelpers;

ColorPickArea::ColorPickArea(QWidget *parent)
    : ColorPickerWidget(parent)
{
    setMouseTracking(false);
}

void ColorPickArea::paintEvent(QPaintEvent *event)
{
    QPainter p(this);

    for (int x = event->rect().x(); x <= event->rect().right(); x++) {
        for (int y = event->rect().y(); y <= event->rect().bottom(); y++) {
            MyFloatType h, s, v;
            QColor c = pointToColor(x, y);
            c.getHsvF(&h, &s, &v);
            c.setHsvF(h, s, 1);
            p.setPen(c);
            p.drawPoint(x, y);
        }
    }

    p.setPen(QPen(Qt::black, 3));
    QPoint curr = colorToPoint(currColor);
    p.drawLine(curr - QPoint(0, 8), curr + QPoint(0, 8));
    p.drawLine(curr - QPoint(8, 0), curr + QPoint(8, 0));

    p.end();
}

void ColorPickArea::setColor(const QColor &c)
{
    if (c == currColor) {
        return;
    }
    QPoint p1 = colorToPoint(currColor);
    QPoint p2 = colorToPoint(c);
    currColor = c;
    repaint(QRect(p2 - QPoint(10, 10), p2 + QPoint(10, 10)));
    repaint(QRect(p1 - QPoint(10, 10), p1 + QPoint(10, 10)));

    emit colorChanged(currColor);
}

ColorPickerWidget::ColorPickerWidget(QWidget *parent)
    : ColorPickWidgetAbstract(parent)
{}

void ColorPickerWidget::mouseReleaseEvent(QMouseEvent *event)
{
    mouseEvent(event);
}

void ColorPickerWidget::mousePressEvent(QMouseEvent *event)
{
    mouseEvent(event);
}

void ColorPickerWidget::mouseMoveEvent(QMouseEvent *event)
{
    mouseEvent(event);
}

QColor ColorPickArea::pointToColor(int x, int y) const
{
    QColor color;
    MyFloatType h, s, v, a;
    currColor.getHsvF(&h, &s, &v, &a);
    color.setHsvF(MyRealType(x) / width(), 1.0 - MyRealType(y) / height(), v, a);
    return color;
}

QPoint ColorPickArea::colorToPoint(const QColor &color) const
{
    MyFloatType h, s, v;
    color.getHsvF(&h, &s, &v);
    return QPointF(h * width(), (1.0 - s) * height()).toPoint();
}

void ColorPickerWidget::mouseEvent(QMouseEvent *event)
{
    QPoint pos = event->pos();
    if (!rect().contains(pos.x(), rect().y())) {
        pos.setX(rect().x() < pos.x() ? rect().right() : rect().x());
    }
    if (!rect().contains(rect().x(), pos.y())) {
        pos.setY(rect().y() < pos.y() ? rect().bottom() : rect().y());
    }
    setColor(pointToColor(pos.x(), pos.y()));
}

void ColorValueBar::setColor(const QColor &c)
{
    if (c == currColor) {
        return;
    }
    currColor = c;
    repaint();

    emit colorChanged(currColor);
}

void ColorValueBar::paintEvent(QPaintEvent *event)
{
    QPainter p(this);
    QColor color = currColor;
    MyFloatType h, s, v;
    currColor.getHsvF(&h, &s, &v);
    v = 1.0 - v;

    const int triangleSize = 10;
    QRect barRect = rect();
    barRect.setWidth(barRect.width() - triangleSize);

    for (int y = barRect.y(); y <= barRect.bottom(); y++) {
        color.setHsvF(h, s, 1.0 - MyRealType(y) / height());
        p.setPen(color);
        p.drawLine(barRect.x(), y, barRect.right(), y);
    }

    QRectF triangleRect
        = QRectF(barRect.right(), v * height() - triangleSize / 2, triangleSize, triangleSize);

    QPainterPath path;
    path.moveTo(triangleRect.left(), triangleRect.top() + triangleRect.height() / 2);
    path.lineTo(triangleRect.topRight());
    path.lineTo(triangleRect.bottomRight());
    path.lineTo(triangleRect.left(), triangleRect.top() + triangleRect.height() / 2);

    p.fillPath(path, palette().text().color());

    p.end();
    QWidget::paintEvent(event);
}

QColor ColorValueBar::pointToColor(int x, int y) const
{
    Q_UNUSED(x)
    QColor color = currColor;
    MyFloatType h, s, v, a;
    color.getHsvF(&h, &s, &v, &a);
    color.setHsvF(h, s, 1.0 - MyRealType(y) / height(), a);
    return color;
}

QPoint ColorValueBar::colorToPoint(const QColor &color) const
{
    MyFloatType h, s, v;
    color.getHsvF(&h, &s, &v);
    return QPoint(rect().x(), int((1.0 - v) * height()));
}

ColorPicker::ColorPicker(QWidget *parent)
    : ColorPickWidgetAbstract(parent)
    , ui(new Ui::ColorPicker)
    , pickingFromScreen(false)
{
    ui->setupUi(this);
    connect(ui->colorPickArea, &ColorPickArea::colorChanged, this, &ColorPicker::setColor);
    connect(ui->valuePickBar, &ColorValueBar::colorChanged, this, &ColorPicker::setColor);
    connect(ui->alphaChannelBar, &AlphaChannelBar::colorChanged, this, [this](const QColor &color) {
        emit colorChanged(color);
    });
    connect(this, &ColorPicker::colorChanged, ui->colorPickArea, &ColorPickArea::setColor);
    connect(this, &ColorPicker::colorChanged, ui->valuePickBar, &ColorValueBar::setColor);
    connect(this, &ColorPicker::colorChanged, ui->colorShow, &ColorShowWidget::setColor);
    connect(this, &ColorPicker::colorChanged, ui->alphaChannelBar, &AlphaChannelBar::setColor);

    connect(
        ui->hueSpinBox,
        static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged),
        this,
        &ColorPicker::colorChannelChanged);
    connect(
        ui->satSpinBox,
        static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged),
        this,
        &ColorPicker::colorChannelChanged);
    connect(
        ui->valSpinBox,
        static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged),
        this,
        &ColorPicker::colorChannelChanged);
    connect(
        ui->redSpinBox,
        static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged),
        this,
        &ColorPicker::colorChannelChanged);
    connect(
        ui->blueSpinBox,
        static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged),
        this,
        &ColorPicker::colorChannelChanged);
    connect(
        ui->greenSpinBox,
        static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged),
        this,
        &ColorPicker::colorChannelChanged);

    connect(ui->hexLineEdit, &QLineEdit::textChanged, this, &ColorPicker::colorChannelChanged);

    connect(
        ui->pickColorFromScreenButton,
        &QPushButton::clicked,
        this,
        &ColorPicker::startPickingFromScreen);
}

ColorPicker::~ColorPicker()
{
    if (pickingFromScreen) {
        setColor(getColorAtMouse());
        stopPickingFromScreen();
    }
}

void ColorPicker::setColor(const QColor &color)
{
    updateColor(color);
    emit colorChanged(currColor);
}

void ColorPicker::colorChannelChanged()
{
    QString txt = ui->hexLineEdit->text();
    // Regex pattern below mimics the behaviour of former RegExp::exactMatch()
    if (!QRegularExpression("\\A(?:#[0-9a-fA-F]{6})\\z").match(txt).hasMatch()) {
        return;
    }
    QColor hexColor = txt;

    int h, s, v;
    h = ui->hueSpinBox->value();
    s = ui->satSpinBox->value();
    v = ui->valSpinBox->value();
    QColor hsvColor;
    hsvColor.setHsv(h, s, v);

    int r, g, b;
    r = ui->redSpinBox->value();
    g = ui->greenSpinBox->value();
    b = ui->blueSpinBox->value();
    QColor rgbColor;
    rgbColor.setRgb(r, g, b);

    if (hexColor.isValid() && hexColor != currColor) {
        setColor(hexColor);
    } else if (rgbColor.isValid() && rgbColor != currColor) {
        setColor(rgbColor);
    } else if (hsvColor.isValid() && hsvColor != currColor) {
        setColor(hsvColor);
    }
}

void ColorPicker::updateColor(const QColor &color)
{
    QSignalBlocker s0(ui->redSpinBox);
    QSignalBlocker s1(ui->blueSpinBox);
    QSignalBlocker s2(ui->greenSpinBox);

    QSignalBlocker s3(ui->valSpinBox);
    QSignalBlocker s4(ui->satSpinBox);
    QSignalBlocker s5(ui->hueSpinBox);

    QSignalBlocker s6(ui->hexLineEdit);

    QSignalBlocker s7(ui->alphaChannelBar);
    QSignalBlocker s8(ui->colorPickArea);
    QSignalBlocker s9(ui->colorShow);
    QSignalBlocker s10(ui->valuePickBar);

    currColor = color;

    ui->hexLineEdit->setText(currColor.name());

    int h, s, v;
    currColor.getHsv(&h, &s, &v);
    ui->hueSpinBox->setValue(h);
    ui->satSpinBox->setValue(s);
    ui->valSpinBox->setValue(v);

    int r, g, b;
    currColor.getRgb(&r, &g, &b);
    ui->redSpinBox->setValue(r);
    ui->greenSpinBox->setValue(g);
    ui->blueSpinBox->setValue(b);

    ui->valuePickBar->setColor(color);
    ui->colorPickArea->setColor(color);
    ui->colorShow->setColor(color);
    ui->alphaChannelBar->setColor(color);
}

void ColorPicker::startPickingFromScreen()
{
    if (!pickingFromScreen) {
        setMouseTracking(true);
        grabMouse(Qt::CursorShape::CrossCursor);
        pickingFromScreen = true;
        bufferColor = currColor;
    }
}

void ColorPicker::mouseReleaseEvent(QMouseEvent *event)
{
    if (pickingFromScreen) {
        setColor(getColorAtMouse());
        pickingFromScreen = false;
        setMouseTracking(false);
        releaseMouse();
    }
    QWidget::mouseReleaseEvent(event);
}

void ColorPicker::mouseMoveEvent(QMouseEvent *event)
{
    if (pickingFromScreen) {
        updateColor(getColorAtMouse());
    }
    QWidget::mouseMoveEvent(event);
}

QColor ColorPicker::getColorAtMouse()
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
    QPoint pos = QCursor::pos();
    auto screen = QGuiApplication::screenAt(pos);
    if (!screen) {
        screen = QGuiApplication::primaryScreen();
    }
    if (screen) {
        auto screenRelativePos = pos - screen->geometry().topLeft();
        const QPixmap pixmap
            = screen->grabWindow(0, screenRelativePos.x(), screenRelativePos.y(), 1, 1);
        return QColor(pixmap.toImage().pixel(0, 0));
    }
    return QColorConstants::Black;
#else
    const QDesktopWidget *desktop = QApplication::desktop();
    auto winid = desktop->winId();
    const QPixmap pixmap = QGuiApplication::screens()
                               .at(desktop->screenNumber())
                               ->grabWindow(winid, QCursor::pos().x(), QCursor::pos().y(), 1, 1);
    return QColor(pixmap.toImage().pixel(0, 0));
#endif
}

bool ColorPicker::isPickingFromScreen() const
{
    return pickingFromScreen;
}

void ColorPicker::setAlphaEnabled(bool enabled)
{
    ui->alphaChannelBar->setVisible(enabled);
}

void ColorPicker::stopPickingFromScreen()
{
    if (pickingFromScreen) {
        pickingFromScreen = false;
        updateColor(bufferColor);
        releaseMouse();
        setMouseTracking(false);
    }
}

ColorShowWidget::ColorShowWidget(QWidget *parent)
    : ColorPickWidgetAbstract(parent)
{}

void ColorShowWidget::setColor(const QColor &c)
{
    currColor = c;
    repaint();
}

void ColorShowWidget::paintEvent(QPaintEvent *event)
{
    QPainter p(this);
    const int miniRectWidth = rect().width() / 2;
    for (int y = rect().topLeft().ry(); y < rect().bottomRight().ry(); y++) {
        for (int x = rect().topLeft().rx(); x < rect().bottomRight().rx(); x++) {
            p.setPen(
                ((x % miniRectWidth) / (miniRectWidth / 2))
                        == ((y % miniRectWidth) / (miniRectWidth / 2))
                    ? Qt::white
                    : Qt::black);
            p.drawPoint(x, y);
        }
    }
    p.setPen(currColor);
    p.setBrush(QBrush(currColor));
    p.drawRect(event->rect());
    p.end();
}

void AlphaChannelBar::setColor(const QColor &c)
{
    if (c == currColor) {
        return;
    }
    currColor = c;
    repaint();
    emit colorChanged(currColor);
}

void AlphaChannelBar::paintEvent(QPaintEvent *event)
{
    QPainter p(this);
    QRect barRect = rect();

    MyFloatType h, s, v, a;
    currColor.getHsvF(&h, &s, &v, &a);
    a = 1.0 - a;
    const int triangleSize = 10;

    barRect.setWidth(barRect.width() - triangleSize);

    const int miniRectWidth = barRect.width() / 2;
    for (int y = barRect.topLeft().ry(); y < barRect.bottomRight().ry(); y++) {
        for (int x = barRect.topLeft().rx(); x < barRect.bottomRight().rx(); x++) {
            p.setPen(
                ((x % miniRectWidth) / (miniRectWidth / 2))
                        == ((y % miniRectWidth) / (miniRectWidth / 2))
                    ? Qt::white
                    : Qt::black);
            p.drawPoint(x, y);
            p.setPen(pointToColor(x, y));
            p.drawPoint(x, y);
        }
    }

    QRectF triangleRect
        = QRectF(barRect.right(), a * height() - triangleSize / 2, triangleSize, triangleSize);

    QPainterPath path;
    path.moveTo(triangleRect.left(), triangleRect.top() + triangleRect.height() / 2);
    path.lineTo(triangleRect.topRight());
    path.lineTo(triangleRect.bottomRight());
    path.lineTo(triangleRect.left(), triangleRect.top() + triangleRect.height() / 2);
    p.fillPath(path, palette().text().color());

    p.end();
    QWidget::paintEvent(event);
}

QColor AlphaChannelBar::pointToColor(int x, int y) const
{
    Q_UNUSED(x)
    QColor color = currColor;
    MyFloatType h, s, v;
    color.getHsvF(&h, &s, &v);
    color.setHsvF(h, s, v, 1.0 - MyRealType(y) / height());
    return color;
}

QPoint AlphaChannelBar::colorToPoint(const QColor &) const
{
    return QPoint();
}
