/***************************** LICENSE START ***********************************

 Copyright 2012 ECMWF and INPE. This software is distributed under the terms
 of the Apache License version 2.0. In applying this license, ECMWF does not
 waive the privileges and immunities granted to it by virtue of its status as
 an Intergovernmental Organization or submit itself to any jurisdiction.

 ***************************** LICENSE END *************************************/

#include <algorithm>
#include <cmath>

#include <QAbstractTextDocumentLayout>
#include <QDebug>
#include <QFile>
#include <QGraphicsDropShadowEffect>
#include <QMouseEvent>
#include <QPainter>
#include <QTextDocument>
#include <QTextStream>

#include "CursorCoordinate.h"
#include "MvQCursorData.h"
#include "MvQPlotView.h"
#include "MvQTheme.h"
#include "MgQPlotScene.h"
#include "MgQLayoutItem.h"
#include "MgQSceneItem.h"

#include "Transformation.h"

MvQCursorData::MvQCursorData(MgQPlotScene* scene, MvQPlotView* view, QGraphicsItem* parent) :
    MvQPlotItem(scene, view, parent)
{
    auto* effect = new QGraphicsDropShadowEffect;
    effect->setXOffset(3.);
    effect->setYOffset(3.);
    setGraphicsEffect(effect);

    textDoc_ = new QTextDocument(this);
    textDoc_->setDefaultStyleSheet(MvQTheme::htmlTableCss("cursor_data"));
}

void MvQCursorData::setActivated(bool flag)
{
    MvQPlotItem::setActivated(flag);

    if (flag) {
        show();
    }
    else {
        hide();
    }
}

void MvQCursorData::prepareForReset()
{
    dataLayout_ = nullptr;
    sceneItem_ = nullptr;
}

void MvQCursorData::reset()
{
    adjustToScenePos(cursorPos_, true);
    update();
}

QRectF MvQCursorData::boundingRect() const
{
    return boundingRect_;
}

void MvQCursorData::paint(QPainter* painter, const QStyleOptionGraphicsItem*, QWidget*)
{
    painter->setPen(QPen());
    painter->setBrush(Qt::white);
    painter->setRenderHint(QPainter::Antialiasing, true);
    painter->drawRoundedRect(boundingRect_.adjusted(1, 1, -1, -1), 5., 5.);

    // Render text
    painter->translate(0, boundingRect_.height());
    painter->scale(1., -1.);
    textDoc_->drawContents(painter);
}

void MvQCursorData::setPositionMode(MvQCursorData::PositionMode mode)
{
    positionMode_ = mode;

    if (positionMode_ == Anchored) {
        setPos(offsetFromCursor_);
    }
}

void MvQCursorData::changePositionMode(const QPointF& cursor)
{
    if (positionMode_ == Anchored) {
        positionMode_ = FollowCursor;
    }
    else {
        positionMode_ = Anchored;
    }

    if (positionMode_ == FollowCursor) {
        offsetFromCursor_ = pos() - cursor;
    }
}

void MvQCursorData::setCursorPos(const QPointF& cursor, bool forced)
{
    cursorPos_ = cursor;

    if (positionMode_ == FollowCursor) {
        setPos(cursor + offsetFromCursor_);
    } else if (forced) {
        setPos(cursor);
    } else {
        update();
    }
}


void MvQCursorData::setText(const CursorCoordinate& coord, QList<ValuesCollector> layerData)
{
    QString nameX, nameY;
    bool hasDistance = false;
    double x = coord.x(), y=coord.y();
    if (coord.type() == CursorCoordinate::LonLatType) {
        nameX = tr("Lat");
        nameY = tr("Lon");
        hasDistance = true;
        std::swap(x, y);
    } else {
        nameX = tr("X");
        nameY = tr("Y");
    }

    QString s;

    s += "<table>";
    s += "<tr><td class=\"first\">" + nameX + "</td><td>";
    s += QString::number(x, 'f', 3);
    s += "</td><td class=\"first\">" + nameY + "</td><td>";
    s += QString::number(y, 'f', 3);
    s += "</td></tr>";
    s += "</table>";

    bool collectedLayerExists = false;
    bool hasValue = false;
    bool hasScaledValue = false;
    bool hasVector = false;
    for (auto d : layerData) {
        if (d.collected()) {
            collectedLayerExists = true;
            if (d.hasValue())
                hasValue = true;
            if (d.scaled())
                hasScaledValue = true;

            if (!hasVector && d[0].size() > 0) {
                // Get values
                currentValues_.clear();
                d[0].at(0)->visit(*this);

                if (currentValues_.size() == 2) {
                    hasVector = true;
                }
            }
        }
    }

    if (!collectedLayerExists) {
        setText(s);
        return;
    }

    //----------------------------------
    // If any of the layers have data
    //----------------------------------

    s += "<br>";
    s += "<table>";

    // Create table header
    s += "<tr><td class=\"first\">Layer</td>";
    if (hasValue) {
        int colSpan = 1;
        QString valueLabel("Value");
        if (hasVector) {
            colSpan = 2;
            valueLabel = "Value(s)";
        }

        if (hasScaledValue) {
            s += "<td class=\"first\"> Scaled Value</td>";
        }

        s += "<td class=\"first\" colspan=\"" + QString::number(colSpan) + "\">" + valueLabel + "</td>";
    }

    s += "<td class=\"first\">" + nameX + "</td><td class=\"first\">" + nameY + "</td>";
    if (hasDistance) {
        s += "<td class=\"first\">Dist (km)</td>";
    }
    s += "</tr>";


    // Loop for each layer
    for (auto d : layerData) {
        if (d.collected() == false)
            continue;

        s += "<tr>";

        // Cell: layer name
        QStringList lst = QString::fromStdString(d.name()).split("/");
        if (!lst.isEmpty()) {
            QString layerName = lst.last();
            layerName.truncate(8);
            s += "<td>" + layerName + "</td>";
        }
        else {
            s += "<td></td>";
        }

        // We only check the first point in the collector (at present there
        // must be only one point anyway!!)
        if (d[0].size() == 0) {
            s += "<td>-</td><td>-</td><td>-</td>";
            if (hasValue) {
                s += "<td>-</td>";
                if (hasVector)
                    s += "<td>-</td>";
                if (hasScaledValue) {
                    s += "<td>-</td>";
                    if (hasVector)
                        s += "<td>-</td>";
                }
            }
        }
        else {
            ValuesCollectorData* data = d[0].at(0);

            // Get values
            if (d.hasValue()) {
                currentValues_.clear();
                d[0].at(0)->visit(*this);

                //----------------------------------------------------
                // Cell: Scalar value (can be scaled or/and unscaled)
                //----------------------------------------------------

                if (currentValues_.size() == 1) {
                    double value;
                    QString valueStr, units;
                    QString scaledTxt, unscaledTxt;

                    if (d.scaled()) {
                        value = data->scaledValue();
                        units = unitsString(QString::fromStdString(d.scaledUnits()));

                        if (data->missing()) {
                            valueStr = tr("Missing");
                            scaledTxt = valueStr;
                        }
                        else {
                            valueStr = formatNumber(value);
                            scaledTxt = valueStr + units;
                        }
                    }

                    value = data->value();
                    units = unitsString(QString::fromStdString(d.units()));
                    if (data->missing()) {
                        valueStr = tr("Missing");
                        unscaledTxt = valueStr;
                    }
                    else {
                        valueStr = formatNumber(value);
                        unscaledTxt = valueStr + units;
                    }

                    if (hasScaledValue) {
                        if (d.scaled()) {
                            s += "<td class=\"highlight\">" + scaledTxt + "</td>";
                            // if(hasVector) s+="<td></td>";
                            s += "<td>" + unscaledTxt + "</td>";
                            if (hasVector)
                                s += "<td></td>";
                        }
                        else {
                            s += "<td></td>";
                            // if(hasVector) s+="<td></td>";
                            s += "<td class=\"highlight\">" + unscaledTxt + "</td>";
                            if (hasVector)
                                s += "<td></td>";
                        }
                    }
                    else {
                        // s+="<td></td>";
                        // if(hasVector) s+="<td></td>";
                        s += "<td class=\"highlight\">" + unscaledTxt + "</td>";
                        if (hasVector)
                            s += "<td></td>";
                    }
                }

                //----------------------------------------------------
                // Cell: vector value (no scaling at the moment)
                //----------------------------------------------------

                if (currentValues_.size() == 2) {
                    double value;
                    QString valueStr;
                    QString value1Txt, value2Txt;
                    QStringList units;

                    value = currentValues_[0];
                    units = QString::fromStdString(d.units()).split("/");
                    for (int i = 0; i < units.count(); i++) {
                        units[i] = unitsString(units[i]);
                    }
                    for (int i = units.count(); i < 2; i++) {
                        units << "";
                    }

                    if (data->missing()) {
                        valueStr = tr("Missing");
                        value1Txt = valueStr;
                    }
                    else {
                        valueStr = formatNumber(value);
                        value1Txt = valueStr + units[0];
                    }


                    value = currentValues_[1];
                    if (data->missing()) {
                        valueStr = tr("Missing");
                        value2Txt = valueStr;
                    }
                    else {
                        valueStr = formatNumber(value);

                        if (units[1].contains("(deg)")) {
                            valueStr = QString::number(value, 'f', 2);
                        }

                        value2Txt = valueStr + units[1];
                    }


                    if (hasScaledValue) {
                        // s+="<td></td><td></td>";
                        s += "<td></td>";
                        s += "<td class=\"highlight\">" + value1Txt + "</td>";
                        s += "<td class=\"highlight\">" + value2Txt + "</td>";
                    }
                    else {
                        s += "<td class=\"highlight\">" + value1Txt + "</td>";
                        s += "<td class=\"highlight\">" + value2Txt + "</td>";
                    }
                }
            }

            // Cell: pos X
            s += "<td>" + QString::number(data->x(), 'f', 2) + "</td>";

            // Cell: pos Y
            s += "<td>" + QString::number(data->y(), 'f', 2) + "</td>";

            // Cell: distance
            if (hasDistance) {
                s += "<td>" + QString::number(data->distance(), 'f', 2) + "</td>";
            }
        }

        s += "</tr>";
    }

    s += "</table>";

    setText(s);
}

void MvQCursorData::setText()
{
    QString s(QObject::tr("Cursor outside plot area!"));
    setText(s);
}

void MvQCursorData::setText(QString str)
{
    text_ = str;
    textDoc_->setHtml(text_);

    QSizeF docSize = textDoc_->documentLayout()->documentSize();
    QRectF rect(0, 0, docSize.width(), docSize.height());

    if (boundingRect_ != rect) {
        prepareGeometryChange();
    }

    boundingRect_ = rect;
}


void MvQCursorData::setData(const CursorCoordinate& coord, MgQLayoutItem* dataLayout)
{
    // if dataLayout_ is not null it should contain the point
    if (dataLayout != nullptr) {

        double searchRadiusX = 2.;
        double searchRadiusY = 2.;

        float plotScale = plotScene_->plotScale();
        double cx = dataLayout->coordRatioX();
        double cy = dataLayout->coordRatioY();
        if (cx != 0.) {
            searchRadiusX = 20. / fabs(cx * plotScale);
        }
        if (cy != 0.) {
            searchRadiusY = 20. / fabs(cy * plotScale);
        }

        // qDebug() << "searchRadius:" <<  cx << cy << plotScale << searchRadiusX << searchRadiusY;

        QList<QPointF> posL;
        posL << QPointF(coord.x(), coord.y());

        QList<ValuesCollector> layerData;
        sceneItem_->collectLayerDataForCurrentStep(posL, layerData,
                                                   searchRadiusX, searchRadiusY);

        setText(coord, layerData);
    }
    else {
        setText();
    }
}

void MvQCursorData::mousePressEventFromView(QMouseEvent* event)
{
    if (activated_ && acceptMouseEvents_) {
        if (event->buttons() & Qt::LeftButton) {
            // Get scene position
            QPointF pos = plotView_->mapToScene(event->pos());
            changePositionMode(pos);
        }
    }
}

void MvQCursorData::mouseMoveEventFromView(QMouseEvent* event)
{   
    if ((!activated_ && !trackCoordinates_) || !acceptMouseEvents_)
        return;

    // Scene position
    QPointF pos = plotView_->mapToScene(event->pos());
    adjustToScenePos(pos, true);
}

void MvQCursorData::mouseReleaseEventFromView(QMouseEvent* event)
{
    if (activated_ && acceptMouseEvents_) {
        if (zoomWasPerformedAfterMouseRelease_ == false) {
            QPointF pos = plotView_->mapToScene(event->pos());
            changePositionMode(pos);
        }
    }
}


QString MvQCursorData::unitsString(QString val) const
{
    QString res = val;
    static std::map<QString, QString> units = {{"m s**-1", "m/s"},
                                              {"deg c", "&#176;C"},
                                              {"degrees", "deg"}};

    auto it = units.find(val);
    if (it != units.end()) {
        res = it->second;
    }

    if (!res.isEmpty())
        res = " (" + res + ")";

    return res;
}


QString MvQCursorData::formatNumber(double value) const
{
    QString valueStr;
    double absValue = std::abs(value);

    // is the number an integer?  - cut off the decimal places
    if (trunc(value) == value) {
        valueStr = QString::number(value, 'f', 0);
    // is the number very small? or very large?
    } else if (absValue < 0.000001 || absValue > 1000000000) {
        // - use scientific notation
        valueStr = QString::number(value, 'e');
    } else {
        // use 6 decimal places (default)
        valueStr = QString::number(value, 'f');
    }
    return valueStr;
}


void MvQCursorData::setMagnifierInfo(bool pointInMag, QPointF magPos, float magZoomFactor)
{
    pointInMagnifier_ = pointInMag;
    magnifierScenePos_ = magPos;
    magnifierZoomFactor_ = magZoomFactor;
}

void MvQCursorData::visit(const ValuesCollectorData& data)
{
    currentValues_.push_back(data.value());
}

void MvQCursorData::visit(const ValuesCollectorUVData& data)
{
    currentValues_.push_back(data.xComponent());
    currentValues_.push_back(data.yComponent());
}

void MvQCursorData::visit(const ValuesCollectorSDData& data)
{
    currentValues_.push_back(data.speed());
    currentValues_.push_back(data.direction());
}

// detach from the cursor and move cursordata box to the middle of the viewport
void MvQCursorData::resetPosition()
{
    // we must switch to anchor mode
    positionMode_ = Anchored;
    QPointF pos;

    // get the middle point of the view
    if (plotView_ != nullptr && plotView_->viewport() !=  nullptr) {
        pos = plotView_->mapToScene(plotView_->viewport()->rect().center());
    }

    // show the data at this pos so that we get a full (i.e. non-empty) cursor data size
    adjustToScenePos(pos, false);

    // using this size we move the centre of the cursor data rect to the
    // centre of the view
    auto br = boundingRect();
    QPointF delta(br.left() - br.center().x(), -br.height());
    pos = mapToParent(mapFromParent(pos) + delta);
    adjustToScenePos(pos, false);
    setCursorPos(pos, true);
}

// extracts the coordinates at the cursor (scene) position
bool MvQCursorData::getCoordinates(const QPointF& scPos, QPointF& coord, MgQLayoutItem** dataLayout)
{
    // Now if dataLayout_ is not null it should contain the point
    if (plotScene_->identifyPos(scPos, &sceneItem_, &(*dataLayout))) {
        Q_ASSERT(*dataLayout != nullptr);
        QPointF posM = scPos;
        if (pointInMagnifier_) {
            posM = magnifierScenePos_ + (scPos - magnifierScenePos_) / magnifierZoomFactor_;
        }

        (*dataLayout)->mapFromSceneToGeoCoords(posM, coord);
        return true;
    }
    return false;
}

void MvQCursorData::adjustToScenePos(const QPointF& pos, bool changeItemPos)
{
    if (!trackCoordinates_ && !activated_) {
        return;
    }

    CursorCoordinate coord;
    MgQLayoutItem* dataLayout = nullptr;
    QPointF cPos;
    if (getCoordinates(pos, cPos, &dataLayout)) {
        const Transformation& transformation = dataLayout->layout().transformation();
        if (transformation.coordinateType() == Transformation::GeoType) {
            coord = {cPos.x(), cPos.y(), CursorCoordinate::LonLatType};
        } else {
            coord = {cPos.x(), cPos.y(), CursorCoordinate::XyType};
        }
    }

    // notify other ui components about the coordinate
    if (trackCoordinates_) {
        emit coordinatesChanged(coord);
    }

    if (activated_) {
        setData(coord, dataLayout);
        if (changeItemPos) {
            setCursorPos(pos);
        }
    }
}
