///|
/// Compressed container / filter format. `Auto` is decode-only.
pub(all) enum Format {
  /// `.xz` magic vs LZMA_Alone properties byte. Encode is rejected.
  Auto
  /// LZMA_Alone (`.lzma`).
  Lzma
  /// Raw LZMA2 (no `.xz` container).
  Lzma2
  /// `.xz` Stream.
  Xz
} derive(Debug, Eq)

///|
pub(all) enum Check {
  None
  Crc32
  Crc64
  /// Enum member only; encode/decode raise `UnsupportedFeature`.
  Sha256
} derive(Debug, Eq)

///|
pub(all) enum Action {
  /// Adds input. The decoder may complete and emit once a full stream is
  /// available; the encoder remains buffered until `Finish`.
  Run
  /// Unsupported; `code` raises `InvalidConfiguration`.
  SyncFlush
  /// Unsupported; `code` raises `InvalidConfiguration`.
  FullFlush
  /// Ends input and drains through `encode` / `decode`.
  Finish
} derive(Debug, Eq)

///|
pub(all) enum Status {
  /// The call made progress without completing the stream.
  Ok
  /// `Finish` completed and pending output is fully drained.
  StreamEnd
  /// More input is required before the stream can complete.
  NeedInput
  /// Output buffer is full; take `written()` and continue `code` or `finish()`.
  NeedOutput
} derive(Debug, Eq)

///|
pub(all) struct Preset {
  level : Int
  extreme : Bool
} derive(Debug, Eq)

///|
pub fn Preset::default() -> Preset {
  { level: 6, extreme: false, }
}

///|
pub fn Preset::new(
  level : Int,
  extreme? : Bool = false,
) -> Preset raise LzmaError {
  if level < 0 || level > 9 {
    raise InvalidConfiguration("preset level must be 0..=9")
  }
  if extreme {
    raise UnsupportedFeature("extreme preset")
  }
  { level, extreme, }
}

///|
/// LZMA1 properties. All fields are required; `lc + lp` must be at most 4.
pub(all) struct LzmaProps {
  lc : Int
  lp : Int
  pb : Int
  dict_size : UInt
} derive(Debug, Eq)

///|
pub const MIN_DICT_SIZE : UInt = 4096U

///|
pub const MAX_DICT_SIZE : UInt = 1610612736U

///|
pub fn LzmaProps::validate(self : LzmaProps) -> Unit raise LzmaError {
  if self.lc < 0 || self.lc > 8 {
    raise InvalidConfiguration("lc must be 0..=8")
  }
  if self.lp < 0 || self.lp > 4 {
    raise InvalidConfiguration("lp must be 0..=4")
  }
  if self.pb < 0 || self.pb > 4 {
    raise InvalidConfiguration("pb must be 0..=4")
  }
  if self.lc + self.lp > 4 {
    raise InvalidConfiguration("lc + lp must be <= 4")
  }
  if self.dict_size < MIN_DICT_SIZE || self.dict_size > MAX_DICT_SIZE {
    raise InvalidConfiguration("dict_size out of range")
  }
}

///|
/// liblzma preset dictionary sizes: 1 << [18,20,21,22,22,23,23,24,25,26].
pub fn LzmaProps::from_preset(preset : Preset) -> LzmaProps raise LzmaError {
  Preset::new(preset.level, extreme=preset.extreme) |> ignore
  let shifts = [18, 20, 21, 22, 22, 23, 23, 24, 25, 26]
  let dict_size = 1U << shifts[preset.level]
  let props = { lc: 3, lp: 0, pb: 2, dict_size, }
  props.validate()
  props
}

///|
pub fn pack_props(lc : Int, lp : Int, pb : Int) -> Byte raise LzmaError {
  if lc < 0 || lc > 8 || lp < 0 || lp > 4 || pb < 0 || pb > 4 || lc + lp > 4 {
    raise InvalidConfiguration("invalid lc/lp/pb")
  }
  ((pb * 5 + lp) * 9 + lc).to_byte()
}

///|
pub fn unpack_props(props : Byte) -> (Int, Int, Int) raise LzmaError {
  let n = props.to_int()
  if n >= 225 {
    raise InvalidInput("LZMA properties byte")
  }
  let lc = n % 9
  let t = n / 9
  let lp = t % 5
  let pb = t / 5
  if lc + lp > 4 {
    raise InvalidInput("LZMA properties byte")
  }
  (lc, lp, pb)
}

///|
/// Extra pre/post filters around the codec. `Lzma1`/`Lzma2` here are configuration errors.
pub(all) enum FilterSpec {
  Lzma1(LzmaProps)
  Lzma2(LzmaProps)
  Delta(distance~ : Int)
  BcjX86(start_offset~ : UInt)
} derive(Debug, Eq)

///|
pub(all) struct EncodeOptions {
  format : Format
  preset : Preset
  check : Check
  props : LzmaProps?
  filters : Array[FilterSpec]
} derive(Debug, Eq)

///|
pub fn EncodeOptions::default() -> EncodeOptions {
  {
    format: Format::Xz,
    preset: Preset::default(),
    check: Check::Crc64,
    props: None,
    filters: [],
  }
}

///|
pub fn EncodeOptions::resolved_props(
  self : EncodeOptions,
) -> LzmaProps raise LzmaError {
  match self.props {
    Some(p) => {
      p.validate()
      p
    }
    None => LzmaProps::from_preset(self.preset)
  }
}

///|
pub fn EncodeOptions::validate(self : EncodeOptions) -> Unit raise LzmaError {
  match self.format {
    Auto =>
      raise InvalidConfiguration("Format::Auto cannot be used for encoding")
    Lzma | Lzma2 | Xz => ()
  }
  Preset::new(self.preset.level, extreme=self.preset.extreme) |> ignore
  self.resolved_props() |> ignore
  validate_extra_filters(self.filters)
}

///|
pub(all) struct DecodeOptions {
  format : Format
  memlimit : UInt64?
  concatenated : Bool
  ignore_check : Bool
  filters : Array[FilterSpec]
} derive(Debug, Eq)

///|
pub fn DecodeOptions::default() -> DecodeOptions {
  {
    format: Format::Auto,
    memlimit: None,
    concatenated: false,
    ignore_check: false,
    filters: [],
  }
}

///|
pub fn DecodeOptions::validate(self : DecodeOptions) -> Unit raise LzmaError {
  if effective_memlimit(self.memlimit) == 0UL {
    raise InvalidConfiguration("memlimit must be > 0")
  }
  validate_extra_filters(self.filters)
}

///|
fn validate_extra_filters(filters : Array[FilterSpec]) -> Unit raise LzmaError {
  for f in filters {
    match f {
      Lzma1(_) | Lzma2(_) => raise InvalidConfiguration("codec filter in chain")
      Delta(distance~) =>
        if distance < 1 || distance > 256 {
          raise InvalidConfiguration("delta distance")
        }
      BcjX86(_) => ()
    }
  }
}

///|
pub const UNKNOWN_SIZE : UInt64 = 0xFFFF_FFFF_FFFF_FFFFUL