///| Classification: matching an extracted glyph feature against a set of

///|

///| labelled references. The simplest robust approach for a small alphabet is

///| nearest-neighbour over the Hamming distance between binarized feature

///| grids, which is what this module implements.

///|
/// A labelled feature grid used as a template.
pub struct Reference {
  label : String
  grid : Array[Int]
}

///|
pub fn Reference::new(label : String, grid : Array[Int]) -> Reference {
  { label, grid, }
}

///| The result of classification: the winning label and its distance (a
///

///|
/// smaller distance means a closer match).
pub struct Match {
  label : String
  distance : Int
}

///|
/// Number of positions at which the two feature vectors differ.
pub fn cell_distance(a : Array[Int], b : Array[Int]) -> Int {
  let n = if a.length() < b.length() { a.length() } else { b.length() }
  let mut d = 0
  for i = 0; i < n; i = i + 1 {
    if a[i] != b[i] {
      d = d + 1
    }
  }
  d
}

///|
/// Weighted Hamming distance: every cell where the two grids differ adds its
/// weight instead of 1, so a disagreement in an important cell counts more.
/// Cells past the end of `weights` fall back to weight 1.
pub fn weighted_distance(
  a : Array[Int],
  b : Array[Int],
  weights : Array[Int],
) -> Int {
  let n = if a.length() < b.length() { a.length() } else { b.length() }
  let mut d = 0
  for i = 0; i < n; i = i + 1 {
    if a[i] != b[i] {
      let w = if i < weights.length() { weights[i] } else { 1 }
      d = d + w
    }
  }
  d
}

///| Return the reference whose grid is closest to `features`, or `None` when
///

///| there are no references. Ties are broken in favour of the earliest

///|
/// reference.
pub fn classify(features : Array[Int], refs : Array[Reference]) -> Match? {
  let mut best : Match? = None
  for i = 0; i < refs.length(); i = i + 1 {
    let d = cell_distance(features, refs[i].grid)
    match best {
      None => best = Some({ label: refs[i].label, distance: d, })
      Some(m) =>
        if d < m.distance {
          best = Some({ label: refs[i].label, distance: d, })
        }
    }
  }
  best
}

///|
/// A 64-cell weight grid that emphasises the glyph interior (weight `2`) over
/// the outer ring (weight `1`). The interior holds the stroke structure that
/// stays stable across fonts, while the border is more sensitive to noise.
pub fn center_weights() -> Array[Int] {
  let w = Array::make(64, 1)
  for y = 0; y < 8; y = y + 1 {
    for x = 0; x < 8; x = x + 1 {
      if x >= 2 && x <= 5 && y >= 2 && y <= 5 {
        w[y * 8 + x] = 2
      }
    }
  }
  w
}

///|
/// Classify by weighted Hamming distance, giving each cell the importance in
/// `weights`. Otherwise identical to `classify`.
pub fn classify_weighted(
  features : Array[Int],
  refs : Array[Reference],
  weights : Array[Int],
) -> Match? {
  let mut best : Match? = None
  for i = 0; i < refs.length(); i = i + 1 {
    let d = weighted_distance(features, refs[i].grid, weights)
    match best {
      None => best = Some({ label: refs[i].label, distance: d, })
      Some(m) =>
        if d < m.distance {
          best = Some({ label: refs[i].label, distance: d, })
        }
    }
  }
  best
}

///|
/// Shift a `size`-by-`size` feature grid by `(dx, dy)` cells, filling the
/// vacated border with paper (`0`). A shift of `(+1, 0)` moves the glyph
/// right, vacating the left column.
fn shift_grid(grid : Array[Int], size : Int, dx : Int, dy : Int) -> Array[Int] {
  let out = Array::make(size * size, 0)
  for y = 0; y < size; y = y + 1 {
    for x = 0; x < size; x = x + 1 {
      let sx = x - dx
      let sy = y - dy
      if sx >= 0 && sx < size && sy >= 0 && sy < size {
        out[y * size + x] = grid[sy * size + sx]
      }
    }
  }
  out
}

///|
/// The five shifted variants of a `size`-by-`size` grid: the original plus
/// one-cell shifts up, down, left and right. Augmenting references with these
/// makes classification tolerant of glyphs that sit slightly off-centre.
pub fn shift_variants(grid : Array[Int], size : Int) -> Array[Array[Int]] {
  let variants : Array[Array[Int]] = Array(capacity=5)
  variants.push(grid)
  variants.push(shift_grid(grid, size, -1, 0))
  variants.push(shift_grid(grid, size, 1, 0))
  variants.push(shift_grid(grid, size, 0, -1))
  variants.push(shift_grid(grid, size, 0, 1))
  variants
}

///|
/// Classify against an augmented reference set in which every template is
/// expanded into its shifted variants. A glyph then matches its class as long
/// as it is close to any one shift of that class's template.
pub fn classify_shifted(
  features : Array[Int],
  refs : Array[Reference],
  size : Int,
) -> Match? {
  let expanded : Array[Reference] = Array(capacity=refs.length() * 5)
  for i = 0; i < refs.length(); i = i + 1 {
    let variants = shift_variants(refs[i].grid, size)
    for j = 0; j < variants.length(); j = j + 1 {
      expanded.push(Reference::new(refs[i].label, variants[j]))
    }
  }
  classify(features, expanded)
}

///|
/// Build the ten digit templates from the built-in font, labelled `"0"`
/// through `"9"`. Split out of `digit_references` so the result can be cached.
fn build_digit_references() -> Array[Reference] {
  let refs : Array[Reference] = Array(capacity=10)
  for ch in "0123456789" {
    refs.push(Reference::new(ch.to_string(), char_grid(ch)))
  }
  refs
}

///|
/// The ten digit templates, computed once and cached.
let digit_refs_cache : Array[Reference] = build_digit_references()

///|
/// The ten digit templates from the built-in font, labelled `"0"` through
/// `"9"`.
pub fn digit_references() -> Array[Reference] {
  digit_refs_cache
}

///|
/// Build the 62 templates for digits `0`-`9` and letters `A`-`Z` / `a`-`z`
/// from the built-in font. Split out of `alphanumeric_references` for caching.
fn build_alphanumeric_references() -> Array[Reference] {
  let refs : Array[Reference] = Array(capacity=62)
  for ch in "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" {
    refs.push(Reference::new(ch.to_string(), char_grid(ch)))
  }
  refs
}

///|
/// The 62 templates for digits `0`-`9` and letters `A`-`Z` / `a`-`z`, computed
/// once and cached.
let alphanumeric_refs_cache : Array[Reference] = build_alphanumeric_references()

///|
/// The 62 templates for digits `0`-`9` and letters `A`-`Z` / `a`-`z`, from
/// the built-in font.
pub fn alphanumeric_references() -> Array[Reference] {
  alphanumeric_refs_cache
}