#include "mapimageexporter.h"
#include "ui_mapimageexporter.h"
#include "qgifimage.h"
#include "editcommands.h"

#include <QFileDialog>
#include <QImage>
#include <QPainter>
#include <QPoint>

#define STITCH_MODE_BORDER_DISTANCE 2

QString getTitle(ImageExporterMode mode) {
    switch (mode)
    {
        case ImageExporterMode::Normal:
            return "Export Map Image";
        case ImageExporterMode::Stitch:
            return "Export Map Stitch Image";
        case ImageExporterMode::Timelapse:
            return "Export Map Timelapse Image";
    }
    return "";
}

MapImageExporter::MapImageExporter(QWidget *parent_, Editor *editor_, ImageExporterMode mode) :
    QDialog(parent_),
    ui(new Ui::MapImageExporter)
{
    ui->setupUi(this);
    this->map = editor_->map;
    this->layout = editor_->layout;
    this->editor = editor_;
    this->mode = mode;
    this->setWindowTitle(getTitle(this->mode));
    this->ui->groupBox_Connections->setVisible(this->mode == ImageExporterMode::Normal);
    this->ui->groupBox_Timelapse->setVisible(this->mode == ImageExporterMode::Timelapse);

    if (this->map) {
        this->ui->comboBox_MapSelection->addItems(editor->project->mapNames);
        this->ui->comboBox_MapSelection->setCurrentText(map->name);
        this->ui->comboBox_MapSelection->setEnabled(false);// TODO: allow selecting map from drop-down
    }

    updatePreview();
}

MapImageExporter::~MapImageExporter() {
    delete scene;
    delete ui;
}

void MapImageExporter::saveImage() {
    QString title = getTitle(this->mode);
    QString defaultFilename;
    switch (this->mode)
    {
        case ImageExporterMode::Normal:
            defaultFilename = this->map? this->map->name : this->layout->name;
            break;
        case ImageExporterMode::Stitch:
            defaultFilename = QString("Stitch_From_%1").arg(this->map? this->map->name : this->layout->name);
            break;
        case ImageExporterMode::Timelapse:
            defaultFilename = QString("Timelapse_%1").arg(this->map? this->map->name : this->layout->name);
            break;
    }

    QString defaultFilepath = QString("%1/%2.%3")
            .arg(editor->project->importExportPath)
            .arg(defaultFilename)
            .arg(this->mode == ImageExporterMode::Timelapse ? "gif" : "png");
    QString filter = this->mode == ImageExporterMode::Timelapse ? "Image Files (*.gif)" : "Image Files (*.png *.jpg *.bmp)";
    QString filepath = QFileDialog::getSaveFileName(this, title, defaultFilepath, filter);
    if (!filepath.isEmpty()) {
        editor->project->setImportExportPath(filepath);
        switch (this->mode) {
            case ImageExporterMode::Normal:
                this->preview.save(filepath);
                break;
        case ImageExporterMode::Stitch: {
                QProgressDialog progress("Building map stitch...", "Cancel", 0, 1, this);
                progress.setAutoClose(true);
                progress.setWindowModality(Qt::WindowModal);
                progress.setModal(true);
                QPixmap pixmap = this->getStitchedImage(&progress, this->showBorder);
                if (progress.wasCanceled()) {
                    progress.close();
                    return;
                }
                pixmap.save(filepath);
                progress.close();
                break;
            }
            case ImageExporterMode::Timelapse:
                // !TODO: also need layout editHistory!
                QGifImage timelapseImg;
                timelapseImg.setDefaultDelay(timelapseDelayMs);
                timelapseImg.setDefaultTransparentColor(QColor(0, 0, 0));

                auto generateTimelapseFromHistory = [=, this, &timelapseImg](QString progressText, QUndoStack &historyStack){
                    //
                    QProgressDialog progress(progressText, "Cancel", 0, 1, this);
                    progress.setAutoClose(true);
                    progress.setWindowModality(Qt::WindowModal);
                    progress.setModal(true);
                    progress.setMaximum(1);
                    progress.setValue(0);

                    int maxWidth = this->layout->getWidth() * 16;
                    int maxHeight = this->layout->getHeight() * 16;
                    if (showBorder) {
                        maxWidth += 2 * STITCH_MODE_BORDER_DISTANCE * 16;
                        maxHeight += 2 * STITCH_MODE_BORDER_DISTANCE * 16;
                    }
                    // Rewind to the specified start of the map edit history.
                    int i = 0;
                    while (historyStack.canUndo()) {
                        progress.setValue(i);
                        historyStack.undo();
                        int width = this->layout->getWidth() * 16;
                        int height = this->layout->getHeight() * 16;
                        if (showBorder) {
                            width += 2 * STITCH_MODE_BORDER_DISTANCE * 16;
                            height += 2 * STITCH_MODE_BORDER_DISTANCE * 16;
                        }
                        if (width > maxWidth) {
                            maxWidth = width;
                        }
                        if (height > maxHeight) {
                            maxHeight = height;
                        }
                        i++;
                    }

                    // Draw each frame, skpping the specified number of map edits in
                    // the undo history.
                    progress.setMaximum(i);
                    while (i > 0) {
                        if (progress.wasCanceled()) {
                            progress.close();
                            while (i > 0 && historyStack.canRedo()) {
                                i--;
                                historyStack.redo();
                            }
                            return;
                        }
                        while (historyStack.canRedo() &&
                               !historyItemAppliesToFrame(historyStack.command(historyStack.index()))) {
                            i--;
                            historyStack.redo();
                        }
                        progress.setValue(progress.maximum() - i);
                        QPixmap pixmap = this->getFormattedMapPixmap(this->map, !this->showBorder);
                        if (pixmap.width() < maxWidth || pixmap.height() < maxHeight) {
                            QPixmap pixmap2 = QPixmap(maxWidth, maxHeight);
                            QPainter painter(&pixmap2);
                            pixmap2.fill(QColor(0, 0, 0));
                            painter.drawPixmap(0, 0, pixmap.width(), pixmap.height(), pixmap);
                            painter.end();
                            pixmap = pixmap2;
                        }
                        timelapseImg.addFrame(pixmap.toImage());
                        for (int j = 0; j < timelapseSkipAmount; j++) {
                            if (i > 0) {
                                i--;
                                historyStack.redo();
                                while (historyStack.canRedo() &&
                                       !historyItemAppliesToFrame(historyStack.command(historyStack.index()))) {
                                    i--;
                                    historyStack.redo();
                                }
                            }
                        }
                    }
                    // The latest map state is the last animated frame.
                    QPixmap pixmap = this->getFormattedMapPixmap(this->map, !this->showBorder);
                    timelapseImg.addFrame(pixmap.toImage());
                    progress.close();
                };

                if (this->layout)
                    generateTimelapseFromHistory("Building layout timelapse...", this->layout->editHistory);

                if (this->map)
                    generateTimelapseFromHistory("Building map timelapse...", this->map->editHistory);

                timelapseImg.save(filepath);
                break;
        }
        this->close();
    }
}

bool MapImageExporter::historyItemAppliesToFrame(const QUndoCommand *command) {
    switch (command->id() & 0xFF) {
        case CommandId::ID_PaintMetatile:
        case CommandId::ID_BucketFillMetatile:
        case CommandId::ID_MagicFillMetatile:
        case CommandId::ID_ShiftMetatiles:
        case CommandId::ID_ResizeLayout:
        case CommandId::ID_ScriptEditLayout:
            return true;
        case CommandId::ID_PaintCollision:
        case CommandId::ID_BucketFillCollision:
        case CommandId::ID_MagicFillCollision:
            return this->showCollision;
        case CommandId::ID_PaintBorder:
            return this->showBorder;
        case CommandId::ID_EventMove:
        case CommandId::ID_EventShift:
        case CommandId::ID_EventCreate:
        case CommandId::ID_EventDelete:
        case CommandId::ID_EventDuplicate: {
            bool eventTypeIsApplicable =
                       (this->showObjects   && (command->id() & IDMask_EventType_Object)  != 0)
                    || (this->showWarps     && (command->id() & IDMask_EventType_Warp)    != 0)
                    || (this->showBGs       && (command->id() & IDMask_EventType_BG)      != 0)
                    || (this->showTriggers  && (command->id() & IDMask_EventType_Trigger) != 0)
                    || (this->showHealSpots && (command->id() & IDMask_EventType_Heal)    != 0);
            return eventTypeIsApplicable;
        }
        default:
            return false;
    }
}

struct StitchedMap {
    int x;
    int y;
    Map* map;
};

QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress, bool includeBorder) {
    // Do a breadth-first search to gather a collection of
    // all reachable maps with their relative offsets.
    QSet<QString> visited;
    QList<StitchedMap> stitchedMaps;
    QList<StitchedMap> unvisited;
    unvisited.append(StitchedMap{0, 0, this->editor->map});

    progress->setLabelText("Gathering stitched maps...");
    while (!unvisited.isEmpty()) {
        if (progress->wasCanceled()) {
            return QPixmap();
        }
        progress->setMaximum(visited.size() + unvisited.size());
        progress->setValue(visited.size());

        StitchedMap cur = unvisited.takeFirst();
        if (visited.contains(cur.map->name))
            continue;
        visited.insert(cur.map->name);
        stitchedMaps.append(cur);

        for (MapConnection *connection : cur.map->connections) {
            if (connection->direction == "dive" || connection->direction == "emerge")
                continue;
            int x = cur.x;
            int y = cur.y;
            int offset = connection->offset;
            Map *connectionMap = this->editor->project->loadMap(connection->map_name);
            if (connection->direction == "up") {
                x += offset;
                y -= connectionMap->getHeight();
            } else if (connection->direction == "down") {
                x += offset;
                y += cur.map->getHeight();
            } else if (connection->direction == "left") {
                x -= connectionMap->getWidth();
                y += offset;
            } else if (connection->direction == "right") {
                x += cur.map->getWidth();
                y += offset;
            }
            unvisited.append(StitchedMap{x, y, connectionMap});
        }
    }

    // Determine the overall dimensions of the stitched maps.
    int maxX = INT_MIN;
    int minX = INT_MAX;
    int maxY = INT_MIN;
    int minY = INT_MAX;
    for (StitchedMap map : stitchedMaps) {
        int left = map.x;
        int right = map.x + map.map->getWidth();
        int top = map.y;
        int bottom = map.y + map.map->getHeight();
        if (left < minX)
            minX = left;
        if (right > maxX)
            maxX = right;
        if (top < minY)
            minY = top;
        if (bottom > maxY)
            maxY = bottom;
    }

    if (includeBorder) {
        minX -= STITCH_MODE_BORDER_DISTANCE;
        maxX += STITCH_MODE_BORDER_DISTANCE;
        minY -= STITCH_MODE_BORDER_DISTANCE;
        maxY += STITCH_MODE_BORDER_DISTANCE;
    }

    // Draw the maps on the full canvas, while taking
    // their respective offsets into account.
    progress->setLabelText("Drawing stitched maps...");
    progress->setValue(0);
    progress->setMaximum(stitchedMaps.size());
    int numDrawn = 0;
    QPixmap stitchedPixmap((maxX - minX) * 16, (maxY - minY) * 16);
    QPainter painter(&stitchedPixmap);
    for (StitchedMap map : stitchedMaps) {
        if (progress->wasCanceled()) {
            return QPixmap();
        }
        progress->setValue(numDrawn);
        numDrawn++;

        int pixelX = (map.x - minX) * 16;
        int pixelY = (map.y - minY) * 16;
        if (includeBorder) {
            pixelX -= STITCH_MODE_BORDER_DISTANCE * 16;
            pixelY -= STITCH_MODE_BORDER_DISTANCE * 16;
        }
        QPixmap pixmap = this->getFormattedMapPixmap(map.map, false);
        painter.drawPixmap(pixelX, pixelY, pixmap);
    }

    // When including the borders, we simply draw all the maps again
    // without their borders, since the first pass results in maps
    // being occluded by other map borders.
    if (includeBorder) {
        progress->setLabelText("Drawing stitched maps without borders...");
        progress->setValue(0);
        progress->setMaximum(stitchedMaps.size());
        numDrawn = 0;
        for (StitchedMap map : stitchedMaps) {
            if (progress->wasCanceled()) {
                return QPixmap();
            }
            progress->setValue(numDrawn);
            numDrawn++;

            int pixelX = (map.x - minX) * 16;
            int pixelY = (map.y - minY) * 16;
            QPixmap pixmapWithoutBorders = this->getFormattedMapPixmap(map.map, true);
            painter.drawPixmap(pixelX, pixelY, pixmapWithoutBorders);
        }
    }

    return stitchedPixmap;
}

void MapImageExporter::updatePreview() {
    if (scene) {
        delete scene;
        scene = nullptr;
    }

    preview = getFormattedMapPixmap(this->map, false);
    scene = new QGraphicsScene;
    scene->addPixmap(preview);
    this->scene->setSceneRect(this->scene->itemsBoundingRect());

    this->ui->graphicsView_Preview->setScene(scene);
    this->ui->graphicsView_Preview->setFixedSize(scene->itemsBoundingRect().width() + 2,
                                                 scene->itemsBoundingRect().height() + 2);
}

// THIS
QPixmap MapImageExporter::getFormattedMapPixmap(Map *map, bool ignoreBorder) {
    QPixmap pixmap;

    Layout *layout;

    // draw background layer / base image
    if (!this->map) {
        layout = this->layout;
        layout->render(true);
        pixmap = layout->pixmap;
    } else {
        layout = map->layout;
        map->layout->render(true);
        pixmap = map->layout->pixmap;
    }

    if (showCollision) {
        QPainter collisionPainter(&pixmap);
        layout->renderCollision(true);
        collisionPainter.setOpacity(editor->collisionOpacity);
        collisionPainter.drawPixmap(0, 0, layout->collision_pixmap);
        collisionPainter.end();
    }

    // draw map border
    // note: this will break when allowing map to be selected from drop down maybe
    int borderHeight = 0, borderWidth = 0;
    bool forceDrawBorder = showUpConnections || showDownConnections || showLeftConnections || showRightConnections;
    if (!ignoreBorder && (showBorder || forceDrawBorder)) {
        int borderDistance = this->mode ? STITCH_MODE_BORDER_DISTANCE : BORDER_DISTANCE;
        layout->renderBorder();
        int borderHorzDist = editor->getBorderDrawDistance(layout->getBorderWidth());
        int borderVertDist = editor->getBorderDrawDistance(layout->getBorderHeight());
        borderWidth = borderDistance * 16;
        borderHeight = borderDistance * 16;
        QPixmap newPixmap = QPixmap(layout->pixmap.width() + borderWidth * 2, layout->pixmap.height() + borderHeight * 2);
        QPainter borderPainter(&newPixmap);
        for (int y = borderDistance - borderVertDist; y < layout->getHeight() + borderVertDist * 2; y += layout->getBorderHeight()) {
            for (int x = borderDistance - borderHorzDist; x < layout->getWidth() + borderHorzDist * 2; x += layout->getBorderWidth()) {
                borderPainter.drawPixmap(x * 16, y * 16, layout->border_pixmap);
            }
        }
        borderPainter.drawImage(borderWidth, borderHeight, pixmap.toImage());
        borderPainter.end();
        pixmap = newPixmap;
    }

    if (!this->map) {
        return pixmap;
    }

    if (!this->mode) {
        // if showing connections, draw on outside of image
        QPainter connectionPainter(&pixmap);
        for (auto connectionItem : editor->connection_items) {
            QString direction = connectionItem->connection->direction;
            if ((showUpConnections && direction == "up")
             || (showDownConnections && direction == "down")
             || (showLeftConnections && direction == "left")
             || (showRightConnections && direction == "right"))
                connectionPainter.drawImage(connectionItem->initialX + borderWidth, connectionItem->initialY + borderHeight,
                                            connectionItem->basePixmap.toImage());
        }
        connectionPainter.end();
    }

    // draw events
    QPainter eventPainter(&pixmap);
    QList<Event *> events = map->getAllEvents();
    int pixelOffset = 0;
    if (!ignoreBorder && showBorder) {
        pixelOffset = this->mode == ImageExporterMode::Normal ? BORDER_DISTANCE * 16 : STITCH_MODE_BORDER_DISTANCE * 16;
    }
    for (Event *event : events) {
        editor->project->setEventPixmap(event);
        Event::Group group = event->getEventGroup();
        if ((showObjects && group == Event::Group::Object)
         || (showWarps && group == Event::Group::Warp)
         || (showBGs && group == Event::Group::Bg)
         || (showTriggers && group == Event::Group::Coord)
         || (showHealSpots && group == Event::Group::Heal))
            eventPainter.drawImage(QPoint(event->getPixelX() + pixelOffset, event->getPixelY() + pixelOffset), event->getPixmap().toImage());
    }
    eventPainter.end();

    // draw grid directly onto the pixmap
    // since the last grid lines are outside of the pixmap, add a pixel to the bottom and right
    if (showGrid) {
        int addX = 1, addY = 1;
        if (borderHeight) addY = 0;
        if (borderWidth) addX = 0;

        QPixmap newPixmap= QPixmap(pixmap.width() + addX, pixmap.height() + addY);
        QPainter gridPainter(&newPixmap);
        gridPainter.drawImage(QPoint(0, 0), pixmap.toImage());
        for (int x = 0; x < newPixmap.width(); x += 16) {
            gridPainter.drawLine(x, 0, x, newPixmap.height());
        }
        for (int y = 0; y < newPixmap.height(); y += 16) {
            gridPainter.drawLine(0, y, newPixmap.width(), y);
        }
        gridPainter.end();
        pixmap = newPixmap;
    }

    return pixmap;
}

void MapImageExporter::on_checkBox_Elevation_stateChanged(int state) {
    showCollision = (state == Qt::Checked);
    updatePreview();
}

void MapImageExporter::on_checkBox_Grid_stateChanged(int state) {
    showGrid = (state == Qt::Checked);
    updatePreview();
}

void MapImageExporter::on_checkBox_Border_stateChanged(int state) {
    showBorder = (state == Qt::Checked);
    updatePreview();
}

void MapImageExporter::on_checkBox_Objects_stateChanged(int state) {
    showObjects = (state == Qt::Checked);
    updatePreview();
}

void MapImageExporter::on_checkBox_Warps_stateChanged(int state) {
    showWarps = (state == Qt::Checked);
    updatePreview();
}

void MapImageExporter::on_checkBox_BGs_stateChanged(int state) {
    showBGs = (state == Qt::Checked);
    updatePreview();
}

void MapImageExporter::on_checkBox_Triggers_stateChanged(int state) {
    showTriggers = (state == Qt::Checked);
    updatePreview();
}

void MapImageExporter::on_checkBox_HealSpots_stateChanged(int state) {
    showHealSpots = (state == Qt::Checked);
    updatePreview();
}

void MapImageExporter::on_checkBox_ConnectionUp_stateChanged(int state) {
    showUpConnections = (state == Qt::Checked);
    updatePreview();
}

void MapImageExporter::on_checkBox_ConnectionDown_stateChanged(int state) {
    showDownConnections = (state == Qt::Checked);
    updatePreview();
}

void MapImageExporter::on_checkBox_ConnectionLeft_stateChanged(int state) {
    showLeftConnections = (state == Qt::Checked);
    updatePreview();
}

void MapImageExporter::on_checkBox_ConnectionRight_stateChanged(int state) {
    showRightConnections = (state == Qt::Checked);
    updatePreview();
}

void MapImageExporter::on_pushButton_Save_pressed() {
    saveImage();
}

void MapImageExporter::on_pushButton_Reset_pressed() {
    for (auto widget : this->findChildren<QCheckBox *>())
        widget->setChecked(false);
}

void MapImageExporter::on_pushButton_Cancel_pressed() {
    this->close();
}

void MapImageExporter::on_spinBox_TimelapseDelay_valueChanged(int delayMs) {
    timelapseDelayMs = delayMs;
}

void MapImageExporter::on_spinBox_FrameSkip_valueChanged(int skip) {
    timelapseSkipAmount = skip;
}
