//
// Created by Dan Crosby on 11/4/21.
//

#include "HexMapHelpers.hpp"

#include "InvalidProtoException.hpp"
#include "TileModifierHelpers.hpp"
#include "src/main/cpp/net/eagle0/common/ByteHasher.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/TerrainHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/HexMapHasher.hpp"

namespace shardok::fb {

using TerrainProto = net::eagle0::shardok::common::Terrain;

using namespace net::eagle0::shardok::storage::fb;
using std::vector;

auto ConvertType(const net::eagle0::shardok::common::Terrain_Type protoType) -> Terrain_::Type {
    using namespace Terrain_;
    switch (protoType) {
        case net::eagle0::shardok::common::Terrain_Type_CITY: return Type_CITY;
        case net::eagle0::shardok::common::Terrain_Type_FOREST: return Type_FOREST;
        case net::eagle0::shardok::common::Terrain_Type_HILL: return Type_HILL;
        case net::eagle0::shardok::common::Terrain_Type_MOUNTAIN: return Type_MOUNTAIN;
        case net::eagle0::shardok::common::Terrain_Type_PLAINS: return Type_PLAINS;
        case net::eagle0::shardok::common::Terrain_Type_RIVER: return Type_RIVER;
        case net::eagle0::shardok::common::Terrain_Type_STILL_WATER: return Type_STILL_WATER;
        case net::eagle0::shardok::common::Terrain_Type_SWAMP: return Type_SWAMP;

        case net::eagle0::shardok::common::Terrain_Type_UNKNOWN:
        case net::eagle0::shardok::common::Terrain_Type_NONE:
        case net::eagle0::shardok::common::Terrain_Type_Terrain_Type_INT_MAX_SENTINEL_DO_NOT_USE_:
        case net::eagle0::shardok::common::Terrain_Type_Terrain_Type_INT_MIN_SENTINEL_DO_NOT_USE_:
            throw InvalidProtoException("Invalid terrain type");
    }

    throw InvalidProtoException("Invalid terrain type");
}

auto ConvertSingleModifier(const net::eagle0::shardok::common::TileModifier_SingleModifier& smProto)
        -> SingleModifier {
    return SingleModifier(
            true,
            smProto.has_integrity() ? static_cast<float>(smProto.integrity().value()) : 0.0f);
}

auto CreateStartingPositionsList(
        FlatBufferBuilder& fbb,
        const net::eagle0::shardok::common::StartingPositionList& splProto)
        -> Offset<StartingPositionList> {
    vector<Coords> coordsVec;
    coordsVec.reserve(splProto.positions_size());
    for (const auto& position : splProto.positions()) {
        coordsVec.emplace_back(position.row(), position.column());
    }
    const auto vecOffset = fbb.CreateVectorOfStructs(coordsVec);
    return CreateStartingPositionList(fbb, vecOffset);
}

auto ToProto(const HexMap* hexMap) -> HexMapProto {
    HexMapProto hexMapProto = {};

    hexMapProto.set_row_count(hexMap->row_count());
    hexMapProto.set_column_count(hexMap->column_count());

    if (hexMap->monthly_weather()) {
        for (const auto& mw : *hexMap->monthly_weather()) {
            auto* mwProto = hexMapProto.add_monthly_weather();
            mwProto->set_is_winter(mw->is_winter());
            mwProto->set_average_temperature(mw->average_temperature());
            mwProto->set_sun_chance(mw->sun_chance());
            mwProto->set_clouds_chance(mw->clouds_chance());
            mwProto->set_rain_chance(mw->rain_chance());
            mwProto->set_thunderstorm_chance(mw->thunderstorm_chance());
            mwProto->set_snow_chance(mw->snow_chance());
            mwProto->set_blizzard_chance(mw->blizzard_chance());
            mwProto->set_max_wind_mph(mw->max_wind_mph());
        }
    }

    for (const auto& terr : *hexMap->terrain()) {
        *hexMapProto.add_terrain() = ToTerrainProto(terr);
    }

    if (hexMap->defender_starting_positions() &&
        hexMap->defender_starting_positions()->positions()) {
        for (const auto& dsp : *hexMap->defender_starting_positions()->positions()) {
            auto* pos = hexMapProto.mutable_defender_starting_positions()->add_positions();
            pos->set_row(dsp->row());
            pos->set_column(dsp->column());
        }
    }

    if (hexMap->attacker_starting_positions()) {
        for (const auto& aspl : *hexMap->attacker_starting_positions()) {
            auto* list = hexMapProto.add_attacker_starting_positions();

            if (aspl->positions()) {
                for (const auto& asp : *aspl->positions()) {
                    auto* pos = list->add_positions();
                    pos->set_row(asp->row());
                    pos->set_column(asp->column());
                }
            }
        }
    }

    hexMapProto.set_base_hash(hexMap->base_hash());
    hexMapProto.set_modifier_hash(hexMap->modifier_hash());

    return hexMapProto;
}

auto ConvertHexMapProto(
        FlatBufferBuilder& fbb,
        const net::eagle0::shardok::common::HexMap& mapProto,
        const bool isWinter,
        const float initialWinterIceIntegrity) -> Offset<HexMap> {
    const auto dspOffset = CreateStartingPositionsList(fbb, mapProto.defender_starting_positions());

    vector<Offset<StartingPositionList>> asplOffsets;
    asplOffsets.reserve(mapProto.attacker_starting_positions_size());
    for (const auto& aspl : mapProto.attacker_starting_positions()) {
        asplOffsets.push_back(CreateStartingPositionsList(fbb, aspl));
    }
    const auto aspOffset = fbb.CreateVector(asplOffsets);

    vector<MonthlyWeather> monthlyWeatherVec;
    monthlyWeatherVec.reserve(mapProto.monthly_weather_size());
    for (const auto& mwp : mapProto.monthly_weather()) {
        monthlyWeatherVec.emplace_back(
                mwp.is_winter(),
                mwp.average_temperature(),
                mwp.sun_chance(),
                mwp.clouds_chance(),
                mwp.rain_chance(),
                mwp.thunderstorm_chance(),
                mwp.snow_chance(),
                mwp.blizzard_chance(),
                mwp.max_wind_mph());
    }
    const auto monthlyWeatherOffset = fbb.CreateVectorOfStructs(monthlyWeatherVec);

    const auto emptyModifier = SingleModifier(false, 0.0);
    vector<Terrain> terrainVec;
    terrainVec.reserve(mapProto.terrain_size());

    const bool calculateHash = mapProto.base_hash() == 0;
    uint64_t terrainHash = calculateHash ? FNV_OFFSET_BASIS : mapProto.base_hash();
    uint64_t modifierHash = calculateHash ? FNV_OFFSET_BASIS : mapProto.modifier_hash();

    for (const auto& terp : mapProto.terrain()) {
        if (calculateHash) {
            MixIn(terrainHash, terp.type());
            const auto modByte = shardok::ComputeModifierByteProto(terp.modifier());
            MixIn(modifierHash, modByte);
            if (terp.modifier().has_snow()) {
                MixIn(modifierHash, static_cast<char>(terp.modifier().snow().integrity().value()));
            }
            MixIn(modifierHash, '/');
        }

        auto& newBack = terrainVec.emplace_back(
                /* type */ ConvertType(terp.type()),
                TileModifier(
                        terp.modifier().has_fire() ? ConvertSingleModifier(terp.modifier().fire())
                                                   : emptyModifier,
                        terp.modifier().has_bridge()
                                ? ConvertSingleModifier(terp.modifier().bridge())
                                : emptyModifier,
                        terp.modifier().has_ice() ? ConvertSingleModifier(terp.modifier().ice())
                                                  : emptyModifier,
                        terp.modifier().has_castle()
                                ? ConvertSingleModifier(terp.modifier().castle())
                                : emptyModifier,
                        terp.modifier().has_snow() ? ConvertSingleModifier(terp.modifier().snow())
                                                   : emptyModifier));

        if (isWinter && (terp.type() == net::eagle0::shardok::common::Terrain_Type_STILL_WATER ||
                         terp.type() == net::eagle0::shardok::common::Terrain_Type_RIVER)) {
            newBack.mutable_modifier().mutable_ice().mutate_present(true);
            newBack.mutable_modifier().mutable_ice().mutate_integrity(initialWinterIceIntegrity);
        }
    }

    const auto terrainOffset = fbb.CreateVectorOfStructs(terrainVec);

    HexMapBuilder mapBuilder(fbb);

    mapBuilder.add_defender_starting_positions(dspOffset);
    mapBuilder.add_attacker_starting_positions(aspOffset);
    mapBuilder.add_monthly_weather(monthlyWeatherOffset);
    mapBuilder.add_terrain(terrainOffset);
    mapBuilder.add_row_count(static_cast<int8_t>(mapProto.row_count()));
    mapBuilder.add_column_count(static_cast<int8_t>(mapProto.column_count()));
    mapBuilder.add_base_hash(terrainHash);
    mapBuilder.add_modifier_hash(modifierHash);

    return mapBuilder.Finish();
}

auto CopyStartingPositionsList(FlatBufferBuilder& fbb, const StartingPositionList* spl) {
    vector<Coords> coordsVec = {};

    if (spl && spl->positions()) {
        coordsVec.reserve(spl->positions()->size());
        for (const Coords* c : *spl->positions()) coordsVec.push_back(*c);
    }

    return CreateStartingPositionListDirect(fbb, &coordsVec);
}

auto CopyHexMap(const HexMap* hexMap) -> HexMapW {
    FlatBufferBuilder fbb;
    fbb.ForceDefaults(true);

    vector<Terrain> terrainVec;
    terrainVec.reserve(hexMap->terrain()->size());
    for (const auto* t : *hexMap->terrain()) terrainVec.push_back(*t);
    const auto terrainOffset = fbb.CreateVectorOfStructs(terrainVec);

    vector<MonthlyWeather> weatherVec;
    weatherVec.reserve(hexMap->monthly_weather()->size());
    for (const auto* w : *hexMap->monthly_weather()) weatherVec.push_back(*w);
    const auto weatherOffset = fbb.CreateVectorOfStructs(weatherVec);

    vector<Offset<StartingPositionList>> asplVec = {};
    asplVec.reserve(hexMap->attacker_starting_positions()->size());
    for (const auto* spl : *hexMap->attacker_starting_positions()) {
        asplVec.push_back(CopyStartingPositionsList(fbb, spl));
    }
    const auto asplOffset = fbb.CreateVector(asplVec);

    const auto dsplOffset = CopyStartingPositionsList(fbb, hexMap->defender_starting_positions());

    HexMapBuilder hmb(fbb);

    hmb.add_row_count(hexMap->row_count());
    hmb.add_column_count(hexMap->column_count());
    hmb.add_terrain(terrainOffset);
    hmb.add_monthly_weather(weatherOffset);
    hmb.add_attacker_starting_positions(asplOffset);
    hmb.add_defender_starting_positions(dsplOffset);

    hmb.add_base_hash(hexMap->base_hash());
    hmb.add_modifier_hash(hexMap->modifier_hash());

    fbb.Finish(hmb.Finish());

    return HexMapW(fbb);
}

}  // namespace shardok::fb
