//
//  ShardokEngine.hpp
//  eagle0
//
//  Created by Dan Crosby on 1/30/15.
//  Copyright (c) 2015 none. All rights reserved.
//

#ifndef __eagle0__ShardokEngine__
#define __eagle0__ShardokEngine__

#include <flatbuffers/flatbuffers.h>

#include <optional>

#include "ShardokAction.hpp"
#include "src/main/cpp/net/eagle0/common/RandomGenerator.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/AvailableCommandsFactory.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/UnitPlacementInfo.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/GameStateHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
#include "src/main/protobuf/net/eagle0/shardok/api/action_result_view.pb.h"
#include "src/main/protobuf/net/eagle0/shardok/api/game_state_view.pb.h"
#include "src/main/protobuf/net/eagle0/shardok/api/unit_view.pb.h"
#include "src/main/protobuf/net/eagle0/shardok/storage/action_with_resulting_state.pb.h"

namespace shardok {
using std::begin;
using std::end;
using std::optional;
using std::shared_ptr;
using std::vector;

using net::eagle0::shardok::api::UnitView;
using PlayerInfoProto = net::eagle0::shardok::common::PlayerInfo;
using net::eagle0::shardok::storage::ShardokActionWithResultingState;
using HexMapProto = net::eagle0::shardok::common::HexMap;

class ShardokEngine {
private:
    const GameSettingsSPtr gameSettings;
    const SettingsGetter settingsGetter;
    const shared_ptr<AvailableCommandsFactory> availableCommandsFactory;

    GameStateW gameState;

    const bool trackHistory;
    const int32_t startingHistoryCount = 0;
    const GameStateW startingHistoryState;
    mutable vector<ShardokActionWithResultingState> actionHistory;

    const CoordsSet criticalTileCoords;

    mutable CommandListSPtr cachedAvailableCommands{};

    void ApplyAndAddActionResult(const ActionResult &result);

    void ApplyAndAddActionResults(const vector<ActionResult> &results);

    [[nodiscard]] auto HandleUnitFallingIntoWater(
            const Terrain *terrain,
            const fb::Unit *unit,
            std::shared_ptr<RandomGenerator> randomGenerator) const -> vector<ActionResult>;

    void HandleActionResult(
            const ActionResult &actionResult,
            const std::shared_ptr<RandomGenerator> &randomGenerator);

    [[nodiscard]] auto GetUnit(const UnitId uid) const -> const fb::Unit * {
        return GetCurrentGameState()->units()->Get(uid);
    }

    void HandlePlayerTurnEnd(const std::shared_ptr<RandomGenerator> &randomGenerator);
    [[nodiscard]] auto UncachedGetAvailableCommands(PlayerId playerId, bool includeFollowUps) const
            -> CommandListSPtr;

    void PostActionUnchecked(
            const CommandSPtr &command,
            const std::shared_ptr<RandomGenerator> &randomGenerator,
            std::optional<int32_t> roll);

public:
    // Constructor using existing game state
    ShardokEngine(
            const GameSettingsSPtr &settings,
            const GameStateW &gsBuffer,
            int32_t startingHistoryCount = 0,
            bool trackHistory = true);

    // Constructor for a brand new game
    ShardokEngine(
            const GameSettingsSPtr &settings,
            vector<Unit> &&unplacedUnorderedUnits,
            const HexMapProto &map,
            int month,
            const GameId &existingGameId,
            const vector<PlayerInfoProto> &playerInfos,
            bool trackHistory = true);

    // Constructor for a game with history
    ShardokEngine(
            const GameSettingsSPtr &settings,
            const vector<ShardokActionWithResultingState> &history,
            bool trackHistory = true);

    // Copy constructor
    ShardokEngine(const ShardokEngine &toCopy, bool trackHistory = true);

    ~ShardokEngine() = default;

    [[nodiscard]] auto GetGameStateAtStartOfAction(ActionId startingActionId) const -> GameStateW;

    [[nodiscard]] auto GetCurrentGameState() const -> const GameStateW & { return gameState; }

    [[nodiscard]] auto GetCurrentGameStateBytes() const -> byte_vector;

    [[nodiscard]] auto GetCurrentPlayerId() const -> PlayerId {
        return GetCurrentGameState()->current_player();
    }

    [[nodiscard]] auto EndGameUnits() const
            -> vector<net::eagle0::shardok::storage::ResolvedUnit>;  // Throws if the game is not
                                                                     // over

    // Controller API
    [[nodiscard]] auto GetGameHistory(ActionId lastUpdatedActionId) const
            -> vector<ShardokActionWithResultingState>;

    [[nodiscard]] auto GetUnfilteredHistoryCount() const -> size_t {
        return actionHistory.size() + startingHistoryCount;
    }

    // Client API
    [[nodiscard]] auto GetGameStateView(PlayerId askingPlayer) const
            -> net::eagle0::shardok::api::GameStateView;

    [[nodiscard]] auto FilterNewResults(PlayerId askingPlayer, int64_t previousActionCount) const
            -> vector<net::eagle0::shardok::api::ActionResultView>;

    [[nodiscard]] auto GetFilteredGameHistory(PlayerId askingPlayer) const
            -> vector<net::eagle0::shardok::api::ActionResultView>;

    [[nodiscard]] auto GetUnitById(PlayerId askingPlayer, UnitId unitId) const -> UnitView;

    void PostPlacementCommands(
            PlayerId player,
            const vector<UnitPlacementInfo> &placementInfos,
            const std::shared_ptr<RandomGenerator> &randomGenerator = nullptr);
    void PostFinishedPlacementCommand(
            PlayerId player,
            std::shared_ptr<RandomGenerator> randomGenerator = nullptr);
    void PostCommand(
            PlayerId player,
            int64_t commandIndex,
            std::shared_ptr<RandomGenerator> randomGenerator = nullptr,
            std::optional<int32_t> roll = std::nullopt);

    void PostWhileCurrentPlayerHasOnlyOneOption(std::shared_ptr<RandomGenerator> randomGenerator);
    auto PostWhilePlayerHasOnlyOneOption(
            PlayerId playerId,
            std::shared_ptr<RandomGenerator> randomGenerator) -> bool;

    [[nodiscard]] auto GetPreviewCommands(PlayerId playerId) const -> vector<CommandProto>;

    [[nodiscard]] auto GetAvailableCommandsForAIPlayer(PlayerId playerId) const -> CommandListSPtr;
    [[nodiscard]] auto GetAvailableCommandProtos(PlayerId playerId, bool includeFollowUps) const
            -> vector<CommandProto>;

    [[nodiscard]] auto GetGameId() const -> GameId {
        return GetCurrentGameState()->game_id()->str();
    }

    [[nodiscard]] auto GetMonth() const -> int { return GetCurrentGameState()->month(); }

    [[nodiscard]] auto GetPlayerInfos() const -> vector<PlayerInfoProto> {
        const auto &currentGameState = GetCurrentGameState();
        vector<PlayerInfoProto> protos{};
        for (const auto *const piFB : *currentGameState->player_infos()) {
            protos.push_back(fb::ToPlayerInfoProto(piFB));
        }
        return protos;
    }

    [[nodiscard]] auto GetGameStatus() const
            -> const net::eagle0::shardok::storage::fb::GameStatus * {
        return GetCurrentGameState()->status();
    }

    [[nodiscard]] auto GetGameSettings() const -> GameSettingsSPtr { return gameSettings; }

    static inline auto GameIsOver(const fb::GameStatus *status) -> bool {
        return (status->state() == net::eagle0::shardok::storage::fb::GameStatus_::State_VICTORY);
    }

    [[nodiscard]] inline auto GameIsOver() const -> bool { return GameIsOver(GetGameStatus()); }
};
}  // namespace shardok

#endif /* defined(__eagle0__ShardokEngine__) */
