#include "guecs/sfml/backend.hpp"
#include "guecs/sfml/components.hpp"
#include "guecs/sfml/sound.hpp"
#include "guecs/ui.hpp"
#include "guecs/theme.hpp"
#include <fmt/xchar.h>
#include <deque>
#include <iostream>
#include <unordered_map>
#include "dbc.hpp"

constexpr const int SCREEN_WIDTH=1280;
constexpr const int SCREEN_HEIGHT=720;
constexpr const int FRAME_LIMIT=60;
constexpr const bool VSYNC=true;

using std::string, std::wstring;

enum class Event {
  CLICKER, GIVE_TREAT, GIVE_FOOD, GIVE_WATER, GIVE_PETS, WORK
};

struct Shake {
  float scale_factor = 0.05f;
  int frames = 10;
  float ease_rate = 0.1f;
  bool playing = false;
  int current = 0;
  float x=0.0;
  float y=0.0;
  float w=0.0;
  float h=0.0;
  sf::Vector2f initial_scale;

  float ease() {
    float tick = float(frames) / float(current) * ease_rate;
    return (std::sin(tick) + 1.0) / 2.0;
  }

  void init(lel::Cell& cell) {
    x = cell.x;
    y = cell.y;
    w = cell.w;
    h = cell.h;
  }

  void play(guecs::Sprite& sprite) {
    if(!playing) {
      playing = true;
      current = 0;
      initial_scale = sprite.sprite->getScale();
    }
  }

  void render(guecs::Sprite& sprite) {
    current++;

    if(playing && current < frames) {
      float tick = ease();
      sf::Vector2f scale{
        std::lerp(initial_scale.x, initial_scale.x + scale_factor, tick),
          std::lerp(initial_scale.y, initial_scale.y + scale_factor, tick)};

      sprite.sprite->setScale(scale);
    } else {
      playing = false;
      current = 0;
      sprite.sprite->setScale(initial_scale);
    }
  }
};

struct GameState {
  bool at_work = false;
};

static GameState GAME;
std::unordered_map<std::string, long> STATS {
  {"Food", 0L},
  {"Water", 0L},
  {"Money", 100L},
  {"Mood", 0L}
};

std::wstring make_stat_label(const std::string& name) {
  return fmt::format(L"{}:\n{}", guecs::to_wstring(name),
      STATS.at(name));
}

std::unordered_map<std::string, Event> EVENTS {
  {"GiveTreat", Event::GIVE_TREAT},
  {"GiveFood", Event::GIVE_FOOD},
  {"GiveWater", Event::GIVE_WATER},
  {"GivePets", Event::GIVE_PETS},
  {"Work", Event::WORK}
};


struct ClickerUI {
  guecs::UI $gui;
  guecs::Entity $clicker;

  ClickerUI() {
    $gui.position(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
    $gui.layout(
        "[Food|_|*%(300,400)clicker|_|_|_]"
        "[Water|_|_               |_|_|_]"
        "[Mood|_|_               |_|_|_]"
        "[Money|_|_               |_|_|_]"
        "[GiveTreat|GiveFood|GiveWater|GivePets|Work]");
  }

  void init() {
    $gui.set<guecs::Background>($gui.MAIN, {$gui.$parser, {0, 0, 0, 255}});

    for(auto& [name, cell] : $gui.cells()) {
      auto id = $gui.entity(name);
      if(name != "clicker") {
        $gui.set<guecs::Rectangle>(id, {});
        if(STATS.contains(name)) {
          $gui.set<guecs::Textual>(id, { make_stat_label(name) });
        } else {
          dbc::check(EVENTS.contains(name), fmt::format("INVALID EVENT {}, not in EVENTS map", name));

          $gui.set<guecs::Effect>(id, {});
          $gui.set<guecs::Clickable>(id, {
              [&](auto, auto) { handle_button(EVENTS.at(name)); }
          });
          $gui.set<guecs::Label>(id, { guecs::to_wstring(name) });
        }
      }
    }

    $clicker = $gui.entity("clicker");
    $gui.set<guecs::Sprite>($clicker, {"clicker_the_dog"});
    $gui.set<guecs::Sound>($clicker, {"clicker_bark"});
    $gui.set<guecs::Clickable>($clicker, {
        [&](auto, auto) { handle_button(Event::CLICKER); }
        });

    // custom components need to be initialized manually
    $gui.set_init<Shake>($clicker, {});

    $gui.init();
  }

  void render(sf::RenderWindow& window) {
    auto& shaker = $gui.get<Shake>($clicker);

    if(shaker.playing) {
      auto& sprite = $gui.get<guecs::Sprite>($clicker);
      shaker.render(sprite);
      window.clear();
    }

    $gui.render(window);

    // $gui.debug_layout(window);
  }

  void mouse(float x, float y, bool hover) {
    $gui.mouse(x, y, hover);
  }

  void update_stats() {
    for(auto& [name, stat] : STATS) {
      auto gui_id = $gui.entity(name);
      auto& text = $gui.get<guecs::Textual>(gui_id);
      text.update(make_stat_label(name));
    }
  }

  void handle_button(Event ev) {
    bool is_happy = false;
    auto& shaker = $gui.get<Shake>($clicker);
    auto& sprite = $gui.get<guecs::Sprite>($clicker);

    using enum Event;
    switch(ev) {
      case CLICKER: // fallthrough
      case GIVE_PETS:
          STATS["Mood"]++;
          is_happy = true;
          break;
      case GIVE_TREAT:
          STATS["Food"]++;
          STATS["Mood"]++;
          STATS["Money"]--;
          is_happy = true;
          break;
      case GIVE_FOOD:
          STATS["Food"]++;
          STATS["Money"]--;
          is_happy = true;
          break;
      case GIVE_WATER:
          STATS["Water"]++;
          STATS["Money"]--;
          is_happy = true;
          break;
      case WORK:
          GAME.at_work = true;
          break;
      default:
          assert(false && "invalid event");
    }


    update_stats();
    if(is_happy) shaker.play(sprite);
  }
};


struct WorkComputerUI {
  guecs::UI $gui;

  WorkComputerUI() {
    $gui.position(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
    $gui.layout(
        "[*%(100,200)clicker|_ |_|_|_               |*%(100,200)postit1]"
        "[_                 |_ |*%(300,300)computer|_|_|_]"
        "[_                 |_|_                  |_|_|*%(100,200)postit2]"
        "[*%(100,200)coffee |*%(100,200)floppy|_  |_|_|_]"
        "[_                 |_                |_|_|_|_]");
  }

  void init() {
    guecs::Background bg{$gui.$parser};
    bg.set_sprite("work_computer");
    $gui.set<guecs::Background>($gui.MAIN, bg);

    for(auto& [name, cell] : $gui.cells()) {
      auto gui_id = $gui.entity(name);

      if(name == "clicker") {
        $gui.set<guecs::Clickable>(gui_id, {
            [&](auto,auto) { GAME.at_work = false; }
        });
      } else if(name == "computer") {
        $gui.set<guecs::Clickable>(gui_id, {
            [&](auto,auto) {
              auto text_id = $gui.entity("postit1");
              auto& text = $gui.get<guecs::Textual>(text_id);
              STATS["Money"]++;
              text.update(make_stat_label("Money"));
            }
        });
      } else {
        $gui.set<guecs::Clickable>(gui_id, {
            [&](auto,auto) { fmt::println("CLICK ON {}", name); }
        });
      }

      if(name == "postit1") {
        $gui.set<guecs::Textual>(gui_id, {
           make_stat_label("Money"), 50, guecs::THEME.BLACK, 25
        });
      }
    }

    $gui.init();
  }

  void render(sf::RenderWindow& window) {
    $gui.render(window);
    // $gui.debug_layout(window);
  }

  void mouse(float x, float y, bool hover) {
    $gui.mouse(x, y, hover);
  }
};


int main() {
  sfml::Backend backend;
  sound::mute(true);
  guecs::init(&backend);

  sf::RenderWindow window(sf::VideoMode({SCREEN_WIDTH, SCREEN_HEIGHT}), "Clicker the Dog");
  window.setFramerateLimit(FRAME_LIMIT);
  window.setVerticalSyncEnabled(VSYNC);

  ClickerUI clicker;
  clicker.init();

  WorkComputerUI computer;
  computer.init();

  while(window.isOpen()) {
    while (const auto event = window.pollEvent()) {
      if(event->is<sf::Event::Closed>()) {
        window.close();
      }

      if(const auto* mouse = event->getIf<sf::Event::MouseButtonPressed>()) {
        if(mouse->button == sf::Mouse::Button::Left) {
          sf::Vector2f pos = window.mapPixelToCoords(mouse->position);
          if(GAME.at_work) {
            computer.mouse(pos.x, pos.y, false);
          } else {
            clicker.mouse(pos.x, pos.y, false);
          }
        }
      }
    }

    if(GAME.at_work) {
      window.clear();
      computer.render(window);
    } else {
      clicker.render(window);
    }
    window.display();
  }
}
