use std::fmt::Display;

use glam::{Vec2, Vec3};
use num_derive::FromPrimitive;

pub trait Interpolable<const N: usize>: Default + Copy {
  fn from_components(components: [f32; N]) -> Self;
  fn components(&self) -> [f32; N];
}

impl Interpolable<1> for f32 {
  fn from_components(components: [f32; 1]) -> Self {
    components[0]
  }
  fn components(&self) -> [f32; 1] {
    [self.clone()]
  }
}

impl Interpolable<2> for Vec2 {
  fn from_components(components: [f32; 2]) -> Self
  where
    Self: Sized,
  {
    Self::from_array(components)
  }
  fn components(&self) -> [f32; 2]
  where
    Self: Sized,
  {
    self.to_array()
  }
}

impl Interpolable<3> for Vec3 {
  fn from_components(components: [f32; 3]) -> Self
  where
    Self: Sized,
  {
    Self::from_array(components)
  }
  fn components(&self) -> [f32; 3]
  where
    Self: Sized,
  {
    self.to_array()
  }
}

pub struct Interpolator<I: Interpolable<N> + Display, const N: usize> {
  initial: I,
  goal: I,
  extra_1: I,
  extra_2: I,
  current: I,
  time: i32,
  end_time: i32,
  mode: Mode,
}

pub type FloatInterpolator = Interpolator<f32, 1>;
pub type Vec2Interpolator = Interpolator<Vec2, 2>;
pub type Vec3Interpolator = Interpolator<Vec3, 3>;

#[derive(Debug, Clone, Copy, FromPrimitive)]
pub enum Mode {
  Linear = 0,
  EaseIn = 1,
  EaseInCubic = 2,
  EaseInQuartic = 3,
  EaseOut = 4,
  EaseOutCubic = 5,
  EaseOutQuartic = 6,
  Constant = 7,
  Bezier = 8,
  EaseInOut = 9,
  EaseInOutCubic = 10,
  EaseInOutQuartic = 11,
  EaseOutIn = 12,
  EaseOutInCubic = 13,
  EaseOutInQuartic = 14,
  DelayedChange = 15,
  Immediate = 16,
  ConstantAcceleration = 17,
}

impl<I: Interpolable<N> + Display, const N: usize> Interpolator<I, N> {
  pub fn new(start: I) -> Interpolator<I, N> {
    Self {
      initial: start,
      goal: start,
      extra_1: Default::default(),
      extra_2: Default::default(),
      current: start,
      time: 0,
      end_time: 0,
      mode: Mode::Constant,
    }
  }

  pub fn reset(&mut self, mode: Mode, end_time: i32, goal: I, extra_1: Option<I>, extra_2: Option<I>) {
    self.initial = self.current;
    self.time = 0;
    self.end_time = end_time;
    self.mode = mode;
    self.goal = goal;
    self.extra_1 = extra_1.unwrap_or_default();
    self.extra_2 = extra_2.unwrap_or_default();
    self.tick();
  }

  pub fn reset_immediate(&mut self, goal: I) {
    self.reset(Mode::Immediate, 0, goal, None, None);
  }

  pub fn current(&self) -> I {
    self.current
  }

  pub fn tick(&mut self) {
    if self.end_time < self.time {
      self.current = self.goal;
      return;
    };

    let (mut initial, goal, extra_1, mut extra_2) = (
      self.initial.components(),
      self.goal.components(),
      self.extra_1.components(),
      self.extra_2.components(),
    );

    let components = std::array::from_fn(|i| {
      let result = self.interpolate_component(initial[i], goal[i], extra_1[i], extra_2[i]);

      if let Some(new_initial) = result.initial {
        initial[i] = new_initial;
        if let Some(new_extra_2) = result.extra_2 {
          extra_2[i] = new_extra_2;
        }
      }

      result.current
    });

    self.current = I::from_components(components);
    self.initial = I::from_components(initial);
    self.extra_2 = I::from_components(extra_2);
    self.time += 1;
    if self.time == self.end_time {
      // println!("ended {} {}", self.current, self.initial);
    }
  }

  fn interpolate_component(&self, initial: f32, goal: f32, extra_1: f32, extra_2: f32) -> Interp {
    if self.end_time == 0 {
      return Interp::simple(goal);
    } // prevent divide by zero

    let (time, end_time) = (self.time as f32, self.end_time as f32);
    let t = time / end_time;

    // helpers
    let lerp = |a: f32, b: f32, t: f32| a + t.clamp(0.0, 1.0) * (b - a);
    let lerp_input = |t: f32| lerp(initial, goal, t);
    let invert = |a: f32| 1.0 - a;

    match self.mode {
      Mode::Constant => Interp::velocity(initial + goal),
      Mode::Bezier => {
        let _ = (extra_1, extra_2);
        todo!()
      }
      Mode::DelayedChange => Interp::simple(if t == 1.0 { 1.0 } else { 0.0 }),
      Mode::Immediate => Interp::simple(goal),
      Mode::ConstantAcceleration => Interp::acceleration(initial + extra_2, extra_2 + goal),
      _ => Interp::simple(lerp_input(match self.mode {
        Mode::Linear => t,
        Mode::EaseIn => t.powf(2.0),
        Mode::EaseInCubic => t.powf(3.0),
        Mode::EaseInQuartic => t.powf(4.0),
        Mode::EaseOut => invert(invert(t).powf(2.0)),
        Mode::EaseOutCubic => invert(invert(t).powf(3.0)),
        Mode::EaseOutQuartic => invert(invert(t).powf(4.0)),
        Mode::EaseInOut => {
          lerp(t.powf(2.0), invert(invert(t).powf(2.0)), t)
        }
        Mode::EaseInOutCubic => todo!(),
        Mode::EaseInOutQuartic => todo!(),
        Mode::EaseOutIn => todo!(),
        Mode::EaseOutInCubic => todo!(),
        Mode::EaseOutInQuartic => todo!(),
        Mode::Constant | Mode::Bezier | Mode::DelayedChange | Mode::Immediate | Mode::ConstantAcceleration => {
          unreachable!()
        }
      })),
    }
  }
}

struct Interp {
  initial: Option<f32>,
  extra_2: Option<f32>,
  current: f32,
}

impl Interp {
  fn simple(value: f32) -> Self {
    Interp {
      initial: None,
      extra_2: None,
      current: value,
    }
  }

  fn velocity(value: f32) -> Self {
    Interp {
      initial: Some(value),
      extra_2: None,
      current: value,
    }
  }

  fn acceleration(initial: f32, extra_2: f32) -> Self {
    Interp {
      initial: Some(initial),
      extra_2: Some(extra_2),
      current: initial,
    }
  }
}
