/*****************************************************************************
 * $CAMITK_LICENCE_BEGIN$
 *
 * CamiTK - Computer Assisted Medical Intervention ToolKit
 * (c) 2001-2025 Univ. Grenoble Alpes, CNRS, Grenoble INP - UGA, TIMC, 38000 Grenoble, France
 *
 * Visit http://camitk.imag.fr for more information
 *
 * This file is part of CamiTK.
 *
 * CamiTK is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License version 3
 * only, as published by the Free Software Foundation.
 *
 * CamiTK is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License version 3 for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * version 3 along with CamiTK.  If not, see <http://www.gnu.org/licenses/>.
 *
 * $CAMITK_LICENCE_END$
 ****************************************************************************/

// -- Core image component stuff
#include "ImageLutWidget.h"
#include "ImageLutModel.h"
#include "HistogramGraphicsRectItem.h"

// -- Core stuff
#include <ImageComponent.h>
#include <Application.h> // to get the action widget size before this widget is painted
#include <Viewer.h>
#include <AbortException.h>

// -- QT stuff
#include <QGraphicsTextItem>
#include <QColorDialog>
#include <QPushButton>
#include <QDoubleValidator>
#include <QDockWidget>

// -- Std stuff
#include <utility> // as_const

using namespace camitk;

// ---------------------------- Constructor ----------------------------
ImageLutWidget::ImageLutWidget(QWidget* parent) : QWidget(parent) {

    myComponent = nullptr;
    model = nullptr;
    histogramGraphicsRectItem = nullptr;

    ui.setupUi(this);

    connect(ui.levelSlider,   SIGNAL(valueChanged(int)),    this, SLOT(levelSliderChanged(int)));
    connect(ui.windowSlider,  SIGNAL(valueChanged(int)),    this, SLOT(windowSliderChanged(int)));
    connect(ui.levelSpinBox,  SIGNAL(valueChanged(double)), this, SLOT(levelSpinBoxChanged(double)));
    connect(ui.windowSpinBox, SIGNAL(valueChanged(double)), this, SLOT(windowSpinBoxChanged(double)));

    connect(ui.maxTransparency, SIGNAL(stateChanged(int)), this, SLOT(applyLUT()));
    connect(ui.minTransparency, SIGNAL(stateChanged(int)), this, SLOT(applyLUT()));

    connect(ui.inversion, SIGNAL(clicked()), this, SLOT(invertButtonClicked()));
    connect(ui.resetLut,  SIGNAL(clicked()), this, SLOT(resetLUT()));

    connect(ui.minColor, SIGNAL(clicked()), this, SLOT(setMinColor()));
    connect(ui.maxColor, SIGNAL(clicked()), this, SLOT(setMaxColor()));

    connect(ui.binComboBox, SIGNAL(currentTextChanged(QString)), this, SLOT(binComboBoxChanged(QString)));

    // compute and draw the histogram
    ui.histogramGraphicsView->setScene(new QGraphicsScene(this));
    ui.histogramGraphicsView->setBackgroundBrush(QBrush(palette().window()));
    ui.histogramGraphicsView->setRenderHint(QPainter::Antialiasing);
    ui.histogramGraphicsView->setFrameShape(QFrame::Box);
    ui.histogramGraphicsView->setFrameShadow(QFrame::Raised);
}

// ---------------------------- Destructor ----------------------------
ImageLutWidget::~ImageLutWidget() {
    QList<ImageLutModel*> allModelsList;
    allModelsList = allModels.values();
    while (!allModelsList.empty()) {
        delete allModelsList.takeFirst();
    }
}

// ---------------------------- updateComponent ----------------------------
void ImageLutWidget::updateComponent(camitk::ImageComponent* imageComponent) {
    myComponent = imageComponent;

    // if there is no lookup table, just don't display this widget!
    if (myComponent->getLut() == nullptr) {
        setEnabled(false);
        throw (AbortException("No LUT in this ImageComponent. Aborting\n"));
    }
    else {
        setEnabled(true);
    }

    //-- update the model
    if (allModels.contains(imageComponent)) {
        model = allModels.value(imageComponent);
    }
    else {
        model = new ImageLutModel(imageComponent->getImageData());
        allModels.insert(imageComponent, model);
    }

    //-- update the GUI
    initLevel(myComponent->getLut()->GetLevel());
    initWindow(myComponent->getLut()->GetWindow());
    double* color = myComponent->getLut()->GetTableValue(0);
    setColor("min", QColor::fromRgbF(color[0], color[1], color[2], color[3]));
    int nbOfColors = myComponent->getLut()->GetNumberOfTableValues();
    color = myComponent->getLut()->GetTableValue(nbOfColors - 1);
    setColor("max", QColor::fromRgbF(color[0], color[1], color[2], color[3]));
    updateGradient();
    updateBinComboBox();

    //-- (re)apply the LUT and draw the graphics
    applyLUT();
}

// ---------------------------- applyLUT ----------------------------
void ImageLutWidget::applyLUT() {
    // set window level and width according to user set up
    myComponent->getLut()->SetLevel(ui.levelSpinBox->value());
    myComponent->getLut()->SetWindow(ui.windowSpinBox->value());

    // use the color to update table values
    myComponent->getLut()->SetMinimumTableValue(minColor.redF(), minColor.greenF(), minColor.blueF(), 1.0);
    myComponent->getLut()->SetMaximumTableValue(maxColor.redF(), maxColor.greenF(), maxColor.blueF(), 1.0);
    // build using the given range
    myComponent->getLut()->Build();

    // BUG since VTK9.0, setting the first/last color to transparent is ignored
    // Uncommenting these lines are messing up the color table and no custom color is taken into account
    // TODO find a way to set the transparency only for all values below the first and above the last window values
    // int nbOfColors = myComponent->getLut()->GetNumberOfTableValues();
    // once the whole colors are build change the min/max color alpha
    // myComponent->getLut()->SetTableValue(0, minColor.redF(), minColor.greenF(), minColor.blueF(), (ui.minTransparency->isChecked() ? 0.0 : 1.0));
    // myComponent->getLut()->SetTableValue(nbOfColors - 1, maxColor.redF(), maxColor.greenF(), maxColor.blueF(), (ui.maxTransparency->isChecked() ? 0.0 : 1.0));

    myComponent->refresh();

    drawGraphics();
}

// ---------------------------- resetLUT ----------------------------
void ImageLutWidget::resetLUT() {
    // init all palette values
    blockAllSignals(true);
    initLevel(model->getMinValue() + (model->getMaxValue() - model->getMinValue()) / 2.0);
    initWindow(model->getMaxValue() - model->getMinValue());
    setColor("min", QColor(Qt::black));
    setColor("max", QColor(Qt::white));
    updateGradient();
    blockAllSignals(false);

    // update palette color applied to the images and the histogram
    applyLUT();
}

// ---------------------------- binComboBoxChanged ----------------------------
void ImageLutWidget::binComboBoxChanged(QString value) {
    int nbOfBins = value.split(" ")[0].toInt();

    if (nbOfBins != model->getNumberOfBins()) {
        model->setNumberOfBins(nbOfBins);
        updateBinComboBox();
        applyLUT();
    }
}

// ---------------------------- binComboBoxChanged ----------------------------
void ImageLutWidget::updateBinComboBox() {
    blockAllSignals(true);
    ui.binComboBox->clear();
    QMap<int, QString> binValuesSorted;
    int n = 16;
    int max = (unsigned) abs(model->getMaxValue() - model->getMinValue() + 1);
    while (n <= 256 && n <= max) {
        binValuesSorted.insert(n, QString("%1 bins").arg(n));
        n *= 2;
    }
    if (n <= 256) {
        binValuesSorted.insert(model->getNumberOfBins(), QString("%1 bins").arg(model->getNumberOfBins()));
        binValuesSorted.insert(max, QString("%1 bins").arg(max));
    }
    for (auto& item : std::as_const(binValuesSorted)) {
        ui.binComboBox->addItem(item);
    }
    ui.binComboBox->setCurrentText(QString("%1 bins").arg(model->getNumberOfBins()));
    blockAllSignals(false);
}

// ---------------------------- drawGraphics ----------------------------
void ImageLutWidget::drawGraphics() {
    //
    //      <-------------------------------------- W --------------------------------------->       ^
    //                                                                                               |
    //      O-------> x                                      L     M-------------------------Q       |
    //      |                                                |    /                                  |
    //      |            <-- w -->                           |   /                                   |
    //   y  V         ^  +-------b                           |  /                                    |
    //                |  |       |                           | /                                     |
    //                |  |       |                           |/
    //      c         |  |       |                           |                               C       H
    //                g  |       |-------+                  /|
    //                |  |       |       |                 / |                                       |
    //                |  |       |       |                /  |                                       |
    //                |  |       |       |               /   |                                       |
    //      P         V  a-------+-------+      --------m    l                               S       V
    //                     bin #i   #i+1
    //
    //   Size of the graphics
    //   W = width = 256
    //   H = height = 256
    //
    //   scaling from histogram to graphics
    //   xScale = W/nbHistoBins
    //   yScale = H/maxBinValue
    //
    //   barPenSize = max(0.5, 1.0/xScale)
    //   lutPenSize = max(1.0, 1.5/xScale)
    //
    //   we need to adjust because of antialiasing (see https://doc.qt.io/qt-5/coordsys.html#anti-aliased-painting)
    //   penAdjust = (barPenSize, barPenSize)
    //
    //   O = topLeft = (0,0)
    //   P = bottomLeft = (0,H)
    //   Q = topRight = (W,0)
    //   S = bottomRight = (W,H)
    //
    //   c = colorStart = (0, H/2) - antiAliasAdjust
    //   C = colorEnd = (W, H/2) + antiAliasAdjust
    //
    //   w = barWidth = xScale
    //   a = (i*barWidth - penAdjust, H)
    //   b = ((i+1)*barWidth - penAdjust, H - binValue*yScale - penAdjust)
    //
    //   m = levelLeft = (getBinIndexAsDouble(level - window/2)*xScale, H)
    //   M = levelRight = (getBinIndexAsDouble(level + window/2)*xScale, 0)
    //
    //   l = levelBottom = (getBinIndexAsDouble(level)*xScale, H)
    //   L = levelTop = (getBinIndexAsDouble(level)*xScale, 0)
    //
    double width = 256.0;
    double height = 256.0;
    double xScale = width / model->getNumberOfBins();
    double yScale = height / model->getMaxBinValue();
    // tricky: for the first call, as ui.histogramGraphicsView has not been painted yet, the width is not correct.
    // → use the action viewer dock widget size instead
    double graphicsViewSize;
    Viewer* actionViewer = Application::getViewer("Action Viewer");
    if (actionViewer != nullptr) {
        graphicsViewSize = actionViewer->getDockWidget()->size().width();
    }
    else {
        graphicsViewSize = ui.histogramGraphicsView->size().width();
    }
    double barPenSize = 0.5 * width / graphicsViewSize; // so that it always take 0.5 pixels on the screen
    double levelPenSize = 4 * barPenSize;
    QPointF penAdjust(barPenSize, barPenSize);
    QPointF topLeft(0.0, 0.0);
    QPointF bottomLeft(0.0, height);
    QPointF topRight(width, 0.0);
    QPointF bottomRight(width, height);
    QPointF colorStart(QPointF(0.0, height / 2.0) - penAdjust);
    QPointF colorEnd(QPointF(width, height / 2.0) + penAdjust);
    double barWidth = xScale;
    double window = myComponent->getLut()->GetWindow();
    double level = myComponent->getLut()->GetLevel();
    QPointF levelLeft(model->getBinIndexAsDouble(level - window / 2.0, false)*xScale, height);
    QPointF levelRight(model->getBinIndexAsDouble(level + window / 2.0, false)*xScale, 0.0);
    QPointF levelBottom(model->getBinIndexAsDouble(level)*xScale, height);
    QPointF levelTop(model->getBinIndexAsDouble(level)*xScale, 0.0);
    QPen pen;
    QBrush brush;
    QColor histogramBrush("#ececfd");
    QColor histogramPen("#343499");
    QColor levelPen("#c80000");

    //-- clean up (delete all items and free memory as ownership is taken the QGraphicsScene when an item is added to it)
    ui.histogramGraphicsView->scene()->clear();
    //-- create the whole scene
    histogramGraphicsRectItem = new HistogramGraphicsRectItem(this, QRectF(0.0, 0.0, width, height));
    histogramGraphicsRectItem->setAcceptHoverEvents(true);
    ui.histogramGraphicsView->scene()->addItem(histogramGraphicsRectItem);

    //-- background gradient
    QLinearGradient colorGradient(colorStart, colorEnd);
    int nbOfColors = myComponent->getLut()->GetNumberOfTableValues();
    double* color;

    for (int i = 0; i < nbOfColors; i++) {
        color = myComponent->getLut()->GetTableValue(i);
        colorGradient.setColorAt(double(i) / double(nbOfColors), QColor::fromRgbF(color[0], color[1], color[2]));
    }

    pen.setColor(palette().window().color());
    brush = QBrush(colorGradient);
    ui.histogramGraphicsView->scene()->addRect(QRectF(topLeft, bottomRight), pen, brush);

    //-- show min/max transparency in the background
    // use a background colored brush to "remove" the transparent part of the gradient
    brush = QBrush(palette().window());
    if (ui.minTransparency->isChecked()) {
        ui.histogramGraphicsView->scene()->addRect(QRectF(topLeft, levelLeft), pen, brush);
    }
    if (ui.maxTransparency->isChecked()) {
        ui.histogramGraphicsView->scene()->addRect(QRectF(levelRight, bottomRight), pen, brush);
    }

    //-- draw histogram
    pen.setColor(histogramPen);
    brush.setColor(histogramBrush);
    pen.setWidthF(barPenSize);
    for (unsigned int i = 0; i < (unsigned) model->getNumberOfBins(); i++) {
        QPointF a(QPointF(i * barWidth, height) - penAdjust);
        QPointF b(QPointF((i + 1)*barWidth, height - model->getBinValue(i)*yScale) - penAdjust);
        ui.histogramGraphicsView->scene()->addRect(QRectF(a, b), pen, brush);
    }

    //-- draw level and window
    pen.setColor(levelPen);
    pen.setWidthF(levelPenSize);
    pen.setStyle(Qt::DashLine);
    ui.histogramGraphicsView->scene()->addLine(QLineF(levelBottom, levelTop), pen);

    pen.setStyle(Qt::SolidLine);
    QPainterPath path;
    path.moveTo(bottomLeft);
    path.lineTo(levelLeft);
    path.lineTo(levelRight);
    path.lineTo(topRight);
    ui.histogramGraphicsView->scene()->addPath(path, pen);
}

// ---------------------------- fitView ----------------------------
void ImageLutWidget::fitView() {
    ui.histogramGraphicsView->fitInView(histogramGraphicsRectItem);
    ui.histogramGraphicsView->centerOn(histogramGraphicsRectItem->rect().center());
    ui.histogramGraphicsView->setSceneRect(histogramGraphicsRectItem->rect());
}

// ---------------------------- resizeEvent ----------------------------
void ImageLutWidget::resizeEvent(QResizeEvent*) {
    fitView();
}

// ---------------------------- showEvent ----------------------------
void ImageLutWidget::showEvent(QShowEvent*) {
    fitView();
}

// ---------------------------- showHistogramTooltip ----------------------------
void ImageLutWidget::showHistogramTooltip(QPointF mousePos) {
    int binId = model->getNumberOfBins() * mousePos.x() / 256.0;
    int nbOfVoxels = model->getBinValue(binId);
    int binMin = model->getMinValue() + double(binId) * (model->getMaxValue() - model->getMinValue()) / double(model->getNumberOfBins());
    int binMax = model->getMinValue() + double(binId + 1) * (model->getMaxValue() - model->getMinValue()) / double(model->getNumberOfBins());
    histogramGraphicsRectItem->setToolTip(tr("Bin #%1 [%2,%3]: %4 voxels").arg(binId + 1).arg(binMin).arg(binMax).arg(nbOfVoxels));
}

// ---------------------------- initLevel ----------------------------
void ImageLutWidget::initLevel(double value) {
    blockAllSignals(true);
    ui.levelSpinBox->setMinimum(model->getMinValue());
    ui.levelSpinBox->setMaximum(model->getMaxValue());
    ui.levelSpinBox->setValue(value);
    // set slider
    ui.levelSlider->setValue(model->getPercentFromLevel(value));
    blockAllSignals(false);
}

// ---------------------------- initWindow ----------------------------
void ImageLutWidget::initWindow(double value) {
    blockAllSignals(true);
    // set limits and value
    ui.windowSpinBox->setMinimum(1.0);
    ui.windowSpinBox->setMaximum(model->getMaxValue() - model->getMinValue());
    ui.windowSpinBox->setValue(value);
    // set slider
    ui.windowSlider->setValue(model->getPercentFromWindow(value));
    blockAllSignals(false);
}

// ---------------------------- invertButtonClicked ----------------------------
void ImageLutWidget::invertButtonClicked() {
    QColor oldMinColor = minColor;
    setColor("min", maxColor);
    setColor("max", oldMinColor);
    updateGradient();
    applyLUT();
}

// ---------------------------- levelSpinBoxChanged ----------------------------
void ImageLutWidget::levelSpinBoxChanged(double level) {
    // update slider value
    blockAllSignals(true);
    ui.levelSlider->setValue(model->getPercentFromLevel(level));
    blockAllSignals(false);

    // update palette color applied to the images and the histogram
    applyLUT();
}

// ---------------------------- sliderLUTLevelChanged ----------------------------
void ImageLutWidget::levelSliderChanged(int level) {
    // update line edit text
    blockAllSignals(true);
    ui.levelSpinBox->setValue(int(model->getLevelFromPercent(level)));
    blockAllSignals(false);

    // update palette color applied to the images and the histogram
    applyLUT();
}

// ---------------------------- windowSpinBoxChanged ----------------------------
void ImageLutWidget::windowSpinBoxChanged(double window) {
    // update line edit text
    blockAllSignals(true);
    ui.windowSlider->setValue(model->getPercentFromWindow(window));
    blockAllSignals(false);

    // update palette color applied to the images and the histogram
    applyLUT();
}

// ---------------------------- sliderLUTWindowChanged ----------------------------
void ImageLutWidget::windowSliderChanged(int window) {
    // update slider value
    blockAllSignals(true);
    ui.windowSpinBox->setValue(int(model->getWindowFromPercent(window)));
    blockAllSignals(false);

    // update palette color applied to the images and the histogram
    applyLUT();
}

// ---------------------------- updateGradient ----------------------------
void ImageLutWidget::updateGradient() {
    QString style = "background-color: qlineargradient(x1:0 y1:0, x2:1 y2:0, stop:0 rgba(" +
                    QString::number(minColor.red()) + ", " +
                    QString::number(minColor.green()) + ", " +
                    QString::number(minColor.blue()) + ", " +
                    QString::number(minColor.alpha()) + " ), stop:1 rgba( " +
                    QString::number(maxColor.red()) + ", " +
                    QString::number(maxColor.green()) + ", " +
                    QString::number(maxColor.blue()) + ", " +
                    QString::number(maxColor.alpha()) + " ) )";
    ui.colorScale->setStyleSheet(style);
}

// ---------------------------- setColor ----------------------------
bool ImageLutWidget::setColor(QString whichColor, QColor newColor) {
    QColor* color;
    QToolButton* colorButton;
    QCheckBox* transparentCheckBox;

    if (!newColor.isValid()) {
        return false;
    }

    if (whichColor == "min") {
        color = &minColor;
        colorButton = ui.minColor;
        transparentCheckBox = ui.minTransparency;
    }
    else {
        if (whichColor == "max") {
            color = &maxColor;
            colorButton = ui.maxColor;
            transparentCheckBox = ui.maxTransparency;
        }
        else {
            return false;
        }
    }

    colorButton->setStyleSheet(QString("background-color: %1").arg(newColor.name()));
    blockAllSignals(true);
    transparentCheckBox->setChecked(newColor.alphaF() < 1e-5);
    newColor.setAlphaF(1.0);
    *color = newColor;
    blockAllSignals(false);

    return true;
}

// ---------------------------- setMaxColor ----------------------------
void ImageLutWidget::setMaxColor() {
    QColor newColor = QColorDialog::getColor(maxColor, this, "Choose the Maximal Value Color");

    if (setColor("max", newColor)) {
        updateGradient();
        applyLUT();
    }
}

// ---------------------------- setMinColor ----------------------------
void ImageLutWidget::setMinColor() {
    QColor newColor = QColorDialog::getColor(minColor, this, "Choose the Minimal Value Color");

    if (setColor("min", newColor)) {
        updateGradient();
        applyLUT();
    }
}

// ---------------------------- blockAllSignals ----------------------------
void ImageLutWidget::blockAllSignals(bool block) {
    ui.levelSlider->blockSignals(block);
    ui.windowSlider->blockSignals(block);
    ui.levelSpinBox->blockSignals(block);
    ui.windowSpinBox->blockSignals(block);
    ui.maxTransparency->blockSignals(block);
    ui.minTransparency->blockSignals(block);
    ui.inversion->blockSignals(block);
    ui.resetLut->blockSignals(block);
    ui.minColor->blockSignals(block);
    ui.maxColor->blockSignals(block);
    ui.binComboBox->blockSignals(block);
}
