#include "graphics/scene.hpp"
#include "graphics/animation.hpp"
#include "graphics/shaders.hpp"
#include <fmt/core.h>
#include "dbc.hpp"

const bool DEBUG=false;

namespace scene {
   Element Engine::config_scene_element(nlohmann::json& config, bool duped) {
      std::string sprite_name = config["sprite"];
      auto st = textures::get_sprite(sprite_name, duped);

      float scale_x = config["scale_x"];
      float scale_y = config["scale_y"];
      float x = config["x"];
      float y = config["y"];
      bool flipped = config["flipped"];

      // BUG: put the .json file to load as a default/optional arg
      auto anim = animation::maybe_load("./assets/animation.json", sprite_name);

      if(anim) {
        // only start it if there's an animation set
        anim->play();
        anim->transform.flipped = flipped;
      }

      std::string cell = config["cell"];
      std::string name = config["name"];

      bool at_mid = config["at_mid"];

      sf::Text text(*$ui.$font, "", 60);

      return {name, st, anim, cell, {scale_x, scale_y}, {x, y}, at_mid, flipped, nullptr, text};
  }

  Engine::Engine(components::AnimatedScene& scene) :
    $scene(scene)
  {
    for(auto& config : $scene.actors) {
      auto element = config_scene_element(config, false);
      dbc::check(!$actor_name_ids.contains(element.name),
          $F("actors key {} already exists", element.name));
      $actors.push_back(element);
      $actor_name_ids.try_emplace(element.name, $actors.size() - 1);
    }

    for(auto& fixture : $scene.fixtures) {
      auto element = config_scene_element(fixture, true);
      $fixtures.push_back(element);
    }

    for(auto& line : $scene.layout) {
      $layout.append(line);
    }
  }

  void Engine::init() {
    dbc::log($F("initialized scene with background {}", $scene.background));

    $ui.position(SCENE_VIEW_X, SCENE_VIEW_Y, SCENE_VIEW_WIDTH, SCENE_VIEW_HEIGHT);
    $ui.set<guecs::Background>($ui.MAIN, {$ui.$parser, guecs::THEME.TRANSPARENT});
    auto& background = $ui.get<guecs::Background>($ui.MAIN);
    background.set_sprite($scene.background, true);

    $ui.layout($layout);

    for(auto& actor : $actors) {
      actor.pos = position_sprite(actor.st, actor.cell,
          actor.scale, actor.at_mid, actor.pos.x, actor.pos.y);
    }

    for(auto& fixture : $fixtures) {
      fixture.pos = position_sprite(fixture.st, fixture.cell,
          fixture.scale, fixture.at_mid, fixture.pos.x, fixture.pos.y);
    }
  }

  void Engine::apply_effect(const std::string& actor, const std::string& shader) {
    auto& element = actor_config(actor);
    element.effect = shaders::get(shader);
  }

  void Engine::attach_text(const std::string& actor, const std::string& text) {
    auto& element = actor_config(actor);
    element.text.setPosition(element.pos);
    element.text.setScale(element.scale);
    element.text.setFillColor(sf::Color::Red);
    element.text.setOutlineThickness(2.0f);
    element.text.setOutlineColor(sf::Color::Black);
    element.text.setString(text);
  }

  bool Engine::mouse(float x, float y, guecs::Modifiers mods) {
    return $ui.mouse(x, y, mods);
  }

  void Engine::render(sf::RenderTexture& view) {
    $ui.render(view);

    for(auto& fixture : $fixtures) {
      view.draw(*fixture.st.sprite, fixture.effect.get());
    }

    for(auto& actor : $actors) {
      view.draw(*actor.st.sprite, actor.effect.get());
      if(actor.anim && actor.anim->playing) view.draw(actor.text);
    }

    $camera.render(view);
    if(DEBUG) $ui.debug_layout(view);

    // BUG: should I do this here or let the caller do it?
    view.display();
  }

  Element& Engine::actor_config(const std::string& actor) {
    dbc::check($actor_name_ids.contains(actor), $F("scene does not contain actor {}", actor));
    return $actors.at($actor_name_ids.at(actor));
  }

  void Engine::move_actor(const std::string& actor, const std::string& cell_name) {
    auto& config = actor_config(actor);
    config.cell = cell_name;
    config.pos = position_sprite(config.st, config.cell, config.scale, config.at_mid);
  }

  void Engine::animate_actor(const std::string& actor, const std::string& form) {
    auto& config = actor_config(actor);

    // no animation set, abort
    if(!config.anim) return;

    config.anim->set_form(form);

    if(!config.anim->playing) {
      config.anim->play();
    }
  }

  inline void this_is_stupid_refactor(std::vector<Element>& elements) {
    for(auto& element : elements) {
      // skip any without animation
      if(element.anim && element.anim->playing) {
        element.anim->update();
        element.anim->motion(*element.st.sprite, element.pos, element.scale);
        element.anim->apply(*element.st.sprite);
        if(element.effect != nullptr) element.anim->apply_effect(element.effect);
      }
    }
  }

  void Engine::update() {
    this_is_stupid_refactor($fixtures);
    this_is_stupid_refactor($actors);
  }

  sf::Vector2f Engine::position_sprite(textures::SpriteTexture& st, const std::string& cell_name, sf::Vector2f scale, bool at_mid, float x_diff, float y_diff) {
    auto& cell = $ui.cell_for(cell_name);
    float x = float(at_mid ? cell.mid_x : cell.x);
    float y = float(at_mid ? cell.mid_y : cell.y);

    sf::Vector2f pos{x + x_diff, y + y_diff};
    st.sprite->setPosition(pos);
    st.sprite->setScale(scale);

    return pos;
  }

  void Engine::zoom(float mid_x, float mid_y, const std::string& style, float scale) {
    $camera.style(style);
    $camera.scale(scale);
    $camera.move(mid_x, mid_y);
    $camera.play();
  }

  void Engine::zoom(const std::string &actor, const std::string& style, float scale) {
    auto& config = actor_config(actor);
    auto bounds = config.st.sprite->getGlobalBounds();
    float mid_x = config.pos.x + bounds.size.x / 2.0f;
    float mid_y = config.pos.y + bounds.size.y / 2.0f;

    zoom(mid_x, mid_y, style, scale);
  }

  void Engine::set_end_cb(std::function<void()> cb) {

    for(auto& actor : $actors) {
      // no animation set so no point doing a end cb
      if(!actor.anim) continue;

      actor.anim->onLoop = [&,cb](auto& seq, auto& tr) -> bool {
        seq.current = tr.toggled ? seq.frame_count - 1 : 0;
        cb();
        actor.effect = nullptr;
        return tr.looped;
      };
    }
  }

  void Engine::reset(sf::RenderTexture& view) {
    $camera.reset(view);
  }
}
