// Low level encoding: codewords to modules.
//
// A PDF417 symbol is drawn row by row.  Every row is
//
//     start pattern
//     left row indicator codeword
//     one symbol character per data column
//     right row indicator codeword
//     stop pattern
//
// and every codeword is one symbol character: a 17 module pattern chosen from
// the cluster of the row, i.e. `row mod 3`.  The clusters are the tables in
// `patterns.mbt`.  A module value of 1 is a bar and 0 is a space.
//
// This module is the bridge from codewords to something a renderer can fill:
// `symbol_matrix` returns one row of modules per symbol row, and the helpers
// below turn that into ASCII or SVG.

///|
/// Modules of the start pattern.
pub let start_modules : Int = 17

///|
/// Modules of the stop pattern.
pub let stop_modules : Int = 18

///|
/// Symbol character for `codeword` in `cluster`, or `None` when the codeword
/// or the cluster is out of range.
pub fn cluster_pattern(cluster : Int, codeword : Int) -> Int? {
  if codeword < 0 || codeword > max_codewords {
    return None
  }
  match cluster {
    0 => Some(cluster0_patterns[codeword])
    1 => Some(cluster1_patterns[codeword])
    2 => Some(cluster2_patterns[codeword])
    _ => None
  }
}

///|
/// `true` when the module is a bar.  Bits below the pattern's most significant
/// bit do not exist and are always spaces.
pub fn module_is_bar(pattern : Int, modules : Int, index : Int) -> Bool {
  if index < 0 || index >= modules {
    return false
  }
  ((pattern >> (modules - 1 - index)) & 1) == 1
}

///|
/// Append the modules of `pattern` to `row`.
fn append_pattern(row : Array[Int], pattern : Int, modules : Int) -> Unit {
  for index in 0.. Unit raise EncodeError {
  match cluster_pattern(cluster, codeword) {
    Some(pattern) => append_pattern(row, pattern, start_modules)
    None => raise UnrepresentableChar(codeword)
  }
}

///|
/// Module row of one symbol row, without a quiet zone.
///
/// `row` is zero based, `codewords` are the symbol codewords in storage order
/// (row major) and `ec_level` is the error correction level the indicators
/// report.  The row indicator codewords are computed from the layout, exactly
/// as `symbol_rows` does.
pub fn symbol_row_modules(
  codewords : Array[Int],
  row : Int,
  columns : Int,
  rows : Int,
  ec_level : Int,
) -> Array[Int] raise EncodeError {
  let modules : Array[Int] = []
  append_pattern(modules, start_pattern, start_modules)
  let cluster = row_cluster(row)
  append_codeword(
    modules,
    cluster,
    left_row_indicator(row, rows, columns, ec_level),
  )
  for column in 0.. Array[Array[Int]] raise EncodeError {
  let matrix : Array[Array[Int]] = []
  for row in 0.. Int {
  if matrix.length() == 0 {
    return 0
  }
  matrix[0].length()
}

///|
/// Check that a matrix is rectangular, that every row starts with the start
/// pattern and ends with the stop pattern, and that the width matches the
/// column count.
pub fn verify_matrix(matrix : Array[Array[Int]], columns : Int) -> Bool {
  if matrix.length() == 0 {
    return false
  }
  let width = symbol_modules(columns)
  for row in matrix {
    if row.length() != width {
      return false
    }
    for index in 0.. (Int, Int, Int, Int) {
  let unit = if scale <= 0 { 1 } else { scale }
  let quiet_zone = if quiet < 0 { 0 } else { quiet }
  if columns <= 0 || column < 0 || column >= columns || rows <= 0 {
    return (0, 0, 0, 0)
  }
  let left = (quiet_zone + start_modules + column * 17) * unit
  (left, quiet_zone * unit, 17 * unit, rows * unit)
}

///|
/// Render a matrix as text: `X` per bar, space per space, scale repeats.  A
/// quiet zone of `quiet` modules is added on both sides so the result can be
/// eyeballed as a symbol.
pub fn matrix_to_ascii(
  matrix : Array[Array[Int]],
  scale : Int,
  quiet : Int,
) -> String {
  let rows : Array[String] = []
  for row in matrix {
    let line = StringBuilder()
    for _ in 0..<(quiet * scale) {
      line.write_char(' ')
    }
    for value in row {
      for _ in 0.. 0 {
      result.write_char('\n')
    }
    result.write_string(rows[index])
    index += 1
  }
  result.to_string()
}

///|
/// Render a matrix as a black and white SVG document.
///
/// `scale` is the width of one module in user units and `quiet` the quiet zone
/// in modules.  The output uses one path per row, so the document stays small
/// even for a thirty column symbol.
pub fn matrix_to_svg(
  matrix : Array[Array[Int]],
  scale : Int,
  quiet : Int,
) -> String {
  let unit = if scale <= 0 { 1 } else { scale }
  let quiet_zone = if quiet < 0 { 0 } else { quiet }
  let width = matrix_row_width(matrix)
  let height = matrix.length()
  let total_width = (width + 2 * quiet_zone) * unit
  let total_height = (height + 2 * quiet_zone) * unit
  let svg = StringBuilder()
  svg.write_string("\n")
  svg.write_string("\n")
  svg.write_string("\n")
  svg.write_string("\n")
  svg.write_string("\n")
  svg.to_string()
}