#include "CommandSerializer.h"

#include <sstream>
#include <vector>

#include "BuildingType.h"
#include "Command.h"
#include "Rotation.h"
#include "ShipLayout.h"

namespace
{
char rotationToChar(Rotation rotation)
{
    switch (rotation)
    {
        case Rotation::North: return 'N';
        case Rotation::East:  return 'E';
        case Rotation::South: return 'S';
        case Rotation::West:  return 'W';
    }
    return 'E';
}

Rotation rotationFromString(const std::string& token)
{
    if (token == "N") { return Rotation::North; }
    if (token == "S") { return Rotation::South; }
    if (token == "W") { return Rotation::West; }
    return Rotation::East;
}

// Reads "<count> (<moduleId> <x> <y> <rot>)*" from the stream. Sets ok=false on
// a stream failure.
ShipLayoutConfig parseLayout(std::istringstream& in, bool& ok)
{
    ShipLayoutConfig layout;
    int count = 0;
    if (!(in >> count) || count < 0) { ok = false; return layout; }
    for (int i = 0; i < count; ++i)
    {
        PlacedModule placed;
        std::string rotToken;
        int x = 0;
        int y = 0;
        if (!(in >> placed.moduleId >> x >> y >> rotToken)) { ok = false; return layout; }
        placed.position = QPoint(x, y);
        placed.rotation = rotationFromString(rotToken);
        layout.placedModules.push_back(placed);
    }
    return layout;
}

// Reads "<countA> (<itemId>)* <countB> (<itemId>)*" from the stream.
void parseFilters(std::istringstream& in,
                  std::vector<ItemType>& filterA,
                  std::vector<ItemType>& filterB,
                  bool& ok)
{
    int countA = 0;
    if (!(in >> countA) || countA < 0) { ok = false; return; }
    for (int i = 0; i < countA; ++i)
    {
        std::string id;
        if (!(in >> id)) { ok = false; return; }
        filterA.push_back(ItemType{id});
    }
    int countB = 0;
    if (!(in >> countB) || countB < 0) { ok = false; return; }
    for (int i = 0; i < countB; ++i)
    {
        std::string id;
        if (!(in >> id)) { ok = false; return; }
        filterB.push_back(ItemType{id});
    }
}

// "<count> (<moduleId> <x> <y> <rot>)*"
void appendLayout(std::ostringstream& out, const ShipLayoutConfig& layout)
{
    out << layout.placedModules.size();
    for (const PlacedModule& placed : layout.placedModules)
    {
        out << ' ' << placed.moduleId
            << ' ' << placed.position.x()
            << ' ' << placed.position.y()
            << ' ' << rotationToChar(placed.rotation);
    }
}

// "<countA> (<itemId>)* <countB> (<itemId>)*"
void appendFilters(std::ostringstream& out,
                   const std::vector<ItemType>& filterA,
                   const std::vector<ItemType>& filterB)
{
    out << filterA.size();
    for (const ItemType& type : filterA) { out << ' ' << type.id; }
    out << ' ' << filterB.size();
    for (const ItemType& type : filterB) { out << ' ' << type.id; }
}
}  // namespace

std::string serializeCommand(const Command& command)
{
    std::ostringstream out;

    switch (command.kind)
    {
    case CommandKind::PlaceBuilding:
    {
        const PlaceBuildingCommand& c = static_cast<const PlaceBuildingCommand&>(command);
        out << "place " << buildingTypeId(c.type)
            << ' ' << c.anchor.x() << ' ' << c.anchor.y()
            << ' ' << rotationToChar(c.rotation);
        if (c.recipeId.has_value())
        {
            out << " recipe " << *c.recipeId;
        }
        if (c.shipLayout.has_value())
        {
            out << " layout ";
            appendLayout(out, *c.shipLayout);
        }
        if (c.hasSplitterFilters)
        {
            out << " filters ";
            appendFilters(out, c.splitterFilterA, c.splitterFilterB);
        }
        break;
    }
    case CommandKind::Deconstruct:
        out << "deconstruct " << static_cast<const DeconstructCommand&>(command).id.value();
        break;
    case CommandKind::CancelDeconstruction:
        out << "cancel_deconstruct "
            << static_cast<const CancelDeconstructionCommand&>(command).id.value();
        break;
    case CommandKind::RotateInPlace:
    {
        const RotateInPlaceCommand& c = static_cast<const RotateInPlaceCommand&>(command);
        out << "rotate " << c.id.value() << ' ' << rotationToChar(c.newRotation);
        break;
    }
    case CommandKind::SetRecipe:
    {
        const SetRecipeCommand& c = static_cast<const SetRecipeCommand&>(command);
        out << "setrecipe " << c.id.value() << ' ' << c.recipeId;
        break;
    }
    case CommandKind::SetShipLayout:
    {
        const SetShipLayoutCommand& c = static_cast<const SetShipLayoutCommand&>(command);
        out << "setlayout " << c.id.value() << ' ';
        appendLayout(out, c.layout);
        break;
    }
    case CommandKind::SetSiteSplitterFilters:
    {
        const SetSiteSplitterFiltersCommand& c =
            static_cast<const SetSiteSplitterFiltersCommand&>(command);
        out << "sitefilters " << c.id.value() << ' ';
        appendFilters(out, c.filterA, c.filterB);
        break;
    }
    case CommandKind::SetSplitterFilters:
    {
        const SetSplitterFiltersCommand& c =
            static_cast<const SetSplitterFiltersCommand&>(command);
        out << "splitterfilters " << c.tile.x() << ' ' << c.tile.y() << ' ';
        appendFilters(out, c.filterA, c.filterB);
        break;
    }
    case CommandKind::ClearBeltTiles:
    {
        const ClearBeltTilesCommand& c = static_cast<const ClearBeltTilesCommand&>(command);
        out << "clearbelt " << c.tiles.size();
        for (const QPoint& tile : c.tiles)
        {
            out << ' ' << tile.x() << ' ' << tile.y();
        }
        break;
    }
    case CommandKind::ApplySchematicChoice:
        out << "schematic " << static_cast<const ApplySchematicChoiceCommand&>(command).choiceIndex;
        break;
    case CommandKind::Reset:
        // A reset rolls the replay file; it is never written as a stream entry.
        break;
    }

    return out.str();
}

std::shared_ptr<Command> parseCommand(const std::string& tokens)
{
    std::istringstream in(tokens);
    std::string verb;
    if (!(in >> verb))
    {
        return nullptr;
    }

    bool ok = true;

    if (verb == "place")
    {
        std::shared_ptr<PlaceBuildingCommand> c = std::make_shared<PlaceBuildingCommand>();
        std::string typeToken;
        std::string rotToken;
        int x = 0;
        int y = 0;
        if (!(in >> typeToken >> x >> y >> rotToken)) { return nullptr; }
        const std::optional<BuildingType> type = parseBuildingType(typeToken);
        if (!type.has_value()) { return nullptr; }
        c->type     = *type;
        c->anchor   = QPoint(x, y);
        c->rotation = rotationFromString(rotToken);

        std::string segment;
        while (in >> segment)
        {
            if (segment == "recipe")
            {
                std::string id;
                if (!(in >> id)) { return nullptr; }
                c->recipeId = id;
            }
            else if (segment == "layout")
            {
                c->shipLayout = parseLayout(in, ok);
                if (!ok) { return nullptr; }
            }
            else if (segment == "filters")
            {
                parseFilters(in, c->splitterFilterA, c->splitterFilterB, ok);
                if (!ok) { return nullptr; }
                c->hasSplitterFilters = true;
            }
            else
            {
                return nullptr;
            }
        }
        return c;
    }
    if (verb == "deconstruct")
    {
        std::shared_ptr<DeconstructCommand> c = std::make_shared<DeconstructCommand>();
        BuildingId id = 0;
        if (!(in >> id)) { return nullptr; }
        c->id = id;
        return c;
    }
    if (verb == "cancel_deconstruct")
    {
        std::shared_ptr<CancelDeconstructionCommand> c =
            std::make_shared<CancelDeconstructionCommand>();
        BuildingId id = 0;
        if (!(in >> id)) { return nullptr; }
        c->id = id;
        return c;
    }
    if (verb == "rotate")
    {
        std::shared_ptr<RotateInPlaceCommand> c = std::make_shared<RotateInPlaceCommand>();
        std::string rotToken;
        BuildingId id = 0;
        if (!(in >> id >> rotToken)) { return nullptr; }
        c->id = id;
        c->newRotation = rotationFromString(rotToken);
        return c;
    }
    if (verb == "setrecipe")
    {
        std::shared_ptr<SetRecipeCommand> c = std::make_shared<SetRecipeCommand>();
        BuildingId id = 0;
        if (!(in >> id >> c->recipeId)) { return nullptr; }
        c->id = id;
        return c;
    }
    if (verb == "setlayout")
    {
        std::shared_ptr<SetShipLayoutCommand> c = std::make_shared<SetShipLayoutCommand>();
        BuildingId id = 0;
        if (!(in >> id)) { return nullptr; }
        c->id = id;
        c->layout = parseLayout(in, ok);
        if (!ok) { return nullptr; }
        return c;
    }
    if (verb == "sitefilters")
    {
        std::shared_ptr<SetSiteSplitterFiltersCommand> c =
            std::make_shared<SetSiteSplitterFiltersCommand>();
        BuildingId id = 0;
        if (!(in >> id)) { return nullptr; }
        c->id = id;
        parseFilters(in, c->filterA, c->filterB, ok);
        if (!ok) { return nullptr; }
        return c;
    }
    if (verb == "splitterfilters")
    {
        std::shared_ptr<SetSplitterFiltersCommand> c =
            std::make_shared<SetSplitterFiltersCommand>();
        int x = 0;
        int y = 0;
        if (!(in >> x >> y)) { return nullptr; }
        c->tile = QPoint(x, y);
        parseFilters(in, c->filterA, c->filterB, ok);
        if (!ok) { return nullptr; }
        return c;
    }
    if (verb == "clearbelt")
    {
        std::shared_ptr<ClearBeltTilesCommand> c = std::make_shared<ClearBeltTilesCommand>();
        int count = 0;
        if (!(in >> count) || count < 0) { return nullptr; }
        for (int i = 0; i < count; ++i)
        {
            int x = 0;
            int y = 0;
            if (!(in >> x >> y)) { return nullptr; }
            c->tiles.push_back(QPoint(x, y));
        }
        return c;
    }
    if (verb == "schematic")
    {
        std::shared_ptr<ApplySchematicChoiceCommand> c =
            std::make_shared<ApplySchematicChoiceCommand>();
        if (!(in >> c->choiceIndex)) { return nullptr; }
        return c;
    }

    return nullptr;
}
