///|
/// A normalized VCD time scale.
pub struct TimeScale {
  magnitude : Int
  unit : String
  femtoseconds_per_tick : Int64
  source : String
} derive(Debug, Eq)

///|
/// Parse a VCD timescale such as `1ns`, `10 ps`, or `100fs`.
pub fn parse_timescale(text : String) -> Result[TimeScale, ParseError] {
  let parts = text
    .trim()
    .to_owned()
    .split(" ")
    .filter(fn(item) { item != "" })
    .map(fn(item) { item.to_owned() })
    .to_array()
  let compact = parts.join("")
  let split = split_timescale_number(compact)
  match split {
    Some((number, unit)) => {
      let magnitude = parse_positive_decimal(number)
      let factor = unit_femtoseconds(unit)
      match (magnitude, factor) {
        (Some(amount), Some(scale)) if amount > 0 =>
          Ok({
            magnitude: amount,
            unit,
            femtoseconds_per_tick: amount.to_int64() * scale,
            source: text,
          })
        _ => Err(InvalidDirective("invalid timescale: " + text))
      }
    }
    None => Err(InvalidDirective("invalid timescale: " + text))
  }
}

///|
/// Parse the header timescale, falling back to one nanosecond for absent data.
pub fn Trace::time_scale(self : Trace) -> TimeScale {
  match self.header.timescale {
    Some(text) =>
      match parse_timescale(text) {
        Ok(scale) => scale
        Err(_) => default_timescale()
      }
    None => default_timescale()
  }
}

///|
/// Return the physical duration of a tick as femtoseconds.
pub fn TimeScale::tick_femtoseconds(self : TimeScale) -> Int64 {
  self.femtoseconds_per_tick
}

///|
/// Convert a VCD timestamp to femtoseconds.
pub fn TimeScale::to_femtoseconds(self : TimeScale, ticks : Int64) -> Int64 {
  ticks * self.femtoseconds_per_tick
}

///|
/// Convert a femtosecond duration to the closest whole VCD tick.
pub fn TimeScale::from_femtoseconds(
  self : TimeScale,
  duration : Int64,
) -> Int64 {
  if self.femtoseconds_per_tick <= 0L {
    0L
  } else {
    duration / self.femtoseconds_per_tick
  }
}

///|
/// Return the original spelling used for this scale.
pub fn TimeScale::source_text(self : TimeScale) -> String {
  self.source
}

///|
/// Return a canonical compact spelling.
pub fn TimeScale::display(self : TimeScale) -> String {
  self.magnitude.to_string() + self.unit
}

///|
/// Convert a trace timestamp into seconds.
pub fn timestamp_seconds(trace : Trace, ticks : Int64) -> Double {
  trace.time_scale().to_femtoseconds(ticks).to_double() /
  1_000_000_000_000_000.0
}

///|
/// Return the physical duration between two timestamps.
pub fn duration_seconds(trace : Trace, start : Int64, end : Int64) -> Double {
  timestamp_seconds(trace, Int64::max(start, end) - start)
}

///|
fn default_timescale() -> TimeScale {
  { magnitude: 1, unit: "ns", femtoseconds_per_tick: 1_000_000L, source: "1ns" }
}

///|
fn split_timescale_number(text : String) -> (String, String)? {
  if text == "" {
    return None
  }
  let mut index = 0
  for char in text {
    if char >= '0' && char <= '9' {
      index += 1
    } else {
      break
    }
  }
  if index == 0 || index == text.length() {
    None
  } else {
    Some(
      (
        text.unsafe_substring(start=0, end=index),
        text.unsafe_substring(start=index, end=text.length()).to_lower(),
      ),
    )
  }
}

///|
fn parse_positive_decimal(text : String) -> Int? {
  if text == "" {
    return None
  }
  let mut result = 0
  for char in text {
    if char < '0' || char > '9' {
      return None
    }
    result = result * 10 + char.to_int() - 48
  }
  Some(result)
}

///|
fn unit_femtoseconds(unit : String) -> Int64? {
  match unit {
    "s" => Some(1_000_000_000_000_000L)
    "ms" => Some(1_000_000_000_000L)
    "us" => Some(1_000_000_000L)
    "ns" => Some(1_000_000L)
    "ps" => Some(1_000L)
    "fs" => Some(1L)
    _ => None
  }
}

///|
/// Convert a timestamp range to seconds while preserving order.
pub fn range_seconds(trace : Trace, range : (Int64, Int64)) -> (Double, Double) {
  (timestamp_seconds(trace, range.0), timestamp_seconds(trace, range.1))
}

///|
/// Return a scale-aware duration report.
pub fn duration_report(trace : Trace, start : Int64, end : Int64) -> String {
  let ticks = Int64::max(start, end) - Int64::min(start, end)
  let scale = trace.time_scale()
  "duration=" +
  ticks.to_string() +
  " ticks (" +
  scale.to_femtoseconds(ticks).to_string() +
  " fs)"
}