#include <chrono>                   // for operator""s, chrono_literals
#include <iostream>                 // for cout, ostream
#include <fstream>
#include <memory>                   // for allocator, shared_ptr
#include <string>                   // for string, operator<<
#include <thread>                   // for sleep_for
#include <array>
#include <algorithm>

#include <ftxui/dom/elements.hpp>  // for hflow, paragraph, separator, hbox, vbox, filler, operator|, border, Element
#include <ftxui/dom/node.hpp>      // for Render
#include <ftxui/screen/box.hpp>    // for ftxui
#include <ftxui/component/loop.hpp>
#include <ftxui/screen/color.hpp>
#include <ftxui/dom/table.hpp>

#include <fmt/core.h>
#include "dbc.hpp"
#include "gui.hpp"
#include "rand.hpp"
#include "systems.hpp"
#include "events.hpp"
#include "render.hpp"
#include "save.hpp"
#include "constants.hpp"

using std::string;
using namespace fmt;
using namespace std::chrono_literals;
using namespace ftxui;
using namespace components;

void DeathUI::create_render() {
  has_border = true;
  $exit_button = Button("EXIT", []{ std::exit(0); });

  $render = Renderer([&] {
    return vflow({
        paragraph($quip) | border,
        $exit_button->Render()
      }) | flex;
  });

  set_renderer($render);
  add($exit_button);
}

void InventoryUI::create_render() {
  has_border = true;
  MenuOption option;
  $inventory_box = Menu(&$menu_list, &$selected, option);

  $inventory_render = Renderer([&] {
    auto &player = $world.get_the<Player>();
    auto &inventory = $world.get<Inventory>(player.entity);
    update_menu_list(inventory);

    return hbox({
        $inventory_box->Render() | frame | size(WIDTH, EQUAL, 35) | yflex_grow,
        separator() | yflex_grow,
        vflow({
          paragraph($item_text) | border
        }) | flex
    }) | flex;
  });

  set_renderer($inventory_render);
  add($inventory_box);
}

void InventoryUI::update_menu_list(Inventory& inventory) {
  $menu_list.clear();
  for(size_t i = 0; i < inventory.count(); i++) {
    auto& item = inventory.get(i);
    $menu_list.push_back(fmt::format("{} ({})",
          string(item.data["name"]),
          item.count));

    if($selected == int(i)) {
      $item_text = item.data["description"];
    }
  }
}

void StatusUI::create_render() {
  auto player = $world.get_the<Player>();

  auto status_rend = Renderer([&, player]{
    const auto& player_combat = $world.get<Combat>(player.entity);
    const auto& inventory = $world.get<Inventory>(player.entity);
    const auto& combat = $world.get<Combat>(player.entity);
    $status_text = player_combat.hp > 0 ? "NOT DEAD" : "DEAD!!!!!!";

    std::vector<Element> log_list;
    for(auto msg : $log.messages) {
      log_list.push_back(text(msg));
    }

    auto log_box = vbox(log_list) | yflex_grow | border;

    return hbox({
        hflow(
          vbox(
              text(format("HP: {: >3}  GOLD: {: >3}  DMG: {: >3}",
                  player_combat.hp,
                  inventory.gold,
                  combat.damage)) | border,
              text($status_text) | border,
              separator(),
              log_box
              )  | flex_grow
        ),
        separator(),
        hbox(),
    });
  });

  set_renderer(status_rend);
}

MapViewUI::MapViewUI(DinkyECS::World& world, LightRender& lights, Map& game_map) :
  Panel(GAME_MAP_PIXEL_POS, 0, 0, 0, true),
  $world(world), $lights(lights), $game_map(game_map)
{}

void MapViewUI::draw_map() {
  const auto& debug = $world.get_the<Debug>();
  const auto& player = $world.get_the<Player>();
  const auto& player_position = $world.get<Position>(player.entity);
  Point start = $game_map.center_camera(player_position.location, width, height);
  auto &tiles = $game_map.tiles();
  auto &paths = $game_map.paths();
  auto &lighting = $lights.lighting();

  // WARN: this is exploiting that -1 in size_t becomes largest
  size_t end_x = std::min(size_t(width), $game_map.width() - start.x);
  size_t end_y = std::min(size_t(height), $game_map.height() - start.y);

  for(size_t y = 0; y < end_y; ++y) {
    for(size_t x = 0; x < end_x; ++x)
    {
      const TileCell& tile = tiles.at(start.x+x, start.y+y);
      // light value is an integer that's a percent
      float light_value = debug.LIGHT ? 80 * PERCENT : lighting[start.y+y][start.x+x] * PERCENT;
      int dnum = debug.PATHS ? paths[start.y+y][start.x+x] : WALL_PATH_LIMIT;

      if(debug.PATHS && dnum != WALL_PATH_LIMIT) {
        string num = dnum > 15 ? "*" : format("{:x}", dnum);

        $canvas.DrawText(x * 2, y * 4, num, [dnum, tile, light_value](auto &pixel) {
            pixel.foreground_color = Color::HSV(dnum * 20, 150, 200);
            pixel.background_color = Color::HSV(30, 20, tile.bg_v * 50 * PERCENT);
            });
      } else {
        $canvas.DrawText(x * 2, y * 4, tile.display, [tile, light_value](auto &pixel) {
            pixel.foreground_color = Color::HSV(tile.fg_h, tile.fg_s, tile.fg_v * light_value);
            pixel.background_color = Color::HSV(tile.bg_h, tile.bg_s, tile.bg_v * light_value);
            });
      }
    }
  }

  System::draw_entities($world, $game_map, lighting, $canvas, start, width, height);
}

void MapViewUI::create_render() {
  set_renderer(Renderer([&] {
    draw_map();
    return canvas($canvas);
  }));
}

void MapViewUI::resize_canvas() {
  // set canvas to best size
  $canvas = Canvas(width * 2, height * 4);
}

GUI::GUI(DinkyECS::World &world, Map& game_map) :
  $world(world),
  $game_map(game_map),
  $status_ui(world),
  $lights(game_map.width(), game_map.height()),
  $map_view($world, $lights, $game_map),
  $inventory_ui(world),
  $sounds("./assets")
{
  // this needs a config file soon
  // $sounds.load("ambient", "ambient_sound.mp3");
  $sounds.load("loot_gold", "loot_gold.mp3");
  $sounds.load("combat_player_hit", "combat_player_hit.mp3");
  $sounds.load("combat_enemy_hit", "combat_enemy_hit.mp3");
  $sounds.load("combat_miss", "combat_miss.mp3");
  resize_map(MAX_FONT_SIZE);
  init_shaders();
}

void GUI::resize_map(int new_size) {
  $renderer.resize_grid(new_size, $map_view);
  $map_view.resize_canvas();
}

void GUI::save_world() {
  $status_ui.log("Game saved!");
  save::to_file("./savefile.world", $world, $game_map);
}

void GUI::create_renderer() {
  $renderer.init_terminal();

  $map_view.create_render();
  $status_ui.create_render();
  $inventory_ui.create_render();
  $death_ui.create_render();

  $active_panels = {&$map_view, &$status_ui};
}

void GUI::handle_world_events() {
  using eGUI = Events::GUI;
  while($world.has_event<eGUI>()) {
    auto [evt, entity, data] = $world.recv<eGUI>();

    switch(evt) {
      case eGUI::COMBAT: {
          auto &damage = std::any_cast<Events::Combat&>(data);
          auto enemy_combat = $world.get<Combat>(entity);

          if(damage.enemy_did > 0) {
            $status_ui.log(format("Enemy HIT YOU for {} damage!", damage.enemy_did));
            $status_ui.log(format("-- Enemy has {} HP left.", enemy_combat.hp));
            $sounds.play("combat_enemy_hit");
            shake();
          } else {
            $status_ui.log("Enemy MISSED YOU.");
          }

          if(damage.player_did > 0) {
            $status_ui.log(format("You HIT enemy for {} damage!", damage.player_did));
            $sounds.play("combat_player_hit");
          } else {
            $sounds.play("combat_miss");
            $status_ui.log("You MISSED the enemy.");
          }
      }
      break;
      case eGUI::DEATH: {
          // auto &dead_data = std::any_cast<Events::Death&>(data);
          auto player = $world.get_the<Player>();
          dbc::check(player.entity == entity, "received death event for something not the player");

          auto player_combat = $world.get<Combat>(entity);
          if(player_combat.dead) {
            toggle_modal(&$death_ui, $player_died);
          }
        }
        break;
      case eGUI::LOOT: {
          auto &item = std::any_cast<InventoryItem&>(data);
          $sounds.play("loot_gold");
          $status_ui.log(fmt::format("You picked up a {}.",
                std::string(item.data["name"])));
        }
        break;
      default:
        $status_ui.log(format("INVALID EVENT! {},{}", evt, entity));
    }
  }
}

void GUI::shutdown() {
  $renderer.close();
}

bool GUI::game_ui_events() {
  using KB = sf::Keyboard;
  auto player = $world.get_the<Player>();
  int map_font_size = $renderer.font_size();
  auto& player_motion = $world.get<Motion>(player.entity);
  bool event_happened = false;

  if(KB::isKeyPressed(KB::Left)) {
    player_motion.dx = -1;
    event_happened = true;
  } else if(KB::isKeyPressed(KB::Right)) {
    player_motion.dx = 1;
    event_happened = true;
  } else if(KB::isKeyPressed(KB::Up)) {
    player_motion.dy = -1;
    event_happened = true;
  } else if(KB::isKeyPressed(KB::Down)) {
    player_motion.dy = 1;
    event_happened = true;
  } else if(KB::isKeyPressed(KB::Equal)) {
    resize_map(map_font_size + 10);
  } else if(KB::isKeyPressed(KB::Hyphen)) {
    resize_map(map_font_size - 10);
  } else if(KB::isKeyPressed(KB::L)) {
    auto &debug = $world.get_the<Debug>();
    debug.LIGHT = !debug.LIGHT;
  } else if(KB::isKeyPressed(KB::I)) {
    toggle_modal(&$inventory_ui, $inventory_open);
  } else if(KB::isKeyPressed(KB::P)) {
    auto &debug = $world.get_the<Debug>();
    debug.PATHS = !debug.PATHS;
  } else if(KB::isKeyPressed(KB::S)) {
    save_world();
  } else if(KB::isKeyPressed(KB::Tab)) {
    $status_ui.key_press(Event::Tab);
  } else if(KB::isKeyPressed(KB::Enter)) {
    $status_ui.key_press(Event::Return);
  }

  return event_happened;
}


bool GUI::modal_ui_events() {
  using KB = sf::Keyboard;
  bool event_happened = false;

  for(Panel *panel : $active_panels) {
    if(KB::isKeyPressed(KB::Tab)) {
      event_happened = true;
      panel->key_press(Event::Tab);
    } else if(KB::isKeyPressed(KB::Enter)) {
      event_happened = true;
      panel->key_press(Event::Return);
    } else if(KB::isKeyPressed(KB::Escape)) {
      // BUG: this is dogshit, rewerite it
      if($inventory_open) {
        toggle_modal(panel, $inventory_open);
      }
    }
  }

  return event_happened;
}

bool GUI::handle_ui_events() {
  using MOUSE = sf::Mouse;
  bool event_happened = false;
  sf::Event event;
  Point pos;

  while($renderer.poll_event(event)) {
    if(event.type == sf::Event::Closed) {
      shutdown();
    } else if(event.type ==  sf::Event::KeyPressed) {
      if($modal_shown) {
        event_happened = modal_ui_events();
      } else {
        event_happened = game_ui_events();
      }
    } else {
      for(Panel *panel : $active_panels) {
        if($renderer.mouse_position(*panel, pos)) {
          if(MOUSE::isButtonPressed(MOUSE::Left)) {
            panel->mouse_click(Mouse::Button::Left, pos);
            event_happened = true;
          } else {
            panel->mouse_release(Mouse::Button::Left, pos);
          }
        }
      }
    }
  }

  return event_happened;
}

void GUI::init_shaders() {
  auto& shader = $paused.load_shader("./shaders/modal.frag");
  shader.setUniform("offsetFactor", sf::Glsl::Vec2{0.001f, 0.001f});
  shader.setUniform("darkness", 0.05f);
}

void GUI::pause_screen() {
  auto &window = $renderer.$window;
  auto size = window.getSize();

  $paused.texture.create(size.x, size.y);
  $paused.texture.update(window);
  $paused.sprite.setTexture($paused.texture);
  $paused.sprite.setPosition(0,0);
}

void GUI::draw_paused() {
  $renderer.draw_sprite($paused.sprite, &$paused.shader);
}

void GUI::run_systems() {
  auto player = $world.get_the<Player>();
  System::motion($world, $game_map);
  System::enemy_pathing($world, $game_map, player);
  System::lighting($world, $game_map, $lights);
  System::collision($world, player);
  System::death($world);
}

void GUI::shake() {
  for(int i = 0; i < 10; ++i) {
    int x = Random::uniform<int>(-10,10);
    int y = Random::uniform<int>(-10,10);
    // add x/y back to draw screen
    $renderer.draw($map_view, x, y);
    $renderer.display();
    std::this_thread::sleep_for(1ms);
  }
}

void GUI::toggle_modal(Panel *panel, bool &is_open_out) {
  if(is_open_out) {
    $active_panels = {&$map_view, &$status_ui};
    is_open_out = false;
  } else {
    pause_screen();
    $active_panels = {panel};
    is_open_out = true;
  }

  $modal_shown = is_open_out;
}

void GUI::render_scene() {
  $renderer.clear();

  if($inventory_open) {
    draw_paused();
    $inventory_ui.render();
    $renderer.draw($inventory_ui);
  } else if($player_died) {
    draw_paused();
    $death_ui.render();
    $renderer.draw($death_ui);
  } else {
    $map_view.render();
    $renderer.draw($map_view);

    $status_ui.render();
    $renderer.draw($status_ui);
  }

  $renderer.display();
}

int GUI::main(bool run_once) {
  $world.set_the<Debug>({});
  create_renderer();
  run_systems();

  do {
    render_scene();

    if(handle_ui_events()) {
      run_systems();
      handle_world_events();
    }

    std::this_thread::sleep_for(10ms);
  } while(!run_once && $renderer.is_open());

  return 0;
}
