///|
pub(all) enum MessageSegment {
  Data(Bytes)
  Text(String, Int)
  Eci(Int)
  Fnc1
}

///|
pub(all) struct AppendHeader {
  index : Int
  count : Int
  file_id1 : Int
  file_id2 : Int
} derive(Eq)

///|
pub extend AppendHeader with Eq::{equal, not_equal}

///|
pub(all) struct MessageOptions {
  symbol : EncodeOptions
  gs1 : Bool
  macro_kind : Int
  reader_programming : Bool
  append : AppendHeader?
}

///|
pub fn MessageOptions::new(
  symbol? : EncodeOptions = { shape: Square, },
  gs1? : Bool = false,
  macro_kind? : Int = 0,
  reader_programming? : Bool = false,
  append? : AppendHeader? = None,
) -> MessageOptions {
  { symbol, gs1, macro_kind, reader_programming, append, }
}

///|
fn valid_append(a : AppendHeader) -> Bool {
  a.count >= 2 &&
  a.count <= 16 &&
  a.index >= 1 &&
  a.index <= a.count &&
  a.file_id1 >= 1 &&
  a.file_id1 <= 254 &&
  a.file_id2 >= 1 &&
  a.file_id2 <= 254
}

///|
fn append_eci(out : Array[Int], eci : Int) -> Unit raise EncodeError {
  if eci < 0 || eci > 999999 {
    raise InvalidOption("ECI must be 0..999999")
  }
  out.push(241)
  if eci <= 126 {
    out.push(eci + 1)
  } else if eci <= 16382 {
    out.push((eci - 127) / 254 + 128)
    out.push((eci - 127) % 254 + 1)
  } else {
    out.push((eci - 16383) / 64516 + 192)
    out.push((eci - 16383) / 254 % 254 + 1)
    out.push((eci - 16383) % 254 + 1)
  }
}

///|
/// Encode explicit protocol segments. Data segments use the active ECI as supplied.
pub fn encode_message(
  segments : Array[MessageSegment],
  options? : MessageOptions = MessageOptions::new(),
) -> Symbol raise EncodeError {
  let normalized = []
  for segment in segments {
    match segment {
      Text(text, eci) => {
        let bytes = @charset.encode(text, eci) catch {
          _ => raise InvalidOption("charset conversion failed")
        }
        if options.gs1 {
          if eci != 3 {
            raise InvalidOption("GS1 text requires Latin-1")
          }
        } else {
          normalized.push(Eci(eci))
        }
        normalized.push(Data(bytes))
      }
      _ => normalized.push(segment)
    }
  }
  let segments = normalized
  if options.macro_kind != 0 &&
    options.macro_kind != 5 &&
    options.macro_kind != 6 {
    raise InvalidOption("macro must be 0, 5 or 6")
  }
  if options.reader_programming &&
    (options.gs1 || options.append is Some(_) || options.macro_kind != 0) {
    raise InvalidOption("incompatible reader programming header")
  }
  if options.macro_kind != 0 && (options.gs1 || options.append is Some(_)) {
    raise InvalidOption("incompatible macro header")
  }
  let out = []
  match options.append {
    Some(a) => {
      if !valid_append(a) {
        raise InvalidOption("invalid structured append header")
      }
      out.push(233)
      out.push(((a.index - 1) << 4) | (17 - a.count))
      out.push(a.file_id1)
      out.push(a.file_id2)
    }
    None => ()
  }
  if options.gs1 {
    out.push(232)
  }
  if options.reader_programming {
    out.push(234)
  }
  if options.macro_kind != 0 {
    out.push(231 + options.macro_kind)
  }
  let mut length = 0
  for segment in segments {
    if segment is Data(b) {
      length += b.length()
      if length > 3116 {
        raise CapacityExceeded
      }
    }
  }
  for i in 0.. raise InvalidOption("internal text normalization")
      Eci(n) => {
        if options.gs1 {
          raise InvalidOption("GS1 uses its element-string character set")
        }
        append_eci(out, n)
      }
      Fnc1 => {
        if options.reader_programming || options.macro_kind != 0 {
          raise InvalidOption("FNC1 with control header")
        }
        out.push(232)
      }
      Data(bytes) =>
        if i == segments.length() - 1 {
          ignore(planned_with_prefix(bytes, out, options.symbol))
        } else {
          let ascii = ascii_codewords(bytes)
          let overhead = if bytes.length() <= 249 { 2 } else { 3 }
          if ascii.length() <= bytes.length() + overhead {
            for cw in ascii {
              out.push(cw)
            }
          } else {
            out.push(231)
            if bytes.length() <= 249 {
              out.push(randomized(bytes.length(), out.length() + 1))
            } else {
              out.push(randomized(bytes.length() / 250 + 249, out.length() + 1))
              out.push(randomized(bytes.length() % 250, out.length() + 1))
            }
            for byte in bytes {
              out.push(randomized(byte.to_int(), out.length() + 1))
            }
          }
        }
    }
  }
  finish(out, options.symbol)
}

///|
/// Encode a GS1 element string with ASCII GS (29) separating variable-length fields.
pub fn encode_gs1(
  elements : Bytes,
  options? : EncodeOptions = { shape: Square, },
) -> Symbol raise EncodeError {
  let segments = []
  let chunk : Array[Byte] = []
  for b in elements {
    if b == b'\x1d' {
      segments.push(Data(Bytes::from_array(chunk)))
      chunk.clear()
      segments.push(Fnc1)
    } else {
      chunk.push(b)
    }
  }
  segments.push(Data(Bytes::from_array(chunk)))
  encode_message(
    segments,
    options=MessageOptions::new(symbol=options, gs1=true),
  )
}

///|
/// Recognize a Macro 05/06 envelope and encode its body with a macro header.
pub fn encode_macro(
  envelope : Bytes,
  options? : EncodeOptions = { shape: Square, },
) -> Symbol raise EncodeError {
  let n = envelope.length()
  if n < 9 ||
    envelope[0] != b'[' ||
    envelope[1] != b')' ||
    envelope[2] != b'>' ||
    envelope[3] != b'\x1e' ||
    envelope[4] != b'0' ||
    (envelope[5] != b'5' && envelope[5] != b'6') ||
    envelope[6] != b'\x1d' ||
    envelope[n - 2] != b'\x1e' ||
    envelope[n - 1] != b'\x04' {
    raise InvalidOption("invalid Macro 05/06 envelope")
  }
  let body = Bytes::makei(n - 9, fn(i) { envelope[i + 7] })
  encode_message(
    [Data(body)],
    options=MessageOptions::new(
      symbol=options,
      macro_kind=envelope[5].to_int() - 48,
    ),
  )
}