///|
/// A deterministic pseudo-random source used by benchmarks and reproducible tests.
pub struct DeterministicRng {
  mut state : Int64
}

///|
pub fn DeterministicRng::new(
  seed? : Int64 = 88172645463393265L,
) -> DeterministicRng {
  { state: if seed == 0L { 88172645463393265L } else { seed } }
}

///|
pub fn DeterministicRng::next(self : DeterministicRng) -> Int64 {
  self.state = (self.state * 2862933555777941757L + 3037000493L) %
    9223372036854775783L
  if self.state < 0L {
    self.state = -self.state
  }
  self.state
}

///|
pub fn DeterministicRng::unit(self : DeterministicRng) -> Double {
  self.next().to_double() / 9223372036854775783.0
}

///|
pub fn DeterministicRng::symmetric(self : DeterministicRng) -> Double {
  self.unit() * 2.0 - 1.0
}

///|
pub fn DeterministicRng::normalish(
  self : DeterministicRng,
  amplitude? : Double = 1.0,
) -> Double {
  let first = if self.unit() < 1.0e-12 { 1.0e-12 } else { self.unit() }
  let second = self.unit()
  amplitude * (first + second - 1.0) * 1.7320508075688772
}

///|
pub(all) enum SignalPattern {
  Stable
  MeanShift
  VarianceShift
  Trend
  Spike
  MeanAndVarianceShift
}

///|
/// A scenario definition shared by tests, examples, and benchmarks.
pub struct SignalScenario {
  name : String
  length : Int
  change_at : Int
  baseline : Double
  shift : Double
  noise : Double
  post_noise : Double
  trend : Double
  pattern : SignalPattern
  seed : Int64
}

///|
pub fn SignalScenario::stable(
  length? : Int = 256,
  baseline? : Double = 0.0,
  noise? : Double = 0.1,
  seed? : Int64 = 17L,
) -> SignalScenario {
  {
    name: "stable",
    length: if length < 1 {
      1
    } else {
      length
    },
    change_at: length,
    baseline,
    shift: 0.0,
    noise: if noise < 0.0 {
      0.0
    } else {
      noise
    },
    post_noise: noise,
    trend: 0.0,
    pattern: Stable,
    seed,
  }
}

///|
pub fn SignalScenario::mean_shift(
  length? : Int = 256,
  change_at? : Int = 128,
  baseline? : Double = 0.0,
  shift? : Double = 2.0,
  noise? : Double = 0.1,
  seed? : Int64 = 17L,
) -> SignalScenario {
  {
    name: "mean-shift",
    length: if length < 1 {
      1
    } else {
      length
    },
    change_at: if change_at < 0 {
      0
    } else if change_at > length {
      length
    } else {
      change_at
    },
    baseline,
    shift,
    noise: if noise < 0.0 {
      0.0
    } else {
      noise
    },
    post_noise: noise,
    trend: 0.0,
    pattern: MeanShift,
    seed,
  }
}

///|
pub fn SignalScenario::variance_shift(
  length? : Int = 256,
  change_at? : Int = 128,
  baseline? : Double = 0.0,
  noise? : Double = 0.1,
  post_noise? : Double = 0.8,
  seed? : Int64 = 17L,
) -> SignalScenario {
  {
    name: "variance-shift",
    length: if length < 1 {
      1
    } else {
      length
    },
    change_at: if change_at < 0 {
      0
    } else if change_at > length {
      length
    } else {
      change_at
    },
    baseline,
    shift: 0.0,
    noise: if noise < 0.0 {
      0.0
    } else {
      noise
    },
    post_noise: if post_noise < 0.0 {
      0.0
    } else {
      post_noise
    },
    trend: 0.0,
    pattern: VarianceShift,
    seed,
  }
}

///|
pub fn SignalScenario::trend(
  length? : Int = 256,
  change_at? : Int = 64,
  baseline? : Double = 0.0,
  trend? : Double = 0.03,
  noise? : Double = 0.05,
  seed? : Int64 = 17L,
) -> SignalScenario {
  {
    name: "trend",
    length: if length < 1 {
      1
    } else {
      length
    },
    change_at: if change_at < 0 {
      0
    } else if change_at > length {
      length
    } else {
      change_at
    },
    baseline,
    shift: 0.0,
    noise: if noise < 0.0 {
      0.0
    } else {
      noise
    },
    post_noise: noise,
    trend,
    pattern: Trend,
    seed,
  }
}

///|
pub fn SignalScenario::spike(
  length? : Int = 256,
  spike_at? : Int = 128,
  baseline? : Double = 0.0,
  spike? : Double = 5.0,
  noise? : Double = 0.1,
  seed? : Int64 = 17L,
) -> SignalScenario {
  {
    name: "spike",
    length: if length < 1 {
      1
    } else {
      length
    },
    change_at: if spike_at < 0 {
      0
    } else if spike_at > length - 1 {
      length - 1
    } else {
      spike_at
    },
    baseline,
    shift: spike,
    noise: if noise < 0.0 {
      0.0
    } else {
      noise
    },
    post_noise: noise,
    trend: 0.0,
    pattern: Spike,
    seed,
  }
}

///|
pub fn SignalScenario::mean_and_variance(
  length? : Int = 256,
  change_at? : Int = 128,
  baseline? : Double = 0.0,
  shift? : Double = 2.0,
  noise? : Double = 0.1,
  post_noise? : Double = 0.6,
  seed? : Int64 = 17L,
) -> SignalScenario {
  {
    name: "mean-and-variance",
    length: if length < 1 {
      1
    } else {
      length
    },
    change_at: if change_at < 0 {
      0
    } else if change_at > length {
      length
    } else {
      change_at
    },
    baseline,
    shift,
    noise: if noise < 0.0 {
      0.0
    } else {
      noise
    },
    post_noise: if post_noise < 0.0 {
      0.0
    } else {
      post_noise
    },
    trend: 0.0,
    pattern: MeanAndVarianceShift,
    seed,
  }
}

///|
/// Generates a deterministic signal and its known change indices.
pub fn generate_signal(scenario : SignalScenario) -> Array[Double] {
  let rng = DeterministicRng::new(seed=scenario.seed)
  let result = Array::make(scenario.length, 0.0)
  for i in 0..= scenario.change_at
    let base = match scenario.pattern {
      Stable => scenario.baseline
      MeanShift =>
        if post_change {
          scenario.baseline + scenario.shift
        } else {
          scenario.baseline
        }
      VarianceShift => scenario.baseline
      Trend =>
        scenario.baseline +
        (if post_change {
          (i - scenario.change_at).to_double() * scenario.trend
        } else {
          0.0
        })
      Spike =>
        if i == scenario.change_at {
          scenario.baseline + scenario.shift
        } else {
          scenario.baseline
        }
      MeanAndVarianceShift =>
        if post_change {
          scenario.baseline + scenario.shift
        } else {
          scenario.baseline
        }
    }
    let amplitude = match scenario.pattern {
      VarianceShift =>
        if post_change {
          scenario.post_noise
        } else {
          scenario.noise
        }
      MeanAndVarianceShift =>
        if post_change {
          scenario.post_noise
        } else {
          scenario.noise
        }
      _ => scenario.noise
    }
    result[i] = base + rng.normalish(amplitude~)
  }
  result
}

///|
pub fn scenario_truth(scenario : SignalScenario) -> Array[Int] {
  match scenario.pattern {
    Stable => []
    Spike => [scenario.change_at]
    _ =>
      if scenario.change_at <= 0 || scenario.change_at >= scenario.length {
        []
      } else {
        [scenario.change_at]
      }
  }
}

///|
/// Converts a signal to timestamped points.
pub fn timestamped_signal(
  values : Array[Double],
  start? : Int64 = 0L,
  step? : Int64 = 1L,
) -> Array[SignalPoint] {
  let result : Array[SignalPoint] = []
  for i, value in values {
    result.push(
      SignalPoint::new(start + i.to_int64() * step, value, sequence=i),
    )
  }
  result
}

///|
/// Applies deterministic missingness without changing the length of the sample.
pub fn mask_every(
  values : Array[Double],
  period : Int,
  offset? : Int = 0,
) -> Array[Double] {
  let result : Array[Double] = []
  let safe_period = if period < 1 { 1 } else { period }
  for i, value in values {
    if (i - offset) % safe_period == 0 {
      result.push(0.0 / 0.0)
    } else {
      result.push(value)
    }
  }
  result
}

///|
/// Reorders every `period`-th point to model a small late-data burst.
pub fn reorder_periodic(
  points : Array[SignalPoint],
  period : Int,
) -> Array[SignalPoint] {
  let result : Array[SignalPoint] = []
  let safe_period = if period < 2 { 2 } else { period }
  let mut i = 0
  while i < points.length() {
    if i + 1 < points.length() && i % safe_period == 0 {
      result.push(points[i + 1])
      result.push(points[i])
      i += 2
    } else {
      result.push(points[i])
      i += 1
    }
  }
  result
}

///|
/// Returns a deterministic checksum useful for detecting accidental benchmark dead-code elimination.
pub fn signal_checksum(values : Array[Double]) -> Double {
  let mut checksum = 0.0
  for i, value in values {
    if is_finite(value) {
      checksum += (i + 1).to_double() * value
    }
  }
  checksum
}