///|
/// Parse the EvoWitness line-oriented contract language.
///
/// Grammar (whitespace is insignificant):
///
/// ```text
/// contract  
/// type  
/// field    [constraint=value ...]
/// end
/// ```
pub fn parse_contract(input : String) -> Result[Contract, ParseError] {
  let normalized = input.replace_all(old="\r\n", new="\n")
  let lines = normalized.split("\n").map(view => view.to_owned()).collect()
  let mut contract_name : String? = None
  let mut contract_version : String? = None
  let objects : Array[ObjectType] = []
  let mut current_name : String? = None
  let mut current_open = false
  let mut current_line = 0
  let current_fields : Array[Field] = []
  for index, raw_line in lines {
    let line_number = index + 1
    let source_line = raw_line
    let code = strip_comment(raw_line).trim().to_owned()
    if code.is_empty() {
      continue
    }
    let tokens = split_words(code)
    if tokens.is_empty() {
      continue
    }
    match tokens[0] {
      "contract" => {
        if contract_name is Some(_) {
          return Err(
            parse_error(
              line_number, "E002", "contract header may appear only once", source_line,
            ),
          )
        }
        if current_name is Some(_) || !objects.is_empty() {
          return Err(
            parse_error(
              line_number, "E003", "contract header must be the first declaration",
              source_line,
            ),
          )
        }
        if tokens.length() != 3 {
          return Err(
            parse_error(
              line_number, "E004", "expected: contract  ", source_line,
            ),
          )
        }
        if !valid_identifier(tokens[1]) {
          return Err(
            parse_error(
              line_number,
              "E005",
              "invalid contract name '" + tokens[1] + "'",
              source_line,
            ),
          )
        }
        if tokens[2].is_empty() {
          return Err(
            parse_error(
              line_number, "E006", "contract version cannot be empty", source_line,
            ),
          )
        }
        contract_name = Some(tokens[1])
        contract_version = Some(tokens[2])
      }
      "type" => {
        if contract_name is None {
          return Err(
            parse_error(
              line_number, "E007", "declare the contract header before any type",
              source_line,
            ),
          )
        }
        if current_name is Some(_) {
          return Err(
            parse_error(
              line_number, "E008", "nested type declarations are not allowed; add 'end' first",
              source_line,
            ),
          )
        }
        if tokens.length() != 3 ||
          (tokens[2] != "open" && tokens[2] != "closed") {
          return Err(
            parse_error(
              line_number, "E009", "expected: type  ", source_line,
            ),
          )
        }
        if !valid_identifier(tokens[1]) {
          return Err(
            parse_error(
              line_number,
              "E010",
              "invalid type name '" + tokens[1] + "'",
              source_line,
            ),
          )
        }
        for object in objects {
          if object.name == tokens[1] {
            return Err(
              parse_error(
                line_number,
                "E011",
                "duplicate type '" + tokens[1] + "'",
                source_line,
              ),
            )
          }
        }
        current_name = Some(tokens[1])
        current_open = tokens[2] == "open"
        current_line = line_number
        current_fields.clear()
      }
      "field" => {
        guard current_name is Some(_) else {
          return Err(
            parse_error(
              line_number, "E012", "field declaration must be inside a type", source_line,
            ),
          )
        }
        match parse_field(tokens, line_number, source_line) {
          Err(error) => return Err(error)
          Ok(field) => {
            for existing in current_fields {
              if existing.name == field.name {
                return Err(
                  parse_error(
                    line_number,
                    "E013",
                    "duplicate field '" + field.name + "'",
                    source_line,
                  ),
                )
              }
            }
            current_fields.push(field)
          }
        }
      }
      "end" => {
        if tokens.length() != 1 {
          return Err(
            parse_error(
              line_number, "E014", "'end' does not accept arguments", source_line,
            ),
          )
        }
        guard current_name is Some(name) else {
          return Err(
            parse_error(
              line_number, "E015", "unexpected 'end' outside a type", source_line,
            ),
          )
        }
        objects.push({
          name,
          open: current_open,
          fields: current_fields.copy(),
          line: current_line,
        })
        current_name = None
        current_fields.clear()
      }
      keyword =>
        return Err(
          parse_error(
            line_number,
            "E001",
            "unknown declaration '" + keyword + "'",
            source_line,
          ),
        )
    }
  }
  if current_name is Some(name) {
    return Err(
      parse_error(
        current_line,
        "E016",
        "type '" + name + "' is missing its closing 'end'",
        "type " + name,
      ),
    )
  }
  guard contract_name is Some(name) && contract_version is Some(version) else {
    return Err(parse_error(1, "E017", "missing contract header", ""))
  }
  if objects.is_empty() {
    return Err(
      parse_error(1, "E018", "contract must declare at least one type", ""),
    )
  }
  let contract : Contract = { name, version, objects }
  match validate_references(contract) {
    Some(error) => Err(error)
    None => Ok(contract)
  }
}

///|
fn parse_field(
  tokens : Array[String],
  line_number : Int,
  source_line : String,
) -> Result[Field, ParseError] {
  if tokens.length() < 4 {
    return Err(
      parse_error(
        line_number, "E020", "expected: field    [constraint=value ...]",
        source_line,
      ),
    )
  }
  let name = tokens[1]
  if !valid_identifier(name) {
    return Err(
      parse_error(
        line_number,
        "E021",
        "invalid field name '" + name + "'",
        source_line,
      ),
    )
  }
  let type_expr = match parse_type_expr(tokens[2], line_number, source_line) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let required = match tokens[3] {
    "required" => true
    "optional" => false
    other =>
      return Err(
        parse_error(
          line_number,
          "E022",
          "field presence must be 'required' or 'optional', found '" +
          other +
          "'",
          source_line,
        ),
      )
  }
  let mut min_int : Int? = None
  let mut max_int : Int? = None
  let mut min_len : Int? = None
  let mut max_len : Int? = None
  let mut default_value : String? = None
  for index = 4; index < tokens.length(); index = index + 1 {
    let token = tokens[index]
    guard token.split_once("=") is Some((key_view, value_view)) else {
      return Err(
        parse_error(
          line_number,
          "E023",
          "constraint must use key=value syntax, found '" + token + "'",
          source_line,
        ),
      )
    }
    let key = key_view.to_owned()
    let value = value_view.to_owned()
    if value.is_empty() {
      return Err(
        parse_error(
          line_number,
          "E024",
          "constraint '" + key + "' cannot have an empty value",
          source_line,
        ),
      )
    }
    match key {
      "min" =>
        match parse_decimal(value) {
          Some(number) => min_int = Some(number)
          None =>
            return Err(
              parse_error(
                line_number, "E025", "min must be a decimal integer", source_line,
              ),
            )
        }
      "max" =>
        match parse_decimal(value) {
          Some(number) => max_int = Some(number)
          None =>
            return Err(
              parse_error(
                line_number, "E026", "max must be a decimal integer", source_line,
              ),
            )
        }
      "minlen" =>
        match parse_non_negative(value) {
          Some(number) => min_len = Some(number)
          None =>
            return Err(
              parse_error(
                line_number, "E027", "minlen must be a non-negative integer", source_line,
              ),
            )
        }
      "maxlen" =>
        match parse_non_negative(value) {
          Some(number) => max_len = Some(number)
          None =>
            return Err(
              parse_error(
                line_number, "E028", "maxlen must be a non-negative integer", source_line,
              ),
            )
        }
      "default" => default_value = Some(value)
      unknown =>
        return Err(
          parse_error(
            line_number,
            "E029",
            "unknown constraint '" + unknown + "'",
            source_line,
          ),
        )
    }
  }
  let constraints : Constraints = {
    min_int,
    max_int,
    min_len,
    max_len,
    default_value,
  }
  match validate_constraints(type_expr, constraints, line_number, source_line) {
    Some(error) => Err(error)
    None => Ok({ name, type_expr, required, constraints, line: line_number })
  }
}

///|
fn parse_type_expr(
  token : String,
  line_number : Int,
  source_line : String,
) -> Result[TypeExpr, ParseError] {
  match token {
    "string" => Ok(StringType)
    "int" => Ok(IntType)
    "bool" => Ok(BoolType)
    "number" => Ok(NumberType)
    _ =>
      if token.has_prefix("enum:") {
        let values = token[5:]
          .split("|")
          .map(value => value.to_owned())
          .collect()
        if values.is_empty() || values.any(value => value.is_empty()) {
          return Err(
            parse_error(
              line_number, "E030", "enum must contain one or more non-empty values",
              source_line,
            ),
          )
        }
        let seen : Array[String] = []
        for value in values {
          if !valid_enum_value(value) {
            return Err(
              parse_error(
                line_number,
                "E031",
                "invalid enum value '" + value + "'",
                source_line,
              ),
            )
          }
          if seen.contains(value) {
            return Err(
              parse_error(
                line_number,
                "E032",
                "duplicate enum value '" + value + "'",
                source_line,
              ),
            )
          }
          seen.push(value)
        }
        Ok(EnumType(values))
      } else if token.has_prefix("ref:") {
        let name = token[4:].to_owned()
        if !valid_identifier(name) {
          Err(
            parse_error(
              line_number,
              "E033",
              "invalid referenced type '" + name + "'",
              source_line,
            ),
          )
        } else {
          Ok(RefType(name))
        }
      } else if token.has_prefix("list:") {
        let item = token[5:].to_owned()
        if valid_list_item(item) {
          Ok(ListType(item))
        } else {
          Err(
            parse_error(
              line_number, "E034", "list item must be string, int, bool, number, or ref:",
              source_line,
            ),
          )
        }
      } else {
        Err(
          parse_error(
            line_number,
            "E035",
            "unsupported field type '" + token + "'",
            source_line,
          ),
        )
      }
  }
}

///|
fn validate_constraints(
  type_expr : TypeExpr,
  constraints : Constraints,
  line_number : Int,
  source_line : String,
) -> ParseError? {
  if constraints.min_int is Some(_) || constraints.max_int is Some(_) {
    if type_expr != IntType && type_expr != NumberType {
      return Some(
        parse_error(
          line_number, "E040", "min/max constraints are valid only for int and number fields",
          source_line,
        ),
      )
    }
  }
  if constraints.min_len is Some(_) || constraints.max_len is Some(_) {
    if type_expr != StringType && !(type_expr is ListType(_)) {
      return Some(
        parse_error(
          line_number, "E041", "minlen/maxlen constraints are valid only for string and list fields",
          source_line,
        ),
      )
    }
  }
  if constraints.min_int is Some(minimum) &&
    constraints.max_int is Some(maximum) {
    if minimum > maximum {
      return Some(
        parse_error(
          line_number, "E042", "min cannot be greater than max", source_line,
        ),
      )
    }
  }
  if constraints.min_len is Some(minimum) &&
    constraints.max_len is Some(maximum) {
    if minimum > maximum {
      return Some(
        parse_error(
          line_number, "E043", "minlen cannot be greater than maxlen", source_line,
        ),
      )
    }
  }
  None
}

///|
fn validate_references(contract : Contract) -> ParseError? {
  for object in contract.objects {
    for field in object.fields {
      match field.type_expr.referenced_name() {
        Some(name) =>
          if contract.find_object(name) is None {
            return Some(
              parse_error(
                field.line,
                "E050",
                "field '" +
                field.name +
                "' references missing type '" +
                name +
                "'",
                "field " + field.name + " " + field.type_expr.render(),
              ),
            )
          }
        None => ()
      }
    }
  }
  None
}

///|
fn strip_comment(line : String) -> String {
  match line.find("#") {
    Some(offset) => line[:offset].to_owned()
    None => line
  }
}

///|
fn split_words(line : String) -> Array[String] {
  let words : Array[String] = []
  for piece in line.split(" ") {
    let word = piece.trim()
    if !word.is_empty() {
      words.push(word.to_owned())
    }
  }
  words
}

///|
fn valid_identifier(value : String) -> Bool {
  if value.is_empty() {
    return false
  }
  let chars = value.to_array()
  if !(chars[0].is_ascii_alphabetic() || chars[0] == '_') {
    return false
  }
  for char in chars {
    if !(char.is_ascii_alphabetic() ||
      char.is_ascii_digit() ||
      char == '_' ||
      char == '-') {
      return false
    }
  }
  true
}

///|
fn valid_enum_value(value : String) -> Bool {
  if value.is_empty() {
    return false
  }
  value.all(char => {
    char.is_ascii_alphabetic() ||
    char.is_ascii_digit() ||
    char == '_' ||
    char == '-' ||
    char == '.'
  })
}

///|
fn valid_list_item(value : String) -> Bool {
  value == "string" ||
  value == "int" ||
  value == "bool" ||
  value == "number" ||
  (value.has_prefix("ref:") && valid_identifier(value[4:].to_owned()))
}

///|
fn parse_non_negative(value : String) -> Int? {
  match parse_decimal(value) {
    Some(number) if number >= 0 => Some(number)
    _ => None
  }
}

///|
fn parse_decimal(value : String) -> Int? {
  if value.is_empty() {
    return None
  }
  let chars = value.to_array()
  let mut index = 0
  let mut sign = 1
  if chars[0] == '-' {
    if chars.length() == 1 {
      return None
    }
    sign = -1
    index = 1
  } else if chars[0] == '+' {
    if chars.length() == 1 {
      return None
    }
    index = 1
  }
  let mut result = 0
  while index < chars.length() {
    let char = chars[index]
    if !char.is_ascii_digit() {
      return None
    }
    let digit = char.to_int() - '0'.to_int()
    result = result * 10 + digit
    index = index + 1
  }
  Some(result * sign)
}