use macros::decode_args;
use num_derive::FromPrimitive;
use num_traits::FromPrimitive;
use truth::llir::RawInstr;
use wgpu::naga::FastHashMap;

#[derive(Debug, FromPrimitive)]
enum Opcode {
  Nop = 0,
  Delete = 1,
  Static = 2,
  Sprite = 3,
  Jump = 4,
  JumpDec = 5,

  IntSet = 6,
  FloatSet = 7,
  IntAdd = 8,
  FloatAdd = 9,
  IntSub = 10,
  FloatSub = 11,
  IntMul = 12,
  FloatMul = 13,
  IntDiv = 14,
  FloatDiv = 15,
  IntMod = 16,
  FloatMod = 17,
  IntSetAdd = 18,
  FloatSetAdd = 19,
  IntSetSub = 20,
  FloatSetSub = 21,
  IntSetMul = 22,
  FloatSetMul = 23,
  IntSetDiv = 24,
  FloatSetDiv = 25,
  IntSetMod = 26,
  FloatSetMod = 27,

  IntJumpEq = 28,
  FloatJumpEq = 29,
  IntJumpNeq = 30,
  FloatJumpNeq = 31,
  IntJumpLess = 32,
  FloatJumpLess = 33,
  IntJumpLessEq = 34,
  FloatJumpLessEq = 35,
  IntJumpGreater = 36,
  FloatJumpGreater = 37,
  IntJumpGreaterEq = 38,
  FloatJumpGreaterEq = 39,

  IntRand = 40,
  FloatRand = 41,
  Sin = 42,
  Cos = 43,
  Tan = 44,
  ArcCos = 45,
  ArcTan = 46,
  ValidRad = 47,

  Pos = 48,
  Rotate = 49,
  Scale = 50,
  Alpha = 51,
  Color = 52,
  AngleVel = 53,
  ScaleGrowth = 54,
  AlphaTimeLinear = 55,
  PosTime = 56,
  ColorTime = 57,
  AlphaTime = 58,
  RotateTime = 59,
  ScaleTime = 60,
  FlipX = 61,
  FlipY = 62,
  Stop = 63,
  InterruptLabel = 64,
  Anchor = 65,
  BlendMode = 66,
  Type = 67,
  Layer = 68,
  StopHide = 69,
  ScrollX = 70,
  ScrollY = 71,
  Visible = 72,
  ZWriteDisable = 73,
  StdRelatedBitflag = 74,
  Wait = 75,
  Color2 = 76,
  Alpha2 = 77,
  Color2Time = 78,
  Alpha2Time = 79,
  ColorMode = 80,
  CaseReturn = 81,
  RotateAuto = 82,
  Ins83CopyPos = 83,
  TexCircle = 84,
  UnknownBitflag = 85,
  SlowdownImmune = 86,
  RandMode = 87,
  ScriptNew = 88,
  ResampleMode = 89,
  ScriptNewUI = 90,
  ScriptNewFront = 91,
  ScriptNewUIFront = 92,
  ScrollXTime = 93,
  ScrollYTime = 94,
  ScriptNewRoot = 95,
  ScriptNewPos = 96,
  ScriptNewRootPos = 97,
  MoveBezier = 100,
  SpriteRand = 102,
  DrawRect = 103,
}

#[derive(Debug, Clone, Copy)]
pub enum PrimOp {
  Set,
  Add,
  Sub,
  Mul,
  Div,
  Mod,
  Rand,
}

#[derive(Debug, Clone, Copy)]
pub enum PrimSetOp {
  Add,
  Sub,
  Mul,
  Div,
  Mod,
}

#[derive(Debug, Clone, Copy)]
pub enum Compare {
  Equal,
  NotEqual,
  Less,
  LessEqual,
  Greater,
  GreaterEqual,
}

#[derive(Debug, Clone, Copy)]
pub enum TrigFunction {
  Sin,
  Cos,
  Tan,
  ArcCos,
  ArcTan,
}

#[derive(Debug, Clone, Copy, FromPrimitive)]
pub enum SpriteType {
  NoRotate = 0,
  RotateZ = 1,
  // like mode 0, but shifts the position by (-0.5, -0.5) pixels,
  // making it appear ever-so-slightly larger and blurrier.
  Rotate3D = 2,
  RotateZ3D = 3,
  RotateBillboard = 4,
}

#[derive(Debug, Clone, Copy)]
pub enum Param<T> {
  Variable(i32),
  Value(T),
}

#[derive(Debug, Clone, Copy)]
pub enum Op {
  Nop,
  Delete,
  Static,
  Sprite(Option<u32>),
  Jump {
    index: usize,
    time: i32,
  },
  JumpDec {
    var: i32,
    index: usize,
    time: i32,
  },
  IntOp {
    op: PrimOp,
    dest: i32,
    input: Param<i32>,
  },
  FloatOp {
    op: PrimOp,
    dest: i32,
    input: Param<f32>,
  },
  IntSetOp {
    op: PrimSetOp,
    dest: i32,
    left: Param<i32>,
    right: Param<i32>,
  },
  FloatSetOp {
    op: PrimSetOp,
    dest: i32,
    left: Param<f32>,
    right: Param<f32>,
  },
  IntJumpCmp {
    cmp_op: Compare,
    left: Param<i32>,
    right: Param<i32>,
    offset: usize,
    time: i32,
  },
  FloatJumpCmp {
    cmp_op: Compare,
    left: Param<f32>,
    right: Param<f32>,
    offset: usize,
    time: i32,
  },
  Trig {
    func: TrigFunction,
    dest: i32,
    input: Param<f32>,
  },
  ValidRad {
    var: i32,
  },
  Pos {
    x: Param<f32>,
    y: Param<f32>,
    z: Param<f32>,
  },
  Rotate {
    x: Param<f32>,
    y: Param<f32>,
    z: Param<f32>,
  },
  Scale {
    x: Param<f32>,
    y: Param<f32>,
  },
  Alpha(Param<i32>),
  Color {
    red: Param<i32>,
    green: Param<i32>,
    blue: Param<i32>,
  },
  AngleVel {
    x: Param<f32>,
    y: Param<f32>,
    z: Param<f32>,
  },
  ScaleGrowth {
    x: Param<f32>,
    y: Param<f32>,
  },
  PosTime {
    time: Param<i32>,
    mode: Param<i32>,
    x: Param<f32>,
    y: Param<f32>,
    z: Param<f32>,
  },
  ColorTime {
    time: Param<i32>,
    mode: Param<i32>,
    red: Param<i32>,
    green: Param<i32>,
    blue: Param<i32>,
  },
  AlphaTime {
    time: Param<i32>,
    mode: Param<i32>,
    alpha: Param<i32>,
  },
  RotateTime {
    time: Param<i32>,
    mode: Param<i32>,
    x: Param<f32>,
    y: Param<f32>,
    z: Param<f32>,
  },
  ScaleTime {
    time: Param<i32>,
    mode: Param<i32>,
    x: Param<f32>,
    y: Param<f32>,
  },
  FlipX,
  FlipY,
  Stop,
  InterruptLabel(i32),
  Anchor {
    horizontal: i16,
    vertical: i16,
  },
  BlendMode {
    mode: Param<i32>,
  },
  Type {
    mode: SpriteType,
  },
  Layer {
    layer: i32,
  },
  StopHide,
  ScrollX {
    value: f32,
  },
  ScrollY {
    value: f32,
  },
  Visible {
    value: i8,
  },
  ZWriteDisable {
    value: i32,
  },
  StdRelatedBitflag {
    enable: i32,
  },
  Wait {
    time: i32,
  },
  CaseReturn,
  UnknownBitflag {
    enable: i32,
  },
  RandMode {
    mode: i32,
  },
  ScriptNew {
    script: i32,
  },
  ResampleMode {
    mode: i32,
  },
  ScriptNewUI {
    script: i32,
  },
  ScriptNewFront {
    script: i32,
  },
  ScriptNewUIFront {
    script: i32,
  },
  ScrollXTime,
}

#[derive(Debug, Clone, Copy)]
pub struct Instruction {
  pub time: i32,
  pub op: Op,
}

impl Instruction {
  pub fn from_raw(inst: RawInstr, instruction_offsets: &FastHashMap<u32, usize>) -> Self {
    let param_int = |value: i32, index: u16| {
      if inst.param_mask & (1 << index) == 0 {
        Param::Value(value)
      } else if value < 10000 || value > 10022 {
        Param::Value(value)
      } else {
        Param::Variable(value)
      }
    };
    let param_float = |value: f32, index: u16| {
      if inst.param_mask & (1 << index) == 0 {
        Param::Value(value)
      } else if value != value.trunc() && value < 10000. || value > 10022. {
        Param::Value(value)
      } else {
        Param::Variable(value as i32)
      }
    };
    let var_int = |value: i32, index: u16| {
      assert!(inst.param_mask & (1 << index) != 0, "param must be a var");
      assert!(value >= 10000 && value <= 10022, "value must be a var");
      value
    };
    let var_float = |value: f32, index: u16| {
      assert!(inst.param_mask & (1 << index) != 0, "param must be a var");
      assert!(
        value.trunc() == value && value >= 10000. && value <= 10022.,
        "value must be a var"
      );
      value as i32
    };
    let offset_to_index = |value: i32| instruction_offsets[&(value as u32)];

    let args = &inst.args_blob;

    let opcode_raw = inst.opcode;
    let opcode = Opcode::from_u16(opcode_raw).expect("failed to convert opcode");

    let op = match opcode {
      Opcode::Nop => Op::Nop,
      Opcode::Delete => Op::Delete,
      Opcode::Static => Op::Static,
      Opcode::Sprite => Op::Sprite({
        let sprite = decode_args!(args, "S");
        if sprite < 0 {
          None
        } else {
          Some(sprite as u32)
        }
      }),
      Opcode::Jump => {
        let (offset, time) = decode_args!(args, "ot");
        Op::Jump {
          index: offset_to_index(offset),
          time,
        }
      }
      Opcode::JumpDec => {
        let (var, offset, time) = decode_args!(args, "Sot");
        Op::JumpDec {
          var: var_int(var, 0),
          index: offset_to_index(offset),
          time,
        }
      }
      opcode @ (Opcode::IntSet
      | Opcode::IntAdd
      | Opcode::IntSub
      | Opcode::IntMul
      | Opcode::IntDiv
      | Opcode::IntMod
      | Opcode::IntRand) => {
        let (dest, input) = decode_args!(args, "SS");
        Op::IntOp {
          op: match opcode {
            Opcode::IntSet => PrimOp::Set,
            Opcode::IntAdd => PrimOp::Add,
            Opcode::IntSub => PrimOp::Sub,
            Opcode::IntMul => PrimOp::Mul,
            Opcode::IntDiv => PrimOp::Div,
            Opcode::IntMod => PrimOp::Mod,
            Opcode::IntRand => PrimOp::Rand,
            _ => unreachable!(),
          },
          dest: var_int(dest, 0),
          input: param_int(input, 1),
        }
      }
      opcode @ (Opcode::FloatSet
      | Opcode::FloatAdd
      | Opcode::FloatSub
      | Opcode::FloatMul
      | Opcode::FloatDiv
      | Opcode::FloatMod
      | Opcode::FloatRand) => {
        let (dest, input) = decode_args!(args, "ff");
        Op::FloatOp {
          op: match opcode {
            Opcode::FloatSet => PrimOp::Set,
            Opcode::FloatAdd => PrimOp::Add,
            Opcode::FloatSub => PrimOp::Sub,
            Opcode::FloatMul => PrimOp::Mul,
            Opcode::FloatDiv => PrimOp::Div,
            Opcode::FloatMod => PrimOp::Mod,
            Opcode::FloatRand => PrimOp::Rand,
            _ => unreachable!(),
          },
          dest: var_float(dest, 0),
          input: param_float(input, 1),
        }
      }
      opcode @ (Opcode::IntSetAdd | Opcode::IntSetSub | Opcode::IntSetMul | Opcode::IntSetDiv | Opcode::IntSetMod) => {
        let (dest, left, right) = decode_args!(args, "SSS");
        Op::IntSetOp {
          op: match opcode {
            Opcode::IntSetAdd => PrimSetOp::Add,
            Opcode::IntSetSub => PrimSetOp::Sub,
            Opcode::IntSetMul => PrimSetOp::Mul,
            Opcode::IntSetDiv => PrimSetOp::Div,
            Opcode::IntSetMod => PrimSetOp::Mod,
            _ => unreachable!(),
          },
          dest: var_int(dest, 0),
          left: param_int(left, 1),
          right: param_int(right, 2),
        }
      }
      opcode @ (Opcode::FloatSetAdd
      | Opcode::FloatSetSub
      | Opcode::FloatSetMul
      | Opcode::FloatSetDiv
      | Opcode::FloatSetMod) => {
        let (dest, left, right) = decode_args!(args, "fff");
        Op::FloatSetOp {
          op: match opcode {
            Opcode::FloatSetAdd => PrimSetOp::Add,
            Opcode::FloatSetSub => PrimSetOp::Sub,
            Opcode::FloatSetMul => PrimSetOp::Mul,
            Opcode::FloatSetDiv => PrimSetOp::Div,
            Opcode::FloatSetMod => PrimSetOp::Mod,
            _ => unreachable!(),
          },
          dest: var_float(dest, 0),
          left: param_float(left, 1),
          right: param_float(right, 2),
        }
      }
      opcode @ (Opcode::IntJumpEq
      | Opcode::IntJumpNeq
      | Opcode::IntJumpLess
      | Opcode::IntJumpLessEq
      | Opcode::IntJumpGreater
      | Opcode::IntJumpGreaterEq) => {
        let (left, right, offset, time) = decode_args!(args, "SSot");
        Op::IntJumpCmp {
          cmp_op: match opcode {
            Opcode::IntJumpEq => Compare::Equal,
            Opcode::IntJumpNeq => Compare::NotEqual,
            Opcode::IntJumpLess => Compare::Less,
            Opcode::IntJumpLessEq => Compare::LessEqual,
            Opcode::IntJumpGreater => Compare::Greater,
            Opcode::IntJumpGreaterEq => Compare::GreaterEqual,
            _ => unreachable!(),
          },
          left: param_int(left, 0),
          right: param_int(right, 1),
          offset: offset_to_index(offset),
          time,
        }
      }
      opcode @ (Opcode::FloatJumpEq
      | Opcode::FloatJumpNeq
      | Opcode::FloatJumpLess
      | Opcode::FloatJumpLessEq
      | Opcode::FloatJumpGreater
      | Opcode::FloatJumpGreaterEq) => {
        let (left, right, offset, time) = decode_args!(args, "ffot");
        Op::FloatJumpCmp {
          cmp_op: match opcode {
            Opcode::FloatJumpEq => Compare::Equal,
            Opcode::FloatJumpNeq => Compare::NotEqual,
            Opcode::FloatJumpLess => Compare::Less,
            Opcode::FloatJumpLessEq => Compare::LessEqual,
            Opcode::FloatJumpGreater => Compare::Greater,
            Opcode::FloatJumpGreaterEq => Compare::GreaterEqual,
            _ => unreachable!(),
          },
          left: param_float(left, 0),
          right: param_float(right, 1),
          offset: offset_to_index(offset),
          time,
        }
      }
      opcode @ (Opcode::Sin | Opcode::Cos | Opcode::Tan | Opcode::ArcCos | Opcode::ArcTan) => {
        let (dest, input) = decode_args!(args, "ff");
        Op::Trig {
          func: match opcode {
            Opcode::Sin => TrigFunction::Sin,
            Opcode::Cos => TrigFunction::Cos,
            Opcode::Tan => TrigFunction::Tan,
            Opcode::ArcCos => TrigFunction::ArcCos,
            Opcode::ArcTan => TrigFunction::ArcTan,
            _ => unreachable!(),
          },
          dest: var_float(dest, 0),
          input: param_float(input, 1),
        }
      }
      Opcode::ValidRad => {
        let var = decode_args!(args, "f");
        Op::ValidRad { var: var_float(var, 0) }
      }
      Opcode::Pos => {
        let (x, y, z) = decode_args!(args, "fff");
        Op::Pos {
          x: param_float(x, 0),
          y: param_float(y, 1),
          z: param_float(z, 2),
        }
      }
      Opcode::Rotate => {
        let (x, y, z) = decode_args!(args, "fff");
        Op::Rotate {
          x: param_float(x, 0),
          y: param_float(y, 1),
          z: param_float(z, 2),
        }
      }
      Opcode::Scale => {
        let (x, y) = decode_args!(args, "ff");
        Op::Scale {
          x: param_float(x, 0),
          y: param_float(y, 1),
        }
      }
      Opcode::Alpha => {
        let alpha = decode_args!(args, "S");
        Op::Alpha(param_int(alpha, 0))
      }
      Opcode::Color => {
        let (x, y, z) = decode_args!(args, "SSS");
        Op::Color {
          red: param_int(x, 0),
          green: param_int(y, 1),
          blue: param_int(z, 2),
        }
      }
      Opcode::AngleVel => {
        let (x, y, z) = decode_args!(args, "fff");
        Op::AngleVel {
          x: param_float(x, 0),
          y: param_float(y, 1),
          z: param_float(z, 2),
        }
      }
      Opcode::ScaleGrowth => {
        let (x, y) = decode_args!(args, "ff");
        Op::ScaleGrowth {
          x: param_float(x, 0),
          y: param_float(y, 1),
        }
      }
      Opcode::PosTime => {
        let (time, mode, x, y, z) = decode_args!(args, "SSfff");
        Op::PosTime {
          time: param_int(time, 0),
          mode: param_int(mode, 1),
          x: param_float(x, 2),
          y: param_float(y, 3),
          z: param_float(z, 4),
        }
      }
      Opcode::ColorTime => {
        let (time, mode, red, green, blue) = decode_args!(args, "SSSSS");
        Op::ColorTime {
          time: param_int(time, 0),
          mode: param_int(mode, 1),
          red: param_int(red, 2),
          green: param_int(green, 3),
          blue: param_int(blue, 4),
        }
      }
      Opcode::AlphaTime => {
        let (time, mode, alpha) = decode_args!(args, "SSS");
        Op::AlphaTime {
          time: param_int(time, 0),
          mode: param_int(mode, 1),
          alpha: param_int(alpha, 2),
        }
      }
      Opcode::RotateTime => {
        let (time, mode, x, y, z) = decode_args!(args, "SSfff");
        Op::RotateTime {
          time: param_int(time, 0),
          mode: param_int(mode, 1),
          x: param_float(x, 2),
          y: param_float(y, 3),
          z: param_float(z, 4),
        }
      }
      Opcode::ScaleTime => {
        let (time, mode, x, y) = decode_args!(args, "SSff");
        Op::ScaleTime {
          time: param_int(time, 0),
          mode: param_int(mode, 1),
          x: param_float(x, 2),
          y: param_float(y, 3),
        }
      }
      Opcode::FlipX => Op::FlipX,
      Opcode::FlipY => Op::FlipY,
      Opcode::Stop => Op::Stop,
      Opcode::InterruptLabel => {
        let label = decode_args!(args, "S");

        Op::InterruptLabel(label)
      }
      Opcode::Anchor => {
        let (horizontal, vertical) = decode_args!(args, "ss");

        Op::Anchor { horizontal, vertical }
      }
      Opcode::BlendMode => {
        let mode = decode_args!(args, "S");

        Op::BlendMode {
          mode: param_int(mode, 0),
        }
      }
      Opcode::Type => {
        let r#type = decode_args!(args, "S");

        Op::Type {
          mode: SpriteType::from_i32(r#type).unwrap_or_else(|| unimplemented!("unexpected type {type}")),
        }
      }
      Opcode::Layer => {
        let layer = decode_args!(args, "S");
        Op::Layer { layer }
      }
      Opcode::StopHide => Op::StopHide,
      Opcode::UnknownBitflag => {
        let enable = decode_args!(args, "S");

        Op::UnknownBitflag { enable }
      }
      Opcode::Wait => {
        let time = decode_args!(args, "S");
        Op::Wait { time }
      }
      Opcode::CaseReturn => Op::CaseReturn,
      Opcode::RandMode => {
        let mode = decode_args!(args, "S");

        Op::RandMode { mode }
      }
      Opcode::ScriptNew => {
        let script = decode_args!(args, "S");
        Op::ScriptNew { script }
      }
      Opcode::ResampleMode => {
        let mode = decode_args!(args, "S");
        Op::ResampleMode { mode }
      }
      Opcode::ScriptNewUI => {
        let script = decode_args!(args, "S");
        Op::ScriptNewUI { script }
      }
      Opcode::ScriptNewFront => {
        let script = decode_args!(args, "S");
        Op::ScriptNewFront { script }
      }
      Opcode::ScriptNewUIFront => {
        let script = decode_args!(args, "S");
        Op::ScriptNewUIFront { script }
      }
      opcode => panic!("unsupported instruction {opcode:?} ({opcode_raw})"),
    };

    Instruction { time: inst.time, op }
  }
}
