src/qrkit.gleam

//// Public API for the qrkit package.

import gleam/list
import gleam/option.{type Option, None, Some}
import qrkit/error
import qrkit/internal/micro
import qrkit/internal/rmqr
import qrkit/internal/standard
import qrkit/internal/structured_append
import qrkit/internal/util
import qrkit/types

/// Type alias for [`qrkit/types.ErrorCorrection`](./qrkit/types.html#ErrorCorrection).
/// To pattern-match on `Low | Medium | Quartile | High`, `import qrkit/types`.
pub type ErrorCorrection =
  types.ErrorCorrection

/// Type alias for [`qrkit/types.Mode`](./qrkit/types.html#Mode).
/// To pattern-match on `Numeric | Alphanumeric | Byte | Kanji`, `import qrkit/types`.
pub type Mode =
  types.Mode

/// Type alias for [`qrkit/types.ModePreference`](./qrkit/types.html#ModePreference).
/// To pattern-match on `Auto | ForceByte`, `import qrkit/types`.
pub type ModePreference =
  types.ModePreference

/// Type alias for [`qrkit/types.Symbol`](./qrkit/types.html#Symbol).
/// To pattern-match on `Standard | Micro | Rectangular`, `import qrkit/types`.
pub type Symbol =
  types.Symbol

/// Type alias for [`qrkit/error.EncodeError`](./qrkit/error.html#EncodeError).
/// To pattern-match on `EmptyInput | InvalidVersion(..) | InvalidEciDesignator(..) | DataExceedsCapacity(..) | UnsupportedCharacter(..) | IncompatibleOptions(..)`, `import qrkit/error`.
pub type EncodeError =
  error.EncodeError

/// Type alias for [`qrkit/error.MatrixAccessError`](./qrkit/error.html#MatrixAccessError).
/// To pattern-match on `ModuleOutOfBounds(..)`, `import qrkit/error`.
pub type MatrixAccessError =
  error.MatrixAccessError

pub opaque type Builder {
  Builder(
    data: String,
    ecc: ErrorCorrection,
    min_version: Option(Int),
    eci: Option(Int),
    symbol: Symbol,
    preference: ModePreference,
  )
}

pub opaque type QrCode {
  QrCode(
    version: Int,
    width: Int,
    height: Int,
    ecc: ErrorCorrection,
    symbol: Symbol,
    mask: Int,
    rows: List(List(Bool)),
  )
}

/// The package version.
pub fn package_version() -> String {
  "0.1.0"
}

/// Create a new builder from input text.
pub fn new(data: String) -> Builder {
  Builder(data, types.Medium, None, None, types.Standard, types.Auto)
}

/// Encode input text using the default builder configuration.
pub fn encode(data: String) -> Result(QrCode, EncodeError) {
  new(data) |> build()
}

/// Set the desired error correction level.
pub fn with_ecc(builder: Builder, ecc: ErrorCorrection) -> Builder {
  let Builder(data, _, min_version, eci, symbol, preference) = builder
  Builder(data, ecc, min_version, eci, symbol, preference)
}

/// Pin the symbol version exactly.
///
/// The value is interpreted relative to the active symbol family: Standard QR
/// accepts 1..40, Micro QR accepts 1..4 (M1..M4), and rMQR accepts 1..32
/// (R7x43..R17x139).
///
/// If the payload, mode, or ECC level cannot be satisfied at the requested
/// version, `build` returns `Error(DataExceedsCapacity)` or
/// `Error(IncompatibleOptions)` instead of bumping to a larger version.
/// When no exact version is configured, the encoder selects the smallest
/// version that fits the payload.
pub fn with_exact_version(builder: Builder, version: Int) -> Builder {
  let Builder(data, ecc, _, eci, symbol, preference) = builder
  Builder(data, ecc, Some(version), eci, symbol, preference)
}

/// Compatibility alias for [`with_exact_version`](#with_exact_version).
///
/// Despite the historical name, this pins the symbol version exactly rather
/// than setting a lower bound. New code should prefer `with_exact_version`.
pub fn with_min_version(builder: Builder, min_version: Int) -> Builder {
  with_exact_version(builder, min_version)
}

/// Add an optional ECI assignment designator before the data segments.
///
/// ECI is only supported for Standard QR. Valid designators are in the range
/// 0..999999; invalid values surface as `Error(InvalidEciDesignator(..))`
/// during `build`.
pub fn with_eci(builder: Builder, designator: Int) -> Builder {
  let Builder(data, ecc, min_version, _, symbol, preference) = builder
  Builder(data, ecc, min_version, Some(designator), symbol, preference)
}

/// Select the symbol family.
pub fn with_symbol(builder: Builder, symbol: Symbol) -> Builder {
  let Builder(data, ecc, min_version, eci, _, preference) = builder
  Builder(data, ecc, min_version, eci, symbol, preference)
}

/// Change the mode optimisation strategy.
pub fn with_mode_preference(
  builder: Builder,
  preference: ModePreference,
) -> Builder {
  let Builder(data, ecc, min_version, eci, symbol, _) = builder
  Builder(data, ecc, min_version, eci, symbol, preference)
}

/// Build a QR code from the accumulated builder configuration.
pub fn build(builder: Builder) -> Result(QrCode, EncodeError) {
  let Builder(data, ecc, min_version, eci, symbol, preference) = builder
  case data == "" {
    True -> Error(error.EmptyInput)
    False ->
      case validate_builder_options(min_version, eci, symbol) {
        Error(error) -> Error(error)
        Ok(Nil) ->
          case symbol {
            types.Standard ->
              case standard.encode(data, ecc, min_version, eci, preference) {
                Ok(encoded) ->
                  Ok(QrCode(
                    standard.version(encoded),
                    standard.width(encoded),
                    standard.height(encoded),
                    ecc,
                    symbol,
                    standard.mask(encoded),
                    standard.rows(encoded),
                  ))
                Error(encode_error) -> Error(encode_error)
              }
            types.Micro ->
              case micro.encode(data, ecc, min_version, preference) {
                Ok(encoded) ->
                  Ok(QrCode(
                    micro.version(encoded),
                    micro.width(encoded),
                    micro.height(encoded),
                    ecc,
                    symbol,
                    micro.mask(encoded),
                    micro.rows(encoded),
                  ))
                Error(encode_error) -> Error(encode_error)
              }
            types.Rectangular ->
              case rmqr.encode(data, ecc, min_version, preference) {
                Ok(encoded) ->
                  Ok(QrCode(
                    rmqr.version(encoded),
                    rmqr.width(encoded),
                    rmqr.height(encoded),
                    ecc,
                    symbol,
                    rmqr.mask(encoded),
                    rmqr.rows(encoded),
                  ))
                Error(encode_error) -> Error(encode_error)
              }
          }
      }
  }
}

fn validate_builder_options(
  min_version: Option(Int),
  eci: Option(Int),
  symbol: Symbol,
) -> Result(Nil, EncodeError) {
  case validate_min_version(min_version, symbol) {
    Error(error) -> Error(error)
    Ok(Nil) -> validate_eci(eci, symbol)
  }
}

fn validate_min_version(
  min_version: Option(Int),
  symbol: Symbol,
) -> Result(Nil, EncodeError) {
  case min_version {
    None -> Ok(Nil)
    Some(value) -> {
      let upper = case symbol {
        types.Standard -> 40
        types.Micro -> 4
        types.Rectangular -> 32
      }
      case value < 1 || value > upper {
        True -> Error(error.InvalidVersion(value))
        False -> Ok(Nil)
      }
    }
  }
}

fn validate_eci(eci: Option(Int), symbol: Symbol) -> Result(Nil, EncodeError) {
  case eci {
    None -> Ok(Nil)
    Some(designator) ->
      case designator < 0 || designator > 999_999 {
        True -> Error(error.InvalidEciDesignator(designator))
        False ->
          case symbol == types.Standard {
            True -> Ok(Nil)
            False ->
              Error(error.IncompatibleOptions(
                "ECI is only supported for Standard QR",
              ))
          }
      }
  }
}

/// Split data into multiple symbols using Structured Append (ISO/IEC 18004 §8.2).
///
/// Each returned QR carries the 20-bit Structured Append header so a compliant
/// reader can reassemble the original message. When `data` fits in a single QR
/// at `max_version`, the returned list contains exactly one symbol with no SA
/// header. Uses the Medium error correction level — call `encode_split_with`
/// for a different level.
pub fn encode_split(
  data: String,
  max_version: Int,
) -> Result(List(QrCode), EncodeError) {
  encode_split_with(data, max_version, types.Medium)
}

/// Same as `encode_split` but with a caller-chosen error correction level.
pub fn encode_split_with(
  data: String,
  max_version: Int,
  ecc: ErrorCorrection,
) -> Result(List(QrCode), EncodeError) {
  case structured_append.encode(data, max_version, ecc) {
    Error(error) -> Error(error)
    Ok(encodes) ->
      Ok(
        list.map(encodes, fn(encoded) {
          QrCode(
            standard.version(encoded),
            standard.width(encoded),
            standard.height(encoded),
            ecc,
            types.Standard,
            standard.mask(encoded),
            standard.rows(encoded),
          )
        }),
      )
  }
}

/// Return the symbol version number. Standard QR returns 1..40, Micro QR 1..4
/// (M1..M4), and rMQR 1..32 (R7x43..R17x139).
pub fn version(qr: QrCode) -> Int {
  let QrCode(version, _, _, _, _, _, _) = qr
  version
}

/// Return the symbol side length in modules. For non-square rMQR symbols this
/// is the width; use `width` and `height` for the explicit dimensions.
pub fn size(qr: QrCode) -> Int {
  let QrCode(_, width, _, _, _, _, _) = qr
  width
}

/// Return the symbol width in modules.
pub fn width(qr: QrCode) -> Int {
  let QrCode(_, width, _, _, _, _, _) = qr
  width
}

/// Return the symbol height in modules.
pub fn height(qr: QrCode) -> Int {
  let QrCode(_, _, height, _, _, _, _) = qr
  height
}

/// Return the symbol dimensions in modules.
pub fn symbol_size(qr: QrCode) -> #(Int, Int) {
  #(width(qr), height(qr))
}

/// Return the error correction level used by this symbol.
pub fn error_correction(qr: QrCode) -> ErrorCorrection {
  let QrCode(_, _, _, ecc, _, _, _) = qr
  ecc
}

/// Return the canonical single-letter ECC designator.
pub fn error_correction_designator(ecc: ErrorCorrection) -> String {
  case ecc {
    types.Low -> "L"
    types.Medium -> "M"
    types.Quartile -> "Q"
    types.High -> "H"
  }
}

/// Return the symbol family used by this QR code.
pub fn symbol(qr: QrCode) -> Symbol {
  let QrCode(_, _, _, _, symbol, _, _) = qr
  symbol
}

/// Return the mask pattern that was applied. Standard QR returns 0..7,
/// Micro QR 0..3, and rMQR always 4 (rMQR uses a single fixed mask).
pub fn mask(qr: QrCode) -> Int {
  let QrCode(_, _, _, _, _, mask, _) = qr
  mask
}

/// Return a single module from the symbol matrix.
///
/// Returns `Error(ModuleOutOfBounds(..))` when `x` or `y` fall outside the
/// matrix dimensions.
pub fn module_at(qr: QrCode, x: Int, y: Int) -> Result(Bool, MatrixAccessError) {
  case rows(qr) |> util.at(y) {
    Ok(row) ->
      case util.at(row, x) {
        Ok(value) -> Ok(value)
        Error(_) -> Error(error.ModuleOutOfBounds(x, y, width(qr), height(qr)))
      }
    Error(_) -> Error(error.ModuleOutOfBounds(x, y, width(qr), height(qr)))
  }
}

/// Return the symbol matrix as rows of booleans.
pub fn rows(qr: QrCode) -> List(List(Bool)) {
  let QrCode(_, _, _, _, _, _, rows) = qr
  rows
}