#include "graphics/camera.hpp"
#include <unordered_map>
#include "game/components.hpp"
#include "game/config.hpp"
#include <algorithm>
#include <iostream>
#include <cstdlib>

namespace cinematic {
  using animation::Animation, std::string, std::min, std::clamp;

  struct CameraManager {
    std::unordered_map<string, Animation> animations;
  };

  static CameraManager MGR;
  static bool initialized = false;

  void init() {
    if(!initialized) {
      // BUG: it should be that you give a camera to load by name, not just one for all cameras
      auto data = settings::get("cameras");

      for(auto [key, value] : data.json().items()) {
        auto anim = components::convert<Animation>(value);
        MGR.animations.try_emplace(key, anim);
      }

      initialized = true;
    }
  }

  Camera::Camera(sf::Vector2f size, const std::string &name) :
    anim(MGR.animations.at(name)),
    size(size),
    base_size(size),
    aimed_at{size.x/2, size.y/2},
    going_to{size.x/2, size.y/2},
    camera_bounds{{0,0}, size},
    view{aimed_at, size}
  {
    anim.sheet.frame_width = base_size.x;
    anim.sheet.frame_height = base_size.y;
  }

  void Camera::update_camera_bounds(sf::Vector2f size) {
    // camera bounds now constrains the x/y so that the mid-point
    // of the size won't go too far outside of the frame
    camera_bounds = {
      {size.x / 2.0f, size.y / 2.0f},
      {base_size.x - size.x / 2.0f, base_size.y - size.y / 2.0f}
    };
  }

  void Camera::scale(float ratio) {
    size.x = base_size.x * ratio;
    size.y = base_size.y * ratio;
    update_camera_bounds(size);
  }

  void Camera::resize(float width) {
    dbc::check(width <= base_size.x, "invalid width for camera");

    size.x = width;
    size.y = base_size.y * (width / base_size.x);
    update_camera_bounds(size);
  }

  void Camera::style(const std::string &name) {
    anim.set_form(name);
  }

  void Camera::position(float x, float y) {
    aimed_at.x = clamp(x, camera_bounds.position.x, camera_bounds.size.x);
    aimed_at.y = clamp(y, camera_bounds.position.y, camera_bounds.size.y);
  }

  void Camera::move(float x, float y) {
    going_to.x = clamp(x, camera_bounds.position.x, camera_bounds.size.x);
    going_to.y = clamp(y, camera_bounds.position.y, camera_bounds.size.y);

    if(!anim.transform.relative) {
      anim.transform.min_x = aimed_at.x;
      anim.transform.min_y = aimed_at.y;
      anim.transform.max_x = going_to.x;
      anim.transform.max_y = going_to.y;
    }
  }

  void Camera::reset(sf::RenderTexture& target) {
    size = {base_size.x, base_size.y};
    aimed_at = {base_size.x/2, base_size.y/2};
    going_to = {base_size.x/2, base_size.y/2};
    view = {aimed_at, size};
    camera_bounds = {{0,0}, base_size};

    // BUG: is getDefaultView different from view?
    target.setView(target.getDefaultView());
  }

  void Camera::render(sf::RenderTexture& target) {
    if(anim.playing) {
      anim.motion(view, going_to, size);
      target.setView(view);
    }
  }

  void Camera::update() {
    if(anim.playing) anim.update();
  }

  bool Camera::playing() {
    return anim.playing;
  }

  void Camera::play() {
    anim.play();
  }

  void Camera::from_story(components::Storyboard& story) {
    anim.sequences.clear();
    anim.forms.clear();

    for(auto& [timecode, cell, transform, duration] : story.beats) {
      animation::Sequence seq{.frames={0}, .durations={std::stoi(duration)}};
      anim.sequences.try_emplace(timecode, seq);

      animation::Form form{timecode, transform};
      anim.forms.try_emplace(timecode, form);
    }
  }
}
