#include "gui/fsm.hpp"
#include <iostream>
#include <chrono>
#include <numeric>
#include <functional>
#include "game/components.hpp"
#include <numbers>
#include "game/systems.hpp"
#include "events.hpp"
#include "game/sound.hpp"
#include "graphics/shaders.hpp"
#include <fmt/xchar.h>
#include "gui/guecstra.hpp"
#include "game/level.hpp"

namespace gui {
  using namespace components;
  using game::Event;

  FSM::FSM(sf::RenderWindow& window) :
    $window(window),
    $main_ui($window),
    $dnd_loot($status_ui, $loot_ui, $window, $router)
    {
      System::init($systems);
    }

  void FSM::event(Event ev, std::any data) {
    switch($state) {
      FSM_STATE(State, START, ev);
      FSM_STATE(State, INTRO, ev);
      FSM_STATE(State, START_SCENE, ev);
      FSM_STATE(State, DEATH_SCENE, ev);
      FSM_STATE(State, WIN_SCENE, ev);
      FSM_STATE(State, NEXT_LEVEL_SCENE, ev);
      FSM_STATE(State, MOVING, ev);
      FSM_STATE(State, ATTACKING, ev, data);
      FSM_STATE(State, ROTATING, ev);
      FSM_STATE(State, IDLE, ev, data);
      FSM_STATE(State, LOOTING, ev, data);
      FSM_STATE(State, END_QUIT, ev);
      FSM_STATE(State, END_PLAY_AGAIN, ev);
      FSM_STATE(State, END_PLAYER_DIED, ev);
    }
  }

  void FSM::START(Event ) {
    $main_ui.update_level();
    $main_ui.init();
    $loot_ui.init();

    for(auto& [name, scene] : $scenes) {
      scene->init();
    }

    // BUG: maybe this is a function on main_ui?
    auto cell = $main_ui.overlay_cell("left");
    $debug_ui.init(cell);
    $status_ui.init();
    run_systems();

    show_scene("STARTING");
    state(State::START_SCENE);
  }

  void FSM::INTRO(Event ev) {
    dbc::check($story != nullptr, "you forgot the stroy");

    if($story->playing()) {
      if(ev == game::Event::MOUSE_CLICK) {
        $story->stop();
      }
    } else {
      $story = nullptr;
      state(State::IDLE);
    }
  }

  void FSM::START_SCENE(Event ev) {
    using enum Event;

    switch(ev) {
      case MOUSE_CLICK:
        mouse_action(guecs::NO_MODS);
        break;
      case MOUSE_MOVE: {
          mouse_action({1 << guecs::ModBit::hover});
       } break;
      case START:
        close_scene();
        $story = std::make_shared<storyboard::UI>("intro_story");
        $story->init();
        state(State::INTRO);
        break;
      case QUIT:
        FSM::IDLE(ev, {});
        break;
      case TICK:
        break;
      default:
        dbc::log($F("unhandled event: {}", int(ev)));
        break;
    }
  }

  void FSM::DEATH_SCENE(Event ev) {
    using enum Event;

    switch(ev) {
      case MOUSE_CLICK:
        mouse_action(guecs::NO_MODS);
        break;
      case MOUSE_MOVE: {
          mouse_action({1 << guecs::ModBit::hover});
       } break;
      case START:
        close_scene();
        state(State::END_PLAYER_DIED);
        break;
      case QUIT:
        FSM::IDLE(ev, {});
        break;
      default:
        break;
    }
  }

  void FSM::WIN_SCENE(Event ev) {
    using enum Event;

    switch(ev) {
      case MOUSE_CLICK:
        mouse_action(guecs::NO_MODS);
        break;
      case MOUSE_MOVE: {
          mouse_action({1 << guecs::ModBit::hover});
       } break;
      case START:
        close_scene();
        state(State::END_PLAY_AGAIN);
        break;
      case QUIT:
        FSM::IDLE(ev, {});
        break;
      default:
        break;
    }
  }

  void FSM::NEXT_LEVEL_SCENE(Event ev) {
    using enum Event;

    switch(ev) {
      case MOUSE_CLICK:
        mouse_action(guecs::NO_MODS);
        break;
      case MOUSE_MOVE: {
          mouse_action({1 << guecs::ModBit::hover});
       } break;
      case START:
        next_level();
        close_scene();
        state(State::IDLE);
        break;
      case QUIT:
        FSM::IDLE(ev, {});
        break;
      default:
        break;
    }
  }

  void FSM::MOVING(Event ) {
    // this should be an optional that returns a point
    if(auto move_to = $main_ui.play_move()) {
      $systems.runMoving(*move_to);
      run_systems();
      $main_ui.dirty();
      state(State::IDLE);
    }
  }

  void FSM::ATTACKING(Event ev, std::any data) {
    using enum Event;
    switch(ev) {
      case TICK: {
        dbc::log("!!!!!! FIX System::combat(0) doesn't use any weapons, only first");
        $systems.runCombat(0);
        run_systems();
        state(State::IDLE);
       } break;
      case ATTACK: {
        int attack_id = std::any_cast<int>(data);
        $systems.runCombat(attack_id);
        run_systems();
       } break;
      default:
        dbc::log($F("In ATTACKING state, unhandled event {}", (int)ev));
        state(State::IDLE);
    }
  }

  void FSM::ROTATING(Event) {
    if(auto aim = $main_ui.play_rotate()) {
      auto& player_pos = GameDB::player_position();
      player_pos.aiming_at = *aim;
      state(State::IDLE);
    }
  }

  void FSM::LOOTING(Event ev, std::any data) {
    using enum Event;

    switch(ev) {
      case MOUSE_DRAG_START:
      case MOUSE_CLICK:
      case MOUSE_DROP:
        mouse_action(guecs::NO_MODS);
        break;
      default:
        if(!$dnd_loot.event(ev, data)) {
          state(State::IDLE);
        }
    }
  }

  void FSM::IDLE(Event ev, std::any data) {
    using enum Event;

    sound::stop("walk");

    switch(ev) {
      case QUIT:
        $window.close();
        state(State::END_QUIT);
        return; // done
      case MOVE_FORWARD:
        try_move(1, false);
        break;
      case MOVE_BACK:
        try_move(-1, false);
        break;
      case MOVE_LEFT:
        try_move(-1, true);
        break;
      case MOVE_RIGHT:
        try_move(1, true);
        break;
      case ROTATE_LEFT:
        $main_ui.plan_rotate(-1, DEFAULT_ROTATE);
        state(State::ROTATING);
        break;
      case ROTATE_RIGHT:
        $main_ui.plan_rotate(1, DEFAULT_ROTATE);
        state(State::ROTATING);
        break;
      case ATTACK:
        $main_ui.play_hands();
        $main_ui.dirty();
        sound::play("Sword_Hit_1");
        $status_ui.update();
        state(State::ATTACKING);
        break;
      case CLOSE:
        dbc::log("Nothing to close.");
        break;
      case LOOT_ITEM:
        $dnd_loot.event(Event::LOOT_ITEM);
        state(State::LOOTING);
        break;
      case LOOT_OPEN:
        $dnd_loot.event(Event::LOOT_OPEN);
        state(State::LOOTING);
        break;
      case INV_SELECT:
        $dnd_loot.event(Event::INV_SELECT, data);
        state(State::LOOTING);
        break;
      case USE_ITEM: {
        auto gui_id = std::any_cast<guecs::Entity>(data);
        auto& slot_name = $status_ui.$gui.name_for(gui_id);
        $systems.runUseItem(slot_name);
        $status_ui.update();
       } break;
      case MOUSE_CLICK:
        mouse_action(guecs::NO_MODS);
        break;
      case MOUSE_MOVE: {
          mouse_action({1 << guecs::ModBit::hover});
       } break;
      case AIM_CLICK:
        $systems.runPickup();
        break;
      case COMBAT_START:
        fmt::println("!!!!!!!!!!!!!! COMBAT START in IDLE");
        break;
      case COMBAT_STOP:
        fmt::println("!!!!!!!!!!!!!! COMBAT STOP in IDLE");
        break;
      default:
        break; // ignore everything else
    }
  }

  void FSM::try_move(int dir, bool strafe) {
    auto& level = GameDB::current_level();
    using enum State;
    // prevent moving into occupied space
    Point move_to = $main_ui.plan_move(dir, strafe);

    if(level.map->can_move(move_to) && !level.collision->occupied(move_to)) {
      sound::play("walk");
      state(MOVING);
    } else {
      state(IDLE);
      $main_ui.abort_plan();
    }
  }

  void FSM::END_QUIT(Event ev) {
    dbc::log($F("END_QUIT: received event after done: {}", int(ev)));
  }

  void FSM::END_PLAYER_DIED(Event ev) {
    dbc::log($F("END_PLAYER_DIED: received event after done: {}", int(ev)));
  }

  void FSM::END_PLAY_AGAIN(Event ev) {
    dbc::log($F("END_PLAY_AGAIN: received event after done: {}", int(ev)));
  }

  sf::Vector2f FSM::mouse_position() {
    return $window.mapPixelToCoords($router.position);
  }

  void FSM::mouse_action(guecs::Modifiers mods) {
    sf::Vector2f pos = mouse_position();

    if($cur_scene != nullptr) {
      $cur_scene->mouse(pos.x, pos.y, mods);
      return;
    }

    if($debug_ui.active) $debug_ui.mouse(pos.x, pos.y, mods);
    $status_ui.mouse(pos.x, pos.y, mods);

    if($loot_ui.active) {
      $loot_ui.mouse(pos.x, pos.y, mods);
    } else {
      $main_ui.mouse(pos.x, pos.y, mods);
    }
  }

  void FSM::handle_keyboard_mouse() {
    while(const auto ev = $window.pollEvent()) {
      auto gui_ev = $router.process_event(ev);

      if(gui_ev == Event::KEY_PRESS) {
        using KEY = sf::Keyboard::Scan;

        switch($router.scancode) {
          case KEY::W:
            event(Event::MOVE_FORWARD);
            break;
          case KEY::S:
            event(Event::MOVE_BACK);
            break;
          case KEY::Q:
            event(Event::ROTATE_LEFT);
            break;
          case KEY::E:
            event(Event::ROTATE_RIGHT);
            break;
          case KEY::D:
            event(Event::MOVE_RIGHT);
            break;
          case KEY::A:
            event(Event::MOVE_LEFT);
            break;
          case KEY::R:
            dbc::log("HEY! DIPSHIT! You need to move debug ui so you can rest stats.");
            break;
          case KEY::M:
            event(Event::MAP_OPEN);
            break;
          case KEY::Escape:
            event(Event::CLOSE);
            break;
          case KEY::Space:
            event(Event::ATTACK);
            break;
          case KEY::P:
            sound::mute(false);
            if(!sound::playing("ambient_1")) sound::play("ambient_1", true);
            $debug_ui.debug();
            shaders::reload();
            break;
          case KEY::X:
            show_scene("NEXT_LEVEL");
            state(State::NEXT_LEVEL_SCENE);
            break;
          case KEY::Z:
            $main_ui.toggle_mind_reading();
            break;
          case KEY::K:
            player_died();
            break;
          case KEY::N:
            player_won();
            break;
          case KEY::F5:
            take_screenshot();
            break;
          default:
            break; // ignored
        }
      } else {
        event(gui_ev);
      }
    }
  }

  void FSM::debug_render() {
    auto start = $debug_ui.time_start();
    $main_ui.render();
    $debug_ui.sample_time(start);
    $debug_ui.render($window);
  }

  void FSM::draw_gui() {
    if($cur_scene != nullptr) {
      $cur_scene->render($window);
    } else if($story != nullptr) {
      $story->render($window);
    } else {
      $main_ui.render();
      $status_ui.render($window);
      if($loot_ui.active) $loot_ui.render($window);
      if(in_state(State::LOOTING)) $dnd_loot.render();

      if($debug_ui.active) {
        debug_render();
      }
    }
  }

  void FSM::update() {
    if($cur_scene != nullptr) {
      $cur_scene->update();
    } else if($story != nullptr) {
      $story->update();
    }
  }

  void FSM::render() {
    // this clears any attack animations, like fire
    $systems.runRender();
    // BUG: this is the render for this class, and where I add an update
    draw_gui();

    $window.display();
  }

  void FSM::run_systems() {
    $systems.runUpdate();
  }

  bool FSM::active() {
    return !(in_state(State::END_QUIT)
        || in_state(State::END_PLAY_AGAIN)
        || in_state(State::END_PLAYER_DIED));
  }

 void FSM::handle_world_events() {
    using eGUI = game::Event;
    auto world = GameDB::current_world();

    while(world->has_event<eGUI>()) {
      auto [evt, entity, data] = world->recv<eGUI>();
      auto player = world->get_the<Player>();

      // HERE: this has to go, unify these events and just use them in the state machine directly

      switch(evt) {
        case eGUI::COMBAT: {
            auto &damage = std::any_cast<components::CombatResult&>(data);
          }
          break;
        case eGUI::COMBAT_START:
          event(Event::COMBAT_START);
          break;
        case eGUI::ENTITY_SPAWN: {
          auto& sprite = world->get<components::Sprite>(entity);
          $main_ui.$rayview->update_sprite(entity, sprite);
          $main_ui.dirty();
          run_systems();
         } break;
        case eGUI::NO_NEIGHBORS:
          event(Event::COMBAT_STOP);
          break;
        case eGUI::LOOT_CLOSE:
          // BUG: need to resolve GUI events vs. FSM events better
          event(Event::LOOT_OPEN);
          break;
        case eGUI::LOOT_SELECT:
          event(Event::LOOT_SELECT, data);
          break;
        case eGUI::INV_SELECT: {
           if($router.left_button) {
             event(Event::INV_SELECT, data);
           } else {
             event(Event::USE_ITEM, data);
           }
         } break;
        case eGUI::AIM_CLICK:
          event(Event::AIM_CLICK);
          break;
        case eGUI::LOOT_ITEM: {
          dbc::check(world->has<components::InventoryItem>(entity),
              "INVALID LOOT_ITEM, that entity has no InventoryItem");
          $loot_ui.add_loose_item(entity);
          event(Event::LOOT_ITEM);
         } break;
        case eGUI::LOOT_CONTAINER: {
          $loot_ui.set_target($loot_ui.$temp_loot);
          $loot_ui.update();
          event(Event::LOOT_OPEN);
         } break;
        case eGUI::ATTACK:
          event(Event::ATTACK, data);
          break;
        case eGUI::STAIRS_DOWN:
          show_scene("NEXT_LEVEL");
          state(State::NEXT_LEVEL_SCENE);
          break;
        case eGUI::START:
          event(Event::START, data);
          break;
        case eGUI::DEATH: {
            $status_ui.update();

            if(entity != player.entity) {
              $main_ui.dead_entity(entity);
            } else {
              player_died();
            }
          } break;
        case eGUI::NOOP: {
            if(data.type() == typeid(std::string)) {
              auto name = std::any_cast<std::string>(data);
            }
          } break;
        default:
          dbc::log($F("Unhandled event: evt={}; enemy={}; data={}",
                evt, entity, data.type().name()));
          event(game::Event(evt), data);
      }
    }
  }

  void FSM::take_screenshot() {
    auto size = $window.getSize();
    sf::Texture shot{size};
    shot.update($window);
    sf::Image out_img = shot.copyToImage();

    bool worked = out_img.saveToFile("./screenshot.png");
    dbc::check(worked, "Failed to write screenshot.png");
  }

  void FSM::next_level() {
    GameDB::create_level();
    $status_ui.update_level();
    $main_ui.update_level();
    $loot_ui.update_level();

    run_systems();
  }

  void FSM::show_scene(const std::string& name) {
    dbc::check($scenes.contains(name), $F("$scenes does not contain {}", name));
    $cur_scene = $scenes[name];
  }

  void FSM::close_scene() {
    $cur_scene = nullptr;
  }

  void FSM::player_died() {
    show_scene("DEATH");
    state(State::DEATH_SCENE);
  }

  void FSM::player_won() {
    show_scene("WIN");
    state(State::WIN_SCENE);
  }
}
