// Port of path-data-parser 0.1.0 src/parser.ts (parsePath, serialize).

///|
/// One SVG path command: its letter (`M`, `l`, `A`, `z`, ...) and numeric
/// parameters, as path-data-parser represents it.
pub(all) struct Segment {
  key : Char
  data : Array[Double]
} derive(Eq, Debug)

///|
/// Why a path string could not be parsed. rough.js throws (or, for
/// `AfterClose`, loops forever) on these inputs; moon_rough reports them.
pub(all) suberror PathError {
  /// A character that is neither whitespace, a command nor a number starts at
  /// this UTF-16 offset. (rough.js crashes with a TypeError.)
  InvalidCharacter(Int32)
  /// A command parameter is not a number, e.g. `L 10 Z`.
  /// (path-data-parser: "Param not a number: ,".)
  ParamNotANumber(Char, String)
  /// The path ends before the last command got all of its parameters.
  /// (path-data-parser: "Path data ended short".)
  EndedShort
  /// Numbers follow a `Z`/`z`, which takes no parameters. (path-data-parser
  /// pushes empty `Z` segments forever without consuming them.)
  NumberAfterClose
} derive(Eq, Debug)

///|
priv enum Token {
  Command(Char)
  Number(Double)
  End
}

///|
/// Number of parameters of each command (`PARAMS`), or -1 for non-commands.
fn param_count(key : Char) -> Int32 {
  match key {
    'A' | 'a' => 7
    'C' | 'c' => 6
    'Q' | 'q' | 'S' | 's' => 4
    'L' | 'l' | 'M' | 'm' | 'T' | 't' => 2
    'H' | 'h' | 'V' | 'v' => 1
    'Z' | 'z' => 0
    _ => -1
  }
}

///|
fn is_digit(c : UInt16) -> Bool {
  c >= '0' && c <= '9'
}

///|
/// Length of the number at `start`, following
/// `^(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)`, or 0.
fn number_length(d : StringView, start : Int32) -> Int32 {
  let n = d.length()
  let mut i = start
  if i < n && (d.unsafe_get(i) == '-' || d.unsafe_get(i) == '+') {
    i += 1
  }
  let int_start = i
  while i < n && is_digit(d.unsafe_get(i)) {
    i += 1
  }
  if i > int_start {
    // [0-9]+(\.[0-9]*)?
    if i < n && d.unsafe_get(i) == '.' {
      i += 1
      while i < n && is_digit(d.unsafe_get(i)) {
        i += 1
      }
    }
  } else {
    // \.[0-9]+
    if !(i + 1 < n && d.unsafe_get(i) == '.' && is_digit(d.unsafe_get(i + 1))) {
      return 0
    }
    i += 2
    while i < n && is_digit(d.unsafe_get(i)) {
      i += 1
    }
  }
  // ([eE][-+]?[0-9]+)?, only taken when complete.
  if i < n && (d.unsafe_get(i) == 'e' || d.unsafe_get(i) == 'E') {
    let mut j = i + 1
    if j < n && (d.unsafe_get(j) == '-' || d.unsafe_get(j) == '+') {
      j += 1
    }
    if j < n && is_digit(d.unsafe_get(j)) {
      while j < n && is_digit(d.unsafe_get(j)) {
        j += 1
      }
      i = j
    }
  }
  i - start
}

///|
fn tokenize(d : StringView) -> Array[Token] raise PathError {
  let tokens = []
  let n = d.length()
  let mut i = 0
  while i < n {
    let c = d.unsafe_get(i)
    if c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == ',' {
      i += 1
    } else if c < 128 && param_count(c.to_int().unsafe_to_char()) >= 0 {
      tokens.push(Command(c.to_int().unsafe_to_char()))
      i += 1
    } else {
      let len = number_length(d, i)
      if len == 0 {
        raise InvalidCharacter(i)
      }
      // The token text is `${parseFloat(match)}` and the parameter is
      // `+text`: a round trip that only turns -0 into 0.
      let value = @jsmath.parse_float(d[i:i + len])
      tokens.push(Number(if value == 0.0 { 0.0 } else { value }))
      i += len
    }
  }
  tokens.push(End)
  tokens
}

///|
/// `parsePath(d)`: splits path data into segments, keeping relative commands
/// and implicit repetitions as written (`M 1 2 3 4` is `M 1 2`, `L 3 4`).
/// Data not starting with a move-to is parsed as `"M0,0" + d`.
pub fn parse_path(d : StringView) -> Array[Segment] raise PathError {
  let tokens = tokenize(d)
  let segments = []
  let mut mode = 'B' // 'BOD' (beginning of data) in path-data-parser
  let mut index = 0
  let mut token = tokens[index]
  while !(token is End) {
    let mut count = 0
    match token {
      _ if mode == 'B' =>
        match token {
          Command('M' | 'm' as key) => {
            index += 1
            count = param_count(key)
            mode = key
          }
          _ => return parse_path("M0,0" + d.to_owned())
        }
      Number(_) => {
        count = param_count(mode)
        if count == 0 {
          raise NumberAfterClose
        }
      }
      Command(key) => {
        index += 1
        count = param_count(key)
        mode = key
      }
      End => ()
    }
    guard index + count < tokens.length() else { raise EndedShort }
    let params = []
    for i in index..<(index + count) {
      match tokens[i] {
        Number(x) => params.push(x)
        Command(key) => raise ParamNotANumber(mode, key.to_string())
        End => raise EndedShort
      }
    }
    segments.push({ key: mode, data: params, })
    index += count
    token = tokens[index]
    if mode == 'M' {
      mode = 'L'
    }
    if mode == 'm' {
      mode = 'l'
    }
  }
  segments
}

///|
/// `serialize(segments)`: the inverse of `parse_path`, formatted like
/// path-data-parser (`C x1 y1, x2 y2, x y`, numbers as in JavaScript).
pub fn serialize(segments : Array[Segment]) -> String {
  let tokens : Array[String] = []
  for segment in segments {
    let data = segment.data
    let s = data.map(@jsmath.number_to_string)
    tokens.push(segment.key.to_string())
    match segment.key {
      'C' | 'c' => {
        tokens.push(s[0])
        tokens.push(s[1] + ",")
        tokens.push(s[2])
        tokens.push(s[3] + ",")
        tokens.push(s[4])
        tokens.push(s[5])
      }
      'S' | 's' | 'Q' | 'q' => {
        tokens.push(s[0])
        tokens.push(s[1] + ",")
        tokens.push(s[2])
        tokens.push(s[3])
      }
      _ => tokens.append(s)
    }
  }
  tokens.join(" ")
}