///|
fn words(line : String) -> Array[String] {
  line
  .split(" ")
  .filter(fn(x) { x != "" })
  .map(fn(x) { x.to_owned() })
  .collect()
}

///|
fn parse_time(value : String) -> Result[Int64, ParseError] {
  if value.trim().to_owned() == "" {
    return Err(InvalidTime(value))
  }
  let mut result : Int64 = 0L
  for char in value {
    if !char.is_ascii_digit() {
      return Err(InvalidTime(value))
    }
    result = result * 10L + (char.to_int().to_int64() - 48L)
  }
  Ok(result)
}

///|
fn parse_width(value : String) -> Int? {
  if value == "" {
    return None
  }
  let mut result = 0
  for char in value {
    if !char.is_ascii_digit() {
      return None
    }
    result = result * 10 + char.to_int() - 48
  }
  Some(result)
}

///|
fn parse_value(line : String, time : Int64) -> Result[Change, ParseError] {
  let fields : Array[String] = line
    .split(" ")
    .filter(fn(x) { x != "" })
    .map(fn(x) { x.to_owned() })
    .collect()
  if fields.length() == 0 {
    return Err(InvalidValue(line))
  }
  let token = fields[0]
  let value = token.get_char(0).unwrap()
  if value == '0' ||
    value == '1' ||
    value == 'x' ||
    value == 'X' ||
    value == 'z' ||
    value == 'Z' {
    if token.length() <= 1 {
      Err(InvalidValue(line))
    } else {
      Ok({
        time,
        id: token.get_view(start=1).unwrap().to_owned(),
        value: Scalar(value),
      })
    }
  } else if value == 'b' || value == 'B' {
    if fields.length() < 2 || token.length() <= 1 {
      if token.length() <= 1 {
        return Err(UnsupportedValue(line))
      }
      Err(InvalidValue(line))
    } else {
      Ok({
        time,
        id: fields[1],
        value: Vector(token.get_view(start=1).unwrap().to_owned()),
      })
    }
  } else if value == 'r' || value == 'R' {
    if fields.length() < 2 || token.length() <= 1 {
      if token.length() <= 1 {
        return Err(UnsupportedValue(line))
      }
      Err(InvalidValue(line))
    } else {
      match parse_real(token.get_view(start=1).unwrap().to_owned()) {
        Some(number) => Ok({ time, id: fields[1], value: Real(number) })
        None => Err(InvalidValue(line))
      }
    }
  } else {
    Err(InvalidValue(line))
  }
}

///|
fn parse_real(value : String) -> Double? {
  let text = value.trim().to_owned()
  if text == "" {
    return None
  }
  let mut position = 0
  let mut negative = false
  let mut seen_digit = false
  let mut after_decimal = false
  let mut whole = 0.0
  let mut fraction = 0.0
  let mut divisor = 10.0
  for char in text {
    if position == 0 && (char == '-' || char == '+') {
      negative = char == '-'
    } else if char == '.' && !after_decimal {
      after_decimal = true
    } else if char.is_ascii_digit() {
      seen_digit = true
      let digit = (char.to_int() - 48).to_double()
      if after_decimal {
        fraction += digit / divisor
        divisor *= 10.0
      } else {
        whole = whole * 10.0 + digit
      }
    } else {
      return None
    }
    position += 1
  }
  if !seen_digit {
    None
  } else if negative {
    Some(0.0 - whole - fraction)
  } else {
    Some(whole + fraction)
  }
}

///|
/// Parse a VCD document held in memory.
pub fn parse(input : String) -> Result[Trace, ParseError] {
  if input.trim().to_owned() == "" {
    return Err(EmptyInput)
  }
  let lines : Array[String] = input
    .split("\n")
    .map(fn(x) { x.to_owned() })
    .collect()
  let mut date : String? = None
  let mut version : String? = None
  let mut timescale : String? = None
  let mut comment : String? = None
  let scope : Array[String] = []
  let signals : Array[Signal] = []
  let changes : Array[Change] = []
  let mut time : Int64 = 0L
  let mut definitions = true
  let mut saw_enddefinitions = false
  let mut i = 0
  while i < lines.length() {
    let line = lines[i].trim().to_owned()
    if line == "" {
      i += 1
      continue
    }
    let ws = words(line)
    if definitions {
      if line.has_prefix("$date") {
        date = Some(
          line
          .replace(old="$date", new="")
          .replace(old="$end", new="")
          .trim()
          .to_owned(),
        )
      } else if line.has_prefix("$version") {
        version = Some(
          line
          .replace(old="$version", new="")
          .replace(old="$end", new="")
          .trim()
          .to_owned(),
        )
      } else if line.has_prefix("$timescale") {
        timescale = Some(
          line
          .replace(old="$timescale", new="")
          .replace(old="$end", new="")
          .trim()
          .to_owned(),
        )
      } else if line.has_prefix("$comment") {
        comment = Some(
          line
          .replace(old="$comment", new="")
          .replace(old="$end", new="")
          .trim()
          .to_owned(),
        )
      } else if line.has_prefix("$scope") {
        if ws.length() < 3 {
          return Err(InvalidDirective(line))
        }
        scope.push(ws[2])
      } else if line.has_prefix("$upscope") {
        if scope.length() == 0 {
          return Err(ScopeUnderflow)
        }
        ignore(scope.pop())
      } else if line.has_prefix("$var") {
        if ws.length() < 5 {
          return Err(InvalidVariable(line))
        }
        let width = match parse_width(ws[2]) {
          Some(value) if value > 0 => value
          _ => return Err(InvalidVariable(line))
        }
        if signals.any(fn(signal) { signal.id == ws[3] }) {
          return Err(DuplicateIdentifier(ws[3]))
        }
        signals.push({
          kind: ws[1],
          width,
          id: ws[3],
          name: ws[4],
          scope: scope.copy(),
        })
      } else if line.has_prefix("$enddefinitions") {
        definitions = false
        saw_enddefinitions = true
      }
    } else if line == "$dumpvars" ||
      line == "$end" ||
      line == "$dumpon" ||
      line == "$dumpoff" ||
      line == "$dumpall" ||
      line == "$dumpportsall" {
      // VCD control blocks delimit value assignments but do not change state.
      ()
    } else if line.has_prefix("#") {
      match parse_time(line.get_view(start=1).unwrap().trim().to_owned()) {
        Ok(n) => time = n
        Err(e) => return Err(e)
      }
    } else {
      match parse_value(line, time) {
        Ok(change) => changes.push(change)
        Err(e) => return Err(e)
      }
    }
    i += 1
  }
  if !saw_enddefinitions {
    return Err(MissingEnddefinitions)
  }
  for change in changes {
    if !signals.any(fn(signal) { signal.id == change.id }) {
      return Err(UnknownIdentifier(change.id))
    }
  }
  Ok({
    header: { date, version, timescale, comment },
    signals,
    changes,
    end_time: time,
  })
}