src/qrkit/internal/segment.gleam

//// Segment building for standard QR Code symbols.

import gleam/list
import gleam/option.{type Option, None, Some}
import qrkit/error.{type EncodeError, InvalidEciDesignator}
import qrkit/internal/bitstream
import qrkit/internal/mode
import qrkit/internal/util
import qrkit/types.{
  type Mode, type ModePreference, Alphanumeric, Auto, Byte, ForceByte, Kanji,
  Numeric,
}

pub opaque type Segment {
  Segment(mode: Mode, data: String, count: Int, bits: Int, index: Int)
}

pub fn mode(segment: Segment) -> Mode {
  let Segment(mode, _, _, _, _) = segment
  mode
}

pub fn data(segment: Segment) -> String {
  let Segment(_, data, _, _, _) = segment
  data
}

pub fn count(segment: Segment) -> Int {
  let Segment(_, _, count, _, _) = segment
  count
}

pub fn bits(segment: Segment) -> Int {
  let Segment(_, _, _, bits, _) = segment
  bits
}

pub fn optimise(
  text: String,
  version: Int,
  preference: ModePreference,
) -> Result(List(Segment), EncodeError) {
  case preference {
    ForceByte -> Ok([build_segment(Byte, text, 0)])
    Auto -> {
      let greedy =
        util.characters(text)
        |> greedy_segments(0, [], None)
        |> normalise_segments
      let single_byte = [build_segment(Byte, text, 0)]
      // Greedy segmentation can be worse than encoding everything as Byte for
      // mixed payloads (vCard, JSON, etc.) because every mode switch costs
      // 4 bits + a character-count indicator. Compare the two and keep the
      // cheaper option.
      case
        encoded_bits(greedy, version, None)
        <= encoded_bits(single_byte, version, None)
      {
        True -> Ok(greedy)
        False -> Ok(single_byte)
      }
    }
  }
}

pub fn encoded_bits(
  segments: List(Segment),
  version: Int,
  eci: Option(Int),
) -> Int {
  let eci_bits = case eci {
    None -> 0
    Some(value) -> 4 + eci_designator_bits(value)
  }
  eci_bits
  + list.fold(segments, 0, fn(acc, segment) {
    acc + 4 + mode.char_count_bits(mode(segment), version) + bits(segment)
  })
}

pub fn append_to_stream(
  stream: bitstream.BitStream,
  segments: List(Segment),
  version: Int,
  eci: Option(Int),
) -> Result(bitstream.BitStream, EncodeError) {
  let with_eci = case eci {
    None -> Ok(stream)
    Some(value) -> append_eci(stream, value)
  }
  case with_eci {
    Error(error) -> Error(error)
    Ok(stream_with_eci) ->
      do_append_segments(stream_with_eci, segments, version)
  }
}

fn greedy_segments(
  chars: List(String),
  index: Int,
  acc: List(Segment),
  current: Option(#(Mode, String, Int)),
) -> List(Segment) {
  case chars {
    [] ->
      case current {
        Some(#(current_mode, current_text, start)) ->
          list.reverse([build_segment(current_mode, current_text, start), ..acc])
        None -> list.reverse(acc)
      }
    [char, ..rest] -> {
      let next_mode = classify_char(char)
      case current {
        Some(#(current_mode, current_text, start)) if current_mode == next_mode ->
          greedy_segments(
            rest,
            index + 1,
            acc,
            Some(#(current_mode, current_text <> char, start)),
          )
        Some(#(current_mode, current_text, start)) ->
          greedy_segments(
            rest,
            index + 1,
            [build_segment(current_mode, current_text, start), ..acc],
            Some(#(next_mode, char, index)),
          )
        None ->
          greedy_segments(rest, index + 1, acc, Some(#(next_mode, char, index)))
      }
    }
  }
}

fn normalise_segments(segments: List(Segment)) -> List(Segment) {
  segments
  |> merge_numeric_with_alphanumeric
  |> merge_adjacent_same_mode
}

fn merge_numeric_with_alphanumeric(segments: List(Segment)) -> List(Segment) {
  case segments {
    [first, second, ..rest] ->
      case promote_pair(first, second) {
        Some(merged) -> merge_numeric_with_alphanumeric([merged, ..rest])
        None -> [first, ..merge_numeric_with_alphanumeric([second, ..rest])]
      }
    _ -> segments
  }
}

fn promote_pair(first: Segment, second: Segment) -> Option(Segment) {
  let first_mode = mode(first)
  let second_mode = mode(second)
  case is_alnum_family(first_mode) && is_alnum_family(second_mode) {
    True ->
      Some(build_segment(
        Alphanumeric,
        data(first) <> data(second),
        segment_index(first),
      ))
    False -> None
  }
}

fn merge_adjacent_same_mode(segments: List(Segment)) -> List(Segment) {
  case segments {
    [first, second, ..rest] ->
      case mode(first) == mode(second) {
        True ->
          merge_adjacent_same_mode([
            build_segment(
              mode(first),
              data(first) <> data(second),
              segment_index(first),
            ),
            ..rest
          ])
        False -> [first, ..merge_adjacent_same_mode([second, ..rest])]
      }
    [first, ..rest] -> [first, ..merge_adjacent_same_mode(rest)]
    [] -> []
  }
}

fn classify_char(char: String) -> Mode {
  case mode.is_numeric_char(char) {
    True -> Numeric
    False ->
      case mode.is_alphanumeric_char(char) {
        True -> Alphanumeric
        False ->
          case mode.is_kanji_char(char) {
            True -> Kanji
            False -> Byte
          }
      }
  }
}

fn is_alnum_family(value: Mode) -> Bool {
  case value {
    Numeric -> True
    Alphanumeric -> True
    _ -> False
  }
}

fn build_segment(current_mode: Mode, text: String, index: Int) -> Segment {
  Segment(
    current_mode,
    text,
    mode.character_count(text, current_mode),
    mode.data_bits_length(text, current_mode),
    index,
  )
}

fn do_append_segments(
  stream: bitstream.BitStream,
  segments: List(Segment),
  version: Int,
) -> Result(bitstream.BitStream, EncodeError) {
  case segments {
    [] -> Ok(stream)
    [segment, ..rest] ->
      case
        mode.encode(
          data(segment),
          mode(segment),
          at_index: segment_index(segment),
        )
      {
        Ok(bits) ->
          do_append_segments(
            bitstream.append_bits(
              stream,
              mode.mode_bits(mode(segment)),
              size: 4,
            )
              |> bitstream.append_bits(
                count(segment),
                size: mode.char_count_bits(mode(segment), version),
              )
              |> bitstream.append_bytes(bits),
            rest,
            version,
          )
        Error(error) -> Error(error)
      }
  }
}

fn append_eci(
  stream: bitstream.BitStream,
  designator: Int,
) -> Result(bitstream.BitStream, EncodeError) {
  case designator < 0 || designator > 999_999 {
    True -> Error(InvalidEciDesignator(designator))
    False ->
      case designator < 128 {
        True ->
          Ok(
            stream
            |> bitstream.append_bits(0b0111, size: 4)
            |> bitstream.append_bits(designator, size: 8),
          )
        False ->
          case designator < 16_384 {
            True ->
              Ok(
                stream
                |> bitstream.append_bits(0b0111, size: 4)
                |> bitstream.append_bits(0b10, size: 2)
                |> bitstream.append_bits(designator, size: 14),
              )
            False ->
              Ok(
                stream
                |> bitstream.append_bits(0b0111, size: 4)
                |> bitstream.append_bits(0b110, size: 3)
                |> bitstream.append_bits(designator, size: 21),
              )
          }
      }
  }
}

fn eci_designator_bits(value: Int) -> Int {
  case value < 128 {
    True -> 8
    False ->
      case value < 16_384 {
        True -> 16
        False -> 24
      }
  }
}

fn segment_index(segment: Segment) -> Int {
  let Segment(_, _, _, _, index) = segment
  index
}