///|
/// Offsets are UTF-16 code units, matching String slicing and browser selection.
pub struct NumberSpan {
  start : Int
  end : Int
  kind : String
  source : String
  replacement : String
} derive(Debug, Eq)

///|
fn text_at(text : String, start : Int, needle : String) -> Bool {
  if start + needle.length() > text.length() {
    return false
  }
  for j in 0.. Bool {
  if i >= text.length() {
    return false
  }
  let c = text[i].to_int()
  (c >= 48 && c <= 57) || (c >= 0xff10 && c <= 0xff19)
}

///|
fn number_end(text : String, start : Int, chinese : Bool) -> Int {
  let mut i = start
  let mut point = false
  while i < text.length() {
    let c = text[i].to_int().unsafe_to_char()
    if ascii_digit_at(text, i) {
      i += 1
      continue
    }
    if (c == '-' || c == '-' || (chinese && (c == '负' || c == '負'))) &&
      i == start {
      if (c == '-' || c == '-') &&
        start > 0 &&
        ascii_digit_at(text, start - 1) {
        break
      }
      if i + 1 < text.length() {
        i += 1
        continue
      }
    }
    if c == '.' || c == '.' || (chinese && (c == '点' || c == '點')) {
      if point || i == start {
        break
      }
      point = true
      i += 1
      continue
    }
    if chinese &&
      "零〇一二两兩三四五六七八九十百千万亿壹贰貳叁參肆伍陆陸柒捌玖拾佰仟萬億".contains(
        c.to_string(),
      ) {
      i += 1
      continue
    }
    break
  }
  while i > start &&
        (text[i - 1].to_int() == 46 || text[i - 1].to_int() == 0xff0e) {
    i -= 1
  }
  i
}

///|
fn sentence_value(
  text : String,
  chinese : Bool,
  direct : Bool,
) -> String raise NumberError {
  if chinese {
    cn2an(text, mode=if direct { "direct" } else { "smart" })
  } else {
    an2cn(text, mode=if direct { "direct" } else { "low" })
  }
}

///|
fn conversion_span(
  text : String,
  start : Int,
  chinese : Bool,
  direct : Bool,
) -> NumberSpan? raise NumberError {
  // A hyphen between Arabic runs is a separator, not a unary minus.
  if start > 0 && text_at(text, start, "-") && ascii_digit_at(text, start - 1) {
    return None
  }
  let below_zero = chinese && !direct && text_at(text, start, "零下")
  let negative_percent = chinese &&
    !direct &&
    text_at(text, start, "负百分之")
  let percent = chinese &&
    !direct &&
    (text_at(text, start, "百分之") || negative_percent)
  let begin = start +
    (if below_zero {
      2
    } else if negative_percent {
      4
    } else if percent {
      3
    } else {
      0
    })
  let end = number_end(text, begin, chinese)
  if end == begin || end - begin > 256 {
    return None
  }
  let original = text[begin:end].to_owned()
  if chinese &&
    !original.iter().any(c => c.to_int() > 127 && c.to_int() < 0xff00) &&
    !percent &&
    !below_zero {
    return None
  }
  let first = sentence_value(original, chinese, direct)
  let mut replacement = first
  let mut finish = end
  let mut kind = if direct { "direct" } else { "number" }
  if !direct {
    if percent {
      replacement = (if negative_percent { "负" } else { "" }) + first + "%"
      kind = "percent"
    } else if chinese && text_at(text, end, "摄氏度") {
      replacement = (if below_zero { "-" } else { "" }) + first + "℃"
      finish += 3
      kind = "temperature"
    } else if below_zero {
      return None
    } else if !chinese &&
      (text_at(text, end, "℃") || text_at(text, end, "°C")) {
      replacement += "摄氏度"
      finish += if text_at(text, end, "℃") { 1 } else { 2 }
      kind = "temperature"
    } else if !chinese && (text_at(text, end, "%") || text_at(text, end, "%")) {
      replacement = "百分之" + first
      finish += 1
      kind = "percent"
    } else if text_at(text, end, if chinese { "分之" } else { "/" }) {
      let second_start = end + (if chinese { 2 } else { 1 })
      let second_end = number_end(text, second_start, chinese)
      if second_end > second_start {
        let second = sentence_value(
          text[second_start:second_end].to_owned(),
          chinese,
          false,
        )
        replacement = second + (if chinese { "/" } else { "分之" }) + first
        finish = second_end
        kind = "fraction"
      }
    } else if !chinese &&
      text_at(text, end, "年") &&
      original.length() >= 2 &&
      original.length() <= 4 &&
      !original.contains(".") &&
      !original.contains("-") {
      replacement = an2cn(original, mode="direct")
      kind = "date-year"
    } else if !chinese &&
      text_at(text, end, "-") &&
      ascii_digit_at(text, end + 1) {
      let second_end = number_end(text, end + 1, false)
      replacement = first + "到" + an2cn(text[end + 1:second_end].to_owned())
      finish = second_end
      kind = "range"
    }
  }
  Some({
    start,
    end: finish,
    kind,
    source: text[start:finish].to_owned(),
    replacement,
  })
}

///|
/// Deterministic lexical conversion, not language understanding. Invalid maximal
/// numeral runs are preserved as a whole. Input limit: 262144 UTF-16 code units.
pub fn transform_spans(
  source : String,
  direction? : String = "cn2an",
  direct? : Bool = false,
) -> Array[NumberSpan] raise NumberError {
  if direction != "cn2an" && direction != "an2cn" {
    raise Invalid("unknown transform direction")
  }
  if source.length() > 262144 {
    raise Invalid("sentence length limit")
  }
  let chinese = direction == "cn2an"
  let spans : Array[NumberSpan] = []
  let mut i = 0
  while i < source.length() {
    let candidate = conversion_span(source, i, chinese, direct) catch {
      _ => None
    }
    if candidate is Some(span) {
      spans.push(span)
      i = span.end
    } else {
      let end = number_end(source, i, chinese)
      i = if end > i { end } else { i + 1 }
    }
  }
  spans
}

///|
pub fn transform(
  source : String,
  direction? : String = "cn2an",
  direct? : Bool = false,
) -> String raise NumberError {
  let spans = transform_spans(source, direction~, direct~)
  let out = StringBuilder()
  let mut start = 0
  for span in spans {
    out.write_string(source[start:span.start].to_owned())
    out.write_string(span.replacement)
    start = span.end
  }
  out.write_string(source[start:].to_owned())
  out.to_string()
}