/***************************** 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 "MvQFeatureItem.h"

#include <algorithm>

#include <QtGlobal>
#include <QApplication>
#include <QAction>
#include <QtAlgorithms>
#include <QClipboard>
#include <QDebug>
#include <QGraphicsSceneMouseEvent>
#include <QImage>
#include <QJsonDocument>
#include <QMenu>
#include <QMouseEvent>
#include <QPainter>
#include <QStyle>
#include <QTextStream>

#include "GenAppService.hpp"
#include "ObjectList.h"

#include "MvQPlotView.h"
#include "MvQFeatureCommand.h"
#include "MvQFeatureCommandTarget.h"
#include "MvQFeatureContextMenu.h"
#include "MvQFeatureItemObserver.h"
#include "MvQFeatureSelector.h"
#include "WsDecoder.h"
#include "WsType.h"
#include "MvQMethods.h"
#include "MvQJson.h"
#include "MgQLayoutItem.h"
#include "MvLog.h"
#include "MvQStreamOper.h"

#include <QStyleOptionGraphicsItem>

//#define MVQFEATUREITEM_DEBUG_
#define MVQFEATUREITEM_PROJ_DEBUG_

// z values should be between 1.3 and 1.4
const float MvQFeatureItem::bottomZValue_{1.3};
const float MvQFeatureItem::topZValue_{1.4};
const float MvQFeatureItem::zValueIncrement_{0.0001};  // this is enough for 10000 items!
float MvQFeatureItem::nextZValue_{MvQFeatureItem::bottomZValue_};
QColor MvQFeatureItem::devModeColor_{Qt::red};
QString MvQFeatureDescriber::indent_{"&nbsp;&nbsp;"};

MvQFeatureDescriber::MvQFeatureDescriber() :
    s_{&txt_}
{
}

void MvQFeatureDescriber::addTitle(QString val)
{
    s_ << MvQ::formatBoldText(val) << "<br>";
}

void MvQFeatureDescriber::add(QString label, void* p)
{
    s_ << indent_ << label << p << "<br>";
}

void MvQFeatureDescriber::add(QString label, QVariant val)
{
    s_ << indent_ << label;

#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
    if (val.typeId() == QMetaType::QPointF) {
#else
    if (val.type() == QVariant::PointF) {
#endif
        auto p = val.toPointF();
        s_ << p.x() << "/" << p.y();
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
    }
    else if (val.typeId() == QMetaType::QRectF) {
#else
    }
    else if (val.type() == QVariant::RectF) {
#endif
        auto r = val.toRectF();
        s_ << "rect(" << r.x() << "," << r.y() << "," << r.width() << "x" << r.height() << ")";
    }
    else {
        s_ << val.toString();
    }
    s_ << "<br>";
}

//========================================
//
// MvQFeatureItem
//
//========================================

MvQFeatureItem::MvQFeatureItem(WsType* feature) :
    QGraphicsItem(nullptr),
    feature_(feature),
    plotView_(MvQFeatureHandler::Instance()->view()),
    selector_(MvQFeatureHandler::Instance()->selector())
{
    Q_ASSERT(plotView_);
    Q_ASSERT(selector_);
    initInner();
}

MvQFeatureItem::MvQFeatureItem(QString featureTypeName, QGraphicsItem* parent) :
    QGraphicsItem(parent),
    plotView_(MvQFeatureHandler::Instance()->view()),
    selector_(MvQFeatureHandler::Instance()->selector()),
    typeName_(featureTypeName)
{
    initInner();
}

MvQFeatureItem::~MvQFeatureItem()
{
    MvLog().dbg() << "DELETE"
                  << " type: " << typeName() << " parent: " << parentItem();
    if (typeName() != "CurvePoint") {
        Q_ASSERT(parentItem() == nullptr);
    }
}

void MvQFeatureItem::init(const QJsonObject& obj)
{
    auto v = reqVerb();
    if (!v.isEmpty()) {
        req_ = MvRequest(v.toStdString().c_str());
        if (feature_) {
            feature_->updateTypeInReq(req_);
        }
    }

    if (!obj.empty()) {
        auto prop = obj["properties"].toObject();
        if (!prop.isEmpty()) {
            MvRequest r;
            MvQJson::toRequest(prop["style"].toObject(), r);
            if (r) {
                const char* verbRef = req_.getVerb();
                const char* verb    = r.getVerb();
                if (verb && ((verbRef && strcmp(verb, verbRef) == 0) || strlen(verb) > 0)) {
                    req_ = r;
                }
            }
        }
    }
    initStyle();
    initGeometry();
}

void MvQFeatureItem::initInner()
{
    Q_ASSERT(plotView_);

    setFlag(QGraphicsItem::ItemIsSelectable);
    setFlag(QGraphicsItem::ItemIsFocusable);

    QTransform tr;
    tr.scale(1., -1.);
    setTransform(tr);

    // TODO: find a better way
    if (typeName() != "CurvePoint") {
        setZValue(nextZValue_);
        nextZValue_ += zValueIncrement_;
    }

    setAcceptHoverEvents(true);
}

void MvQFeatureItem::toJson(QJsonObject& d)
{
    QJsonDocument doc;
    WsDecoder::toJson(feature_, scenePos(), geoCoords(), req_, doc);
    d = doc.object();
}

QString MvQFeatureItem::typeName() const
{
    if (feature_) {
        return feature_->name();
    }
    return typeName_;
}

QString MvQFeatureItem::typeDescription() const
{
    if (feature_) {
        return feature_->description();
    }
    return typeName();
}

QString MvQFeatureItem::reqVerb() const
{
    if (feature_) {
        return feature_->reqVerb();
    }
    return {};
}

QPointF MvQFeatureItem::scenePos() const
{
    if (parentItem()) {
        return parentItem()->mapToScene(pos());
    }
    return {};
}

// called after zoom or when we drop a new view into the plot
void MvQFeatureItem::projectionChanged(MgQLayoutItem* dataLayout, bool init)
{
    dataLayout_ = dataLayout;
    sceneId_.clear();
    if (dataLayout_) {
        sceneId_ = dataLayout_->layout().id();
    }
    inspectProjection();
    reproject(init);
}

// Recompute the position of the item using the current geoCoord
void MvQFeatureItem::reproject(bool init)
{
#ifdef MVQFEATUREITEM_PROJ_DEBUG_
    qDebug() << "MvQFeatureItem::reproject()" << typeName() << "outOfPlot:" << outOfView_ << "hasValidGeoCoord:" << hasValidGeoCoord_;
#endif
    QPointF scPos = parentItem()->mapToScene(pos());

    if (dataLayout_) {
        if (init) {
            outOfView_        = true;
            hasValidGeoCoord_ = false;
            if (isValidPoint(geoCoord_)) {
                hasValidGeoCoord_ = true;
                outOfView_        = !dataLayout_->containsGeoCoords(geoCoord_);
            }
        }
        else if (hasValidGeoCoord_) {
            outOfView_ = !dataLayout_->containsGeoCoords(geoCoord_);
        }

        if (hasValidGeoCoord_) {
            // in the view
            if (!outOfView_) {
#ifdef MVQFEATUREITEM_PROJ_DEBUG_
                qDebug() << "geoCoord:" << geoCoord_ << "in_view";
#endif
                dataLayout_->mapFromGeoToSceneCoords(geoCoord_, scPos);
                setPos(parentItem()->mapFromScene(scPos));
                show();
                return;
                // outside the view
            }
            else {
#ifdef MVQFEATUREITEM_PROJ_DEBUG_
                qDebug() << "geoCoord:" << geoCoord_ << "out_of_view";
#endif
                hide();
                return;
            }
        }
    }

    outOfView_        = true;
    hasValidGeoCoord_ = false;
    show();
}

// called when we add the item to the scene or when an item is moved into another layout
void MvQFeatureItem::initScenePosition(MgQLayoutItem* dataLayout, const QPointF& scPos)
{
    dataLayout_ = dataLayout;
    sceneId_.clear();
    outOfView_        = true;
    hasValidGeoCoord_ = false;
    inspectProjection();
    if (dataLayout_) {
        sceneId_       = dataLayout_->layout().id();
        QPointF scPosA = scPos;
        if (dataLayout_->containsSceneCoords(scPosA)) {
            outOfView_ = false;
            QPointF geoCoord;
            dataLayout_->mapFromSceneToGeoCoords(scPos, geoCoord);
            if (isValidPoint(geoCoord)) {
                hasValidGeoCoord_ = true;
                initAtGeoCoord(geoCoord);
                return;
            }
        }
    }
    initAtScenePos(scPos);
}

void MvQFeatureItem::initAtScenePos(const QPointF& scPos)
{
    setPos(parentItem()->mapFromScene(scPos));
}

void MvQFeatureItem::initAtGeoCoord(QPointF geoCoord)
{
    geoCoord_         = geoCoord;
    hasValidGeoCoord_ = isValidPoint(geoCoord_);
    if (dataLayout_ && hasValidGeoCoord_) {
        QPointF scPos;
        dataLayout_->mapFromGeoToSceneCoords(geoCoord_, scPos);
        setPos(parentItem()->mapFromScene(scPos));
    }
}

void MvQFeatureItem::adjustGeoCoord()
{
    if (dataLayout_) {
        QPointF scPos = parentItem()->mapToScene(pos());
        dataLayout_->mapFromSceneToGeoCoords(scPos, geoCoord_);
        hasValidGeoCoord_ = isValidPoint(geoCoord_);
    }
    else {
        hasValidGeoCoord_ = false;
    }
}

QPointF MvQFeatureItem::computeGeoCoord()
{
    QPointF gc;
    if (dataLayout_) {
        QPointF scPos = parentItem()->mapToScene(pos());
        return computeGeoCoord(scPos);
    }
    return gc;
}

QPointF MvQFeatureItem::computeGeoCoord(QPointF scPos)
{
    QPointF gc;
    if (dataLayout_) {
        dataLayout_->mapFromSceneToGeoCoords(scPos, gc);
    }
    return gc;
}

void MvQFeatureItem::inspectProjection()
{
    projPeriodic_ = false;
    if (dataLayout_) {
        QPointF sc1, sc2;
        QPointF gc1(190, 0), gc2(-170, 0);
        dataLayout_->mapFromGeoToSceneCoords(gc1, sc1);
        dataLayout_->mapFromGeoToSceneCoords(gc2, sc2);
        double eps = 1E-6;
        if (isValidPoint(sc1) && isValidPoint(sc2) &&
            fabs(sc1.x() - sc2.x()) < eps && fabs(sc1.y() - sc2.y()) < eps) {
            projPeriodic_ = true;
        }
    }
}

void MvQFeatureItem::checkLayoutOnMove()
{
    MgQLayoutItem* layoutItem = nullptr;
    QPointF scPos             = parentItem()->mapToScene(pos());
    MvQFeatureHandler::Instance()->currentLayout(getptr(), &layoutItem);
    if (layoutItem) {
        if (layoutItem == dataLayout_) {
            if (outOfView_) {
                dataLayout_->mapFromSceneToGeoCoords(scPos, geoCoord_);
                if (isValidPoint(geoCoord_) && dataLayout_->containsGeoCoords(geoCoord_)) {
                    outOfView_        = false;
                    hasValidGeoCoord_ = true;
                }
            }
            else {
                if (!isValidPoint(geoCoord_) || !dataLayout_->containsGeoCoords(geoCoord_)) {
                    outOfView_        = true;
                    hasValidGeoCoord_ = !isValidPoint(geoCoord_);
                }
            }
            // change the dataLayout!
        }
        else {
            initScenePosition(layoutItem, scPos);
        }
        // keep the last geo coord?
    }
    else {
        if (!outOfView_) {
            outOfView_        = true;
            hasValidGeoCoord_ = !isValidPoint(geoCoord_);
        }
    }
}

bool MvQFeatureItem::isValidPoint(QPointF p)
{
    return std::isfinite(p.x()) && std::isfinite(p.y());
}

void MvQFeatureItem::setHighlighted(bool flag)
{
    if (highlighted_ != flag) {
        highlighted_ = flag;
        update();
    }
}

void MvQFeatureItem::mousePressEvent(QGraphicsSceneMouseEvent* event)
{
#ifdef MVQFEATUREITEM_DEBUG_
    qDebug() << "MvQFeatureItem::mousePress"
             << " event.pos:" << event->pos() << " event.pos(parent):" << mapToParent(event->pos()) << "typeName:" << typeName() << "isSelected:" << isSelected() << this;
    qDebug() << " zValue" << zValue();
#endif
    dragPos_        = mapToParent(event->pos());
    lastPressedPos_ = event->pos();
    // this will make the item selected, and the selector will be notified
    QGraphicsItem::mousePressEvent(event);
    lastPressed_.start();
}

void MvQFeatureItem::mouseMoveEvent(QGraphicsSceneMouseEvent* event)
{
#ifdef MVQFEATUREITEM_DEBUG_
    qDebug() << "MvQFeatureItem::mouseMove";
    qDebug() << " typeName:" << typeName_ << "isSelected:" << isSelected() << this;
    qDebug() << " event.pos:" << event->pos() << "lastPressedPos:" << lastPressedPos_;
#endif
    Q_ASSERT(!isPoint());
    if (!isGeoLocked()) {
        auto evtPos = mapToParent(event->pos());
        auto delta  = evtPos - dragPos_;
        moveBy(delta);
        dragPos_ += delta;
    }
}

void MvQFeatureItem::mouseReleaseEvent(QGraphicsSceneMouseEvent* event)
{
#ifdef MVQFEATUREITEM_DEBUG_
    qDebug() << "MvQFeatureItem::mouseRelease";
    qDebug() << " " << typeName_ << isSelected() << this;
#endif
    QGraphicsItem::mouseReleaseEvent(event);
}


void MvQFeatureItem::hoverEnterEvent(QGraphicsSceneHoverEvent*)
{
#ifdef MVQFEATUREITEM_DEBUG_
    qDebug() << "MvQFeatureItem::hoverEnterEvent" << this;
#endif
    if (currentAction_ != PointEditAction &&
        MvQFeatureHandler::Instance()->canAcceptMouseEvent()) {
        MvQFeatureHandler::Instance()->setDragCursor();
    }
}

void MvQFeatureItem::hoverLeaveEvent(QGraphicsSceneHoverEvent*)
{
    if (MvQFeatureHandler::Instance()->canAcceptMouseEvent()) {
        MvQFeatureHandler::Instance()->resetCursor();
    }
}

QVariant MvQFeatureItem::itemChange(GraphicsItemChange change, const QVariant& value)
{
    if (change == ItemSelectedHasChanged) {
        //qDebug() << "MvQFeatureItem::itemChange" << typeName_ << isSelected() << this;
        plotView_->itemSelected(this);
        return value;
    }

    return QGraphicsItem::itemChange(change, value);
}

void MvQFeatureItem::moveBy(QPointF delta)
{
    prepareGeometryChange();

    QPointF prPos = pos() + delta;
    QPointF scPos = parentItem()->mapToScene(prPos);
    if (dataLayout_ && !outOfView_) {
        auto gc = computeGeoCoord(scPos);
        if (isValidPoint(gc)) {
            geoCoord_ = gc;
        }
    }

    setPos(prPos);
    checkLayoutOnMove();

#ifdef MVQFEATUREITEM_DEBUG
    qDebug() << "MvQFeatureItem::moveBy -> pos:" << pos() << "gc:" << geoCoord_;
#endif
}

void MvQFeatureItem::moveTo(const MvQFeatureGeometry& g)
{
    prepareGeometryChange();
    if (dataLayout_) {
        if (isValidPoint(g.geoCoord_)) {
            geoCoord_ = g.geoCoord_;
        }
    }

    setPos(g.pos_);
    checkLayoutOnMove();
#ifdef MVQFEATUREITEM_DEBUG
    qDebug() << "MvQFeatureItem::moveTo -> pos:" << pos() << "gc:" << geoCoord_;
#endif
}

// input rects are in parent's coordinates
QRectF MvQFeatureItem::rectToSelector(QRectF rectNew, QRectF rectOri)
{
    //QRectF r = mapToParent(itemRect()).boundingRect();
    QRectF r = mapRectToParent(boundingRect());

#ifdef MVQFEATUREITEM_DEBUG_
    qDebug() << "MvQFeatureItem::rectToSelector --->";
    qDebug() << " rectNew:" << rectNew << "rectOri:" << rectOri << "itemRect:" << itemRect();
#endif
    if (keepAspectRatio_) {
        float hFactor = rectNew.height() / rectOri.height();
        float wFactor = rectNew.width() / rectOri.width();
        float factor  = std::min(hFactor, wFactor);

        auto ratio = r.height() / r.width();
        auto w     = factor * r.width();  //rectNew.width();
        auto h     = w * ratio;
#ifdef MVQFEATUREITEM_DEBUG_
        qDebug() << " ratio:" << ratio << "w:" << w << "h:" << h;
#endif
        //        if (h > rectNew.height()) {
        //            h = rectNew.height();
        //            w = h/ratio;
        //        }

        float top  = rectNew.top() + (r.top() - rectOri.top()) * hFactor;
        float left = rectNew.left() + (r.left() - rectOri.left()) * wFactor;
        auto a     = QRectF(left, top, w, h);
#ifdef MVQFEATUREITEM_DEBUG_
        qDebug() << " ratio:" << ratio << "w:" << w << "h:" << h;
        qDebug() << " result:" << a;
#endif
        return a;
    }
    else {
        if (selector_->managedItems().size() == 1) {
            return rectNew;
        }
        else {
            float hRatio = rectNew.height() / rectOri.height();
            float wRatio = rectNew.width() / rectOri.width();

            // see note about QRectF.right() and bottom() in the Qt docs!
            float top    = rectNew.top() + (r.top() - rectOri.top()) * hRatio;
            float bottom = rectNew.y() + rectNew.height() - (rectOri.bottom() - r.bottom()) * hRatio;
            float left   = rectNew.left() + (r.left() - rectOri.left()) * wRatio;
            float right  = rectNew.x() + rectNew.width() - (rectOri.right() - r.right()) * wRatio;

            auto a = QRectF(left, top, right - left, bottom - top);
#ifdef MVQFEATUREITEM_DEBUG_
            qDebug() << " result:" << a;
#endif
            return a;
        }
    }
    return {};
}

// update item when the selector size changed. The selector size is based on the items
// boundingRects!
void MvQFeatureItem::adjustSizeToSelector(QRectF rectNew, QRectF rectOri)
{
    QRectF r = rectToSelector(rectNew, rectOri);
#ifdef MVQFEATUREITEM_DEBUG_
//    qDebug() << "MvQFeatureItem::adjustSizeToSelector" << rectNew << rectOri << r;
#endif
    resize(r);
}

bool MvQFeatureItem::identifyCommandTarget(MvQFeatureCommandTarget* s)
{
    QPointF itemPos = mapFromScene(s->scenePos());
    if (bRect_.contains(itemPos)) {
        s->setTargetItem(getptr());
        return true;
    }
    return false;
}

void MvQFeatureItem::editStyle()
{
    // We send the style request to Desktop.
    CallGenAppService(*this, &MvQFeatureItem::styleEdited, req_);
}

void MvQFeatureItem::styleEdited(MvRequest& req)
{
    if (plotView_) {
        auto *w = plotView_->window();
        if(w) {
            w->raise();
        }
    }

    // Nothing to be edited. Button CANCEL selected by user.
    if (!req)
        return;

    if (feature_) {
        feature_->updateTypeInReq(req);
    }

    MvQFeatureHandler::Instance()->updateStyle(getptr(), req);
}

void MvQFeatureItem::styleEditedInRibbon(MvRequest& req, StyleEditInfo info)
{
    // Nothing to be edited. Button CANCEL selected by user.
    if (!req)
        return;

    MvQFeatureHandler::Instance()->updateStyle(getptr(), req, info);
}

// should only be called from the handler or on init
void MvQFeatureItem::setStyle(const MvRequest& req, bool init)
{
    if (req) {
        req_ = req;
    }

    if (!getRequestParameters() && !init) {
        return;
    }

    adjustGeoLocked();
    adjustToolTip();
    updateSymbols();
    makePixmap();

    if (parentItem()) {
        adjustBRect();
        selector_->itemStyleChanged(getptr());
    }

    update();
}

void MvQFeatureItem::initStyle()
{
    // req_ is already set, we just apply the settings
    setStyle({}, true);
}

void MvQFeatureItem::expandStyleRequest()
{
    if (!req_) {
        auto v = reqVerb();
        if (!v.isEmpty()) {
            req_ = MvRequest(v.toStdString().c_str());
        }
    }

    MvRequest r = ObjectList::ExpandRequest(req_, EXPAND_DEFAULTS);
    if (r) {
        req_ = r;
    }
}

void MvQFeatureItem::adjustToolTip()
{
    QString t;
    if (const char* ch = req_("TOOLTIP")) {
        t = QString(ch);
    }
    if (t != toolTip()) {
        setToolTip(t);
    }
}

void MvQFeatureItem::adjustGeoLocked()
{
    const char* ch = req_("GEOLOCK");
    geoLocked_     = ch && strcmp(ch, "ON") == 0;
}

double MvQFeatureItem::realZValue() const
{
    return zValue();
}

void MvQFeatureItem::setRealZValue(double z)
{
    setZValue(z);
}

void MvQFeatureItem::setClipping(QPainter* painter)
{
    // clip to map boundaries
    if (dataLayout_) {
        auto b = mapRectFromScene(dataLayout_->mapRectToScene(dataLayout_->boundingRect()));
        QPainterPath ptp;
        ptp.addRect(b);
        painter->setClipPath(ptp);
    }
}

QSizeF MvQFeatureItem::minSize() const
{
    return QSizeF(8 + 2. * halo(), 8 + 2. * halo());
}

MvQFeatureGeometry MvQFeatureItem::getGeometry() const
{
    MvQFeatureGeometry g;
    g.bRect_    = bRect_;
    g.itemRect_ = itemRect_;
    g.pos_      = pos();
    g.geoCoord_ = g.geoCoord_;
    return g;
}

QString MvQFeatureItem::describe() const
{
    MvQFeatureDescriber ds;
    ds.addTitle(typeName());
    ds.add("canBeCreatedOutOfView=", canBeCreatedOutOfView());
    ds.add("canBePastedOfView=", canBePastedOutOfView());
    ds.add("dataLayout=", dataLayout_);
    ds.add("sceneId=", QString::fromStdString(sceneId_));
    ds.add("outOfView=", outOfView_);
    ds.add("hasValidGeoCoord=", hasValidGeoCoord_);
    ds.add("geoCoord=", geoCoord_);
    ds.add("pos=", pos());
    ds.add("scPos=", scenePos());
    ds.add("itemRect=", itemRect_);
    ds.add("bRect=", bRect_);
    ds.add("halo=", halo());
    return ds.text();
}

void MvQFeatureItem::addObserver(MvQFeatureItemObserver* o)
{
    if (std::find(observers_.begin(), observers_.end(), o) == observers_.end()) {
        observers_.push_back(o);
    }
}

void MvQFeatureItem::removeObserver(MvQFeatureItemObserver* o)
{
    auto it = std::find(observers_.begin(), observers_.end(), o);
    if (it != observers_.end()) {
        observers_.erase(it);
    }
}

void MvQFeatureItem::broadcastRequestChanged()
{
    std::vector<MvQFeatureItemObserver*> oCopy = observers_;
    for (auto o : oCopy) {
        o->notifyFeatureRequestChanged(getptr());
    }
}

QPixmap MvQFeatureItem::toPixmap()
{
    QPixmap pixmap(boundingRect().size().toSize());
    pixmap.fill(Qt::transparent);
    QPainter painter(&pixmap);
    painter.setRenderHint(QPainter::Antialiasing);
    QStyleOptionGraphicsItem opt;
    painter.translate(-1 * (boundingRect().topLeft()));
    paint(&painter, &opt);
    return pixmap;
}
