#pragma once
#include <memory>
#include <chrono>
#include <SFML/Graphics/Rect.hpp>
#include <SFML/Graphics/Sprite.hpp>
#include <SFML/Graphics/Shader.hpp>
#include <SFML/Graphics/View.hpp>
#include <SFML/System/Clock.hpp>
#include <SFML/System/Time.hpp>
#include <functional>
#include "graphics/easing.hpp"
#include <fmt/core.h>
#include "game/json_mods.hpp"
#include <source_location>
#include "algos/dinkyecs.hpp"

namespace animation {

  template <typename T> struct NameOf;

  struct Sheet {
    int frames{0};
    int frame_width{0};
    int frame_height{0};
  };

  struct Timer {
    double DELTA = 1.0/60.0;
    double accumulator = 0.0;
    double prev_time = 0.0;
    double current_time = 0.0;
    double frame_duration = 0.0;
    double alpha = 0.0;
    int elapsed_ticks = 0;
    sf::Clock clock{};

    std::pair<int, double> commit();
    void start();
    void reset();
    void restart();
    sf::Time getElapsedTime();
  };

  struct Sequence {
    std::vector<int> frames{};
    std::vector<int> durations{}; // in ticks
    size_t current{0};
    int loop_count{0};
    size_t frame_count{frames.size()};
    Timer timer{};
    int subframe{0};
    float easing_duration{0.0f};
    float easing_position{0.0f};

    void INVARIANT(const std::source_location location = std::source_location::current());
  };

  struct Transform {
    // how to know when a transform ends?
    float min_x{1.0f};
    float min_y{1.0f};
    float max_x{1.0f};
    float max_y{1.0f};
    bool flipped{false};
    bool scaled{false};
    bool relative{false};

    // handled by onLoop
    bool toggled{false};
    bool looped{false};
    std::string easing{"in_out_back"};
    std::string motion{"move_rush"};

    // change to using a callback function for these
    ease2::EaseFunc easing_func{ease2::get_easing(easing)};
    ease2::MotionFunc motion_func{ease2::get_motion(motion)};

    std::shared_ptr<sf::Shader> shader{nullptr};

    void apply(Sequence& seq, sf::Vector2f& pos_out, sf::Vector2f& scale_out);
  };

  /* Gets the number of times it looped, and returns if it should stop. */
  using OnLoopHandler = std::function<bool(Sequence& seq, Transform& tr)>;
  using OnFrameHandler = std::function<void()>;

  inline bool DefaultOnLoop(Sequence& seq, Transform& tr) {
    if(tr.toggled) {
      seq.current = seq.frame_count - 1;
    } else {
      seq.current = 0;
    }

    return tr.looped;
  }

  using Form = std::pair<std::string, std::string>;
  using Sound = std::pair<size_t, std::string>;

  class Animation {
    public:
      Sheet sheet;
      std::unordered_map<std::string, Sequence> sequences;
      std::unordered_map<std::string, Transform> transforms;
      std::unordered_map<std::string, Form> forms;
      std::unordered_map<std::string, std::vector<Sound>> sounds;
      OnFrameHandler onFrame = nullptr;

      Sequence sequence{};
      Transform transform{};

      std::vector<sf::IntRect> $frame_rects{calc_frames()};
      OnLoopHandler onLoop = DefaultOnLoop;
      bool playing = false;

      // mostly for debugging purposes
      std::string form_name="idle";
      std::string sequence_name="";
      std::string transform_name="";
      std::string name="";

      std::vector<sf::IntRect> calc_frames();
      void play();
      void play_sound();
      void stop();
      bool has_form(const std::string& as_form);
      void set_form(const std::string& form);
      void apply(sf::Sprite& sprite);
      void apply(sf::Sprite& sprite, sf::IntRect& rect_io);
      void apply_effect(std::shared_ptr<sf::Shader> effect);
      void update();
      void motion(sf::Transformable& sprite, sf::Vector2f pos, sf::Vector2f scale);
      void motion(sf::View& view_out, sf::Vector2f pos, sf::Vector2f scale);
  };

  Animation load(const std::string &file, const std::string &anim_name);
  std::optional<Animation> maybe_load(const std::string &file, const std::string &anim_name);

  // BUG: brought over from animation to finish the refactor, but these may not be needed or maybe they go in system.cpp?
  bool has(const std::string& name);

  void configure(DinkyECS::World& world, DinkyECS::Entity entity);

  void animate_entity(DinkyECS::World &world, DinkyECS::Entity entity);

  ENROLL_COMPONENT(Sheet, frames, frame_width, frame_height);
  ENROLL_COMPONENT(Sequence, frames, durations);
  ENROLL_COMPONENT(Transform, min_x, min_y, max_x, max_y,
      flipped, scaled, relative, toggled, looped, easing, motion);
  ENROLL_COMPONENT(Animation, sheet, sequences, transforms, forms, sounds);
}
