/*
 * wbitem.cpp - the item classes for the SVG WB
 * 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 "wbitem.h"

#include "wbscene.h"
#include "wbwidget.h"
#include <iconaction.h>

#include <QActionGroup>
#include <QDebug>
#include <QRegularExpression>
#include <QSvgRenderer>

#include <math.h>

static QTransform parseTransformationMatrix(const QString &value);

/*
 *    WbItemMenu
 */

WbItemMenu::WbItemMenu(QWidget *parent) : QMenu(parent) { connect(this, SIGNAL(aboutToHide()), SLOT(destructSelf())); }

WbItemMenu::~WbItemMenu()
{
    for (QActionGroup *g : std::as_const(groups_)) {
        const auto &acts = g->actions();
        for (QAction *a : acts) {
            a->deleteLater();
        }
        g->deleteLater();
    }
}

void WbItemMenu::addActionGroup(QActionGroup *g)
{
    groups_.append(g);
    addActions(g->actions());
}

void WbItemMenu::destructSelf() { deleteLater(); }

/*
 *   WbItem
 */

WbItem::WbItem(SxeSession *session, QSvgRenderer *renderer, QDomElement node, WbScene *scene, WbWidget *widget) :
    QGraphicsSvgItem()
{
    // Store a pointer to the underlying session, scene and node
    session_ = session;
    scene_   = scene;
    widget_  = widget;
    node_    = node;

    // qDebug() << QString("constructing %1.").arg(id()).toLatin1();

    if (node.isNull()) {
        qDebug("Trying to create a WbItem from a null QDomNode.");
        return;
    }

    // Make the item selectable and movable
    setFlag(QGraphicsItem::ItemIsSelectable);
    setFlag(QGraphicsItem::ItemIsMovable);

    // Enable drag-n-drop
    setAcceptDrops(true);

    // Don't cache the SVG
    setCachingEnabled(false);

    // Set the renderer for the item
    setSharedRenderer(renderer);

    // add the new item to the scene
    addToScene();
}

WbItem::~WbItem()
{
    // qDebug() << QString("destructing %1.").arg(id()).toLatin1();
}

QString WbItem::id() { return node_.toElement().attribute("id"); }

QDomNode WbItem::node() { return node_; }

void WbItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
{
    constructContextMenu()->exec(event->screenPos());
}

void WbItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
    event->accept();

    if (isSelected()) {
        if (widget_->mode() == WbWidget::Mode::Translate) {

            // Translate each selected item
            const auto &gItems = scene()->selectedItems();
            for (QGraphicsItem *graphicsitem : gItems) {
                if (!graphicsitem->parentItem() || !graphicsitem->parentItem()->isSelected()) {
                    QPointF d = graphicsitem->mapFromScene(event->scenePos())
                        - graphicsitem->mapFromScene(event->lastScenePos());
                    graphicsitem->setTransform(QTransform::fromTranslate(d.x(), d.y()), true);
                    // qDebug() QString("Translated %1 by %2 %3").arg((unsigned int)
                    // graphicsitem).arg(d.x()).arg(d.y()).toLatin1();

                    // Regenerate the SVG transformation matrix later
                    WbItem *wbitem = dynamic_cast<WbItem *>(graphicsitem);
                    if (wbitem)
                        scene_->queueTransformationRegeneration(wbitem);
                }
            }

        } else if (widget_->mode() == WbWidget::Mode::Rotate) {

            // Rotate each selected item
            // get center coordinates of selected items in scene coordinates
            QPointF scenePivot = scene_->selectionCenter();
            // Determine the direction relative to the center
            // Divide the sum by two to reduce the "speed" of rotation
            QPointF difference = event->scenePos() - event->lastScenePos();
            //          qDebug() << QString("d: %1 %2 = %3 %4 + %5
            //          %6").arg(difference.x()).arg(difference.y()).arg(event->scenePos().x()).arg(event->scenePos().y()).arg(event->lastScenePos().x()).arg(event->lastScenePos().y());
            QPointF    p = event->scenePos();
            QTransform delta;
            if (p.x() >= scenePivot.x() && p.y() >= scenePivot.y()) {
                delta.rotate((-difference.x() + difference.y()) / 2);
            } else if (p.x() < scenePivot.x() && p.y() >= scenePivot.y()) {
                delta.rotate((-difference.x() - difference.y()) / 2);
            } else if (p.x() < scenePivot.x() && p.y() < scenePivot.y()) {
                delta.rotate((difference.x() - difference.y()) / 2);
            } else if (p.x() >= scenePivot.x() && p.y() < scenePivot.y()) {
                delta.rotate((difference.x() + difference.y()) / 2);
            }

            const auto &gItems = scene()->selectedItems();
            for (QGraphicsItem *graphicsitem : gItems) {
                if (!graphicsitem->parentItem() || !graphicsitem->parentItem()->isSelected()) {
                    QTransform translation;
                    // get center coordinates of selected items in item coordinates
                    QPointF itemPivot = graphicsitem->mapFromScene(scenePivot);
                    // translates the the item's center to the origin of the item coordinates
                    translation.translate(-itemPivot.x(), -itemPivot.y());
                    // set the matrix
                    graphicsitem->setTransform(QTransform(translation * delta * translation.inverted()), true);

                    // Regenerate the SVG transformation matrix later
                    WbItem *wbitem = dynamic_cast<WbItem *>(graphicsitem);
                    if (wbitem)
                        scene_->queueTransformationRegeneration(wbitem);
                }
            }

        } else if (widget_->mode() == WbWidget::Mode::Scale) {

            // Scale each selected item
            const auto &gItems = scene()->selectedItems();
            for (QGraphicsItem *graphicsitem : gItems) {
                if (!graphicsitem->parentItem() || !graphicsitem->parentItem()->isSelected()) {

                    // get center coordinates in item coordinates
                    QPointF    c = center();
                    QTransform translation, delta;
                    // translates the the item's center to the origin of the item coordinates
                    translation.translate(-c.x(), -c.y());
                    // Scale.
                    // Moving mouse up enlarges, down shrinks, y axis.
                    // Moving mouse right enlarges, left shrinks, x axis.
                    QPointF difference = event->scenePos() - event->lastScenePos();
                    // Note: the y axis points downwards in scene coordinates
                    delta.scale(1 + difference.x() / 50, 1 - difference.y() / 50);
                    // set the matrix
                    setTransform(translation * delta * translation.inverted(), true);

                    // Regenerate the SVG transformation matrix later
                    scene_->queueTransformationRegeneration(dynamic_cast<WbItem *>(graphicsitem));
                }
            }
        }
    }
}

void WbItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
    QGraphicsSvgItem::mouseReleaseEvent(event);

    scene_->regenerateTransformations();
}

QTransform WbItem::parseSvgTransform(QString string)
{
    string = string.trimmed();
    if (string.isEmpty())
        return {};

    return parseTransformationMatrix(string);
}

QString WbItem::toSvgTransform(const QTransform &m)
{
    return QString("matrix(%1 %2 %3 %4 %5 %6)")
        .arg(m.m11())
        .arg(m.m12())
        .arg(m.m21())
        .arg(m.m22())
        .arg(m.dx())
        .arg(m.dy());
}

void WbItem::regenerateTransform()
{
    // Replace the QGraphicsItem transformation with an SVG transformation.
    // Possible as long as no perspective transformations have been applied.

    // delta is the new transformation to be applied
    QTransform delta = transform();
    if (delta.isIdentity())
        return;

    // get the existing SVG transformation
    QString    oldValue     = node_.attribute("transform");
    QTransform oldTransform = parseSvgTransform(oldValue);

    // construct a translation that translates the item to (0,0) in scene coordinates
    QTransform translation;
    // translates the the item's center to the origin of the item coordinates
    translation.translate(-pos().x(), -pos().y());

    // generate the string representation
    QString newValue = toSvgTransform(oldTransform * translation * delta * translation.inverted());

    if (newValue != oldValue) {
        resetTransform();
        session_->setAttribute(node_, "transform", newValue);
    }
}

void WbItem::addToScene()
{
    // qDebug() << QString("adding %1 to scene.").arg(id()).toLatin1();

    // Do nothing if already added and id matches
    if (scene_ == scene() && node_.hasAttribute("id") && node_.attribute("id") == elementId()) {
        return;
    }

    // Read or generate an xml:id for the node
    QString id;
    if (node_.hasAttribute("id")) {
        id = node_.attribute("id");
    } else {
        id = "e" + SxeSession::generateUUID();
        // qDebug() << QString("Setting new id to %1").arg(id).toLatin1();
        session_->setAttribute(node_, "id", id);
        session_->flush();
    }

    // Only render the indicated item
    setElementId(id);

    // Set the position
    resetPos();

    scene_->addItem(this);
}

void WbItem::removeFromScene()
{
    // qDebug() << QString("removing %1 from scene.").arg(id()).toLatin1();

    scene_->removeItem(this);

    setElementId(QString());
}

void WbItem::resetPos()
{
    // set the x & y approriately;
    setPos(renderer()->boundsOnElement(id()).topLeft());

    // set the drawing order
    // TODO: optimize
    int          i        = 0;
    QDomNodeList children = node_.parentNode().childNodes();
    while (children.at(i) != node_) {
        i++;
    }

    setZValue(i);
}

WbItemMenu *WbItem::constructContextMenu()
{
    WbItemMenu *menu = new WbItemMenu(nullptr);

    if (scene_) {
        // Add the default actions
        QActionGroup *group;
        // QAction* qaction;
        [[maybe_unused]] QPixmap pixmap;

        // group = new QActionGroup(0);
        // pixmap(2, 2);
        // pixmap.fill(QColor(Qt::black));
        // qaction = new QAction(QIcon(pixmap), tr("Thin stroke"), group);
        // qaction->setData(QVariant(1));
        // pixmap = QPixmap(6, 6);
        // pixmap.fill(QColor(Qt::black));
        // qaction = new QAction(QIcon(pixmap), tr("Medium stroke"), group);
        // qaction->setData(QVariant(3));
        // pixmap = QPixmap(12, 12);
        // pixmap.fill(QColor(Qt::black));
        // qaction = new QAction(QIcon(pixmap), tr("Thick stroke"), group);
        // qaction->setData(QVariant(6));
        // connect(group, SIGNAL(triggered(QAction*)), wbscene, SLOT(setStrokeWidth(QAction*)));
        // menu->addActionGroup(group);

        // menu->addSeparator();
        // group = new QActionGroup(0);
        // pixmap = QPixmap(16, 16);
        // pixmap.fill(QColor(Qt::darkCyan));
        // qaction = new QAction(QIcon(pixmap), tr("Change stroke color"), group);
        // connect(qaction, SIGNAL(triggered()), wbscene, SLOT(setStrokeColor()));
        // pixmap.fill(QColor(Qt::darkYellow));
        // qaction = new QAction(QIcon(pixmap), tr("Change fill color"), group);
        // connect(qaction, SIGNAL(triggered()), wbscene, SLOT(setFillColor()));
        // menu->addActionGroup(group);

        // menu->addSeparator();
        group              = new QActionGroup(nullptr);
        IconAction *action = new IconAction(tr("Bring forward"), "psi/bringForwards", tr("Bring forward"), 0, group);
        connect(action, SIGNAL(triggered()), scene_, SLOT(bringForward()));
        action = new IconAction(tr("Bring to front"), "psi/bringToFront", tr("Bring to front"), 0, group);
        connect(action, SIGNAL(triggered()), scene_, SLOT(bringToFront()));
        action = new IconAction(tr("Send backwards"), "psi/sendBackwards", tr("Send backwards"), 0, group);
        connect(action, SIGNAL(triggered()), scene_, SLOT(sendBackwards()));
        action = new IconAction(tr("Send to back"), "psi/sendToBack", tr("Send to back"), 0, group);
        connect(action, SIGNAL(triggered()), scene_, SLOT(sendToBack()));
        menu->addActionGroup(group);

        menu->addSeparator();
        group  = new QActionGroup(nullptr);
        action = new IconAction(tr("Group"), "psi/group", tr("Group"), 0, group);
        connect(action, SIGNAL(triggered()), scene_, SLOT(group()));
        action = new IconAction(tr("Ungroup"), "psi/ungroup", tr("Ungroup"), 0, group);
        connect(action, SIGNAL(triggered()), scene_, SLOT(ungroup()));
        menu->addActionGroup(group);
    }

    return menu;
}

QPointF WbItem::center()
{
    // Determine the center of the item in item coordinates before transformation
    QRectF  r = boundingRect();
    QPointF c(r.x() + r.width() / 2, r.y() + r.height() / 2);
    //  qDebug() << QString("center: %1 + %2    %3 + %4").arg(r.x()).arg(r.width()/2).arg(r.y()).arg(r.height()/2);
    // return the center with transformation applied
    return transform().map(c);
}

// The following functions are from qt-mac-opensource-src-4.3.1/src/svg/qsvghandler.cpp
/****************************************************************************
**
** Copyright (C) 1992-2007  Trolltech ASA. All rights reserved.
**
** This file is part of the QtSVG module of the Qt Toolkit.
**
** This file may be used under the terms of the GNU General Public
** License version 2.0 as published by the Free Software Foundation
** and appearing in the file LICENSE.GPL included in the packaging of
** this file.  Please review the following information to ensure GNU
** General Public Licensing requirements will be met:
** http://trolltech.com/products/qt/licenses/licensing/opensource/
**
** If you are unsure which license is appropriate for your use, please
** review the following information:
** http://trolltech.com/products/qt/licenses/licensing/licensingoverview
** or contact the sales department at sales@trolltech.com.
**
** In addition, as a special exception, Trolltech gives you certain
** additional rights. These rights are described in the Trolltech GPL
** Exception version 1.0, which can be found at
** http://www.trolltech.com/products/qt/gplexception/ and in the file
** GPL_EXCEPTION.txt in this package.
**
** In addition, as a special exception, Trolltech, as the sole copyright
** holder for Qt Designer, grants users of the Qt/Eclipse Integration
** plug-in the right for the Qt/Eclipse Integration to link to
** functionality provided by Qt Designer and its related libraries.
**
** Trolltech reserves all rights not expressly granted herein.
**
** Trolltech ASA (c) 2007
**
** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
**
****************************************************************************/

static qreal toDouble(const QChar *&str)
{
    const int maxLen = 255; // technically doubles can go til 308+ but whatever
    char      temp[maxLen + 1];
    int       pos = 0;

    if (*str == QLatin1Char('-')) {
        temp[pos++] = '-';
        ++str;
    } else if (*str == QLatin1Char('+')) {
        ++str;
    }
    while (*str >= QLatin1Char('0') && *str <= QLatin1Char('9') && pos < maxLen) {
        temp[pos++] = str->toLatin1();
        ++str;
    }
    if (*str == QLatin1Char('.') && pos < maxLen) {
        temp[pos++] = '.';
        ++str;
    }
    while (*str >= QLatin1Char('0') && *str <= QLatin1Char('9') && pos < maxLen) {
        temp[pos++] = str->toLatin1();
        ++str;
    }
    bool exponent = false;
    if (*str == QLatin1Char('e') && pos < maxLen) {
        exponent    = true;
        temp[pos++] = 'e';
        ++str;
        if ((*str == QLatin1Char('-') || *str == QLatin1Char('+')) && pos < maxLen) {
            temp[pos++] = str->toLatin1();
            ++str;
        }
        while (*str >= QLatin1Char('0') && *str <= QLatin1Char('9') && pos < maxLen) {
            temp[pos++] = str->toLatin1();
            ++str;
        }
    }
    temp[pos] = '\0';

    qreal val;
    if (!exponent && pos < 10) {
        int         ival = 0;
        const char *t    = temp;
        bool        neg  = false;
        if (*t == '-') {
            neg = true;
            ++t;
        }
        while (*t && *t != '.') {
            ival *= 10;
            ival += (*t) - '0';
            ++t;
        }
        if (*t == '.') {
            ++t;
            int div = 1;
            while (*t) {
                ival *= 10;
                ival += (*t) - '0';
                div *= 10;
                ++t;
            }
            val = (qreal(ival)) / (qreal(div));
        } else {
            val = ival;
        }
        if (neg)
            val = -val;
    } else {
#ifdef Q_WS_QWS
        if (sizeof(qreal) == sizeof(float))
            val = strtof(temp, 0);
        else
#endif
        {
            bool ok = false;
            val     = QByteArray::fromRawData(temp, pos).toDouble(&ok);
        }
    }
    return val;
}

static QVector<qreal> parseNumbersList(const QChar *&str)
{
    QVector<qreal> points;
    if (!str)
        return points;
    points.reserve(32);

    while (*str == QLatin1Char(' ')) {
        ++str;
    }
    while ((*str >= QLatin1Char('0') && *str <= QLatin1Char('9')) || *str == QLatin1Char('-')
           || *str == QLatin1Char('+') || *str == QLatin1Char('.')) {

        points.append(::toDouble(str));

        while (*str == QLatin1Char(' ')) {
            ++str;
        }
        if (*str == QLatin1Char(',')) {
            ++str;
        }

        // eat the rest of space
        while (*str == QLatin1Char(' ')) {
            ++str;
        }
    }

    return points;
}

static QTransform parseTransformationMatrix(const QString &value)
{
    QTransform   matrix;
    const QChar *str = value.constData();

    while (*str != QLatin1Char(0)) {
        if (str->isSpace() || *str == QLatin1Char(',')) {
            ++str;
            continue;
        }
        enum class State : char { Matrix, Translate, Rotate, Scale, SkewX, SkewY };
        State state = State::Matrix;
        if (*str == QLatin1Char('m')) { // matrix
            const char *ident = "atrix";
            for (int i = 0; i < 5; ++i) {
                if (*(++str) != QLatin1Char(ident[i])) {
                    goto error;
                }
            }
            ++str;
            state = State::Matrix;
        } else if (*str == QLatin1Char('t')) { // translate
            const char *ident = "ranslate";
            for (int i = 0; i < 8; ++i)
                if (*(++str) != QLatin1Char(ident[i]))
                    goto error;
            ++str;
            state = State::Translate;
        } else if (*str == QLatin1Char('r')) { // rotate
            const char *ident = "otate";
            for (int i = 0; i < 5; ++i)
                if (*(++str) != QLatin1Char(ident[i]))
                    goto error;
            ++str;
            state = State::Rotate;
        } else if (*str == QLatin1Char('s')) { // scale, skewX, skewY
            ++str;
            if (*str == QLatin1Char('c')) {
                const char *ident = "ale";
                for (int i = 0; i < 3; ++i)
                    if (*(++str) != QLatin1Char(ident[i]))
                        goto error;
                ++str;
                state = State::Scale;
            } else if (*str == QLatin1Char('k')) {
                if (*(++str) != QLatin1Char('e'))
                    goto error;
                if (*(++str) != QLatin1Char('w'))
                    goto error;
                ++str;
                if (*str == QLatin1Char('X'))
                    state = State::SkewX;
                else if (*str == QLatin1Char('Y'))
                    state = State::SkewY;
                else
                    goto error;
                ++str;
            } else {
                goto error;
            }
        } else {
            goto error;
        }

        while (str->isSpace()) {
            ++str;
        }
        if (*str != QLatin1Char('(')) {
            goto error;
        }
        ++str;
        QVector<qreal> points = parseNumbersList(str);
        if (*str != QLatin1Char(')')) {
            goto error;
        }
        ++str;

        if (state == State::Matrix) {
            if (points.count() != 6) {
                goto error;
            }
            matrix = matrix * QTransform(points[0], points[1], points[2], points[3], points[4], points[5]);
        } else if (state == State::Translate) {
            if (points.count() == 1)
                matrix.translate(points[0], 0);
            else if (points.count() == 2)
                matrix.translate(points[0], points[1]);
            else
                goto error;
        } else if (state == State::Rotate) {
            if (points.count() == 1) {
                matrix.rotate(points[0]);
            } else if (points.count() == 3) {
                matrix.translate(points[1], points[2]);
                matrix.rotate(points[0]);
                matrix.translate(-points[1], -points[2]);
            } else {
                goto error;
            }
        } else if (state == State::Scale) {
            if (points.count() < 1 || points.count() > 2)
                goto error;
            qreal sx = points[0];
            qreal sy = sx;
            if (points.count() == 2)
                sy = points[1];
            matrix.scale(sx, sy);
        } else if (state == State::SkewX) {
            if (points.count() != 1)
                goto error;
            const qreal deg2rad = qreal(0.017453292519943295769);
            matrix.shear(tan(points[0] * deg2rad), 0);
        } else if (state == State::SkewY) {
            if (points.count() != 1)
                goto error;
            const qreal deg2rad = qreal(0.017453292519943295769);
            matrix.shear(0, tan(points[0] * deg2rad));
        }
    }
error:
    return matrix;
}
