#include "game.h"

#include "constants.h"
#include "pc.h"
#include "dragon.h"

game::game(const enum race starting_race,
           const feature enabled_features,
           input   *new_in,
           output *new_out,
           RNG     *new_rng,
           const std::vector<level_data> &lvls):
        enabled_features{enabled_features}, lvls{lvls},
        in{new_in}, out{new_out}, rng{new_rng},
        curr_turn{0} {
        const position nil{0, 0};
        the_world = false;
        star_platinum = false;

        // TODO: add the other races
        player = init_player(rng, enabled_features, starting_race);

        if (enabled_features & FEATURE_EXTRA_LEVELS)
                max_level = rng->rand_between(MIN_LEVEL_CNT, MAX_LEVEL_CNT + 1);
        else
                max_level = DEFAULT_MAX_LEVEL;

        levels.reserve(max_level);
        curr_level = 0;

        hostile_merchants = false;

        new_level();
        msg += "Player character has spawned. ";
        print();
}

void game::new_level() {

        if (enabled_features & FEATURE_RAND_MAP)
                levels.push_back(std::make_unique<level>(player.get(),
                                 rng, enabled_features));
        else if (enabled_features & FEATURE_READ_MAP)
                levels.push_back(std::make_unique<level>(lvls[curr_level],
                                 player.get(), rng, enabled_features));
        else
                levels.push_back(std::make_unique<level>(
                                         level_data{POS_NIL, {}, {}, {},
                                                    DEFAULT_MAP, POS_NIL},
                                         player.get(), rng,
                                         enabled_features));
}

enemy_list game::sort_enemies(const enemy_list &elist) {
        enemy_list result;
        result.reserve(elist.size());

        for (auto enemy : elist) {
                result.push_back(enemy);

                for (int i = result.size() - 1; i > 0 &&
                     enemy->get_pos() < result[i - 1]->get_pos(); --i)
                        std::swap(result[i], result[i - 1]);
        }

        return result;
}

character *game::move_enemies() {
        auto enemies = sort_enemies(levels[curr_level]->get_elist());

        for (auto ch : enemies) {
                ch->start_turn();
                ch->calc_effects();

                if (ch->is_dead()) {
                        int g = ch->dies(levels[curr_level].get());

                        if (g)
                                levels[curr_level]->add_gold(gold{ch->get_pos(), g});
                        msg += ch->get_abbrev() + " was killed by a potion. ";
                }
        }

        if (star_platinum)
                return nullptr;

        for (auto ch : enemies) {
                if (ch->is_dead()) {
                        levels[curr_level]->erase_enemy(ch);
                        continue;
                }

                if (the_world && ch->get_race() == MERCHANT) {
                        if (!hostile_merchants)
                                continue;
                } else if (the_world &&
                           !(is_adjacent(ch->get_pos(), player->get_pos()) ||
                             (ch->get_race() == DRAGON &&
                              is_adjacent(static_cast<dragon * >(ch)->get_guards(),
                                          player->get_pos()))))
                        continue;

                bool hostile = ch->get_race() == MERCHANT ?
                               hostile_merchants : true;
                auto res =
                        ch->act(levels[curr_level].get(), player.get(), hostile);

                if (player->is_dead())
                        return ch;

                msg += res.msg;
        }

        return nullptr;
}

game_result game::run() {
        auto res = player->interpret_command(levels[curr_level].get(),
                                             in->get_command());
        msg = res.msg;

        if (!hostile_merchants && res.msg.find(" M") != std::string::npos) {
                hostile_merchants = true;
                msg += "PC has angered the merchants. ";
        }

        switch (res.res) {
        case result::TERMINATE:
                return {TERMINATED, ""};

        case result::DIED:
                return {DEAD, "You died: killed by your own stupidity!"};

        case result::GO_DOWN: {
                if (curr_level == max_level - 1)
                        return {WON, "You won! Your final score is: " +
                                str_score() + ". "};

                player->start_turn();

                player->discard_level_effects();

                ++curr_level;

                if (curr_level == levels.size())
                        new_level();

                if (enabled_features & FEATURE_REVISIT)
                        player->set_pos(levels[curr_level]->get_up_stairs());

                break;
        }

        case result::GO_UP: {
                if (curr_level == 0)
                        return {ESCAPED,
                                "You escaped the dungeon with a score of " +
                                str_score() + ". Coward!"};

                player->start_turn();

                player->discard_level_effects();

                --curr_level;

                player->set_pos(levels[curr_level]->get_down_stairs());

                break;
        }

        case result::TOGGLE_THE_WORLD:
                the_world = !the_world;
                player->start_turn();

                if (star_platinum) {
                        msg += "Star Platinum is automatically turned off. ";
                        star_platinum = false;
                }

                break;

        case result::TOGGLE_STAR_PLATINUM:
                star_platinum = !star_platinum;
                player->start_turn();

                if (the_world) {
                        msg += "The World is automatically turned off. ";
                        the_world = false;
                }

                break;

        case RESTART_GAME:
                return {RESTART, ""};

        case UNKNOWN:
        case NOTHING:
        case APPLIED_NOTHING:
        case INVENTORY:
                return {IN_GAME, ""};

        default:
                player->start_turn();
                break;
        }

        ++curr_turn;

        player->calc_effects();

        if (player->is_dead())
                return {DEAD, "You died: killed by your own stupidity!"};

        auto killer = move_enemies();

        if (player->is_dead()) {
                if (killer == nullptr)
                        return {DEAD,
                                "You died: killed by your own stupidity!"};

                return {DEAD, std::string{"You died: killed by a(n) "} +
                        killer->get_race_name() + ". "};
        }

        return {IN_GAME, ""};
}

const position STATUS_RACE{0, 25};
const position STATUS_FLOOR{69, 25};
const position STATUS_HP{0, 26};
const position STATUS_ATK{0, 27};
const position STATUS_DEF{0, 28};
const position STATUS_ACTION{0, 29};
const std::string BLANK(DISPLAY_BUFFER_SIZE, ' ');

size_t game::get_curr_turn() const {
        return curr_turn;
}

void game::print() {
        if (msg.length() > MAX_MSG_LENGTH) {
                msg.resize(MAX_MSG_LENGTH);
                msg += "...";
        }

        out->print_str({0, 0}, BLANK);

        levels[curr_level]->print(out);
        player->print(out);

        std::string tmp = "Race: ";
        tmp += player->get_race_name();
        tmp += " Gold: " + std::to_string(player->get_gold());
        out->print_str(STATUS_RACE, tmp);
        out->print_str(STATUS_FLOOR, "Floor " + std::to_string(curr_level + 1));
        out->print_str(STATUS_HP, "HP: " + std::to_string(player->get_HP()));
        out->print_str(STATUS_ATK, "Atk: " + std::to_string(player->get_ATK()));
        out->print_str(STATUS_DEF, "Def: " + std::to_string(player->get_DEF()));
        out->print_str(STATUS_ACTION, "Action: " + msg);
}

std::string game::str_score() const {
        if (player->get_race() == SHADE)
                return std::to_string(static_cast<int>(player->get_gold() *
                                                       SHADE_SCORE_MUL));

        return std::to_string(player->get_gold());
}
