#include "map.h"

#include <QTime>
#include <QDebug>
#include <QPainter>
#include <QImage>
#include <QRegularExpression>

Map::Map(QObject *parent) : QObject(parent)
{
    blockdata = new Blockdata;
    cached_blockdata = new Blockdata;
    cached_collision = new Blockdata;
    cached_border = new Blockdata;
    paint_tile_index = 1;
    paint_collision = 0;
    paint_elevation = 3;
}

void Map::setName(QString mapName) {
    name = mapName;
    constantName = mapConstantFromName(mapName);
}

QString Map::mapConstantFromName(QString mapName) {
    // Transform map names of the form 'GraniteCave_B1F` into map constants like 'MAP_GRANITE_CAVE_B1F'.
    QString nameWithUnderscores = mapName.replace(QRegularExpression("([a-z])([A-Z])"), "\\1_\\2");
    QString withMapAndUppercase = "MAP_" + nameWithUnderscores.toUpper();
    QString constantName = withMapAndUppercase.replace(QRegularExpression("_+"), "_");

    // Handle special cases.
    // SSTidal needs to be SS_TIDAL, rather than SSTIDAL
    constantName = constantName.replace("SSTIDAL", "SS_TIDAL");

    return constantName;
}

int Map::getWidth() {
    return width.toInt(nullptr, 0);
}

int Map::getHeight() {
    return height.toInt(nullptr, 0);
}

Tileset* Map::getBlockTileset(int metatile_index) {
    int primary_size = 0x200;//tileset_primary->metatiles->length();
    if (metatile_index < primary_size) {
        return tileset_primary;
    } else {
        return tileset_secondary;
    }
}

QList<QList<QRgb>> Map::getBlockPalettes(int metatile_index) {
    QList<QList<QRgb>> palettes;
    for (int i = 0; i < 6; i++) {
        palettes.append(tileset_primary->palettes->at(i));
    }
    for (int i = 6; i < tileset_secondary->palettes->length(); i++) {
        palettes.append(tileset_secondary->palettes->at(i));
    }
    return palettes;
}

int Map::getBlockIndex(int index) {
    int primary_size = 0x200;
    if (index < primary_size) {
        return index;
    } else {
        return index - primary_size;
    }
}

int Map::getSelectedBlockIndex(int index) {
    if (index < tileset_primary->metatiles->length()) {
        return index;
    } else {
        return 0x200 + (index - tileset_primary->metatiles->length());
    }
}

int Map::getDisplayedBlockIndex(int index) {
    if (index < tileset_primary->metatiles->length()) {
        return index;
    } else {
        return index - 0x200 + tileset_primary->metatiles->length();
    }
}

QImage Map::getMetatileTile(int tile) {
    Tileset *tileset = getBlockTileset(tile);
    int local_index = getBlockIndex(tile);
    if (!tileset || !tileset->tiles) {
        return QImage();
    }
    return tileset->tiles->value(local_index, QImage());
}

Metatile* Map::getMetatile(int index) {
    Tileset *tileset = getBlockTileset(index);
    int local_index = getBlockIndex(index);
    if (!tileset || !tileset->metatiles) {
        return NULL;
    }
    Metatile *metatile = tileset->metatiles->value(local_index, NULL);
    return metatile;
}

QImage Map::getCollisionMetatileImage(Block block) {
    return getCollisionMetatileImage(block.collision);
}

QImage Map::getCollisionMetatileImage(int collision) {
    QImage metatile_image(16, 16, QImage::Format_RGBA8888);
    QColor color;
    if (collision == 0) {
        color.setGreen(0xff);
    } else if (collision == 1) {
        color.setRed(0xff);
    } else if (collision == 2) {
        color.setBlue(0xff);
    } else if (collision == 3) {
        // black
    }
    metatile_image.fill(color);
    return metatile_image;
}

QImage Map::getElevationMetatileImage(Block block) {
    return getElevationMetatileImage(block.elevation);
}

QImage Map::getElevationMetatileImage(int elevation) {
    QImage metatile_image(16, 16, QImage::Format_RGBA8888);
    QColor color;
    if (elevation < 15) {
        uint saturation = (elevation + 1) * 16 + 15;
        color.setGreen(saturation);
        color.setRed(saturation);
        color.setBlue(saturation);
    } else {
        color.setGreen(0xd0);
        color.setBlue(0xd0);
        color.setRed(0);
    }
    metatile_image.fill(color);
    //QPainter painter(&metatile_image);
    //painter.end();
    return metatile_image;
}

QImage Map::getMetatileImage(int tile) {

    QImage metatile_image(16, 16, QImage::Format_RGBA8888);

    Metatile* metatile = getMetatile(tile);
    if (!metatile || !metatile->tiles) {
        metatile_image.fill(0xffffffff);
        return metatile_image;
    }

    Tileset* blockTileset = getBlockTileset(tile);
    if (!blockTileset) {
        metatile_image.fill(0xffffffff);
        return metatile_image;
    }
    QList<QList<QRgb>> palettes = getBlockPalettes(tile);

    QPainter metatile_painter(&metatile_image);
    for (int layer = 0; layer < 2; layer++)
    for (int y = 0; y < 2; y++)
    for (int x = 0; x < 2; x++) {
        Tile tile_ = metatile->tiles->value((y * 2) + x + (layer * 4));
        QImage tile_image = getMetatileTile(tile_.tile);
        if (tile_image.isNull()) {
            // Some metatiles specify tiles that are outside the valid range.
            // These are treated as completely transparent, so they can be skipped without
            // being drawn.
            continue;
        }

        // Colorize the metatile tiles with its palette.
        QList<QRgb> palette = palettes.value(tile_.palette);
        for (int j = 0; j < palette.length(); j++) {
            tile_image.setColor(j, palette.value(j));
        }

        // The top layer of the metatile has its last color displayed at transparent.
        if (layer > 0) {
            QColor color(tile_image.color(15));
            color.setAlpha(0);
            tile_image.setColor(15, color.rgba());
        }

        QPoint origin = QPoint(x*8, y*8);
        metatile_painter.drawImage(origin, tile_image.mirrored(tile_.xflip == 1, tile_.yflip == 1));
    }
    metatile_painter.end();

    return metatile_image;
}

bool Map::blockChanged(int i, Blockdata *cache) {
    if (cache == NULL || cache == nullptr) {
        return true;
    }
    if (blockdata == NULL || blockdata == nullptr) {
        return true;
    }
    if (cache->blocks == NULL || cache->blocks == nullptr) {
        return true;
    }
    if (blockdata->blocks == NULL || blockdata->blocks == nullptr) {
        return true;
    }
    if (cache->blocks->length() <= i) {
        return true;
    }
    if (blockdata->blocks->length() <= i) {
        return true;
    }
    return blockdata->blocks->value(i) != cache->blocks->value(i);
}

void Map::cacheBorder() {
    if (cached_border) delete cached_border;
    cached_border = new Blockdata;
    if (border && border->blocks) {
        for (int i = 0; i < border->blocks->length(); i++) {
            Block block = border->blocks->value(i);
            cached_border->blocks->append(block);
        }
    }
}

void Map::cacheBlockdata() {
    if (cached_blockdata) delete cached_blockdata;
    cached_blockdata = new Blockdata;
    if (blockdata && blockdata->blocks) {
        for (int i = 0; i < blockdata->blocks->length(); i++) {
            Block block = blockdata->blocks->value(i);
            cached_blockdata->blocks->append(block);
        }
    }
}

void Map::cacheCollision() {
    if (cached_collision) delete cached_collision;
    cached_collision = new Blockdata;
    if (blockdata && blockdata->blocks) {
        for (int i = 0; i < blockdata->blocks->length(); i++) {
            Block block = blockdata->blocks->value(i);
            cached_collision->blocks->append(block);
        }
    }
}

QPixmap Map::renderCollision() {
    bool changed_any = false;
    int width_ = getWidth();
    int height_ = getHeight();
    if (
            collision_image.isNull()
            || collision_image.width() != width_ * 16
            || collision_image.height() != height_ * 16
    ) {
        collision_image = QImage(width_ * 16, height_ * 16, QImage::Format_RGBA8888);
        changed_any = true;
    }
    if (!(blockdata && blockdata->blocks && width_ && height_)) {
        collision_pixmap = collision_pixmap.fromImage(collision_image);
        return collision_pixmap;
    }
    QPainter painter(&collision_image);
    for (int i = 0; i < blockdata->blocks->length(); i++) {
        if (cached_collision && !blockChanged(i, cached_collision)) {
            continue;
        }
        changed_any = true;
        Block block = blockdata->blocks->value(i);
        QImage metatile_image = getMetatileImage(block.tile);
        QImage collision_metatile_image = getCollisionMetatileImage(block);
        QImage elevation_metatile_image = getElevationMetatileImage(block);
        int map_y = width_ ? i / width_ : 0;
        int map_x = width_ ? i % width_ : 0;
        QPoint metatile_origin = QPoint(map_x * 16, map_y * 16);
        painter.setOpacity(1);
        painter.drawImage(metatile_origin, metatile_image);

        painter.save();
        if (block.elevation == 15) {
            painter.setOpacity(0.5);
        } else if (block.elevation == 0) {
            painter.setOpacity(0);
        } else {
            painter.setOpacity(1);//(block.elevation / 16.0) * 0.8);
            painter.setCompositionMode(QPainter::CompositionMode_Overlay);
        }
        painter.drawImage(metatile_origin, elevation_metatile_image);
        painter.restore();

        painter.save();
        if (block.collision == 0) {
            painter.setOpacity(0.1);
        } else {
            painter.setOpacity(0.4);
        }
        painter.drawImage(metatile_origin, collision_metatile_image);
        painter.restore();

        painter.save();
        painter.setOpacity(0.6);
        painter.setPen(QColor(255, 255, 255, 192));
        painter.setFont(QFont("Helvetica", 8));
        painter.drawText(QPoint(metatile_origin.x(), metatile_origin.y() + 8), QString("%1").arg(block.elevation));
        painter.restore();
    }
    painter.end();
    cacheCollision();
    if (changed_any) {
        collision_pixmap = collision_pixmap.fromImage(collision_image);
    }
    return collision_pixmap;
}

QPixmap Map::render() {
    bool changed_any = false;
    int width_ = getWidth();
    int height_ = getHeight();
    if (
            image.isNull()
            || image.width() != width_ * 16
            || image.height() != height_ * 16
    ) {
        image = QImage(width_ * 16, height_ * 16, QImage::Format_RGBA8888);
        changed_any = true;
    }
    if (!(blockdata && blockdata->blocks && width_ && height_)) {
        pixmap = pixmap.fromImage(image);
        return pixmap;
    }

    QPainter painter(&image);
    for (int i = 0; i < blockdata->blocks->length(); i++) {
        if (!blockChanged(i, cached_blockdata)) {
            continue;
        }
        changed_any = true;
        Block block = blockdata->blocks->value(i);
        QImage metatile_image = getMetatileImage(block.tile);
        int map_y = width_ ? i / width_ : 0;
        int map_x = width_ ? i % width_ : 0;
        QPoint metatile_origin = QPoint(map_x * 16, map_y * 16);
        painter.drawImage(metatile_origin, metatile_image);
    }
    painter.end();
    if (changed_any) {
        cacheBlockdata();
        pixmap = pixmap.fromImage(image);
    }
    return pixmap;
}

QPixmap Map::renderBorder() {
    bool changed_any = false;
    int width_ = 2;
    int height_ = 2;
    if (border_image.isNull()) {
        border_image = QImage(width_ * 16, height_ * 16, QImage::Format_RGBA8888);
        changed_any = true;
    }
    if (!(border && border->blocks)) {
        border_pixmap = border_pixmap.fromImage(border_image);
        return border_pixmap;
    }
    QPainter painter(&border_image);
    for (int i = 0; i < border->blocks->length(); i++) {
        if (!blockChanged(i, cached_border)) {
            continue;
        }
        changed_any = true;
        Block block = border->blocks->value(i);
        QImage metatile_image = getMetatileImage(block.tile);
        int map_y = i / width_;
        int map_x = i % width_;
        painter.drawImage(QPoint(map_x * 16, map_y * 16), metatile_image);
    }
    painter.end();
    if (changed_any) {
        cacheBorder();
        border_pixmap = border_pixmap.fromImage(border_image);
    }
    return border_pixmap;
}

QPixmap Map::renderConnection(Connection connection) {
    render();
    int x, y, w, h;
    if (connection.direction == "up") {
        x = 0;
        y = getHeight() - 6;
        w = getWidth();
        h = 6;
    } else if (connection.direction == "down") {
        x = 0;
        y = 0;
        w = getWidth();
        h = 6;
    } else if (connection.direction == "left") {
        x = getWidth() - 6;
        y = 0;
        w = 6;
        h = getHeight();
    } else if (connection.direction == "right") {
        x = 0;
        y = 0;
        w = 6;
        h = getHeight();
    } else {
        // this should not happen
        x = 0;
        y = 0;
        w = getWidth();
        h = getHeight();
    }
    QImage connection_image = image.copy(x * 16, y * 16, w * 16, h * 16);
    //connection_image = connection_image.convertToFormat(QImage::Format_Grayscale8);
    return QPixmap::fromImage(connection_image);
}

QPixmap Map::renderCollisionMetatiles() {
    int length_ = 4;
    int height_ = 1;
    int width_ = length_ / height_;
    QImage image(width_ * 16, height_ * 16, QImage::Format_RGBA8888);
    QPainter painter(&image);
    for (int i = 0; i < length_; i++) {
        int y = i / width_;
        int x = i % width_;
        QPoint origin(x * 16, y * 16);
        QImage metatile_image = getCollisionMetatileImage(i);
        painter.drawImage(origin, metatile_image);
    }
    drawSelection(paint_collision, width_, 1, 1, &painter);
    painter.end();
    return QPixmap::fromImage(image);
}

QPixmap Map::renderElevationMetatiles() {
    int length_ = 16;
    int height_ = 2;
    int width_ = length_ / height_;
    QImage image(width_ * 16, height_ * 16, QImage::Format_RGBA8888);
    QPainter painter(&image);
    for (int i = 0; i < length_; i++) {
        int y = i / width_;
        int x = i % width_;
        QPoint origin(x * 16, y * 16);
        QImage metatile_image = getElevationMetatileImage(i);
        painter.drawImage(origin, metatile_image);
    }
    drawSelection(paint_elevation, width_, 1, 1, &painter);
    painter.end();
    return QPixmap::fromImage(image);
}

void Map::drawSelection(int i, int w, int selectionWidth, int selectionHeight, QPainter *painter) {
    int x = i % w;
    int y = i / w;
    painter->save();

    QColor penColor = smart_paths_enabled ? QColor(0xff, 0x0, 0xff) : QColor(0xff, 0xff, 0xff);
    painter->setPen(penColor);
    int rectWidth = selectionWidth * 16;
    int rectHeight = selectionHeight * 16;
    painter->drawRect(x * 16, y * 16, rectWidth - 1, rectHeight -1);
    painter->setPen(QColor(0, 0, 0));
    painter->drawRect(x * 16 - 1, y * 16 - 1, rectWidth + 1, rectHeight + 1);
    painter->drawRect(x * 16 + 1, y * 16 + 1, rectWidth - 3, rectHeight - 3);
    painter->restore();
}

QPixmap Map::renderMetatiles() {
    if (!tileset_primary || !tileset_primary->metatiles || !tileset_secondary || !tileset_secondary->metatiles) {
        return QPixmap();
    }
    int primary_length = tileset_primary->metatiles->length();
    int length_ = primary_length + tileset_secondary->metatiles->length();
    int width_ = 8;
    int height_ = length_ / width_;
    QImage image(width_ * 16, height_ * 16, QImage::Format_RGBA8888);
    QPainter painter(&image);
    for (int i = 0; i < length_; i++) {
        int tile = i;
        if (i >= primary_length) {
            tile += 0x200 - primary_length;
        }
        QImage metatile_image = getMetatileImage(tile);
        int map_y = i / width_;
        int map_x = i % width_;
        QPoint metatile_origin = QPoint(map_x * 16, map_y * 16);
        painter.drawImage(metatile_origin, metatile_image);
    }

    drawSelection(paint_tile_index, width_, paint_tile_width, paint_tile_height, &painter);

    painter.end();
    return QPixmap::fromImage(image);
}

Block* Map::getBlock(int x, int y) {
    if (blockdata && blockdata->blocks) {
        if (x >= 0 && x < getWidth())
        if (y >= 0 && y < getHeight()) {
            int i = y * getWidth() + x;
            return new Block(blockdata->blocks->value(i));
        }
    }
    return NULL;
}

void Map::_setBlock(int x, int y, Block block) {
    int i = y * getWidth() + x;
    if (blockdata && blockdata->blocks) {
        blockdata->blocks->replace(i, block);
    }
}

void Map::_floodFill(int x, int y, uint tile) {
    QList<QPoint> todo;
    todo.append(QPoint(x, y));
    while (todo.length()) {
            QPoint point = todo.takeAt(0);
            x = point.x();
            y = point.y();
            Block *block = getBlock(x, y);
            if (block == NULL) {
                continue;
            }
            uint old_tile = block->tile;
            if (old_tile == tile) {
                continue;
            }
            block->tile = tile;
            _setBlock(x, y, *block);
            if ((block = getBlock(x + 1, y)) && block->tile == old_tile) {
                todo.append(QPoint(x + 1, y));
            }
            if ((block = getBlock(x - 1, y)) && block->tile == old_tile) {
                todo.append(QPoint(x - 1, y));
            }
            if ((block = getBlock(x, y + 1)) && block->tile == old_tile) {
                todo.append(QPoint(x, y + 1));
            }
            if ((block = getBlock(x, y - 1)) && block->tile == old_tile) {
                todo.append(QPoint(x, y - 1));
            }
    }
}

void Map::_floodFillCollision(int x, int y, uint collision) {
    QList<QPoint> todo;
    todo.append(QPoint(x, y));
    while (todo.length()) {
            QPoint point = todo.takeAt(0);
            x = point.x();
            y = point.y();
            Block *block = getBlock(x, y);
            if (block == NULL) {
                continue;
            }
            uint old_coll = block->collision;
            if (old_coll == collision) {
                continue;
            }
            block->collision = collision;
            _setBlock(x, y, *block);
            if ((block = getBlock(x + 1, y)) && block->collision == old_coll) {
                todo.append(QPoint(x + 1, y));
            }
            if ((block = getBlock(x - 1, y)) && block->collision == old_coll) {
                todo.append(QPoint(x - 1, y));
            }
            if ((block = getBlock(x, y + 1)) && block->collision == old_coll) {
                todo.append(QPoint(x, y + 1));
            }
            if ((block = getBlock(x, y - 1)) && block->collision == old_coll) {
                todo.append(QPoint(x, y - 1));
            }
    }
}

void Map::_floodFillElevation(int x, int y, uint elevation) {
    QList<QPoint> todo;
    todo.append(QPoint(x, y));
    while (todo.length()) {
            QPoint point = todo.takeAt(0);
            x = point.x();
            y = point.y();
            Block *block = getBlock(x, y);
            if (block == NULL) {
                continue;
            }
            uint old_z = block->elevation;
            if (old_z == elevation) {
                continue;
            }
            Block block_(*block);
            block_.elevation = elevation;
            _setBlock(x, y, block_);
            if ((block = getBlock(x + 1, y)) && block->elevation == old_z) {
                todo.append(QPoint(x + 1, y));
            }
            if ((block = getBlock(x - 1, y)) && block->elevation == old_z) {
                todo.append(QPoint(x - 1, y));
            }
            if ((block = getBlock(x, y + 1)) && block->elevation == old_z) {
                todo.append(QPoint(x, y + 1));
            }
            if ((block = getBlock(x, y - 1)) && block->elevation == old_z) {
                todo.append(QPoint(x, y - 1));
            }
    }
}

void Map::_floodFillCollisionElevation(int x, int y, uint collision, uint elevation) {
    QList<QPoint> todo;
    todo.append(QPoint(x, y));
    while (todo.length()) {
            QPoint point = todo.takeAt(0);
            x = point.x();
            y = point.y();
            Block *block = getBlock(x, y);
            if (block == NULL) {
                continue;
            }
            uint old_coll = block->collision;
            uint old_elev = block->elevation;
            if (old_coll == collision && old_elev == elevation) {
                continue;
            }
            block->collision = collision;
            block->elevation = elevation;
            _setBlock(x, y, *block);
            if ((block = getBlock(x + 1, y)) && block->collision == old_coll && block->elevation == old_elev) {
                todo.append(QPoint(x + 1, y));
            }
            if ((block = getBlock(x - 1, y)) && block->collision == old_coll && block->elevation == old_elev) {
                todo.append(QPoint(x - 1, y));
            }
            if ((block = getBlock(x, y + 1)) && block->collision == old_coll && block->elevation == old_elev) {
                todo.append(QPoint(x, y + 1));
            }
            if ((block = getBlock(x, y - 1)) && block->collision == old_coll && block->elevation == old_elev) {
                todo.append(QPoint(x, y - 1));
            }
    }
}


void Map::undo() {
    if (blockdata) {
        Blockdata *commit = history.back();
        if (commit != NULL) {
            blockdata->copyFrom(commit);
            emit mapChanged(this);
        }
    }
}

void Map::redo() {
    if (blockdata) {
        Blockdata *commit = history.next();
        if (commit != NULL) {
            blockdata->copyFrom(commit);
            emit mapChanged(this);
        }
    }
}

void Map::commit() {
    if (blockdata) {
        if (!blockdata->equals(history.current())) {
            Blockdata* commit = blockdata->copy();
            history.push(commit);
            emit mapChanged(this);
        }
    }
}

void Map::setBlock(int x, int y, Block block) {
    Block *old_block = getBlock(x, y);
    if (old_block && (*old_block) != block) {
        _setBlock(x, y, block);
        commit();
    }
}

void Map::floodFill(int x, int y, uint tile) {
    Block *block = getBlock(x, y);
    if (block && block->tile != tile) {
        _floodFill(x, y, tile);
        commit();
    }
}

void Map::floodFillCollision(int x, int y, uint collision) {
    Block *block = getBlock(x, y);
    if (block && block->collision != collision) {
        _floodFillCollision(x, y, collision);
        commit();
    }
}

void Map::floodFillElevation(int x, int y, uint elevation) {
    Block *block = getBlock(x, y);
    if (block && block->elevation != elevation) {
        _floodFillElevation(x, y, elevation);
        commit();
    }
}
void Map::floodFillCollisionElevation(int x, int y, uint collision, uint elevation) {
    Block *block = getBlock(x, y);
    if (block && (block->collision != collision || block->elevation != elevation)) {
        _floodFillCollisionElevation(x, y, collision, elevation);
        commit();
    }
}

QList<Event *> Map::getAllEvents() {
    QList<Event*> all;
    for (QList<Event*> list : events.values()) {
        all += list;
    }
    return all;
}

QList<Event *> Map::getEventsByType(QString type)
{
    return events.value(type);
}

void Map::removeEvent(Event *event) {
    for (QString key : events.keys()) {
        events[key].removeAll(event);
    }
}

void Map::addEvent(Event *event) {
    events[event->get("event_type")].append(event);
}

bool Map::hasUnsavedChanges() {
    return !history.isSaved() || !isPersistedToFile;
}

void Map::hoveredTileChanged(int x, int y, int block) {
    emit statusBarMessage(QString("X: %1, Y: %2, Block: 0x%3")
                          .arg(x)
                          .arg(y)
                          .arg(QString("%1").arg(block, 3, 16, QChar('0')).toUpper()));
}

void Map::clearHoveredTile() {
    emit statusBarMessage(QString(""));
}

void Map::hoveredMetatileChanged(int blockIndex) {
    int tile = getSelectedBlockIndex(blockIndex);
    emit statusBarMessage(QString("Block: 0x%1")
                          .arg(QString("%1").arg(tile, 3, 16, QChar('0')).toUpper()));
}

void Map::clearHoveredMetatile() {
    emit statusBarMessage(QString(""));
}

void Map::hoveredCollisionTileChanged(int collision) {
    emit statusBarMessage(QString("Collision: %1").arg(collision));
}

void Map::clearHoveredCollisionTile() {
    emit statusBarMessage(QString(""));
}

void Map::hoveredElevationTileChanged(int elevation) {
    emit statusBarMessage(QString("Elevation: %1").arg(elevation));
}

void Map::clearHoveredElevationTile() {
    emit statusBarMessage(QString(""));
}