// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
suberror FfError {
  NotSupported
  NoGamepads
  Disconnected(GamepadId)
  FfNotSupported(GamepadId)
  InvalidDistanceModel(DistanceModelError)
}

///|
suberror DistanceModelError {
  InvalidReferenceDistance
  InvalidRolloffFactor
  InvalidMaxDistance
  InvalidModelParameter
}

///|
pub struct Ticks {
  ms : Int64
}

///|
pub fn Ticks::from_ms(ms : Int64) -> Ticks {
  { ms, }
}

///|
pub fn Ticks::as_ms(self : Ticks) -> Int64 {
  self.ms
}

///|
pub(all) enum BaseEffectType {
  Weak(Int)
  Strong(Int)
}

///|
pub struct BaseEffect {
  kind : BaseEffectType
  scheduling : Replay
  envelope : Envelope
}

///|
pub fn BaseEffect::default() -> BaseEffect {
  {
    kind: BaseEffectType::Weak(0),
    scheduling: Replay::default(),
    envelope: Envelope::default(),
  }
}

///|
pub enum FfRepeat {
  Infinitely
  For(Int64)
}

///|
let _ff_repeat_keepalive : Array[FfRepeat] = [FfRepeat::For(0L)]

///|
pub enum DistanceModel {
  None
  Linear(
    ref_distance~ : Double,
    rolloff_factor~ : Double,
    max_distance~ : Double
  )
  LinearClamped(
    ref_distance~ : Double,
    rolloff_factor~ : Double,
    max_distance~ : Double
  )
  Inverse(ref_distance~ : Double, rolloff_factor~ : Double)
  InverseClamped(
    ref_distance~ : Double,
    rolloff_factor~ : Double,
    max_distance~ : Double
  )
  Exponential(ref_distance~ : Double, rolloff_factor~ : Double)
  ExponentialClamped(
    ref_distance~ : Double,
    rolloff_factor~ : Double,
    max_distance~ : Double
  )
}

///|
let _distance_model_keepalive : Array[DistanceModel] = [
  DistanceModel::Linear(ref_distance=1.0, rolloff_factor=1.0, max_distance=2.0),
  DistanceModel::LinearClamped(
    ref_distance=1.0,
    rolloff_factor=1.0,
    max_distance=2.0,
  ),
  DistanceModel::Inverse(ref_distance=1.0, rolloff_factor=1.0),
  DistanceModel::InverseClamped(
    ref_distance=1.0,
    rolloff_factor=1.0,
    max_distance=2.0,
  ),
  DistanceModel::Exponential(ref_distance=1.0, rolloff_factor=1.0),
  DistanceModel::ExponentialClamped(
    ref_distance=1.0,
    rolloff_factor=1.0,
    max_distance=2.0,
  ),
]

///|
pub fn DistanceModel::validate(
  self : DistanceModel,
) -> Unit raise DistanceModelError {
  let (ref_distance, rolloff_factor, max_distance) = match self {
    DistanceModel::None => (0.0, 0.0, 0.0)
    DistanceModel::Linear(ref_distance~, rolloff_factor~, max_distance~) => {
      if ref_distance == max_distance {
        raise DistanceModelError::InvalidModelParameter
      }
      (ref_distance, rolloff_factor, max_distance)
    }
    DistanceModel::LinearClamped(ref_distance~, rolloff_factor~, max_distance~) => {
      if ref_distance == max_distance {
        raise DistanceModelError::InvalidModelParameter
      }
      (ref_distance, rolloff_factor, max_distance)
    }
    DistanceModel::Inverse(ref_distance~, rolloff_factor~) => {
      if ref_distance <= 0.0 {
        raise DistanceModelError::InvalidModelParameter
      }
      (ref_distance, rolloff_factor, 0.0)
    }
    DistanceModel::InverseClamped(ref_distance~, rolloff_factor~, max_distance~) => {
      if ref_distance <= 0.0 {
        raise DistanceModelError::InvalidModelParameter
      }
      (ref_distance, rolloff_factor, max_distance)
    }
    DistanceModel::Exponential(ref_distance~, rolloff_factor~) => {
      if ref_distance <= 0.0 {
        raise DistanceModelError::InvalidModelParameter
      }
      (ref_distance, rolloff_factor, 0.0)
    }
    DistanceModel::ExponentialClamped(
      ref_distance~,
      rolloff_factor~,
      max_distance~
    ) => {
      if ref_distance <= 0.0 {
        raise DistanceModelError::InvalidModelParameter
      }
      (ref_distance, rolloff_factor, max_distance)
    }
  }
  if ref_distance < 0.0 {
    raise DistanceModelError::InvalidReferenceDistance
  }
  if rolloff_factor < 0.0 {
    raise DistanceModelError::InvalidRolloffFactor
  }
  if max_distance < 0.0 {
    raise DistanceModelError::InvalidMaxDistance
  }
}

///|
pub struct Envelope {
  attack_length : Int
  attack_level : Double
  fade_length : Int
  fade_level : Double
}

///|
pub fn Envelope::default() -> Envelope {
  { attack_length: 0, attack_level: 1.0, fade_length: 0, fade_level: 1.0 }
}

///|
pub fn Envelope::at(self : Envelope, ticks : Int, dur : Int) -> Double {
  if ticks < self.attack_length {
    self.attack_level +
    ticks.to_double() *
    (1.0 - self.attack_level) /
    self.attack_length.to_double()
  } else if ticks + self.fade_length > dur {
    1.0 +
    (ticks + self.fade_length - dur).to_double() *
    (self.fade_level - 1.0) /
    self.fade_length.to_double()
  } else {
    1.0
  }
}

///|
pub struct Replay {
  after : Int
  play_for : Int
  with_delay : Int
}

///|
pub fn Replay::default() -> Replay {
  { after: 0, play_for: 1, with_delay: 0 }
}

///|
pub fn Replay::at(self : Replay, ticks : Int) -> Double {
  if ticks >= self.play_for {
    0.0
  } else {
    1.0
  }
}

///|
pub struct Effect {
  gilrs : Gilrs
  mut gamepads : Array[GamepadId]
  strong : Double
  weak : Double
  duration_ms : Int64
  base_effects : Array[BaseEffect]
  mut repeat_mode : FfRepeat
  mut distance_model : DistanceModel
  mut position : (Double, Double, Double)
  mut gain : Double
}

///|
pub struct EffectBuilder {
  gamepads : Array[GamepadId]
  base_effects : Array[BaseEffect]
  mut repeat_mode : FfRepeat
  mut distance_model : DistanceModel
  mut position : (Double, Double, Double)
  mut gain : Double
  mut strong : Double
  mut weak : Double
  mut duration_ms : Int64
}

///|
pub fn EffectBuilder::new() -> EffectBuilder {
  {
    gamepads: [],
    base_effects: [],
    repeat_mode: FfRepeat::Infinitely,
    distance_model: DistanceModel::None,
    position: (0.0, 0.0, 0.0),
    gain: 1.0,
    strong: 0.0,
    weak: 0.0,
    duration_ms: 1000L,
  }
}

///|
fn contains_gamepad_id(ids : Array[GamepadId], id : GamepadId) -> Bool {
  for x in ids {
    if x.value() == id.value() {
      return true
    }
  }
  false
}

///|
pub fn EffectBuilder::gamepads(
  self : EffectBuilder,
  ids : Array[GamepadId],
) -> EffectBuilder {
  for id in ids {
    if !contains_gamepad_id(self.gamepads, id) {
      self.gamepads.push(id)
    }
  }
  self
}

///|
pub fn EffectBuilder::add_gamepad(
  self : EffectBuilder,
  gamepad : Gamepad,
) -> EffectBuilder {
  let id = gamepad.id()
  if !contains_gamepad_id(self.gamepads, id) {
    self.gamepads.push(id)
  }
  self
}

///|
pub fn EffectBuilder::add_gamepad_id(
  self : EffectBuilder,
  id : GamepadId,
) -> EffectBuilder {
  if !contains_gamepad_id(self.gamepads, id) {
    self.gamepads.push(id)
  }
  self
}

///|
pub fn EffectBuilder::rumble(
  self : EffectBuilder,
  strong : Double,
  weak : Double,
) -> EffectBuilder {
  self.strong = strong
  self.weak = weak
  self
}

///|
pub fn EffectBuilder::duration(
  self : EffectBuilder,
  duration_ms : Int64,
) -> EffectBuilder {
  self.duration_ms = duration_ms
  self
}

///|
pub fn EffectBuilder::add_effect(
  self : EffectBuilder,
  effect : BaseEffect,
) -> EffectBuilder {
  self.base_effects.push(effect)
  self
}

///|
pub fn EffectBuilder::repeat(
  self : EffectBuilder,
  repeat_mode : FfRepeat,
) -> EffectBuilder {
  self.repeat_mode = repeat_mode
  self
}

///|
pub fn EffectBuilder::distance_model(
  self : EffectBuilder,
  model : DistanceModel,
) -> EffectBuilder {
  self.distance_model = model
  self
}

///|
pub fn EffectBuilder::position(
  self : EffectBuilder,
  position : (Double, Double, Double),
) -> EffectBuilder {
  self.position = position
  self
}

///|
pub fn EffectBuilder::gain(
  self : EffectBuilder,
  gain : Double,
) -> EffectBuilder {
  self.gain = if gain < 0.0 { 0.0 } else { gain }
  self
}

///|
fn to_duration_int(ms : Int64) -> Int {
  if ms <= 0L {
    0
  } else if ms >= 0x7FFF_FFFFL {
    0x7FFF_FFFF
  } else {
    ms.to_int()
  }
}

///|
pub fn EffectBuilder::finish(
  self : EffectBuilder,
  gilrs : Gilrs,
) -> Effect raise FfError {
  if self.gamepads.length() == 0 {
    raise FfError::NoGamepads
  }
  // Validate: connected + ff supported.
  for id in self.gamepads {
    match gilrs.gamepad(id) {
      None => raise FfError::Disconnected(id)
      Some(gp) =>
        if !gp.is_connected() {
          raise FfError::Disconnected(id)
        } else if !gp.is_ff_supported() {
          raise FfError::FfNotSupported(id)
        }
    }
  }
  // Must have a native backend to talk to.
  match gilrs.backend {
    None => raise FfError::NotSupported
    Some(_) => ()
  }
  self.distance_model.validate() catch {
    err => raise FfError::InvalidDistanceModel(err)
  }
  {
    gilrs,
    gamepads: self.gamepads,
    strong: self.strong,
    weak: self.weak,
    duration_ms: self.duration_ms,
    base_effects: self.base_effects,
    repeat_mode: self.repeat_mode,
    distance_model: self.distance_model,
    position: self.position,
    gain: self.gain,
  }
}

///|
pub fn Effect::set_gamepads(
  self : Effect,
  ids : Array[GamepadId],
  gilrs : Gilrs,
) -> Unit raise FfError {
  let out : Array[GamepadId] = []
  for id in ids {
    match gilrs.connected_gamepad(id) {
      None => raise FfError::Disconnected(id)
      Some(gp) =>
        if !gp.is_ff_supported() {
          raise FfError::FfNotSupported(id)
        } else if !contains_gamepad_id(out, id) {
          out.push(id)
        }
    }
  }
  self.gamepads = out
}

///|
pub fn Effect::add_gamepad(
  self : Effect,
  gamepad : Gamepad,
) -> Unit raise FfError {
  let id = gamepad.id()
  if !gamepad.is_connected() {
    raise FfError::Disconnected(id)
  }
  if !gamepad.is_ff_supported() {
    raise FfError::FfNotSupported(id)
  }
  if !contains_gamepad_id(self.gamepads, id) {
    self.gamepads.push(id)
  }
}

///|
pub fn Effect::set_gain(self : Effect, gain : Double) -> Unit {
  self.gain = if gain < 0.0 { 0.0 } else { gain }
}

///|
pub fn Effect::set_repeat(self : Effect, repeat_mode : FfRepeat) -> Unit {
  self.repeat_mode = repeat_mode
}

///|
pub fn Effect::set_distance_model(
  self : Effect,
  model : DistanceModel,
) -> Unit raise FfError {
  model.validate() catch {
    err => raise FfError::InvalidDistanceModel(err)
  }
  self.distance_model = model
}

///|
pub fn Effect::set_position(
  self : Effect,
  position : (Double, Double, Double),
) -> Unit {
  self.position = position
}

///|
fn magnitude_to_amp(magnitude : Int) -> Double {
  clamp(magnitude.to_double() / 65535.0, 0.0, 1.0)
}

///|
fn base_effect_motor_amplitudes(effect : BaseEffect) -> (Double, Double) {
  match effect.kind {
    BaseEffectType::Weak(magnitude) => (0.0, magnitude_to_amp(magnitude))
    BaseEffectType::Strong(magnitude) => (magnitude_to_amp(magnitude), 0.0)
  }
}

///|
pub fn Effect::play(self : Effect) -> Unit raise FfError {
  match self.gilrs.backend {
    None => raise FfError::NotSupported
    Some(b) => {
      let dur = to_duration_int(self.duration_ms)
      let mut base_strong = 0.0
      let mut base_weak = 0.0
      for effect in self.base_effects {
        let (s, w) = base_effect_motor_amplitudes(effect)
        base_strong = base_strong + s
        base_weak = base_weak + w
      }
      ignore(self.repeat_mode)
      ignore(self.distance_model)
      ignore(self.position)
      let strong = clamp((self.strong + base_strong) * self.gain, 0.0, 1.0)
      let weak = clamp((self.weak + base_weak) * self.gain, 0.0, 1.0)
      for id in self.gamepads {
        let ok = b.set_rumble(id.value(), strong, weak, dur)
        if !ok {
          raise FfError::NotSupported
        }
      }
    }
  }
}

///|
pub fn Effect::stop(self : Effect) -> Unit raise FfError {
  match self.gilrs.backend {
    None => raise FfError::NotSupported
    Some(b) =>
      for id in self.gamepads {
        let ok = b.set_rumble(id.value(), 0.0, 0.0, 0)
        if !ok {
          raise FfError::NotSupported
        }
      }
  }
}