#include "BalancingConfig.h"

#include <stdexcept>
#include <string>

#include "toml.hpp"

#include "Rotation.h"

namespace
{

std::runtime_error makeError(const std::string& path, const std::string& why)
{
    return std::runtime_error("balancing.toml: '" + path + "' " + why);
}

template <typename NodeView>
int64_t requireInt(NodeView node, const std::string& path)
{
    const std::optional<int64_t> value = node.template value<int64_t>();
    if (!value)
    {
        throw makeError(path, "missing or not an integer");
    }
    return *value;
}

template <typename NodeView>
std::string requireString(NodeView node, const std::string& path)
{
    const std::optional<std::string> value = node.template value<std::string>();
    if (!value)
    {
        throw makeError(path, "missing or not a string");
    }
    return *value;
}

Rotation parseRotation(const std::string& s)
{
    if (s == "east")  { return Rotation::East; }
    if (s == "south") { return Rotation::South; }
    if (s == "west")  { return Rotation::West; }
    return Rotation::North;
}

} // namespace

BalancingConfig loadBalancingConfig(const std::string& path)
{
    toml::table root;
    try
    {
        root = toml::parse_file(path);
    }
    catch (const toml::parse_error& e)
    {
        throw std::runtime_error(std::string("balancing.toml: parse error: ") + e.what());
    }

    const toml::array* arenaArray = root["arena"].as_array();
    if (!arenaArray)
    {
        throw makeError("arena", "missing or not an array of tables");
    }

    BalancingConfig config;

    for (std::size_t ai = 0; ai < arenaArray->size(); ++ai)
    {
        const toml::table* arenaTbl = (*arenaArray)[ai].as_table();
        if (!arenaTbl)
        {
            throw makeError("arena[" + std::to_string(ai) + "]", "not a table");
        }

        const std::string prefix = "arena[" + std::to_string(ai) + "]";

        ArenaConfig arena;
        arena.name = requireString((*arenaTbl)["name"], prefix + ".name");
        arena.heightTiles = static_cast<int>(
            requireInt((*arenaTbl)["height_tiles"], prefix + ".height_tiles"));
        arena.playerBufferWidth_tiles = static_cast<int>(
            requireInt((*arenaTbl)["player_buffer_width_tiles"], prefix + ".player_buffer_width_tiles"));
        arena.contestZoneWidth_tiles = static_cast<int>(
            requireInt((*arenaTbl)["contest_zone_width_tiles"], prefix + ".contest_zone_width_tiles"));
        arena.enemyBufferWidth_tiles = static_cast<int>(
            requireInt((*arenaTbl)["enemy_buffer_width_tiles"], prefix + ".enemy_buffer_width_tiles"));

        const toml::array* teamArray = (*arenaTbl)["team"].as_array();
        if (!teamArray || teamArray->size() != 2)
        {
            throw makeError(prefix + ".team", "must contain exactly 2 teams");
        }

        for (int ti = 0; ti < 2; ++ti)
        {
            const toml::table* teamTbl = (*teamArray)[static_cast<std::size_t>(ti)].as_table();
            if (!teamTbl)
            {
                throw makeError(prefix + ".team[" + std::to_string(ti) + "]", "not a table");
            }

            const std::string tPrefix = prefix + ".team[" + std::to_string(ti) + "]";

            ArenaTeamConfig& team = arena.teams[ti];
            team.name = requireString((*teamTbl)["name"], tPrefix + ".name");

            const toml::array* shipArray = (*teamTbl)["ship"].as_array();
            if (shipArray)
            {
                for (std::size_t si = 0; si < shipArray->size(); ++si)
                {
                    const toml::table* shipTbl = (*shipArray)[si].as_table();
                    if (!shipTbl)
                    {
                        throw makeError(tPrefix + ".ship[" + std::to_string(si) + "]",
                                        "not a table");
                    }

                    const std::string sPrefix =
                        tPrefix + ".ship[" + std::to_string(si) + "]";

                    ArenaShipEntry entry;
                    entry.schematicId = requireString((*shipTbl)["schematic"],
                                                     sPrefix + ".schematic");
                    entry.count = static_cast<int>(
                        requireInt((*shipTbl)["count"], sPrefix + ".count"));

                    const toml::array* modArray = (*shipTbl)["modules"].as_array();
                    if (modArray && !modArray->empty())
                    {
                        ShipLayoutConfig layout;
                        for (std::size_t mi = 0; mi < modArray->size(); ++mi)
                        {
                            const toml::table* modTbl = (*modArray)[mi].as_table();
                            if (!modTbl) { continue; }

                            const std::optional<std::string> type =
                                (*modTbl)["type"].value<std::string>();
                            const std::optional<int64_t> x =
                                (*modTbl)["x"].value<int64_t>();
                            const std::optional<int64_t> y =
                                (*modTbl)["y"].value<int64_t>();
                            const std::optional<std::string> rotStr =
                                (*modTbl)["rotation"].value<std::string>();
                            if (!type || !x || !y || !rotStr) { continue; }

                            PlacedModule pm;
                            pm.moduleId = *type;
                            pm.position = QPoint(static_cast<int>(*x),
                                                 static_cast<int>(*y));
                            pm.rotation = parseRotation(*rotStr);
                            layout.placedModules.push_back(std::move(pm));
                        }
                        entry.layout = std::move(layout);
                    }

                    team.ships.push_back(entry);
                }
            }

            const toml::array* stationArray = (*teamTbl)["station"].as_array();
            if (stationArray)
            {
                for (std::size_t si = 0; si < stationArray->size(); ++si)
                {
                    const toml::table* stationTbl = (*stationArray)[si].as_table();
                    if (!stationTbl)
                    {
                        throw makeError(tPrefix + ".station[" + std::to_string(si) + "]",
                                        "not a table");
                    }

                    const std::string sPrefix =
                        tPrefix + ".station[" + std::to_string(si) + "]";

                    ArenaStationEntry entry;
                    entry.stationType = requireString((*stationTbl)["type"],
                                                     sPrefix + ".type");
                    entry.level = static_cast<int>(
                        requireInt((*stationTbl)["level"], sPrefix + ".level"));
                    entry.position = QPoint(
                        static_cast<int>(requireInt((*stationTbl)["x"], sPrefix + ".x")),
                        static_cast<int>(requireInt((*stationTbl)["y"], sPrefix + ".y")));

                    if (entry.stationType != "player_station"
                        && entry.stationType != "enemy_station")
                    {
                        throw makeError(sPrefix + ".type",
                                        "must be 'player_station' or 'enemy_station'");
                    }

                    team.stations.push_back(entry);
                }
            }
        }

        config.arenas.push_back(arena);
    }

    return config;
}
