///|
pub(all) struct ParsedStoichTerm {
  name : String
  formula : Formula
  coefficient : Double
} derive(Debug, Eq)

///|
pub(all) struct ParsedReaction {
  label : String
  reactants : Array[ParsedStoichTerm]
  products : Array[ParsedStoichTerm]
} derive(Debug, Eq)

///|
fn is_space(char : Char) -> Bool {
  char == ' ' || char == '\t' || char == '\r' || char == '\n'
}

///|
fn trim_string(text : String) -> String {
  let chars = text.to_array()
  let mut start = 0
  let mut finish = chars.length()
  while start < finish && is_space(chars[start]) {
    start += 1
  }
  while finish > start && is_space(chars[finish - 1]) {
    finish -= 1
  }
  let mut out = ""
  let mut index = start
  while index < finish {
    out = out + chars[index].to_string()
    index += 1
  }
  out
}

///|
fn slice_chars(text : String, start : Int, finish : Int) -> String {
  let chars = text.to_array()
  let mut out = ""
  let mut index = start
  while index < finish && index < chars.length() {
    out = out + chars[index].to_string()
    index += 1
  }
  out
}

///|
fn find_arrow(text : String) -> (Int?, Bool) {
  let chars = text.to_array()
  let mut found : Int? = None
  let mut multiple = false
  let mut index = 0
  while index + 1 < chars.length() {
    if chars[index] == '-' && chars[index + 1] == '>' {
      match found {
        Some(_) => multiple = true
        None => found = Some(index)
      }
    }
    index += 1
  }
  (found, multiple)
}

///|
fn first_non_space_column(text : String, base_column : Int) -> Int {
  let chars = text.to_array()
  let mut index = 0
  while index < chars.length() && is_space(chars[index]) {
    index += 1
  }
  base_column + index
}

///|
fn split_spaces(text : String) -> Array[String] {
  let chars = text.to_array()
  let parts : Array[String] = []
  let mut token = ""
  for char in chars {
    if is_space(char) {
      if token.length() > 0 {
        parts.push(token)
        token = ""
      }
    } else {
      token = token + char.to_string()
    }
  }
  if token.length() > 0 {
    parts.push(token)
  }
  parts
}

///|
fn parse_double_maybe(text : String) -> Double? {
  Some(@string.parse_double(text)) catch {
    _ => None
  }
}

///|
fn parse_term(
  text : String,
  base_column : Int,
) -> ParsedStoichTerm raise ThermoError {
  let trimmed = trim_string(text)
  if trimmed.length() == 0 {
    raise ThermoError::ParseError(
      line=1,
      column=base_column,
      message="expected species term",
    )
  }
  let tokens = split_spaces(trimmed)
  if tokens.length() > 2 {
    raise ThermoError::ParseError(
      line=1,
      column=base_column,
      message="expected optional coefficient followed by formula",
    )
  }
  let mut coefficient = 1.0
  let mut name = ""
  if tokens.length() == 1 {
    name = tokens[0]
  } else {
    match parse_double_maybe(tokens[0]) {
      Some(value) => {
        coefficient = value
        name = tokens[1]
      }
      None =>
        raise ThermoError::ParseError(
          line=1,
          column=base_column,
          message="invalid stoichiometric coefficient",
        )
    }
  }
  if coefficient <= 0.0 {
    raise ThermoError::ParseError(
      line=1,
      column=base_column,
      message="expected positive stoichiometric coefficient",
    )
  }
  let formula_column = base_column + trimmed.length() - name.length()
  let formula = parse_formula(name) catch {
    ThermoError::ParseError(column=local_column, message~, ..) =>
      raise ThermoError::ParseError(
        line=1,
        column=formula_column + local_column - 1,
        message~,
      )
    err => raise err
  }
  { name, formula, coefficient }
}

///|
fn parse_side(
  text : String,
  base_column : Int,
) -> Array[ParsedStoichTerm] raise ThermoError {
  let chars = text.to_array()
  let terms : Array[ParsedStoichTerm] = []
  let mut start = 0
  let mut index = 0
  while index <= chars.length() {
    if index == chars.length() || chars[index] == '+' {
      let piece = slice_chars(text, start, index)
      let column = first_non_space_column(piece, base_column + start)
      terms.push(parse_term(piece, column))
      start = index + 1
    }
    index += 1
  }
  if terms.length() == 0 {
    raise ThermoError::ParseError(
      line=1,
      column=base_column,
      message="expected species term",
    )
  }
  terms
}

///|
pub fn parse_reaction_equation(
  text : String,
) -> ParsedReaction raise ThermoError {
  let (arrow, multiple) = find_arrow(text)
  match arrow {
    Some(index) if !multiple => {
      let left = slice_chars(text, 0, index)
      let right = slice_chars(text, index + 2, text.length())
      let reactants = parse_side(left, 1)
      let products = parse_side(right, index + 3)
      { label: trim_string(text), reactants, products }
    }
    _ =>
      raise ThermoError::ParseError(
        line=1,
        column=1,
        message="expected exactly one reaction arrow",
      )
  }
}

///|
pub fn ParsedReaction::to_reaction(
  self : ParsedReaction,
  lookup : (String) -> Species raise ThermoError,
) -> Reaction raise ThermoError {
  let reactants : Array[StoichTerm] = []
  let products : Array[StoichTerm] = []
  for term in self.reactants {
    reactants.push(
      StoichTerm::new(species=lookup(term.name), coefficient=term.coefficient),
    )
  }
  for term in self.products {
    products.push(
      StoichTerm::new(species=lookup(term.name), coefficient=term.coefficient),
    )
  }
  Reaction::new(label=self.label, reactants~, products~)
}