///|
/// A contiguous run of light or dark modules.
pub struct ModuleRun {
  dark : Bool
  width : Int
} derive(Eq, Debug)

///|
/// Basic structural statistics for a barcode module stream.
pub struct ModuleStats {
  module_count : Int
  dark_count : Int
  light_count : Int
  transitions : Int
  narrowest_run : Int
  widest_run : Int
} derive(Eq, Debug)

///|
pub fn module_runs(modules : Array[Bool]) -> Array[ModuleRun] {
  let out : Array[ModuleRun] = []
  if modules.is_empty() {
    return out
  }
  let mut dark = modules[0]
  let mut width = 1
  for i = 1; i < modules.length(); i = i + 1 {
    if modules[i] == dark {
      width = width + 1
    } else {
      out.push({ dark, width })
      dark = modules[i]
      width = 1
    }
  }
  out.push({ dark, width })
  out
}

///|
pub fn analyze_modules(modules : Array[Bool]) -> ModuleStats {
  let runs = module_runs(modules)
  let mut dark_count = 0
  let mut narrowest = if runs.is_empty() { 0 } else { runs[0].width }
  let mut widest = 0
  for run in runs {
    if run.dark {
      dark_count = dark_count + run.width
    }
    if run.width < narrowest {
      narrowest = run.width
    }
    if run.width > widest {
      widest = run.width
    }
  }
  {
    module_count: modules.length(),
    dark_count,
    light_count: modules.length() - dark_count,
    transitions: if runs.is_empty() {
      0
    } else {
      runs.length() - 1
    },
    narrowest_run: narrowest,
    widest_run: widest,
  }
}

///|
pub fn modules_to_binary(modules : Array[Bool]) -> String {
  let out = Buffer()
  for bit in modules {
    out.write_byte(if bit { b'1' } else { b'0' })
  }
  @utf8.decode_lossy(out.to_bytes())
}

///|
pub fn modules_from_binary(value : String) -> Result[Array[Bool], String] {
  if value.is_empty() {
    return Err("module string must not be empty")
  }
  let out : Array[Bool] = []
  let bytes = @utf8.encode(value)
  for i = 0; i < bytes.length(); i = i + 1 {
    match bytes[i] {
      b'0' => out.push(false)
      b'1' => out.push(true)
      _ => return Err("invalid module character at position \{i}")
    }
  }
  Ok(out)
}

///|
pub fn reverse_modules(modules : Array[Bool]) -> Array[Bool] {
  let out : Array[Bool] = []
  for i = modules.length() - 1; i >= 0; i = i - 1 {
    out.push(modules[i])
  }
  out
}

///|
pub fn invert_modules(modules : Array[Bool]) -> Array[Bool] {
  let out : Array[Bool] = []
  for bit in modules {
    out.push(!bit)
  }
  out
}

///|
pub fn add_quiet_zone(modules : Array[Bool], width : Int) -> Array[Bool] {
  let quiet = if width < 0 { 0 } else { width }
  let out : Array[Bool] = []
  for i = 0; i < quiet; i = i + 1 {
    out.push(false)
  }
  for bit in modules {
    out.push(bit)
  }
  for i = 0; i < quiet; i = i + 1 {
    out.push(false)
  }
  out
}

///|
pub fn trim_quiet_zone(modules : Array[Bool]) -> Array[Bool] {
  let mut start = 0
  while start < modules.length() && !modules[start] {
    start = start + 1
  }
  let mut end = modules.length()
  while end > start && !modules[end - 1] {
    end = end - 1
  }
  let out : Array[Bool] = []
  for i = start; i < end; i = i + 1 {
    out.push(modules[i])
  }
  out
}

///|
pub fn hamming_distance(left : Array[Bool], right : Array[Bool]) -> Int? {
  if left.length() != right.length() {
    return None
  }
  let mut distance = 0
  for i = 0; i < left.length(); i = i + 1 {
    if left[i] != right[i] {
      distance = distance + 1
    }
  }
  Some(distance)
}