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

namespace gui {
  using namespace components;

  FSM::FSM() :
    $window(sf::VideoMode({SCREEN_WIDTH, SCREEN_HEIGHT}), "Zed's Raycaster Thing"),
    $debug_ui($levels),
    $main_ui($window),
    $level($levels.current()),
    $combat_ui($level),
    $status_ui($level),
    $map_ui($level),
    $mini_map($level),
    $loot_ui($level),
    $font{FONT_FILE_NAME},
    $dnd_loot($status_ui, $loot_ui, $window, $router)
    {
      $window.setPosition({0,0});
    }

  void FSM::event(Event ev, std::any data) {
    switch($state) {
      FSM_STATE(State, START, ev);
      FSM_STATE(State, MOVING, ev);
      FSM_STATE(State, ATTACKING, ev);
      FSM_STATE(State, ROTATING, ev);
      FSM_STATE(State, IDLE, ev, data);
      FSM_STATE(State, IN_COMBAT, ev);
      FSM_STATE(State, COMBAT_ROTATE, ev);
      FSM_STATE(State, NEXT_LEVEL, ev);
      FSM_STATE(State, END, ev);
      FSM_STATE(State, LOOTING, ev, data);
    }
  }

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

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

    $combat_ui.init();
    $status_ui.init();

    $boss_fight_ui = $levels.create_bossfight($level.world);
    $boss_fight_ui->init();

    $map_ui.init();
    $map_ui.log(L"Welcome to the game!");
    $mini_map.init($main_ui.$overlay_ui.$gui);

    run_systems();

    state(State::IDLE);
  }

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

  void FSM::ATTACKING(Event ev) {
    using enum Event;
    switch(ev) {
      case TICK: {
        System::combat($level, $temp_attack_id);
        run_systems();
        state(State::IN_COMBAT);
       } break;
      case STOP_COMBAT:
        state(State::IDLE);
        break;
      case ATTACK:
        // ignore these since they're just from SFML not having discrete events
        break;
      default:
        dbc::log(fmt::format("In ATTACKING state, unhandled event {}", (int)ev));
        state(State::IDLE);
    }
  }

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

  void FSM::COMBAT_ROTATE(Event) {
    if(auto aim = $main_ui.play_rotate()) {
      auto& player_pos = System::player_position($level);
      player_pos.aiming_at = *aim;
      state(State::IN_COMBAT);
    }
  }

  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(false);
        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);
        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, 0.25f);
        state(State::ROTATING);
        break;
      case ROTATE_RIGHT:
        $main_ui.plan_rotate(1, 0.25f);
        state(State::ROTATING);
        break;
      case MAP_OPEN:
        $map_open = !$map_open;
        break;
      case ATTACK:
        state(State::ATTACKING);
        break;
      case START_COMBAT:
        $map_open = false;
        state(State::IN_COMBAT);
        break;
      case CLOSE:
        dbc::log("Nothing to close.");
        break;
      case STAIRS_DOWN:
        sound::stop("ambient");
        state(State::NEXT_LEVEL);
        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 MOUSE_CLICK:
        mouse_action(false);
        break;
      case MOUSE_MOVE:
        mouse_action(true);
        break;
      case AIM_CLICK: {
          auto aimed_at = $main_ui.camera_aim();

          if(aimed_at) {
            // this will then send LOOT_ITEM if it's valid
            System::pickup($level, aimed_at);
          }
        } break;
      default:
        break; // ignore everything else
    }
  }

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

    switch(ev) {
      case STAIRS_DOWN:
        sound::play("ambient");
        next_level();
        state(State::IDLE);
        break;
      case MOUSE_CLICK: {
        sf::Vector2f pos = mouse_position();
        $boss_fight_ui->mouse(pos.x, pos.y, false);

        if($boss_fight_ui->boss_dead()) {
          event(Event::STAIRS_DOWN);
        }
      } break;
      default:
        break; // do nothing for now
    }
  }

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

    switch(ev) {
      case MOUSE_CLICK:
        mouse_action(false);
        break;
      case MOUSE_MOVE:
        mouse_action(true);
        break;
      case ATTACK:
        $main_ui.dirty();
        sound::play("Sword_Hit_1");
        state(State::ATTACKING);
        break;
      case ROTATE_LEFT:
        $main_ui.plan_rotate(-1, DEFAULT_ROTATE);
        state(State::COMBAT_ROTATE);
        break;
      case ROTATE_RIGHT:
        $main_ui.plan_rotate(1, DEFAULT_ROTATE);
        state(State::COMBAT_ROTATE);
        break;
      case STOP_COMBAT:
        $main_ui.$overlay_ui.close_sprite("top_right");
        state(State::IDLE);
        break;
      case QUIT:
        $window.close();
        state(State::END);
        return;
      default:
        break;
    }
  }

  void FSM::try_move(int dir, bool strafe) {
    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(Event ev) {
    dbc::log(fmt::format("END: received event after done: {}", int(ev)));
  }

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

  void FSM::mouse_action(bool hover) {
    sf::Vector2f pos = mouse_position();
    if($debug_ui.active) $debug_ui.mouse(pos.x, pos.y, hover);
    $combat_ui.mouse(pos.x, pos.y, hover);
    $status_ui.mouse(pos.x, pos.y, hover);

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

  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::O:
            autowalking = true;
            break;
          case KEY::L:
            // This will go away as soon as containers work
            $loot_ui.set_target($loot_ui.$temp_loot);
            $loot_ui.update();
            event(Event::LOOT_OPEN);
            break;
          case KEY::X:
            event(Event::STAIRS_DOWN);
            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(in_state(State::NEXT_LEVEL)) {
      $boss_fight_ui->render($window);
    } else {
      if($debug_ui.active) {
        debug_render();
      } else {
        $main_ui.render();
      }

      $status_ui.render($window);
      $combat_ui.render($window);
      if($loot_ui.active) $loot_ui.render($window);

      if(in_state(State::LOOTING)) $dnd_loot.render();

      if($map_open) {
        $map_ui.render($window, $main_ui.$compass_dir);
      } else {
        // $mini_map.render($window, $main_ui.$compass_dir);
      }
    }
  }

  void FSM::render() {
    if(in_state(State::NEXT_LEVEL)) {
      $window.clear();
      $boss_fight_ui->render($window);
    } else {
      draw_gui();
    }

    $window.display();
  }

  void FSM::run_systems() {
    System::generate_paths($level);
    System::enemy_ai_initialize($level);
    System::enemy_pathing($level);
    System::collision($level);
    System::motion($level);
    System::lighting($level);
    System::death($level);
  }

  bool FSM::active() {
    return !in_state(State::END);
  }

  void FSM::handle_world_events() {
    using eGUI = Events::GUI;
    auto& world = *$level.world;

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

      switch(evt) {
        case eGUI::COMBAT: {
            auto &damage = std::any_cast<Events::Combat&>(data);

            if(damage.enemy_did > 0) {
              $map_ui.log(fmt::format(L"Enemy HIT YOU for {} damage!", damage.enemy_did));
            } else {
              $map_ui.log(L"Enemy MISSED YOU.");
            }

            if(damage.player_did > 0) {
              $map_ui.log(fmt::format(L"You HIT enemy for {} damage!", damage.player_did));
            } else {
              $map_ui.log(L"You MISSED the enemy.");
            }
          }
          break;
        case eGUI::COMBAT_START:
          event(Event::START_COMBAT);
          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::STOP_COMBAT);
          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:
          event(Event::INV_SELECT, 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: {
          dbc::log("YEP container works.");
          $loot_ui.set_target($loot_ui.$temp_loot);
          $loot_ui.update();
          event(Event::LOOT_OPEN);
         } break;
        case eGUI::HP_STATUS:
            System::player_status($level);
            break;
        case eGUI::NEW_RITUAL:
            $combat_ui.init();
            break;
        case eGUI::ATTACK:
          $temp_attack_id = std::any_cast<int>(data);
          event(Event::ATTACK);
          break;
        case eGUI::STAIRS_DOWN:
          event(Event::STAIRS_DOWN);
          break;
        case eGUI::DEATH: {
            $status_ui.update();
            if(entity != player.entity) {
              $main_ui.dead_entity(entity);
            }
          } break;
        case eGUI::NOOP: {
            if(data.type() == typeid(std::string)) {
              auto name = std::any_cast<std::string>(data);
              $map_ui.log(fmt::format(L"NOOP EVENT! {},{}", evt, entity));
            }
          } break;
        default:
          $map_ui.log(fmt::format(L"INVALID EVENT! {},{}", evt, entity));
      }
    }
  }

  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() {
    $levels.create_level($level.world);
    $level = $levels.next();

    $status_ui.update_level($level);
    $map_ui.update_level($level);
    $mini_map.update_level($level);
    $combat_ui.update_level($level);
    $main_ui.update_level($level);
    $loot_ui.update_level($level);

    $boss_fight_ui = $levels.create_bossfight($level.world);
    $boss_fight_ui->init();

    run_systems();
  }
}
