#include "graphics/animation.hpp"
#include <memory>
#include <chrono>
#include "dbc.hpp"
#include "algos/rand.hpp"
#include <iostream>
#include <fstream>
#include "game/sound.hpp"
#include "game/components.hpp"

constexpr float SUB_FRAME_SENSITIVITY = 0.999f;

namespace animation {
  using namespace std::chrono_literals;

  std::vector<sf::IntRect> Animation::calc_frames() {
    dbc::check(sequence.frames.size() == sequence.durations.size(), "sequence.frames.size() != sequence.durations.size()");

    std::vector<sf::IntRect> frames;

    for(int frame_i : sequence.frames) {
      dbc::check(frame_i < sheet.frames, "frame index greater than sheet frames");
      frames.emplace_back(
          sf::Vector2i{sheet.frame_width * frame_i, 0}, // NOTE: one row only for now
          sf::Vector2i{sheet.frame_width,
          sheet.frame_height});
    }

    return frames;
  }

  void Animation::play() {
    dbc::check(!playing, "can't call play while playing?");
    sequence.current = 0;
    sequence.subframe = 0.0f;
    sequence.loop_count = 0;
    playing = true;
    sequence.timer.start();
    sequence.INVARIANT();
  }

  void Animation::stop() {
    playing = false;
    sequence.timer.reset();
  }

  // need one for each kind of thing to animate
  // NOTE: possibly find a way to only run apply on frame change?
  void Animation::apply(sf::Sprite& sprite) {
    dbc::check(sequence.current < $frame_rects.size(), "current frame past $frame_rects");
    // NOTE: pos is not updated yet
    auto& rect = $frame_rects.at(sequence.current);
    sprite.setTextureRect(rect);
  }


  /*
   * Alternative mostly used in raycaster.cpp that
   * DOES NOT setTextureRect() but just points
   * the rect_io at the correct frame, but leaves
   * it's size and base position alone.
   */
  void Animation::apply(sf::Sprite& sprite, sf::IntRect& rect_io) {
    dbc::check(sequence.current < $frame_rects.size(), "current frame past $frame_rects");
    auto& rect = $frame_rects.at(sequence.current);
    rect_io.position.x += rect.position.x;
    rect_io.position.y += rect.position.y;
  }

  void Animation::motion(sf::Transformable& sprite, sf::Vector2f pos, sf::Vector2f scale) {
    sequence.INVARIANT();

    transform.apply(sequence, pos, scale);

    if(transform.flipped) {
      scale.x *= -1;
    }

    sprite.setPosition(pos);

    if(transform.scaled) {
      sprite.setScale(scale);
    }
  }

  void Animation::motion(sf::View& view_out, sf::Vector2f pos, sf::Vector2f size) {
    dbc::check(size.x > 1.0f && size.y > 1.0f, "motion size must be above 1.0 since it's not a ratio");
    dbc::check(transform.flipped == false, "transform must be false, has no effect on View");

    sf::Vector2f scale{transform.min_x, transform.min_y};

    transform.apply(sequence, pos, scale);

    view_out.setCenter(pos);

    if(transform.scaled) {
      view_out.setSize({size.x * scale.x, size.y * scale.y});
    } else {
      view_out.setSize(size);
    }
  }

  void Animation::apply_effect(std::shared_ptr<sf::Shader> effect) {
    dbc::check(effect != nullptr, "can't apply null effect");
    effect->setUniform("u_time", sequence.timer.getElapsedTime().asSeconds());
    sf::Vector2f u_resolution{float(sheet.frame_width), float(sheet.frame_height)};
    effect->setUniform("u_resolution", u_resolution);
  }

  void Animation::play_sound() {
    // BUG: this can be optimized way better
    if(sounds.contains(form_name)) {
      for(auto& [at_frame, sound_name] : sounds.at(form_name)) {
        if(sequence.current == at_frame) {
          sound::play(sound_name);
        }
      }
    } else {
      fmt::println("Animation has not sound {}", form_name);
    }
  }

  /* REFACTOR: I believe this is wrong still.  If ::commit() determines number of ticks+alpha since last
   * render then update needs to be called 1/tick. The Timer will keep track of alpha as the error
   * between commit calls, so this function only really needs to care about ticks.  But, I'm still
   * calling getElapsedTime() when I already did that in commit(), so should I just ignore that and assume
   * elapsed is DELTA, or use elapsed here?
   */
  void Animation::update() {
    dbc::check(playing, "attempt to update animation that's not playing");
    sequence.INVARIANT();

    auto [ticks, alpha] = sequence.timer.commit();
    int duration = sequence.durations.at(sequence.current);
    sequence.subframe += ticks;

    sequence.easing_position += ticks;

    bool frame_change = false;

    if(sequence.subframe >= duration) {
      sequence.timer.restart();
      sequence.current++;
      sequence.subframe = 0;
      frame_change = true;
    }

    if(sequence.current >= sequence.frame_count) {
      sequence.loop_count++;
      sequence.easing_position = 0;
      playing = onLoop(sequence, transform);
      sequence.INVARIANT();
    }

    if(frame_change) play_sound();

    if(frame_change && onFrame != nullptr) onFrame();
  }

  void Timer::start() {
    clock.start();
    prev_time = clock.getElapsedTime().asSeconds();
  }

  void Timer::reset() {
    elapsed_ticks = 0;
    clock.reset();
  }

  void Timer::restart() {
    elapsed_ticks = 0;
    clock.restart();
    prev_time = clock.getElapsedTime().asSeconds();
  }

  sf::Time Timer::getElapsedTime() {
    return clock.getElapsedTime();
  }

  std::pair<int, double> Timer::commit() {
    // determine frame duration based on previous time
    current_time = clock.getElapsedTime().asSeconds();
    frame_duration = current_time - prev_time;

    // update prev_time for the next call
    prev_time = current_time;

    // update accumulator, retaining previous errors
    accumulator += frame_duration;

    // find the tick count based on DELTA
    double tick_count = floor(accumulator / DELTA);

    // reduce accumulator by the number of DELTAS
    accumulator -= tick_count * DELTA;
    // that leaves the remaining errors for next loop

    elapsed_ticks += tick_count;

    // alpha is then what we lerp...but WHY?!
    alpha = accumulator / DELTA;

    // return the number of even DELTA ticks and the alpha
    return {int(tick_count), alpha};
  }

  void Transform::apply(Sequence& seq, sf::Vector2f& pos_out, sf::Vector2f& scale_out) {
    float tick = easing_func(seq.easing_position / seq.easing_duration);

    motion_func(*this, pos_out, scale_out, tick, relative);
  }

  bool Animation::has_form(const std::string& as_form) {
    return forms.contains(as_form);
  }

  void Animation::set_form(const std::string& as_form) {
    dbc::check(forms.contains(as_form),
        $F("form {} does not exist in animation", as_form));
    stop();

    const auto& [seq_name, tr_name] = forms.at(as_form);

    dbc::check(sequences.contains(seq_name),
        $F("sequences do NOT have \"{}\" name", seq_name));

    dbc::check(transforms.contains(tr_name),
        $F("transforms do NOT have \"{}\" name", tr_name));

    // everything good, do the update
    form_name = as_form;
    sequence_name = seq_name;
    transform_name = tr_name;
    sequence = sequences.at(seq_name);
    transform = transforms.at(tr_name);

    sequence.frame_count = sequence.frames.size();

    // BUG: should this be configurable instead?
    for(auto duration : sequence.durations) {
      sequence.easing_duration += float(duration);
    }
    dbc::check(sequence.easing_duration > 0.0, "bad easing duration");

    $frame_rects = calc_frames();
    transform.easing_func = ease2::get_easing(transform.easing);
    transform.motion_func = ease2::get_motion(transform.motion);

    sequence.INVARIANT();
  }

  std::optional<Animation> maybe_load(const std::string &file, const std::string &anim_name) {
    using nlohmann::json;
    std::ifstream infile(file);
    auto data = json::parse(infile);

    if(!data.contains(anim_name)) return std::nullopt;

    Animation anim;
    animation::from_json(data[anim_name], anim);
    anim.name = anim_name;

    dbc::check(anim.forms.contains("idle"),
        $F("animation {} must have 'idle' form", anim_name));

    anim.set_form("idle");

    return anim;
  }

  Animation load(const std::string &file, const std::string &anim_name) {
    auto anim = maybe_load(file, anim_name);

    dbc::check(anim != std::nullopt,
        $F("FAILED to load animation {} from file {}", anim_name, file));

    return *anim;
  }

  void Sequence::INVARIANT(const std::source_location location) {
    dbc::check(frames.size() == durations.size(),
        $F("frames.size={} doesn't match durations.size={}",
          frames.size(), durations.size()), location);

    dbc::check(easing_duration > 0.0, $F("bad easing duration: {}", easing_duration), location);

    dbc::check(frame_count == frames.size(),
        $F("frame_count={} doesn't match frames.size={}", frame_count, frames.size()), location);

    dbc::check(frame_count == durations.size(),
        $F("frame_count={} doesn't match durations.size={}", frame_count, durations.size()), location);

    dbc::check(current < durations.size(),
        $F("current={} went past end of fame durations.size={}", current, durations.size()), location);
  }

  // BUG: BAAADD REMOVE
  bool has(const std::string& name) {
    using nlohmann::json;
    std::ifstream infile("assets/animation.json");
    auto data = json::parse(infile);
    return data.contains(name);
  }

  void configure(DinkyECS::World& world, DinkyECS::Entity entity) {
    auto sprite = world.get_if<components::Sprite>(entity);

    // sprite doesn't exist, abort
    if(sprite == nullptr) return;

    // lazy load any animations
    if(auto anim = animation::maybe_load("assets/animation.json", sprite->name)) {
      world.set<Animation>(entity, *anim);
    }
  }

  void animate_entity(DinkyECS::World &world, DinkyECS::Entity entity) {
    auto anim = world.get_if<Animation>(entity);

    if(anim != nullptr && !anim->playing) {
      anim->play();
    }
  }
}
