/*
    <one line to give the program's name and a brief idea of what it does.>
    Copyright (C) 2011  <copyright holder> <email>

    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 3 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 "Map.h"

#include "core/Constants.h"
#include "core/Utility.h"
#include "resource/TerrainSprite.h"
#include "resource/AssetManager.h"
#include "resource/DataManager.h"

#include <unordered_set>

#include "core/Constants.h"
#include "core/Logger.h"
#include "core/Types.h"
#include "core/Utility.h"
#include "resource/TerrainSprite.h"
#include "mechanics/Entity.h"

#include <genie/script/scn/MapDescription.h>

Map::Map() //: map_txt_(0)
{
//    DBG << DataManager::Inst().datFile().TerrainBlock.TileSizes.size();
}

void Map::setupBasic() noexcept
{
    cols_ = 22;
    rows_ = 22;

    tiles_.clear();

    MapTile grass;
    grass.elevation = 0;
    grass.terrainId = 0;

    const size_t tileCount = size_t(cols_) * size_t(rows_); // explicit casting to silence static analyzers
    tiles_.resize(tileCount, grass);
    m_tileUnits.resize(tileCount);

    for (int i=6; i<10; i++) {
        getTileAt(0, i).terrainId = 2;
        getTileAt(1, i).terrainId = 2;
        getTileAt(2, i).terrainId = 2;
    }
    for (int i=4; i<6; i++) {
        getTileAt(i, 10).terrainId = 1;
    }

    for (int i=3; i<6; i++) {
        getTileAt(14, i).elevation = 1;
    }

    for (int i=3; i<6; i++) {
        getTileAt(15, i).elevation = 1;
    }

    getTileAt(13, 4).elevation = 1;
    getTileAt(16, 4).elevation = 1;
    getTileAt(13, 4).terrainId = 2;
    getTileAt(17, 4).elevation = 1;
    getTileAt(18, 5).elevation = 1;
}

void Map::setupAllunitsMap() noexcept
{
    cols_ = 30;
    rows_ = 30;

    tiles_.clear();

    MapTile water;
    water.elevation = 0;
    water.terrainId = 1;

    const size_t tileCount = size_t(cols_) * size_t(rows_); // explicit cast -> silent static analyzers
    tiles_.resize(tileCount, water);
    m_tileUnits.resize(tileCount);

    // add some grass
    for (int i = 0; i < 20; i++) {
        for (int j = 0; j < 15; j++) {
            getTileAt(i, j).elevation = 0;
            getTileAt(i, j).terrainId = 0;
        }
    }

    // creates hill
    std::function<void(int, int, int, int)> elevate = [&] (int x, int y, int cols, int rows) {
        for (int i = x - 1; i < x + cols + 1; i++) {
            getTileAt(i, y - 1).elevation = 1;
            getTileAt(i, y - 1).terrainId = 0;
        }

        for (int i = y; i < y + rows; i++) {
            getTileAt(x-1, i).elevation = 1;
            getTileAt(x-1, i).terrainId = 0;

            for (int j = x; j < x + cols; j++) {

                getTileAt(j, i).elevation = 2;
                getTileAt(j, i).terrainId = 0;

            }
            getTileAt(x+cols, i).elevation = 1;
            getTileAt(x+cols, i).terrainId = 0;
        }

        for (int i = x - 1; i < x + cols + 1; i++) {
            getTileAt(i, y + rows).elevation = 1;
            getTileAt(i, y + rows).terrainId = 0;
        }
    };

    elevate(5, 5, 10, 5);
    elevate(5, 17, 10, 5);
    elevate(5, 14, 1, 1);
}

void Map::create(const genie::ScnMap &mapDescription)
{
    DBG << "tile count:" << mapDescription.tiles.size();
    DBG << "size:" << mapDescription.width << "x" << mapDescription.height;
    tiles_.clear();

    rows_ = mapDescription.width;
    cols_ = mapDescription.height;

    if (cols_ <= 0 || cols_ >= 46340) {
        throw std::out_of_range("Map width (" + std::to_string(cols_) + ") out of range");
    }

    if (rows_ <= 0 || rows_ >= 46340) {
        throw std::out_of_range("Map height (" + std::to_string(rows_) + ") out of range");
    }

    const size_t tileCount = size_t(cols_) * size_t(rows_); // explicit casts make static analyzers (lgtm) happy
    tiles_.resize(tileCount);
    m_tileUnits.resize(tileCount);

    for (size_t i = 0; i < tiles_.size(); i++) {
        const int col = i % cols_;
        const int row = i / rows_;
        genie::MapTile tile = mapDescription.tiles[i];

        getTileAt(row, col).elevation = tile.elevation;
        getTileAt(row, col).terrainId = tile.terrainID;
    }
}

float Map::elevationAt(const MapPos &position) noexcept
{
    const int tileX = position.x / Constants::TILE_SIZE;
    const int tileY = position.y / Constants::TILE_SIZE;
    const MapTile &tile = getTileAt(tileX, tileY);
    const float localX = position.x / Constants::TILE_SIZE - tileX;
    const float localY = position.y / Constants::TILE_SIZE - tileY;

    float elevation = tile.elevation;

    switch(tile.slopes.self) {
    case Slope::NorthWestUp:
        elevation += 1. - localX;
        break;
    case Slope::SouthEastUp:
        elevation += localX;
        break;
    case Slope::SouthWestUp:
        elevation += 1. - localY;
        break;
    case Slope::NorthEastUp:
        elevation += localY;
        break;
    case Slope::EastUp:
        elevation += localX * localY;
        break;
    case Slope::WestUp:
        elevation += (1. - localX) * (1. - localY);
        break;
    case Slope::NorthUp:
        elevation += (1. - localX) * localY;
        break;
    case Slope::SouthUp:
        elevation += localX * (1. - localY);
        break;
    case Slope::NorthWestEastUp:
        elevation += 1. - localX * (1. - localY);
        break;
    case Slope::SouthWestEastUp:
        elevation += 1. - (1. - localX) * localY;
        break;
    case Slope::NorthSouthEastUp:
        elevation += 1. - (1. - localX) * (1. - localY);
        break;
    case Slope::NorthSouthWestUp:
        elevation += 1. - localX * localY;
        break;
    case Slope::Flat:
        break;
    default:
        WARN << "Unhanhdled slope" << tile.slopes.self;
        break;
    }
    return elevation * DataManager::Inst().terrainBlock().ElevHeight;
}



void Map::setTileAt(unsigned col, unsigned row, unsigned id) noexcept
{
    unsigned int index = row * cols_ + col;

    if (index >= tiles_.size()) {
        WARN << "Trying to get MapTile out of range!";
        return;
    }

    tiles_[index].terrainId = id;
    m_updated = true;
}

bool Map::updateTileAt(const int col, const int row, unsigned id) noexcept
{
    unsigned int index = row * cols_ + col;

    if (index >= tiles_.size()) {
        return false;
    }

    tiles_[index].terrainId = id;
    tiles_[index].frame = AssetManager::Inst()->getTerrain(tiles_[index].terrainId)->coordinatesToFrame(col, row);
    for (int col_ = std::max(col - 1, 0); col_ < std::min(col + 2, cols_); col_++) {
        for (int row_ = std::max(row - 1, 0); row_ < std::min(row + 2, rows_); row_++) {
            getTileAt(col_, row_).reset();
        }
    }

    for (int col_ = std::max(col - 1, 0); col_ < std::min(col + 2, cols_); col_++) {
        for (int row_ = std::max(row - 1, 0); row_ < std::min(row + 2, rows_); row_++) {
            updateTileBlend(col_, row_);
        }
    }
    for (int col_ = std::max(col - 1, 0); col_ < std::min(col + 2, cols_); col_++) {
        for (int row_ = std::max(row - 1, 0); row_ < std::min(row + 2, rows_); row_++) {
            updateTileSlopes(col, row);
        }
    }

    m_updated = true;

    return true;
}

void Map::removeEntityAt(unsigned int col, unsigned int row, const size_t entityId) noexcept
{
    unsigned int index = row * cols_ + col;

    if (IS_UNLIKELY(index >= m_tileUnits.size())) {
        WARN << "Trying to remove unit out of range" << col << row;
        return;
    }

    std::vector<std::weak_ptr<Entity>>::iterator it=m_tileUnits[index].begin();
    while (it != m_tileUnits[index].end()) {
        if (it->expired()) {
            it = m_tileUnits[index].erase(it);
            continue;
        }
        EntityPtr entity = it->lock();
        if (!entity) {
            it = m_tileUnits[index].erase(it);
            continue;
        }

        if (entity->id == entityId) {
            m_tileUnits[index].erase(it);

            emit(Signals::UnitsChanged);

            return;
        }

        it++;
    }
}

void Map::addEntityAt(int col, int row, const EntityPtr &entity, int foundationTerrain) noexcept
{
    unsigned int index = row * cols_ + col;

    if (IS_UNLIKELY(index >= m_tileUnits.size())) {
        WARN << "Trying to add unit out of range" << col << row;
        return;
    }

    // just to be sure
    removeEntityAt(col, row, entity->id);

    m_tileUnits[index].push_back(entity);

    emit(Signals::UnitsChanged);

    if (!entity->isUnit()) {
        return;
    }

    if (foundationTerrain < 0) {
        return;
    }

    const int width = entity->tileSize().width;
    const int height = entity->tileSize().height;
    bool gotError = false;
    for (int x = 0; x < width*2; x++) {
        for (int y = 0; y < height*2; y++) {
            gotError = !updateTileAt(col + x - width, row + y - width, foundationTerrain) || gotError;
        }
    }

    if (gotError) {
        WARN << "Unit" << entity->debugName << "size extends out of map from" << (col - width) << (row - width) << "to" << (col + width) << (row + width);
    }

    for (int col_ = std::max(col - 1, 0); col_ < std::min(col + width + 2, cols_); col_++) {
        for (int row_ = std::max(row - 1, 0); row_ < std::min(row + height + 2, rows_); row_++) {
            getTileAt(col_, row_).reset();
            updateTileBlend(col_, row_);
            updateTileSlopes(col_, row_);
        }
    }
}





void Map::updateMapData() noexcept
{
    TIME_THIS;

    // Not the most efficient, but meh
    if (DataManager::Inst().gameVersion() <= genie::GV_AoK) {
        // AoK at least
        // Not really sure if it is correct, kind of guessed the values
        m_blendmodeTable = {{
            {{ 3, 2, 3, 1, 1, 2, 2, 2 }},
            {{ 2, 2, 2, 1, 1, 2, 2, 2 }},
            {{ 3, 2, 3, 1, 1, 2, 1, 2 }},
            {{ 1, 1, 1, 0, 0, 2, 2, 2 }},
            {{ 1, 1, 1, 0, 0, 2, 2, 2 }},
            {{ 2, 2, 2, 2, 2, 2, 2, 2 }},
            {{ 2, 2, 1, 2, 2, 2, 2, 2 }},
            {{ 2, 2, 2, 2, 2, 2, 2, 2 }}
        }};
    } else {
        m_blendmodeTable = {{ // AoK at least
            {{ 2, 3, 2, 1, 1, 6, 5, 4 }},
            {{ 3, 3, 3, 1, 1, 6, 5, 4 }},
            {{ 2, 3, 2, 1, 1, 6, 1, 4 }},
            {{ 1, 1, 1, 0, 7, 6, 5, 4 }},
            {{ 1, 1, 1, 7, 7, 6, 5, 4 }},
            {{ 6, 6, 6, 6, 6, 6, 5, 4 }},
            {{ 5, 5, 1, 5, 5, 5, 5, 4 }},
            {{ 4, 3, 4, 4, 4, 4, 4, 4 }}
        }};
    }

    for (int col = 0; col < cols_; col++) {
        for (int row = 0; row < rows_; row++) {
            MapTile &tile = tiles_[row * cols_ + col];
            tile.reset();
            tile.frame = AssetManager::Inst()->getTerrain(tile.terrainId)->coordinatesToFrame(col, row);
        }
    }

    for (int col = 0; col < cols_; col++) {
        for (int row = 0; row < rows_; row++) {
            updateTileBlend(col, row);
        }
    }

    TIME_TICK;
    for (int col = 0; col < cols_; col++) {
        for (int row = 0; row < rows_; row++) {
            updateTileSlopes(col, row);
        }
    }
    m_updated = true;

    emit(Signals::TerrainChanged);
}

MapPos Map::snapPositionToGrid(const MapPos &position, const Size unitSize) noexcept
{
    MapPos newPos = position;
    newPos /= Constants::TILE_SIZE;
    newPos += unitSize;
    newPos.round();
    newPos -= unitSize;
    newPos *= Constants::TILE_SIZE;

    if (isValidPosition(newPos)) {
        newPos.z = elevationAt(newPos);
    }

    return newPos;

}

enum Direction : uint8_t {
    None = 0,
    West = 1 << 0,
    North = 1 << 1,
    East = 1 << 2,
    South = 1 << 3,
    NorthWest = 1 << 4,
    NorthEast = 1 << 5,
    SouthWest = 1 << 6,
    SouthEast = 1 << 7,
};

void Map::updateTileBlend(int tileX, int tileY) noexcept
{
    MapTile &tile = getTileAt(tileX, tileY);
    const genie::Terrain &tileData = DataManager::Inst().getTerrain(tile.terrainId);

    int32_t tileId = tile.terrainId;

    std::unordered_map<uint8_t, int> blendDirections;
    std::unordered_set<uint8_t> neighborIds;

    std::unordered_map<uint8_t, int32_t> blendPriorities;

//    std::unordered_map<uint8_t, genie::Terrain*> neighborTerrains;

    uint8_t neighborsAbove = 0;
    uint8_t neighborsBelow = 0;

    for (int dx = -1; dx <= 1; dx++) {
        for (int dy = -1; dy <= 1; dy++) {
            if (!dx && !dy) {
                continue;
            }

            if (tileX + dx < 0 || tileX + dx >= cols_) {
                continue;
            }
            if (tileY + dy < 0 || tileY + dy >= rows_) {
                continue;
            }

            Direction direction = None;
            if (dx < 0) {
                if (dy > 0) {
                    direction = SouthWest;
                } else if (dy < 0) {
                    direction = NorthWest;
                } else {
                    direction = West;
                }
            } else if (dx > 0) {
                if (dy > 0) {
                    direction = SouthEast;
                } else if (dy < 0) {
                    direction = NorthEast;
                } else {
                    direction = East;
                }
            } else {
                if (dy > 0) {
                    direction = South;
                } else {
                    direction = North;
                }
            }

            MapTile &neighbor = getTileAt(tileX + dx, tileY + dy);
            if (neighbor.elevation == -1) {
                continue;
            }

            if (neighbor.elevation > tile.elevation) {
                neighborsAbove |= direction;
            } else if (neighbor.elevation < tile.elevation) {
                neighborsBelow |= direction;
            }

            const int32_t neighborId = neighbor.terrainId;
            if (tileId == neighborId) {
                continue;
            }

            const genie::Terrain &neighborData = DataManager::Inst().getTerrain(neighbor.terrainId);

            if (tileData.BlendPriority >= neighborData.BlendPriority) {
                continue;
            }
            blendPriorities[neighborId] = neighborData.BlendPriority;
//            neighborTerrains[neighborId] = &neighborData;

            neighborIds.insert(neighborId);

            blendDirections[neighborId] |= direction;
        }
    }

//    if (neighborsAbove & West) {
//        neighborsAbove &= ~NorthWest;
//        neighborsAbove &= ~SouthWest;
//    }
//    if (neighborsAbove & West) {
//        neighborsAbove &= ~NorthWest;
//        neighborsAbove &= ~SouthWest;
//    }

//        if (direction & South) {
//            direction &= ~SouthWest;
//            direction &= ~SouthEast;
//        }
//        if (direction & North) {
//            direction &= ~NorthWest;
//            direction &= ~NorthEast;
//        }
//        if (direction & West) {
//            direction &= ~NorthWest;
//            direction &= ~SouthWest;
//        }
//        if (direction & East) {
//            direction &= ~NorthEast;
//            direction &= ~SouthEast;
//        }

    if (neighborsAbove & West && neighborsAbove & South) {
        tile.slopes.self = Slope::NorthWestEastUp;
    } else if (neighborsAbove & East && neighborsAbove & North) {
        tile.slopes.self = Slope::SouthWestEastUp;
    } else if (neighborsAbove & West && neighborsAbove & North) {
        tile.slopes.self = Slope::NorthSouthWestUp;
    } else if (neighborsAbove & East && neighborsAbove & South) {
        tile.slopes.self = Slope::NorthSouthEastUp;
    } else if (neighborsAbove == NorthWest) {
        tile.slopes.self = Slope::WestUp;
    } else if (neighborsAbove == NorthEast) {
        tile.slopes.self = Slope::SouthUp;
    } else if (neighborsAbove == SouthWest) {
        tile.slopes.self = Slope::NorthUp;
    } else if (neighborsAbove == SouthEast) {
        tile.slopes.self = Slope::EastUp;
    } else if (neighborsAbove & North) {
        tile.slopes.self = Slope::SouthWestUp;
    } else if (neighborsAbove & South) {
        tile.slopes.self = Slope::NorthEastUp ;
    } else if (neighborsAbove & East) {
        tile.slopes.self = Slope::SouthEastUp;
    } else if (neighborsAbove & West) {
        tile.slopes.self = Slope::NorthWestUp;
    }

    tile.yOffset = DataManager::Inst().terrainBlock().TileSizes[tile.slopes.self.toGenie()].DeltaY;

//    if (tile.slopes.self != Slope::Flat) {
//        return;
//    }

    if (neighborIds.empty()) {
        return;
    }

    std::vector<uint8_t> idsToDraw(neighborIds.begin(), neighborIds.end());
    std::sort(idsToDraw.begin(), idsToDraw.end(), [&](const uint8_t a, const uint8_t b){
        return blendPriorities[a] < blendPriorities[b];
    });

    for (const uint8_t id : idsToDraw) {
        Blend blends;

        uint8_t direction = blendDirections[id];
        if (direction & South) {
            direction &= ~SouthWest;
            direction &= ~SouthEast;
        }
        if (direction & North) {
            direction &= ~NorthWest;
            direction &= ~NorthEast;
        }
        if (direction & West) {
            direction &= ~NorthWest;
            direction &= ~SouthWest;
        }
        if (direction & East) {
            direction &= ~NorthEast;
            direction &= ~SouthEast;
        }

        if (direction & NorthWest) {
            blends.addBlend(Blend::Left);
        }
        if (direction & NorthEast) {
            blends.addBlend(Blend::Down);
        }
        if (direction & SouthWest) {
            blends.addBlend(Blend::Up);
        }
        if (direction & SouthEast) {
            blends.addBlend(Blend::Right);
        }

        if (direction & East) {
            blends.addBlend(Blend::LowerLeft1);
        }
        if (direction & South) {
            blends.addBlend(Blend::UpperLeft1);
        }
        if (direction & West) {
            blends.addBlend(Blend::UpperRight1);
        }
        if (direction & North) {
            blends.addBlend(Blend::LowerRight1);
        }

        switch (direction & 0xF) {
        case None:
            break;
//        case East:
//            blendFrame = LowerLeft1;
//            break;
//        case South:
//            blendFrame = UpperLeft1;
//            break;
//        case West:
//            blendFrame = UpperRight1;
//            break;
//        case North:
//            blendFrame = LowerRight1;
//            break;

        case East | West:
            blends.addBlend(Blend::UpperLeftAndLowerRight);
            break;
        case North | South:
            blends.addBlend(Blend::UpperRightAndLowerLeft);
            break;

        case North | East:
            blends.addBlend(Blend::OnlyUp);
            break;
        case North | West:
            blends.addBlend(Blend::OnlyRight);
            break;
        case South | West:
            blends.addBlend(Blend::OnlyDown);
            break;
        case South | East:
            blends.addBlend(Blend::OnlyLeft);
            break;
            //// OK ///////

        case South | East | West:
            blends.addBlend(Blend::KeepLowerLeft);
            break;
        case North | East | West:
            blends.addBlend(Blend::KeepUpperRight);
            break;
        case North | South | West:
            blends.addBlend(Blend::KeepLowerRight);
            break;
        case North | South | East:
            blends.addBlend(Blend::KeepUpperLeft);
            break;

        case North | South | East | West:
            blends.addBlend(Blend::All);
            break;

        default:
//            log.warn("Unhandled terrain borders: 0x%x (raw: 0x%x)", direction, blendDirections[id]);
            break;
        }

        const genie::Terrain &neighbor = DataManager::Inst().getTerrain(id); //neighborTerrains[id];
        if (IS_UNLIKELY(tileData.BlendType < 0 || tileData.BlendType > 7 || neighbor.BlendType < 0 || neighbor.BlendType > 7)) {
            continue;
        }

        blends.blendMode = m_blendmodeTable[tileData.BlendType][neighbor.BlendType];
        blends.terrainId = id;
        blends.frame = AssetManager::Inst()->getTerrain(id)->coordinatesToFrame(tileX, tileY);
        if (IS_UNLIKELY(blends.frame == -1)) {
            continue;
        }

        tile.blends.push_back(blends);
    }
}

void Map::updateTileSlopes(int tileX, int tileY) noexcept
{
    MapTile &tile = getTileAt(tileX, tileY);
    if (tile.slopes.self == Slope::Flat) {
        return;
    }

    tile.slopes.north = slopeAt(tileX - 1, tileY + 1);
    tile.slopes.south = slopeAt(tileX + 1, tileY - 1);

    tile.slopes.west = slopeAt(tileX - 1, tileY - 1);
    tile.slopes.east = slopeAt(tileX + 1, tileY + 1);

    tile.slopes.southWest = slopeAt(tileX, tileY - 1);
    tile.slopes.southEast = slopeAt(tileX + 1, tileY);

    tile.slopes.northWest = slopeAt(tileX - 1, tileY);
    tile.slopes.northEast = slopeAt(tileX, tileY + 1);

    // Preload
//    tile.terrain_->slopedImage(tile.slopes, tileX, tileY);
}


