//
// Created by Dan Crosby on 6/26/20.
//

#include "GameStateViewDiffer.hpp"

#include <google/protobuf/repeated_field.h>

#include "src/main/cpp/net/eagle0/shardok/library/map/TileModifierWithCoords.hpp"

namespace shardok {

using net::eagle0::shardok::api::GameStateViewDiff;
using net::eagle0::shardok::api::UnitView;

void PopulateRepeatedUnitDiffs(
        const ::google::protobuf::RepeatedPtrField<UnitView> &beforeUnits,
        const ::google::protobuf::RepeatedPtrField<UnitView> &afterUnits,
        ::google::protobuf::RepeatedPtrField<UnitView> *mutableChangedOut,
        ::google::protobuf::RepeatedField<::google::protobuf::int32> *mutableRemovedIdsOut,
        bool &changed) {
    for (const auto &afterUnit : afterUnits) {
        const auto &beforeUnit = std::find_if(
                beforeUnits.begin(),
                beforeUnits.end(),
                [afterUnit](const UnitView &u) { return u.unit_id() == afterUnit.unit_id(); });

        // FIXME: this should really be a HeroViewDiff
        if (beforeUnit == beforeUnits.end() ||
            beforeUnit->SerializeAsString() != afterUnit.SerializeAsString()) {
            (*mutableChangedOut->Add()) = afterUnit;
            changed = true;
        }
    }

    // removed units
    for (const auto &beforeUnit : beforeUnits) {
        const auto &afterUnit =
                std::find_if(afterUnits.begin(), afterUnits.end(), [beforeUnit](const UnitView &u) {
                    return u.unit_id() == beforeUnit.unit_id();
                });

        if (afterUnit == afterUnits.end()) {
            mutableRemovedIdsOut->Add(beforeUnit.unit_id());
            changed = true;
        }
    }
}

auto MakeGameStateViewDiff(const GameStateView &before, const GameStateView &after)
        -> std::optional<GameStateViewDiff> {
    bool changed = false;
    GameStateViewDiff diff{};

    PopulateRepeatedUnitDiffs(
            before.units(),
            after.units(),
            diff.mutable_changed_units(),
            diff.mutable_removed_units(),
            changed);
    PopulateRepeatedUnitDiffs(
            before.reserve_units(),
            after.reserve_units(),
            diff.mutable_changed_reserve_units(),
            diff.mutable_removed_reserve_unit_ids(),
            changed);

    // tile modifiers
    const int afterRowCount = after.hex_map().row_count();
    const int afterColumnCount = after.hex_map().column_count();
    const int afterCount = afterRowCount * afterColumnCount;

    for (int tileIndex = 0; tileIndex < afterCount; tileIndex++) {
        Coords coords =
                Coords(int8_t(tileIndex / afterColumnCount), int8_t(tileIndex % afterColumnCount));
        const auto afterModifier = after.hex_map().terrain(tileIndex).modifier();

        if (tileIndex >= before.hex_map().terrain_size()) {
            *diff.add_changed_tile_modifiers() = MakeTmc(coords, afterModifier);
            changed = true;
        } else {
            const auto beforeModifier = before.hex_map().terrain(tileIndex).modifier();
            if (afterModifier.has_fire() != beforeModifier.has_fire() ||
                afterModifier.has_ice() != beforeModifier.has_ice() ||
                afterModifier.has_bridge() != beforeModifier.has_bridge() ||
                afterModifier.has_castle() != beforeModifier.has_castle() ||
                afterModifier.has_snow() != beforeModifier.has_snow() ||
                afterModifier.ice().integrity().value() !=
                        beforeModifier.ice().integrity().value() ||
                afterModifier.bridge().integrity().value() !=
                        beforeModifier.bridge().integrity().value() ||
                afterModifier.castle().integrity().value() !=
                        beforeModifier.castle().integrity().value() ||
                afterModifier.snow().integrity().value() !=
                        beforeModifier.snow().integrity().value()) {
                *diff.add_changed_tile_modifiers() = MakeTmc(coords, afterModifier);
                changed = true;
            }
        }
    }

    // weather
    if (after.weather().conditions() != before.weather().conditions() ||
        after.weather().wind().speed_in_mph() != before.weather().wind().speed_in_mph() ||
        after.weather().wind().direction() != before.weather().wind().speed_in_mph()) {
        *diff.mutable_next_weather() = after.weather();
        changed = true;
    }

    if (after.critical_tile_count() != before.critical_tile_count()) {
        diff.mutable_new_critical_tile_count()->set_value(after.critical_tile_count());
    }

    // next round
    if (after.current_round() != before.current_round()) {
        diff.mutable_next_round()->set_value(after.current_round());
        changed = true;
    }

    // next player
    if (after.current_player() != before.current_player()) {
        diff.mutable_next_player()->set_value(after.current_player());
        changed = true;
    }

    // charger info
    // Ordinarily every action either sets the eligible charger ID and possible chargees, or clears
    // them all. So for simplicitly, just clear it if it isn't set in "after", otherwise set it to
    // whatever the "after" values are.
    if (!after.has_eligible_charger_id()) {
        if (before.has_eligible_charger_id()) {
            diff.set_clear_charger_info(true);
            changed = true;
        }
    } else {
        diff.mutable_new_eligible_charger_id()->set_value(after.eligible_charger_id().value());
        for (const auto &chargee : after.possible_chargee_ids()) {
            diff.add_new_possible_chargee_ids(chargee);
        }
        changed = true;
    }

    // game status
    if (after.status().state() != before.status().state()) {
        *diff.mutable_game_status() = after.status();
        changed = true;
    }

    for (int i = 0; i < after.player_totals_size(); i++) {
        const auto &afterTotal = after.player_totals(i);
        if (before.player_totals_size() <= i) {
            *diff.add_changed_player_totals() = afterTotal;
        } else {
            const auto &beforeTotal = before.player_totals(i);
            if ((afterTotal.hero_count() != beforeTotal.hero_count()) ||
                (afterTotal.troop_count() != beforeTotal.troop_count()) ||
                (afterTotal.occupied_critical_tile_count() !=
                 beforeTotal.occupied_critical_tile_count())) {
                *diff.add_changed_player_totals() = afterTotal;
            }
        }
    }

    return changed ? diff : std::optional<GameStateViewDiff>();
}

}  // namespace shardok