pub(all) enum PatternFragment {
  Lit(String)
  Capture(String, String)
  Repetition(Array[PatternFragment], String, RepeatOp)
}

pub(all) enum RepeatOp {
  ZeroOrMore
  OneOrMore
}

pub fn parse_pattern(s : String) -> Array[PatternFragment] {
  let frags : Array[PatternFragment] = []
  let mut pos : Int = 0
  let len = s.length()
  let mut literal_start : Int = 0
  while pos < len {
    if pos + 1 < len && s[pos] == ('$' : UInt16) && s[pos + 1] == ('(' : UInt16) {
      if literal_start < pos {
        frags.push(Lit(s[literal_start:pos].to_owned()))
      }
      pos = pos + 2
      let inner_res = read_balanced(s, pos - 1)
      let inner_raw = inner_res.0
      pos = inner_res.1
      let sep_start = pos
      while pos < len && s[pos] != ('*' : UInt16) && s[pos] != ('+' : UInt16) {
        pos = pos + 1
      }
      let sep = s[sep_start:pos].to_owned()
      let op = if pos < len && s[pos] == ('+' : UInt16) { OneOrMore } else { ZeroOrMore }
      if pos < len { pos = pos + 1 }
      if inner_raw.length() >= 2 {
        let inner_pat = inner_raw[1:inner_raw.length() - 1].to_owned()
        let inner_frags = parse_pattern(inner_pat)
        frags.push(Repetition(inner_frags, sep, op))
      }
      literal_start = pos
    } else if pos + 1 < len && s[pos] == ('$' : UInt16) && s[pos + 1] != ('$' : UInt16) {
      if literal_start < pos {
        frags.push(Lit(s[literal_start:pos].to_owned()))
      }
      pos = pos + 1
      let name_start = pos
      while pos < len {
        let c = s[pos]
        let is_valid = (c >= ('a' : UInt16) && c <= ('z' : UInt16)) ||
          (c >= ('A' : UInt16) && c <= ('Z' : UInt16)) ||
          c == ('_' : UInt16) ||
          (pos > name_start && c >= ('0' : UInt16) && c <= ('9' : UInt16))
        if is_valid { pos = pos + 1 } else { break }
      }
      let name = s[name_start:pos].to_owned()
      if pos < len && s[pos] == (':' : UInt16) {
        pos = pos + 1
        let kind_start = pos
        while pos < len {
          let c = s[pos]
          let is_kind = (c >= ('a' : UInt16) && c <= ('z' : UInt16)) ||
            (c >= ('A' : UInt16) && c <= ('Z' : UInt16)) ||
            c == ('_' : UInt16)
          if is_kind { pos = pos + 1 } else { break }
        }
        frags.push(Capture(name, s[kind_start:pos].to_owned()))
      } else {
        frags.push(Capture(name, "expr"))
      }
      literal_start = pos
    } else if pos + 1 < len && s[pos] == ('$' : UInt16) && s[pos + 1] == ('$' : UInt16) {
      if literal_start < pos {
        frags.push(Lit(s[literal_start:pos].to_owned()))
      }
      frags.push(Lit("$"))
      pos = pos + 2
      literal_start = pos
    } else {
      pos = pos + 1
    }
  }
  if literal_start < len {
    frags.push(Lit(s[literal_start:len].to_owned()))
  }
  frags
}

pub fn match_pattern(pattern : String, input : String) -> Map[String, Array[String]]? {
  let fragments = parse_pattern(pattern)
  let captures : Map[String, Array[String]] = Map([])
  let input_str = input.trim().to_owned()
  let result = match_fragments(fragments, captures, input_str, 0)
  match result {
    None => None
    Some((final_captures, end_pos)) => {
      if end_pos < input_str.length() { None }
      else { Some(final_captures) }
    }
  }
}

fn match_fragments(
  frags : Array[PatternFragment],
  captures : Map[String, Array[String]],
  input : String,
  start : Int
) -> (Map[String, Array[String]], Int)? {
  let mut cap = captures
  let mut pos = start
  let len = input.length()
  for frag in frags {
    match frag {
      Lit(lit) => {
        let trimmed = lit.trim()
        if trimmed.length() > 0 {
          let view = input[pos:len]
          match view.find(trimmed) {
            None => { return None }
            Some(found_pos) => {
              if found_pos != 0 { return None }
              pos = pos + found_pos + trimmed.length()
            }
          }
        }
      }
      Capture(name, _kind) => {
        let after_frags = fragments_after(frags, frag)
        let raw_delim = next_literal_text(after_frags)
        let delim = raw_delim.trim().to_owned()
        let captured = if delim.length() > 0 {
          let view = input[pos:len]
          match view.find(delim) {
            None => {
              if pos < len { input[pos:len].to_owned() } else { return None }
            }
            Some(end_pos) => { input[pos:pos + end_pos].to_owned() }
          }
        } else {
          if pos < len { input[pos:len].to_owned() } else { return None }
        }
        let trimmed_val = captured.trim().to_owned()
        if cap.contains(name) {
          cap[name].push(trimmed_val)
        } else {
          cap[name] = [trimmed_val]
        }
        pos = if delim.length() > 0 {
          let view = input[pos:len]
          match view.find(delim) {
            None => len
            Some(p) => pos + p
          }
        } else { len }
      }
      Repetition(inner, sep, op) => {
        let trimmed_sep = sep.trim().to_owned()
        let after_frags = fragments_after(frags, frag)
        let stop_delim = next_literal_text(after_frags)
        let portion_end = if stop_delim.length() > 0 {
          let view = input[pos:len]
          match view.find(stop_delim) {
            None => len
            Some(p) => pos + p
          }
        } else { len }
        let portion = input[pos:portion_end].to_owned()
        let parts = if trimmed_sep.length() > 0 {
          split_balanced(portion, trimmed_sep, 0)
        } else {
          if portion.trim().length() > 0 { [portion.trim().to_owned()] } else { [] }
        }
        if parts.length() == 0 && op is OneOrMore { return None }
        if parts.length() == 0 {
          let inner_names = collect_capture_names(inner)
          for n in inner_names {
            if !cap.contains(n) { cap[n] = [] }
          }
        } else {
          for part in parts {
            let inner_result = match_fragments(inner, cap, part, 0)
            match inner_result {
              None => { if op is OneOrMore { return None } }
              Some((new_cap, _end)) => {
                cap = new_cap
              }
            }
          }
        }
        pos = portion_end
      }
    }
  }
  Some((cap, pos))
}

fn split_balanced(s : String, sep : String, start : Int) -> Array[String] {
  let result : Array[String] = []
  let mut pos = start
  let len = s.length()
  let mut depth = 0
  let mut segment_start = start
  while pos < len {
    let c = s[pos]
    if c == ('(' : UInt16) || c == ('[' : UInt16) || c == ('{' : UInt16) { depth = depth + 1 }
    else if c == (')' : UInt16) || c == (']' : UInt16) || c == ('}' : UInt16) { depth = depth - 1 }
    else if depth == 0 {
      let is_sep = if pos + sep.length() <= len {
        s[pos:pos + sep.length()].to_owned() == sep
      } else { false }
      if is_sep {
        result.push(s[segment_start:pos].trim().to_owned())
        pos = pos + sep.length()
        segment_start = pos
        // skip leading whitespace after separator
        while segment_start < len {
          let ch = s[segment_start]
          if ch == (' ' : UInt16) || ch == ('\t' : UInt16) || ch == ('\n' : UInt16) || ch == ('\r' : UInt16) { segment_start = segment_start + 1 } else { break }
        }
        pos = segment_start
        continue
      }
    }
    pos = pos + 1
  }
  if segment_start < len {
    let last = s[segment_start:len].trim().to_owned()
    if last.length() > 0 { result.push(last) }
  }
  result
}

fn collect_capture_names(frags : Array[PatternFragment]) -> Array[String] {
  let names : Array[String] = []
  for f in frags {
    match f {
      Capture(n, _) => names.push(n)
      Repetition(inner, _, _) => {
        let inner_names = collect_capture_names(inner)
        for n in inner_names { names.push(n) }
      }
      _ => ()
    }
  }
  names
}

fn fragments_after(frags : Array[PatternFragment], current : PatternFragment) -> Array[PatternFragment] {
  let mut found = false
  let result : Array[PatternFragment] = []
  for f in frags {
    if found {
      result.push(f)
    } else {
      if fragment_equal(f, current) { found = true }
    }
  }
  result
}

fn next_literal_text(frags : Array[PatternFragment]) -> String {
  for f in frags {
    match f {
      Lit(s) => {
        let t = s.trim()
        if t.length() > 0 { return t.to_owned() }
      }
      _ => ()
    }
  }
  ""
}

fn repeat_op_equal(a : RepeatOp, b : RepeatOp) -> Bool {
  match (a, b) {
    (ZeroOrMore, ZeroOrMore) => true
    (OneOrMore, OneOrMore) => true
    _ => false
  }
}

fn fragment_equal(a : PatternFragment, b : PatternFragment) -> Bool {
  match (a, b) {
    (Lit(s1), Lit(s2)) => s1 == s2
    (Capture(n1, k1), Capture(n2, k2)) => n1 == n2 && k1 == k2
    (Repetition(i1, s1, o1), Repetition(i2, s2, o2)) => s1 == s2 && repeat_op_equal(o1, o2) && i1.length() == i2.length()
    _ => false
  }
}