/*
 * wbwidget.cpp - a widget for processing and showing whiteboard
 *                messages.
 * Copyright (C) 2006  Joonas Govenius
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

#include "wbwidget.h"

#include "wbnewimage.h"
#include "wbnewpath.h"

#include <QApplication>
#include <QMouseEvent>
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
#include <QRandomGenerator>
#endif

WbWidget::WbWidget(SxeSession *session, QWidget *parent) : QGraphicsView(parent)
{
    newWbItem_   = nullptr;
    adding_      = nullptr;
    addVertex_   = false;
    strokeColor_ = Qt::black;
    fillColor_   = Qt::transparent;
    strokeWidth_ = 1;
    session_     = session;

    //    setCacheMode(CacheBackground);
    setRenderHint(QPainter::Antialiasing);
    setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);

    // Make the scroll bars always stay on because otherwise the resize event can cause
    // an infinite loop as the effective size of the widget changes when scroll bars are
    // added/removed
    setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
    setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);

    // create the scene
    scene_ = new WbScene(session_, this);
    scene_->setItemIndexMethod(QGraphicsScene::NoIndex);
    setRenderHint(QPainter::Antialiasing);
    setTransformationAnchor(AnchorUnderMouse);
    setResizeAnchor(AnchorViewCenter);
    setScene(scene_);

    // render the initial document
    rerender();
    // rerender on update
    connect(session_, SIGNAL(documentUpdated(bool)), SLOT(handleDocumentUpdated(bool)));

    // add the initial items
    const QDomNodeList children = session_->document().documentElement().childNodes();
    for (int i = 0; i < int(children.length()); i++) {
        const QDomNode node = children.at(i);
        if (node.isElement()) {
            queueNodeInspection(node.toElement());
        }
    }
    inspectNodes();

    // add new items as nodes are added
    // remove/add items if corresponding nodes are moved
    connect(session_, SIGNAL(nodeAdded(QDomNode, bool)), SLOT(queueNodeInspection(QDomNode)));
    connect(session_, SIGNAL(nodeMoved(QDomNode, bool)), SLOT(queueNodeInspection(QDomNode)));
    // remove items if corresponding nodes are deleted
    connect(session_, SIGNAL(nodeToBeRemoved(QDomNode, bool)), SLOT(removeWbItem(QDomNode)));
    connect(session_, SIGNAL(nodeToBeRemoved(QDomNode, bool)), SLOT(checkForRemovalOfId(QDomNode)));
    // adjust the viewBox as necessary
    connect(session_, SIGNAL(nodeAdded(QDomNode, bool)), SLOT(checkForViewBoxChange(QDomNode)));
    connect(session_, SIGNAL(nodeMoved(QDomNode, bool)), SLOT(checkForViewBoxChange(QDomNode)));
    connect(session_, SIGNAL(chdataChanged(QDomNode, bool)), SLOT(checkForViewBoxChange(QDomNode)));

    // set the default mode to select
    setMode(Mode::Select);

    // set the initial size
    if (session_->document().documentElement().hasAttribute("viewBox"))
        checkForViewBoxChange(session_->document().documentElement().attributeNode("viewBox"));
    else {
        QSize  size;
        QRectF rect = scene_->sceneRect();
        size.setWidth(int(rect.x() + rect.width()));
        size.setHeight(int(rect.y() + rect.height()));
        if (size.width() > 0 && size.height() > 0)
            setSize(size);
        else
            setSize(QSize(400, 600));
    }
}

SxeSession *WbWidget::session() { return session_; }

WbWidget::Mode WbWidget::mode() { return mode_; }

void WbWidget::setMode(Mode mode)
{
    mode_ = mode;

    if (mode_ < Mode::DrawPath) {
        if (newWbItem_) {
            delete newWbItem_;
            newWbItem_ = nullptr;
        }
    }

    if (mode_ >= Mode::Erase) {
        setDragMode(QGraphicsView::NoDrag);
        setInteractive(false);
        setCursor(Qt::CrossCursor);
    } else {
        setInteractive(true);
    }

    if (mode_ == Mode::Select) {
        setDragMode(QGraphicsView::RubberBandDrag);
        setCursor(Qt::ArrowCursor);
    } else if (mode_ == Mode::Translate) {
        setDragMode(QGraphicsView::RubberBandDrag);
        setCursor(Qt::SizeAllCursor);
    } else if (mode_ == Mode::Rotate) {
        setDragMode(QGraphicsView::RubberBandDrag);
        unsetCursor();
        // TODO: load cursor from image
    } else if (mode_ == Mode::Scale) {
        setDragMode(QGraphicsView::RubberBandDrag);
        setCursor(Qt::SizeBDiagCursor);
    } else if (mode_ == Mode::Scroll) {
        setDragMode(QGraphicsView::ScrollHandDrag);
    }
}

void WbWidget::setSize(const QSize &size)
{
    session_->setAttribute(session_->document().documentElement(), "viewBox",
                           QString("0 0 %1 %2").arg(size.width()).arg(size.height()));
    session_->flush();
}

/*! \brief Generates a QRectF based on \a string provided in the SVG viewBox format. */
static QRectF parseSvgViewBox(QString string)
{
    QString strings[4];
    qreal   numbers[4];
    for (int i = 0, j = 0; i < 4; i++) {
        // skip spaces before number
        while (string[j].isSpace() && j < string.length())
            j++;

        while (!string[j].isSpace() && j < string.length()) {
            if (string[j].isNumber())
                strings[i] += string[j];
            j++;
        }

        numbers[i] = strings[i].toDouble();
    }

    // qDebug() << QString("QRectF(%1 %2 %3
    // %4)").arg(numbers[0]).arg(numbers[1]).arg(numbers[2]).arg(numbers[3]).toLatin1();
    return QRect(int(numbers[0]), int(numbers[1]), int(numbers[2]), int(numbers[3]));
}

void WbWidget::checkForViewBoxChange(const QDomNode &node)
{
    if (node.isAttr() && node.nodeName().toLower() == "viewbox"
        && node.parentNode() == session_->document().documentElement()) {
        QRectF box = parseSvgViewBox(node.nodeValue());
        if (box.width() > 0 && box.height() > 0) {
            scene_->setSceneRect(box);
        }
    }
}

void WbWidget::setStrokeColor(const QColor &color) { strokeColor_ = color; }

void WbWidget::setFillColor(const QColor &color) { fillColor_ = color; }

void WbWidget::setStrokeWidth(int width) { strokeWidth_ = width; }

void WbWidget::clear()
{
    const auto &gItems = scene_->items();
    for (QGraphicsItem *graphicsitem : gItems) {
        WbItem *wbitem = dynamic_cast<WbItem *>(graphicsitem);
        if (wbitem)
            session_->removeNode(wbitem->node());
    }
    session_->flush();
}

QSize WbWidget::sizeHint() const
{
    if (scene_)
        return scene_->sceneRect().size().toSize();
    else
        return QSize();
}

void WbWidget::resizeEvent(QResizeEvent *event)
{
    // Never show areas outside the sceneRect
    // Doesn't consider rotated views.
    QTransform t;
    qreal      sx = event->size().width() / scene_->sceneRect().width();
    qreal      sy = event->size().height() / scene_->sceneRect().height();

    // Never shrink the view. Only enlarge if necessary.
    if (sx > 1 || sy > 1) {
        if (sx > sy)
            t.scale(sx, sx);
        else
            t.scale(sy, sy);
    }

    setTransform(t);
    QGraphicsView::resizeEvent(event);
}

void WbWidget::mousePressEvent(QMouseEvent *event)
{
    // ignore non-leftclicks when not in Select mode
    if (event->button() != Qt::LeftButton && mode_ != Mode::Select)
        return;

    // delete any temporary item being drawn
    if (newWbItem_) {
        delete newWbItem_;
        newWbItem_ = nullptr;
    }

    QPointF startPoint = mapToScene(mapFromGlobal(event->globalPos()));
    if (mode_ == Mode::DrawPath) {
        // // Create the element with starting position
        // QPointF sp = mapToScene(mapFromGlobal(event->globalPos()));
        // qDebug() << QString("1: (%1, %2)").arg(sp.x()).arg(sp.y());
        newWbItem_ = new WbNewPath(scene_, startPoint, strokeWidth_, strokeColor_, fillColor_);
        return;
    } else if (mode_ == Mode::DrawText) {

    } else if (mode_ == Mode::DrawRectangle) {

    } else if (mode_ == Mode::DrawEllipse) {

    } else if (mode_ == Mode::DrawCircle) {

    } else if (mode_ == Mode::DrawLine) {

    } else if (mode_ == Mode::DrawImage) {
        QString filename = QFileDialog::getOpenFileName(this, "Choose an image", QString(), "Images (*.png *.jpg)");
        if (!filename.isEmpty()) {
            newWbItem_ = new WbNewImage(scene_, startPoint, filename);
            QDomDocument tempDoc;
            session_->insertNodeAfter(newWbItem_->serializeToSvg(&tempDoc), session_->document().documentElement());
            session_->flush();
            delete newWbItem_;
            newWbItem_ = nullptr;
        }
    }

    QGraphicsView::mousePressEvent(event);
}

void WbWidget::mouseMoveEvent(QMouseEvent *event)
{
    if (mode_ < Mode::Erase) {
        QGraphicsView::mouseMoveEvent(event);
        return;
    }

    if (QApplication::mouseButtons() != Qt::MouseButtons(Qt::LeftButton)) {
        if (newWbItem_) {
            delete newWbItem_;
            newWbItem_ = nullptr;
        }
        return;
    }

    if (mode_ == Mode::Erase) {
        if (event->buttons() != Qt::MouseButtons(Qt::LeftButton))
            return;
        // Erase all items that appear in a 2*strokeWidth_ square with center at the event position
        QPointF            p = mapToScene(mapFromGlobal(event->globalPos()));
        QGraphicsRectItem *eraseRect
            = scene_->addRect(QRectF(p.x() - strokeWidth_, p.y() - strokeWidth_, 2 * strokeWidth_, 2 * strokeWidth_));
        const auto &gItems = eraseRect->collidingItems();
        for (QGraphicsItem *item : gItems) {
            WbItem *wbitem = dynamic_cast<WbItem *>(item);
            if (wbitem)
                session_->removeNode(wbitem->node());
        }
        delete eraseRect;
        eraseRect = nullptr;

        event->ignore();
        return;
    } else if (mode_ >= Mode::DrawPath && newWbItem_) {
        newWbItem_->parseCursorMove(mapToScene(mapFromGlobal(event->globalPos())));
    }
}

void WbWidget::mouseReleaseEvent(QMouseEvent *event)
{

    if (event->button() != Qt::LeftButton && mode_ >= Mode::Erase)
        return;

    if (newWbItem_ && mode_ >= Mode::DrawPath && mode_ != Mode::DrawImage) {
        QDomDocument tempDoc;
        session_->insertNodeAfter(newWbItem_->serializeToSvg(&tempDoc), session_->document().documentElement());
        session_->flush();
        delete newWbItem_;
        newWbItem_ = nullptr;
        return;
    }

    QGraphicsView::mouseReleaseEvent(event);
}

WbItem *WbWidget::wbItem(const QDomNode &node)
{
    for (WbItem *wbitem : qAsConst(items_)) {
        if (wbitem->node() == node)
            return wbitem;
    }
    return nullptr;
}

void WbWidget::handleDocumentUpdated(bool remote)
{
    Q_UNUSED(remote);
    inspectNodes();
    rerender();
}

void WbWidget::inspectNodes()
{
    while (!recentlyRelocatedNodes_.isEmpty()) {
        QDomNode node = recentlyRelocatedNodes_.takeFirst();

        if (!node.isElement())
            continue;

        // check if we already have a WbItem for the node
        WbItem *item = wbItem(node);

        // We don't need to do anything iff node is child of <svg/> and item exists or vice versa
        if ((item != nullptr) == (node.parentNode() == session_->document().documentElement()))
            continue;

        // Otherwise, either item exists and needs to be removed
        //              or it doesn't exist and needs to be added
        if (item)
            removeWbItem(item);
        else
            items_.append(new WbItem(session_, &renderer_, node.toElement(), scene_, this));
    }
}

void WbWidget::queueNodeInspection(const QDomNode &node)
{
    if (node.isElement())
        recentlyRelocatedNodes_.append(node);
}

void WbWidget::removeWbItem(const QDomNode &node) { removeWbItem(wbItem(node)); }

void WbWidget::removeWbItem(WbItem *wbitem)
{
    if (wbitem) {
        qDebug("delete WbItem");
        // Remove from the lookup table to avoid infinite loop of deletes
        items_.removeAll(wbitem);
        // items_.takeAt(items_.indexOf(wbitem));

        idlessItems_.removeAll(wbitem);

        delete wbitem;
    }
}

void WbWidget::addIds()
{
    while (!idlessItems_.isEmpty())
        idlessItems_.takeFirst()->addToScene();

    session_->flush();
}

void WbWidget::addToIdLess(const QDomElement &element)
{
    if (element.parentNode() != session_->document().documentElement())
        return;

    WbItem *item = wbItem(element);
    if (item && !idlessItems_.contains(item)) {
        // Remove from the scene until a new 'id' attribute is added
        item->removeFromScene();

        idlessItems_.append(item);

        // Try adding the 'id' attribute after a random delay of 0 to 2s
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
        QTimer::singleShot(QRandomGenerator::global()->bounded(2000), this, SLOT(addIds()));
#else
        QTimer::singleShot(2000 * qrand() / RAND_MAX, this, SLOT(addIds()));
#endif
    }
}

void WbWidget::checkForRemovalOfId(QDomNode node)
{
    if (node.isAttr()) {
        if (node.nodeName() == "id") {
            while (!(node.isElement() || node.isNull()))
                node = node.parentNode();

            if (!node.isNull())
                addToIdLess(node.toElement());
        }
    }
}

void WbWidget::rerender()
{
    QString     xmldump;
    QTextStream stream(&xmldump);
    session_->document().save(stream, 1);

    // qDebug("Document in WbWidget:");
    // qDebug() << xmldump.toLatin1();

    renderer_.load(xmldump.toLatin1());

    // Update all positions if changed
    for (WbItem *wbitem : qAsConst(items_)) {
        // resetting elementId is necessary for rendering some updates to the element (e.g. adding child elements to
        // <g/>)
        wbitem->setElementId(wbitem->id());

        // qDebug() << QString("Rerendering %1").arg((unsigned int) wbitem).toLatin1();
        wbitem->resetPos();
    }
}
