///|
fn Cursor::expect_char(
  self : Cursor,
  expected_character : Char,
) -> Unit raise ParseError {
  self.skip_space()
  match self.advance() {
    Some(character) if character == expected_character => ()
    Some(character) =>
      raise ParseError::UnexpectedCharacter(
        position=self.position(),
        character~,
      )
    None => raise unexpected_end(self.offset, self.line, self.column)
  }
}

///|
fn Cursor::expect_symbol(
  self : Cursor,
  expected_symbol : String,
) -> Unit raise ParseError {
  let actual = self.read_symbol()
  if actual != expected_symbol {
    raise expected(self.offset, self.line, self.column, expected_symbol)
  }
}

///|
fn Cursor::read_step_string(self : Cursor) -> String raise ParseError {
  self.skip_space()
  match self.advance() {
    Some('\'') => ()
    Some(character) =>
      raise ParseError::UnexpectedCharacter(
        position=self.position(),
        character~,
      )
    None => raise unexpected_end(self.offset, self.line, self.column)
  }
  let output = StringBuilder()
  for ;; {
    match self.advance() {
      None => raise unexpected_end(self.offset, self.line, self.column)
      Some('\\') => {
        output.write_char('\\')
        match self.advance() {
          None => raise unexpected_end(self.offset, self.line, self.column)
          Some(character) => output.write_char(character)
        }
      }
      Some('\'') =>
        match self.peek() {
          Some('\'') => {
            ignore(self.advance())
            output.write_char('\'')
          }
          _ => return decode_step_string(output.to_string())
        }
      Some(character) => output.write_char(character)
    }
  }
}

///|
fn Cursor::read_number_text(self : Cursor) -> String raise ParseError {
  self.skip_space()
  let start = self.offset
  match self.peek() {
    Some('-' | '+') => ignore(self.advance())
    _ => ()
  }
  let mut saw_digit = false
  for ;; {
    match self.peek() {
      Some('0'..='9' | '.' | 'e' | 'E' | '+' | '-') => {
        let character = self.peek()
        if character is Some('0'..='9') {
          saw_digit = true
        }
        ignore(self.advance())
      }
      _ => break
    }
  }
  if !saw_digit {
    raise ParseError::InvalidNumber(
      position=self.position(),
      text=self.source[start:self.offset].to_owned(),
    )
  }
  self.source[start:self.offset].to_owned()
}

///|
fn Cursor::read_value(self : Cursor) -> Value raise ParseError {
  self.skip_space()
  match self.peek() {
    Some('$') => {
      ignore(self.advance())
      Null
    }
    Some('*') => {
      ignore(self.advance())
      Omitted
    }
    Some('#') => {
      ignore(self.advance())
      let position = self.position()
      let text = self.read_number_text()
      Reference(parse_int64(text, position).to_int())
    }
    Some('\'') => String(self.read_step_string())
    Some('"') => {
      ignore(self.advance())
      let text = self.read_until('"')
      Binary(text)
    }
    Some('.') => {
      ignore(self.advance())
      let value = self.read_symbol()
      self.expect_char('.')
      Enumeration(value)
    }
    Some('(') => self.read_list()
    Some('+' | '-' | '0'..='9') => {
      let position = self.position()
      let text = self.read_number_text()
      if text.contains_char('.') ||
        text.contains_char('e') ||
        text.contains_char('E') {
        Real(parse_double(text, position))
      } else {
        Integer(parse_int64(text, position))
      }
    }
    Some(_) => {
      let name = self.read_symbol()
      self.skip_space()
      match self.peek() {
        Some('(') => {
          ignore(self.advance())
          let inner = self.read_value()
          self.expect_char(')')
          Typed(name~, value=inner)
        }
        _ => Enumeration(name)
      }
    }
    None => raise unexpected_end(self.offset, self.line, self.column)
  }
}

///|
fn Cursor::read_list(self : Cursor) -> Value raise ParseError {
  self.expect_char('(')
  let values = Array::new()
  self.skip_space()
  if self.peek() is Some(')') {
    ignore(self.advance())
    return List(values)
  }
  for ;; {
    values.push(self.read_value())
    self.skip_space()
    match self.advance() {
      Some(',') => ()
      Some(')') => break
      Some(character) =>
        raise ParseError::UnexpectedCharacter(
          position=self.position(),
          character~,
        )
      None => raise unexpected_end(self.offset, self.line, self.column)
    }
  }
  List(values)
}

///|
fn Cursor::read_statement(
  self : Cursor,
) -> (String, Array[Value]) raise ParseError {
  let name = self.read_symbol()
  self.expect_char('(')
  let values = Array::new()
  self.skip_space()
  if self.peek() is Some(')') {
    ignore(self.advance())
  } else {
    for ;; {
      values.push(self.read_value())
      self.skip_space()
      match self.advance() {
        Some(',') => ()
        Some(')') => break
        Some(character) =>
          raise ParseError::UnexpectedCharacter(
            position=self.position(),
            character~,
          )
        None => raise unexpected_end(self.offset, self.line, self.column)
      }
    }
  }
  self.expect_char(';')
  (name, values)
}

///|
fn values_to_strings(value : Value) -> Array[String] {
  match value {
    List(values) =>
      values.fold(init=Array::new(), (result, item) => {
        match item.as_string() {
          Some(text) => {
            result.push(text)
            result
          }
          None => result
        }
      })
    _ => []
  }
}

///|
fn value_string(value : Value?) -> String? {
  match value {
    Some(item) => item.as_string()
    None => None
  }
}

///|
fn parse_header_section(cursor : Cursor) -> Header raise ParseError {
  let mut description : Array[String] = []
  let mut file_name : String? = None
  let mut time_stamp : String? = None
  let mut authors : Array[String] = []
  let mut organizations : Array[String] = []
  let mut preprocessor_version : String? = None
  let mut originating_system : String? = None
  let mut authorization : String? = None
  let mut schema_identifiers : Array[String] = []
  for ;; {
    let name = cursor.read_symbol()
    if name == "ENDSEC" {
      cursor.expect_char(';')
      break
    }
    cursor.expect_char('(')
    let values = Array::new()
    cursor.skip_space()
    if cursor.peek() is Some(')') {
      ignore(cursor.advance())
    } else {
      for ;; {
        values.push(cursor.read_value())
        cursor.skip_space()
        match cursor.advance() {
          Some(',') => ()
          Some(')') => break
          Some(character) =>
            raise ParseError::UnexpectedCharacter(
              position=cursor.position(),
              character~,
            )
          None =>
            raise unexpected_end(cursor.offset, cursor.line, cursor.column)
        }
      }
    }
    cursor.expect_char(';')
    if name == "FILE_DESCRIPTION" {
      description = values.get(0).map(values_to_strings).unwrap_or([])
    } else if name == "FILE_NAME" {
      file_name = value_string(values.get(0))
      time_stamp = value_string(values.get(1))
      authors = values.get(2).map(values_to_strings).unwrap_or([])
      organizations = values.get(3).map(values_to_strings).unwrap_or([])
      preprocessor_version = value_string(values.get(4))
      originating_system = value_string(values.get(5))
      authorization = value_string(values.get(6))
    } else if name == "FILE_SCHEMA" {
      schema_identifiers = values.get(0).map(values_to_strings).unwrap_or([])
    }
  }
  Header::{
    description,
    file_name,
    time_stamp,
    authors,
    organizations,
    preprocessor_version,
    originating_system,
    authorization,
    schema_identifiers,
  }
}

///|
fn parse_data_section(cursor : Cursor) -> Array[Entity] raise ParseError {
  let entities = Array::new()
  for ;; {
    cursor.skip_space()
    if cursor.peek() is Some('E') {
      let save_offset = cursor.offset
      let save_line = cursor.line
      let save_column = cursor.column
      let word = cursor.read_symbol()
      if word == "ENDSEC" {
        cursor.expect_char(';')
        break
      }
      cursor.offset = save_offset
      cursor.line = save_line
      cursor.column = save_column
    }
    cursor.expect_char('#')
    let position = cursor.position()
    let id = parse_int64(cursor.read_number_text(), position).to_int()
    if id <= 0 {
      raise ParseError::InvalidEntity(position~, text="#" + id.to_string())
    }
    cursor.expect_char('=')
    let (name, arguments) = cursor.read_statement()
    entities.push(Entity::{ id, name, arguments })
  }
  entities
}

///|
/// Parse an IFC2x3 or IFC4 ISO 10303-21 physical file.
pub fn parse(source : String) -> Model raise ParseError {
  let cursor = Cursor::new(source)
  cursor.expect_symbol("ISO-10303-21")
  cursor.expect_char(';')
  cursor.expect_symbol("HEADER")
  cursor.expect_char(';')
  let header = parse_header_section(cursor)
  if header.schema_identifiers.length() == 0 {
    raise ParseError::InvalidHeader(
      position=SourcePosition::{ offset: 0, line: 1, column: 1 },
      text="FILE_SCHEMA is missing",
    )
  }
  cursor.expect_symbol("DATA")
  cursor.expect_char(';')
  let entities = parse_data_section(cursor)
  cursor.expect_symbol("END-ISO-10303-21")
  cursor.expect_char(';')
  cursor.skip_space()
  if !cursor.eof() {
    raise ParseError::UnsupportedFile(
      text="trailing data after END-ISO-10303-21",
    )
  }
  Model::{ header, entities }
}

///|
pub fn parse_many(source : String) -> Array[Model] raise ParseError {
  [parse(source)]
}