// Scale system: maps data values to pixel coordinates.

///|
/// Trait for coordinate mapping from data space to pixel space.
pub trait Scale {
  /// Map a data value to a pixel coordinate within [0, plotSize].
  fn map(Self, Double) -> Double
  /// Generate tick positions and labels for this scale.
  fn ticks(Self, Int) -> Array[(Double, String)]
  /// The size (width or height) of the plot area in pixels.
  fn plotSize(Self) -> Double
}

// ──── LinearScale ────

///|
/// Linear numeric scale mapping [min, max] to [0, plotPixels].
pub struct LinearScale {
  min : Double
  max : Double
  plotPixels : Double
}

///|
/// Create a linear scale.
pub fn LinearScale::new(
  min : Double,
  max : Double,
  plotPixels : Double,
) -> LinearScale {
  { min, max, plotPixels }
}

///|
/// Map a data value to pixel position (linear interpolation).
pub fn LinearScale::map(self : LinearScale, value : Double) -> Double {
  (value - self.min) / (self.max - self.min) * self.plotPixels
}

///|
/// Inverse map: pixel position to data value.
pub fn LinearScale::unmap(self : LinearScale, pixel : Double) -> Double {
  pixel / self.plotPixels * (self.max - self.min) + self.min
}

///|
/// Generate nice tick positions using the 1/2/5×10^n algorithm.
pub fn LinearScale::ticks(
  self : LinearScale,
  desired : Int,
) -> Array[(Double, String)] {
  let range = self.max - self.min
  let roughStep = range / desired.to_double()
  // Snap to nice number: 1, 2, or 5 × 10^n
  let magnitude = pow10(orderOfMagnitude(roughStep))
  let ratio = roughStep / magnitude
  let niceStep = if ratio < 1.5 {
    magnitude
  } else if ratio < 3.0 {
    2.0 * magnitude
  } else if ratio < 7.0 {
    5.0 * magnitude
  } else {
    10.0 * magnitude
  }
  // Start at the first nice tick >= min
  let start = Double::ceil(self.min / niceStep) * niceStep
  // Generate ticks
  let ticks : Array[(Double, String)] = []
  let mut val = start
  while val <= self.max + niceStep * 0.5 {
    let pixel = self.map(val)
    let label = formatTick(val)
    ticks.push((pixel, label))
    val = val + niceStep
  }
  ticks
}

///|
/// Return the plot size (width or height) in pixels.
pub fn LinearScale::plotSize(self : LinearScale) -> Double {
  self.plotPixels
}

// ──── LogScale ────

///|
/// Logarithmic scale mapping [min, max] to [0, plotPixels].
pub struct LogScale {
  base : Double
  min : Double
  max : Double
  plotPixels : Double
}

///|
/// Create a logarithmic scale.
pub fn LogScale::new(
  base : Double,
  min : Double,
  max : Double,
  plotPixels : Double,
) -> LogScale {
  { base, min, max, plotPixels }
}

///|
/// Map data value to pixel using log mapping.
pub fn LogScale::map(self : LogScale, value : Double) -> Double {
  let logMin = logBase(self.base, self.min)
  let logMax = logBase(self.base, self.max)
  let logVal = logBase(self.base, value)
  (logVal - logMin) / (logMax - logMin) * self.plotPixels
}

///|
/// Generate ticks at powers of the base.
pub fn LogScale::ticks(
  self : LogScale,
  _desired : Int,
) -> Array[(Double, String)] {
  let logMin = Double::ceil(logBase(self.base, self.min))
  let logMax = Double::floor(logBase(self.base, self.max))
  let ticks : Array[(Double, String)] = []
  let mut exp = logMin
  while exp <= logMax {
    let val = @math.pow(self.base, exp)
    if val >= self.min && val <= self.max {
      let pixel = self.map(val)
      let label = formatTick(val)
      ticks.push((pixel, label))
    }
    exp = exp + 1.0
  }
  ticks
}

///|
/// Return the plot size (width or height) in pixels.
pub fn LogScale::plotSize(self : LogScale) -> Double {
  self.plotPixels
}

// ──── CategoryScale ────

///|
/// Category/band scale: maps data x-values to band centers.
/// Supports explicit tick positions/labels and auto-inference.
pub struct CategoryScale {
  /// Tick positions in data coordinates.
  tickPositions : Array[Double]
  /// Tick labels for display.
  tickLabels : Array[String]
  /// Plot width in pixels.
  plotPixels : Double
  /// Band width (for bar/box dodging).
  bandWidth : Double
  /// Minimum data x value (for continuous mapping).
  dataMin : Double
  /// Maximum data x value (for continuous mapping).
  dataMax : Double
}

///|
/// Create a category scale from tick positions and labels.
/// When positions is empty, auto-inference is used (handled by layout).
pub fn CategoryScale::new(
  tickPositions : Array[Double],
  tickLabels : Array[String],
  plotPixels : Double,
) -> CategoryScale {
  let n = tickPositions.length()
  // Compute band width based on minimum gap between adjacent positions.
  // This correctly handles non-uniform tick spacing.
  let bandWidth = if n <= 1 {
    plotPixels
  } else {
    let mut minGap = tickPositions[1] - tickPositions[0]
    for i = 1; i < n - 1; i = i + 1 {
      let gap = tickPositions[i + 1] - tickPositions[i]
      if gap < minGap {
        minGap = gap
      }
    }
    if minGap <= 0.0 {
      minGap = 1.0
    }
    // Convert min data gap to pixel space via linear scaling
    let dataRange = tickPositions[n - 1] - tickPositions[0]
    if dataRange > 0.0 {
      minGap / dataRange * plotPixels
    } else {
      plotPixels
    }
  }
  let (dataMin, dataMax) = if n == 0 {
    (0.0, 1.0)
  } else if n == 1 {
    (tickPositions[0] - 0.5, tickPositions[0] + 0.5)
  } else {
    let halfStep = (tickPositions[n - 1] - tickPositions[0]) /
      (n.to_double() - 1.0) /
      2.0
    (tickPositions[0] - halfStep, tickPositions[n - 1] + halfStep)
  }
  { tickPositions, tickLabels, plotPixels, bandWidth, dataMin, dataMax }
}

///|
/// Map a data value to pixel using linear interpolation.
pub fn CategoryScale::map(self : CategoryScale, value : Double) -> Double {
  let range = self.dataMax - self.dataMin
  if range == 0.0 {
    self.plotPixels / 2.0
  } else {
    (value - self.dataMin) / range * self.plotPixels
  }
}

///|
/// Inverse map: pixel position to data value.
pub fn CategoryScale::unmap(self : CategoryScale, pixel : Double) -> Double {
  let range = self.dataMax - self.dataMin
  if range == 0.0 {
    self.dataMin
  } else {
    pixel / self.plotPixels * range + self.dataMin
  }
}

///|
/// Find the band center pixel for a data value.
/// Uses data-coordinate mapping to correctly handle non-uniform tick spacing.
pub fn CategoryScale::bandCenter(
  self : CategoryScale,
  value : Double,
) -> Double {
  self.map(value)
}

///|
/// Get the band width (for bar width calculations).
pub fn CategoryScale::getBandWidth(self : CategoryScale) -> Double {
  self.bandWidth
}

///|
/// Number of tick positions.
pub fn CategoryScale::count(self : CategoryScale) -> Int {
  self.tickPositions.length()
}

///|
/// Get tick label by index.
pub fn CategoryScale::label(self : CategoryScale, index : Int) -> String {
  if index >= 0 && index < self.tickLabels.length() {
    self.tickLabels[index]
  } else {
    ""
  }
}

///|
/// Get tick position by index.
pub fn CategoryScale::position(self : CategoryScale, index : Int) -> Double {
  if index >= 0 && index < self.tickPositions.length() {
    self.tickPositions[index]
  } else {
    0.0
  }
}

///|
/// Generate tick marks at data-coordinate positions.
/// Tick positions are mapped via data-coordinate interpolation (not index-based).
pub fn CategoryScale::ticks(
  self : CategoryScale,
  _desired : Int,
) -> Array[(Double, String)] {
  let n = self.tickPositions.length()
  if n == 0 {
    return []
  }
  let ticks : Array[(Double, String)] = []
  for i = 0; i < n; i = i + 1 {
    let pixel = self.map(self.tickPositions[i])
    let label = if i < self.tickLabels.length() {
      self.tickLabels[i]
    } else {
      self.tickPositions[i].to_string()
    }
    ticks.push((pixel, label))
  }
  ticks
}

///|
/// Return the plot size (width or height) in pixels.
pub fn CategoryScale::plotSize(self : CategoryScale) -> Double {
  self.plotPixels
}

///|
/// Get min/max data range for the category scale.
pub fn CategoryScale::dataRange(self : CategoryScale) -> (Double, Double) {
  (self.dataMin, self.dataMax)
}

// ──── Helper functions ────

///|
/// Compute log with arbitrary base: log_base(x) = ln(x) / ln(base).
fn logBase(base : Double, x : Double) -> Double {
  @math.ln(x) / @math.ln(base)
}

///|
/// 10 raised to power n using math.pow.
fn pow10(n : Double) -> Double {
  @math.pow(10.0, n)
}

///|
/// Order of magnitude (floor of log10).
fn orderOfMagnitude(x : Double) -> Double {
  Double::floor(@math.log10(x))
}

///|
/// Format a tick value to a nice string (avoid trailing zeros).
fn formatTick(value : Double) -> String {
  if value == Double::round(value) {
    Double::round(value).to_string()
  } else {
    value.to_string()
  }
}