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

///|
pub(all) struct PackageImport {
  path : String
  alias_ : String?
}

///|
pub(all) struct ImportParseResult {
  code : String
  imports : Array[PackageImport]
}

///|
fn default_import_alias(path : String) -> String {
  let mut slash = -1
  for i = 0; i < path.length(); i = i + 1 {
    if path.get_char(i).unwrap_or('\u{0}') == '/' {
      slash = i
    }
  }
  if slash >= 0 {
    path[slash + 1:].to_owned()
  } else {
    path
  }
}

///|
fn parse_moon_config_import_item(ast : @moon_config.Ast) -> PackageImport? {
  match ast {
    Str(path, ..) => Some({ path, alias_: Some(default_import_alias(path)) })
    Obj(fields, ..) => {
      let mut path = None
      let mut alias_ = None
      for field in fields {
        match field {
          ("path", Str(value, ..)) => path = Some(value)
          ("alias", Str(value, ..)) => alias_ = Some(value)
          _ => ()
        }
      }
      match path {
        Some(path) =>
          Some({
            path,
            alias_: Some(alias_.unwrap_or(default_import_alias(path))),
          })
        None => None
      }
    }
    _ => None
  }
}

///|
priv struct ImportSpan {
  start : Int
  end : Int
}

///|
fn collect_imports_from_moon_config(
  ast : @moon_config.Ast,
) -> (Array[PackageImport], Array[ImportSpan]) {
  let imports = []
  let spans = []
  match ast {
    Obj(fields, ..) =>
      for field in fields {
        match field {
          ("import", Arr(content, loc~)) => {
            for item in content {
              if parse_moon_config_import_item(item) is Some(import_) {
                imports.push(import_)
              }
            }
            spans.push({ start: loc.start.cnum, end: loc.end.cnum })
          }
          _ => ()
        }
      }
    _ => ()
  }
  (imports, spans)
}

///|
fn remove_import_spans(source : String, spans : Array[ImportSpan]) -> String {
  let buf = StringBuilder::new()
  let mut cursor = 0
  for span in spans {
    let start = span.start.clamp(min=0, max=source.length())
    let end = span.end.clamp(min=start, max=source.length())
    if cursor < start {
      buf.write_view(source[cursor:start])
    }
    cursor = end
    while cursor < source.length() &&
          is_ascii_space(source.get_char(cursor).unwrap_or('\u{0}')) {
      cursor += 1
    }
  }
  if cursor < source.length() {
    buf.write_view(source[cursor:])
  }
  buf.to_string()
}

///|
pub fn parse_package_imports(source : String) -> ImportParseResult {
  let (ast, _diagnostics) = @moon_config.parse_moon_pkg(source)
  let (imports, spans) = collect_imports_from_moon_config(ast)
  { code: remove_import_spans(source, spans), imports }
}

///|
fn source_char(source : String, index : Int) -> Char {
  source.get_char(index).unwrap_or('\u{0}')
}

///|
fn is_ascii_ident_char(ch : Char) -> Bool {
  (ch >= 'a' && ch <= 'z') ||
  (ch >= 'A' && ch <= 'Z') ||
  (ch >= '0' && ch <= '9') ||
  ch == '_'
}

///|
fn starts_with_at(source : String, index : Int, needle : String) -> Bool {
  if index < 0 {
    return false
  }
  let end = index + needle.length()
  if end > source.length() {
    return false
  }
  for offset = 0; offset < needle.length(); offset = offset + 1 {
    if source.get_char(index + offset) != needle.get_char(offset) {
      return false
    }
  }
  true
}

///|
fn word_at(source : String, index : Int, word : String) -> Bool {
  if !starts_with_at(source, index, word) {
    return false
  }
  let before_ok = if index == 0 {
    true
  } else {
    !is_ascii_ident_char(source_char(source, index - 1))
  }
  let after = index + word.length()
  let after_ok = if after >= source.length() {
    true
  } else {
    !is_ascii_ident_char(source_char(source, after))
  }
  before_ok && after_ok
}

///|
fn skip_ascii_space_at(source : String, index : Int) -> Int {
  let mut i = index
  while i < source.length() && is_ascii_space(source_char(source, i)) {
    i += 1
  }
  i
}

///|
fn skip_quoted_source(source : String, index : Int) -> Int {
  let quote = source_char(source, index)
  let mut i = index + 1
  let mut escaped = false
  while i < source.length() {
    let ch = source_char(source, i)
    if escaped {
      escaped = false
    } else if ch == '\\' {
      escaped = true
    } else if ch == quote {
      return i + 1
    }
    i += 1
  }
  source.length()
}

///|
fn skip_non_code_source(source : String, index : Int) -> Int {
  if index >= source.length() {
    return index
  }
  let ch = source_char(source, index)
  if ch == '"' || ch == '\'' {
    return skip_quoted_source(source, index)
  }
  if starts_with_at(source, index, "//") {
    let mut i = index + 2
    while i < source.length() && source_char(source, i) != '\n' {
      i += 1
    }
    return i
  }
  if starts_with_at(source, index, "/*") {
    let mut i = index + 2
    while i + 1 < source.length() && !starts_with_at(source, i, "*/") {
      i += 1
    }
    return (i + 2).clamp(min=0, max=source.length())
  }
  index
}

///|
fn find_list_comprehension_body_brace(source : String, start : Int) -> Int {
  let mut i = start
  let mut paren_depth = 0
  let mut square_depth = 0
  while i < source.length() {
    let skipped = skip_non_code_source(source, i)
    if skipped != i {
      i = skipped
      continue
    }
    if paren_depth == 0 && square_depth == 0 {
      if starts_with_at(source, i, "=>") {
        return -1
      }
      match source_char(source, i) {
        '{' => return i
        ']' => return -1
        _ => ()
      }
    }
    match source_char(source, i) {
      '(' => paren_depth += 1
      ')' => paren_depth -= 1
      '[' => square_depth += 1
      ']' => square_depth -= 1
      _ => ()
    }
    i += 1
  }
  -1
}

///|
fn find_matching_curly(source : String, open_index : Int) -> Int {
  let mut i = open_index
  let mut depth = 0
  while i < source.length() {
    let skipped = skip_non_code_source(source, i)
    if skipped != i {
      i = skipped
      continue
    }
    match source_char(source, i) {
      '{' => depth += 1
      '}' => {
        depth -= 1
        if depth == 0 {
          return i
        }
      }
      _ => ()
    }
    i += 1
  }
  -1
}

///|
fn normalize_list_comprehension_blocks(source : String) -> String {
  let buf = StringBuilder::new()
  let mut cursor = 0
  let mut i = 0
  while i < source.length() {
    let skipped = skip_non_code_source(source, i)
    if skipped != i {
      i = skipped
      continue
    }
    if source_char(source, i) == '[' {
      let content_start = skip_ascii_space_at(source, i + 1)
      if word_at(source, content_start, "for") {
        let body_brace = find_list_comprehension_body_brace(
          source,
          content_start + 3,
        )
        if body_brace >= 0 {
          let body_end = find_matching_curly(source, body_brace)
          let after_body = skip_ascii_space_at(source, body_end + 1)
          if body_end > body_brace &&
            after_body < source.length() &&
            source_char(source, after_body) == ']' {
            buf.write_view(source[cursor:body_brace])
            buf.write_string("=> ")
            buf.write_view(source[body_brace + 1:body_end])
            cursor = body_end + 1
            i = body_end + 1
            continue
          }
        }
      }
    }
    i += 1
  }
  if cursor < source.length() {
    buf.write_view(source[cursor:])
  }
  buf.to_string()
}

///|
fn find_loop_body_brace(source : String, start : Int) -> Int {
  let mut i = start
  let mut paren_depth = 0
  let mut square_depth = 0
  while i < source.length() {
    let skipped = skip_non_code_source(source, i)
    if skipped != i {
      i = skipped
      continue
    }
    if paren_depth == 0 && square_depth == 0 {
      if source_char(source, i) == '{' {
        return i
      }
      if source_char(source, i) == ';' {
        return -1
      }
    }
    match source_char(source, i) {
      '(' => paren_depth += 1
      ')' => paren_depth -= 1
      '[' => square_depth += 1
      ']' => square_depth -= 1
      _ => ()
    }
    i += 1
  }
  -1
}

///|
fn normalize_loop_expressions(source : String) -> String {
  let loop_arg = "__moonbit_eval_loop_arg"
  let buf = StringBuilder::new()
  let mut cursor = 0
  let mut i = 0
  while i < source.length() {
    let skipped = skip_non_code_source(source, i)
    if skipped != i {
      i = skipped
      continue
    }
    if word_at(source, i, "loop") {
      let arg_start = skip_ascii_space_at(source, i + 4)
      let body_brace = find_loop_body_brace(source, arg_start)
      if body_brace >= 0 {
        let body_end = find_matching_curly(source, body_brace)
        if body_end > body_brace {
          buf.write_view(source[cursor:i])
          buf.write_string("for ")
          buf.write_string(loop_arg)
          buf.write_string(" = ")
          buf.write_view(source[arg_start:body_brace])
          buf.write_string(" { match ")
          buf.write_string(loop_arg)
          buf.write_string(" {")
          buf.write_view(source[body_brace + 1:body_end])
          buf.write_string("} }")
          cursor = body_end + 1
          i = body_end + 1
          continue
        }
      }
    }
    i += 1
  }
  if cursor < source.length() {
    buf.write_view(source[cursor:])
  }
  buf.to_string()
}

///|
fn find_header_brace(source : String, start : Int) -> Int {
  let mut i = start
  while i < source.length() {
    let skipped = skip_non_code_source(source, i)
    if skipped != i {
      i = skipped
      continue
    }
    if source_char(source, i) == '{' {
      return i
    }
    if source_char(source, i) == '\n' {
      return -1
    }
    i += 1
  }
  -1
}

///|
fn normalize_enum_header(header : String) -> String {
  let buf = StringBuilder::new()
  let mut i = 0
  while i < header.length() {
    if source_char(header, i) == '@' {
      i += 1
      while i < header.length() && source_char(header, i) != '.' {
        i += 1
      }
      if i < header.length() && source_char(header, i) == '.' {
        i += 1
      }
      continue
    }
    if starts_with_at(header, i, "+=") {
      i += 2
      continue
    }
    buf.write_char(source_char(header, i))
    i += 1
  }
  buf.to_string()
}

///|
fn normalize_extenum_decls(source : String) -> String {
  let buf = StringBuilder::new()
  let mut i = 0
  while i < source.length() {
    let skipped = skip_non_code_source(source, i)
    if skipped != i {
      buf.write_view(source[i:skipped])
      i = skipped
      continue
    }
    let mut keyword_len = 0
    if word_at(source, i, "extenum") {
      keyword_len = 7
    } else if word_at(source, i, "enum") {
      keyword_len = 4
    }
    if keyword_len > 0 {
      buf.write_string("enum")
      i += keyword_len
      let brace = find_header_brace(source, i)
      if brace >= 0 {
        buf.write_string(normalize_enum_header(source[i:brace].to_owned()))
        i = brace
      }
      continue
    }
    buf.write_char(source_char(source, i))
    i += 1
  }
  buf.to_string()
}

///|
fn scan_left_type_ref(source : String, index : Int) -> Int {
  let mut i = index
  while i >= 0 && is_ascii_space(source_char(source, i)) {
    i -= 1
  }
  while i >= 0 {
    let ch = source_char(source, i)
    if is_ascii_ident_char(ch) || ch == '.' || ch == '@' {
      i -= 1
    } else {
      break
    }
  }
  i + 1
}

///|
fn normalize_extensible_constructor_refs(source : String) -> String {
  let buf = StringBuilder::new()
  let mut cursor = 0
  let mut i = 0
  while i < source.length() {
    let skipped = skip_non_code_source(source, i)
    if skipped != i {
      i = skipped
      continue
    }
    if starts_with_at(source, i, "::@") {
      let left_start = scan_left_type_ref(source, i - 1)
      if left_start >= cursor {
        buf.write_view(source[cursor:left_start])
        cursor = i + 2
      }
      i += 2
      continue
    }
    i += 1
  }
  if cursor < source.length() {
    buf.write_view(source[cursor:])
  }
  buf.to_string()
}

///|
fn previous_non_space(source : String, index : Int) -> Int {
  let mut i = index
  while i >= 0 && is_ascii_space(source_char(source, i)) {
    i -= 1
  }
  i
}

///|
fn find_matching_open_paren(source : String, close_index : Int) -> Int {
  let mut i = close_index
  let mut depth = 0
  while i >= 0 {
    let ch = source_char(source, i)
    if ch == ')' {
      depth += 1
    } else if ch == '(' {
      depth -= 1
      if depth == 0 {
        return i
      }
    }
    i -= 1
  }
  -1
}

///|
fn has_call_args(source : String, open_index : Int, close_index : Int) -> Bool {
  let mut i = open_index + 1
  while i < close_index {
    if !is_ascii_space(source_char(source, i)) {
      return true
    }
    i += 1
  }
  false
}

///|
fn find_reverse_pipe_rhs_end(source : String, start : Int) -> Int {
  let mut i = start
  let mut paren_depth = 0
  let mut brace_depth = 0
  let mut square_depth = 0
  while i < source.length() {
    let skipped = skip_non_code_source(source, i)
    if skipped != i {
      i = skipped
      continue
    }
    let ch = source_char(source, i)
    if paren_depth == 0 && brace_depth == 0 && square_depth == 0 {
      match ch {
        ';' | ',' | ')' | ']' | '}' => return i
        _ => ()
      }
    }
    match ch {
      '(' => paren_depth += 1
      ')' => paren_depth -= 1
      '{' => brace_depth += 1
      '}' => brace_depth -= 1
      '[' => square_depth += 1
      ']' => square_depth -= 1
      _ => ()
    }
    i += 1
  }
  i
}

///|
fn normalize_reverse_pipe_methods(source : String) -> String {
  let buf = StringBuilder::new()
  let mut cursor = 0
  let mut i = 0
  while i + 1 < source.length() {
    let skipped = skip_non_code_source(source, i)
    if skipped != i {
      i = skipped
      continue
    }
    if starts_with_at(source, i, "<|") {
      let lhs_close = previous_non_space(source, i - 1)
      if lhs_close >= cursor && source_char(source, lhs_close) == ')' {
        let lhs_open = find_matching_open_paren(source, lhs_close)
        let rhs_start = skip_ascii_space_at(source, i + 2)
        let rhs_end = find_reverse_pipe_rhs_end(source, rhs_start)
        if lhs_open >= cursor && rhs_start < rhs_end {
          buf.write_view(source[cursor:lhs_close])
          if has_call_args(source, lhs_open, lhs_close) {
            buf.write_string(", ")
          }
          buf.write_view(source[rhs_start:rhs_end])
          buf.write_char(')')
          cursor = rhs_end
          i = rhs_end
          continue
        }
      }
    }
    i += 1
  }
  if cursor < source.length() {
    buf.write_view(source[cursor:])
  }
  buf.to_string()
}

///|
fn normalize_v092_syntax(source : String) -> String {
  source
  |> normalize_list_comprehension_blocks
  |> normalize_loop_expressions
  |> normalize_extenum_decls
  |> normalize_extensible_constructor_refs
  |> normalize_reverse_pipe_methods
}