/*
 * GMX Tiled Plugin
 * Copyright 2016, Jones Blunt <mrjonesblunt@gmail.com>
 * Copyright 2016-2020, Thorbjørn Lindeijer <bjorn@lindeijer.nl>
 *
 * This file is part of Tiled.
 *
 * 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 <http://www.gnu.org/licenses/>.
 */

#include "gmxplugin.h"

#include "logginginterface.h"
#include "map.h"
#include "mapobject.h"
#include "objectgroup.h"
#include "savefile.h"
#include "tile.h"
#include "tilelayer.h"

#include <QCoreApplication>
#include <QDir>
#include <QFileInfo>
#include <QRegularExpression>
#include <QXmlStreamWriter>

using namespace Tiled;
using namespace Gmx;

template <typename T>
static T optionalProperty(const Object *object, const QString &name, const T &def)
{
    const QVariant var = object->resolvedProperty(name);
    return var.isValid() ? var.value<T>() : def;
}

template <typename T>
static QString toString(T number)
{
    return QString::number(number);
}

static QString toString(bool b)
{
    return QString::number(b ? -1 : 0);
}

static QString toString(const QString &string)
{
    return string;
}

template <typename T>
static void writeProperty(QXmlStreamWriter &writer,
                          const Object *object,
                          const QString &name,
                          const T &def)
{
    const T value = optionalProperty(object, name, def);
    writer.writeTextElement(name, toString(value));
}

static QString sanitizeName(QString name)
{
    static const QRegularExpression regexp(QLatin1String("[^a-zA-Z0-9]"));
    return name.replace(regexp, QStringLiteral("_"));
}

static bool checkIfViewsDefined(const Map *map)
{
    LayerIterator iterator(map);
    while (const Layer *layer = iterator.next()) {

        if (layer->layerType() != Layer::ObjectGroupType)
            continue;

        const ObjectGroup *objectLayer = static_cast<const ObjectGroup*>(layer);

        for (const MapObject *object : objectLayer->objects()) {
            if (object->effectiveClassName() == "view")
                return true;
        }
    }

    return false;
}

GmxPlugin::GmxPlugin()
{
}

bool GmxPlugin::write(const Map *map, const QString &fileName, Options options)
{
    SaveFile file(fileName);
    if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
        mError = QCoreApplication::translate("File Errors", "Could not open file for writing.");
        return false;
    }

    QXmlStreamWriter stream(file.device());

    stream.setAutoFormatting(!options.testFlag(WriteMinimized));
    stream.setAutoFormattingIndent(2);

    stream.writeComment("This Document is generated by Tiled, if you edit it by hand then you do so at your own risk!");

    stream.writeStartElement("room");

    int mapPixelWidth = map->tileWidth() * map->width();
    int mapPixelHeight = map->tileHeight() * map->height();

    stream.writeTextElement("width", QString::number(mapPixelWidth));
    stream.writeTextElement("height", QString::number(mapPixelHeight));
    stream.writeTextElement("vsnap", QString::number(map->tileHeight()));
    stream.writeTextElement("hsnap", QString::number(map->tileWidth()));
    stream.writeTextElement("isometric", toString(map->orientation() == Map::Orientation::Isometric));
    writeProperty(stream, map, "speed", 30);
    writeProperty(stream, map, "persistent", false);
    writeProperty(stream, map, "clearViewBackground", false);
    writeProperty(stream, map, "clearDisplayBuffer", true);
    writeProperty(stream, map, "code", QString());

    // Check if views are defined
    bool enableViews = checkIfViewsDefined(map);
    writeProperty(stream, map, "enableViews", enableViews);

    // Write out views
    // Last view in Object layer is the first view in the room
    if (enableViews) {
        stream.writeStartElement("views");
        int viewCount = 0;
        for (const Layer *layer : map->objectGroups()) {
            const ObjectGroup *objectLayer = static_cast<const ObjectGroup*>(layer);

            for (const MapObject *object : objectLayer->objects()) {
                const QString &className = object->effectiveClassName();
                if (className != "view")
                    continue;

                // GM only has 8 views so drop anything more than that
                if (viewCount > 7) {
                    Tiled::ERROR(QLatin1String("GMX plugin: Can't export more than 8 views."),
                                 Tiled::JumpToObject { object });
                    break;
                }

                viewCount++;
                stream.writeStartElement("view");

                stream.writeAttribute("visible", toString(object->isVisible()));
                stream.writeAttribute("objName", QString(optionalProperty(object, "objName", QString())));
                QPointF pos = object->position();
                // Note: GM only supports ints for positioning
                // so views could be off if user doesn't align to whole number
                stream.writeAttribute("xview", QString::number(qRound(pos.x())));
                stream.writeAttribute("yview", QString::number(qRound(pos.y())));
                stream.writeAttribute("wview", QString::number(qRound(object->width())));
                stream.writeAttribute("hview", QString::number(qRound(object->height())));
                // Round these in case user adds properties as floats and not ints
                stream.writeAttribute("xport", QString::number(qRound(optionalProperty(object, "xport", 0.0))));
                stream.writeAttribute("yport", QString::number(qRound(optionalProperty(object, "yport", 0.0))));
                stream.writeAttribute("wport", QString::number(qRound(optionalProperty(object, "wport", 1024.0))));
                stream.writeAttribute("hport", QString::number(qRound(optionalProperty(object, "hport", 768.0))));
                stream.writeAttribute("hborder", QString::number(qRound(optionalProperty(object, "hborder", 32.0))));
                stream.writeAttribute("vborder", QString::number(qRound(optionalProperty(object, "vborder", 32.0))));
                stream.writeAttribute("hspeed", QString::number(qRound(optionalProperty(object, "hspeed", -1.0))));
                stream.writeAttribute("vspeed", QString::number(qRound(optionalProperty(object, "vspeed", -1.0))));

                stream.writeEndElement();
            }
        }

        stream.writeEndElement();
    }

    stream.writeStartElement("instances");

    QSet<QString> usedNames;
    int layerCount = 0;

    // Write out object instances
    LayerIterator iterator(map);
    while (const Layer *layer = iterator.next()) {
        ++layerCount;

        if (layer->layerType() != Layer::ObjectGroupType)
            continue;

        const ObjectGroup *objectLayer = static_cast<const ObjectGroup*>(layer);
        const bool locked = !layer->isUnlocked();
        auto color = layer->effectiveTintColor();
        color.setAlphaF(color.alphaF() * layer->effectiveOpacity());
        const auto colorString = QString::number(color.rgba());

        for (const MapObject *object : objectLayer->objects()) {
            const QString &className = object->effectiveClassName();
            if (className.isEmpty())
                continue;
            if (className == "view")
                continue;

            stream.writeStartElement("instance");

            // The class is used to refer to the name of the object
            stream.writeAttribute("objName", sanitizeName(className));

            qreal scaleX = 1;
            qreal scaleY = 1;

            QPointF origin(optionalProperty(object, "originX", 0.0),
                           optionalProperty(object, "originY", 0.0));

            if (!object->cell().isEmpty()) {
                // For tile objects we can support scaling and flipping, though
                // flipping in combination with rotation doesn't work in GameMaker.
                if (auto tile = object->cell().tile()) {
                    const QSize tileSize = tile->size();
                    scaleX = object->width() / tileSize.width();
                    scaleY = object->height() / tileSize.height();

                    if (object->cell().flippedHorizontally()) {
                        scaleX *= -1;
                        origin += QPointF(object->width() - 2 * origin.x(), 0);
                    }
                    if (object->cell().flippedVertically()) {
                        scaleY *= -1;
                        origin += QPointF(0, object->height() - 2 * origin.y());
                    }
                }

                // Tile objects don't necessarily have top-left origin in Tiled,
                // so the position needs to be translated for top-left origin in
                // GameMaker, taking into account the rotation.
                origin -= alignmentOffset(object->size(), object->alignment());
            }

            // Allow overriding the scale using custom properties
            scaleX = optionalProperty(object, "scaleX", scaleX);
            scaleY = optionalProperty(object, "scaleY", scaleY);

            // Adjust the position based on the origin
            QTransform transform;
            transform.rotate(object->rotation());
            const QPointF pos = object->position() + transform.map(origin);

            stream.writeAttribute("x", QString::number(qRound(pos.x())));
            stream.writeAttribute("y", QString::number(qRound(pos.y())));

            // Include object ID in the name when necessary because duplicates are not allowed
            if (object->name().isEmpty()) {
                stream.writeAttribute("name", QStringLiteral("inst_%1").arg(object->id()));
            } else {
                QString name = sanitizeName(object->name());

                while (usedNames.contains(name))
                    name += QStringLiteral("_%1").arg(object->id());

                usedNames.insert(name);
                stream.writeAttribute("name", name);
            }

            stream.writeAttribute("locked", toString(locked));
            stream.writeAttribute("code", optionalProperty(object, "code", QString()));
            stream.writeAttribute("scaleX", QString::number(scaleX));
            stream.writeAttribute("scaleY", QString::number(scaleY));
            stream.writeAttribute("colour", colorString);
            stream.writeAttribute("rotation", QString::number(-object->rotation()));

            stream.writeEndElement();
        }
    }

    stream.writeEndElement();


    stream.writeStartElement("tiles");

    uint tileId = 0u;

    // Write out tile instances
    iterator.toFront();
    while (const Layer *layer = iterator.next()) {
        --layerCount;
        QString depth = QString::number(optionalProperty(layer, QLatin1String("depth"),
                                                         layerCount + 1000000));
        const bool locked = !layer->isUnlocked();
        auto color = layer->effectiveTintColor();
        color.setAlphaF(color.alphaF() * layer->effectiveOpacity());
        const auto colorString = QString::number(color.rgba());

        switch (layer->layerType()) {
        case Layer::TileLayerType: {
            auto tileLayer = static_cast<const TileLayer*>(layer);

            for (int y = 0; y < tileLayer->height(); ++y) {
                for (int x = 0; x < tileLayer->width(); ++x) {
                    const Cell &cell = tileLayer->cellAt(x, y);
                    if (const Tile *tile = cell.tile()) {
                        const Tileset *tileset = tile->tileset();

                        stream.writeStartElement("tile");

                        int pixelX = x * map->tileWidth();
                        int pixelY = y * map->tileHeight();
                        qreal scaleX = 1;
                        qreal scaleY = 1;

                        if (cell.flippedHorizontally()) {
                            scaleX = -1;
                            pixelX += tile->width();
                        }
                        if (cell.flippedVertically()) {
                            scaleY = -1;
                            pixelY += tile->height();
                        }
                        if (cell.flippedAntiDiagonally()) {
                            Tiled::WARNING(QStringLiteral("GMX plugin: Rotated tiles are not supported."),
                                           Tiled::JumpToTile { tileLayer->map(), QPoint(x, y), tileLayer });
                        }

                        QString bgName;
                        int xo = 0;
                        int yo = 0;

                        if (tileset->isCollection()) {
                            bgName = QFileInfo(tile->imageSource().path()).baseName();
                        } else {
                            bgName = tileset->name();

                            const int xInTilesetGrid = tile->id() % tileset->columnCount();
                            const int yInTilesetGrid = static_cast<int>(tile->id() / tileset->columnCount());

                            xo = tileset->margin() + (tileset->tileSpacing() + tileset->tileWidth()) * xInTilesetGrid;
                            yo = tileset->margin() + (tileset->tileSpacing() + tileset->tileHeight()) * yInTilesetGrid;
                        }

                        stream.writeAttribute("bgName", bgName);
                        stream.writeAttribute("x", QString::number(pixelX));
                        stream.writeAttribute("y", QString::number(pixelY));
                        stream.writeAttribute("w", QString::number(tile->width()));
                        stream.writeAttribute("h", QString::number(tile->height()));

                        stream.writeAttribute("xo", QString::number(xo));
                        stream.writeAttribute("yo", QString::number(yo));

                        stream.writeAttribute("id", QString::number(++tileId));
                        stream.writeAttribute("depth", depth);
                        stream.writeAttribute("locked", toString(locked));
                        stream.writeAttribute("colour", colorString);

                        stream.writeAttribute("scaleX", QString::number(scaleX));
                        stream.writeAttribute("scaleY", QString::number(scaleY));

                        stream.writeEndElement();
                    }
                }
            }
            break;
        }

        case Layer::ObjectGroupType: {
            auto objectGroup = static_cast<const ObjectGroup*>(layer);
            auto objects = objectGroup->objects();

            // Make sure the objects export in the rendering order
            if (objectGroup->drawOrder() == ObjectGroup::TopDownOrder) {
                std::stable_sort(objects.begin(), objects.end(),
                                 [](const MapObject *a, const MapObject *b) { return a->y() < b->y(); });
            }

            for (const MapObject *object : std::as_const(objects)) {
                // Objects with a class are already exported as instances
                if (!object->effectiveClassName().isEmpty())
                    continue;

                // Non-typed tile objects are exported as tiles. Rotation is
                // not supported here, but scaling is.
                if (const Tile *tile = object->cell().tile()) {
                    const Tileset *tileset = tile->tileset();

                    const QSize tileSize = tile->size();
                    qreal scaleX = object->width() / tileSize.width();
                    qreal scaleY = object->height() / tileSize.height();
                    qreal x = object->x();
                    qreal y = object->y() - object->height();

                    if (object->cell().flippedHorizontally()) {
                        scaleX *= -1;
                        x += object->width();
                    }
                    if (object->cell().flippedVertically()) {
                        scaleY *= -1;
                        y += object->height();
                    }

                    stream.writeStartElement("tile");

                    QString bgName;
                    int xo = 0;
                    int yo = 0;

                    if (tileset->isCollection()) {
                        bgName = QFileInfo(tile->imageSource().path()).baseName();
                    } else {
                        bgName = tileset->name();

                        int xInTilesetGrid = tile->id() % tileset->columnCount();
                        int yInTilesetGrid = tile->id() / tileset->columnCount();

                        xo = tileset->margin() + (tileset->tileSpacing() + tileset->tileWidth()) * xInTilesetGrid;
                        yo = tileset->margin() + (tileset->tileSpacing() + tileset->tileHeight()) * yInTilesetGrid;
                    }

                    stream.writeAttribute("bgName", bgName);
                    stream.writeAttribute("x", QString::number(qRound(x)));
                    stream.writeAttribute("y", QString::number(qRound(y)));
                    stream.writeAttribute("w", QString::number(tile->width()));
                    stream.writeAttribute("h", QString::number(tile->height()));

                    stream.writeAttribute("xo", QString::number(xo));
                    stream.writeAttribute("yo", QString::number(yo));

                    stream.writeAttribute("id", QString::number(++tileId));
                    stream.writeAttribute("depth", depth);
                    stream.writeAttribute("locked", toString(locked));
                    stream.writeAttribute("colour", colorString);

                    stream.writeAttribute("scaleX", QString::number(scaleX));
                    stream.writeAttribute("scaleY", QString::number(scaleY));

                    stream.writeEndElement();
                } else {
                    Tiled::WARNING(QStringLiteral("GMX plugin: Ignoring non-tile object %1 without type.").arg(object->id()),
                                   Tiled::JumpToObject { object });
                }
            }
            break;
        }

        case Layer::ImageLayerType:
            // todo: maybe export as backgrounds?
            Tiled::WARNING(QStringLiteral("GMX plugin: Ignoring image layer \"%1\" (not currently supported).").arg(layer->name()),
                           Tiled::SelectLayer { layer });
            break;

        case Layer::GroupLayerType:
            // Recursion handled by LayerIterator
            break;
        }
    }

    stream.writeEndElement();

    writeProperty(stream, map, "PhysicsWorld", false);
    writeProperty(stream, map, "PhysicsWorldTop", 0);
    writeProperty(stream, map, "PhysicsWorldLeft", 0);
    writeProperty(stream, map, "PhysicsWorldRight", mapPixelWidth);
    writeProperty(stream, map, "PhysicsWorldBottom", mapPixelHeight);
    writeProperty(stream, map, "PhysicsWorldGravityX", 0.0);
    writeProperty(stream, map, "PhysicsWorldGravityY", 10.0);
    writeProperty(stream, map, "PhysicsWorldPixToMeters", 0.1);

    stream.writeEndDocument();

    if (!file.commit()) {
        mError = file.errorString();
        return false;
    }

    return true;
}

QString GmxPlugin::errorString() const
{
    return mError;
}

QString GmxPlugin::nameFilter() const
{
    return tr("GameMaker room files (*.room.gmx)");
}

QString GmxPlugin::shortName() const
{
    return QStringLiteral("gmx");
}
