///|
/// A location in a JSON document from which typed child lenses can be created.
pub struct ObjectLens {
  priv lens : Lens[Map[String, Json]]
}

///|
/// A reusable typed accessor for one location in a JSON document.
pub struct Lens[T] {
  priv pointer : Pointer
  priv decoder : Decoder[T]
  priv encoder : Encoder[T]
}

///|
/// A typed accessor whose missing and `null` behavior can be replaced without nesting options.
pub struct PresenceLens[T] {
  priv lens : Lens[T]
  priv mode : PresenceMode
}

///|
priv enum PresenceMode {
  Nullable
  Optional
  Nullish(NullishEncodeMode)
}

///|
/// Controls how a `nullish` lens encodes `None`.
pub(all) enum NullishEncodeMode {
  Omit
  Null
} derive(Eq, Debug)

///|
/// Returns an object lens positioned at the document root.
pub fn root() -> ObjectLens {
  ObjectLens::{
    lens: Lens::{
      pointer: Pointer::root(),
      decoder: object_decoder(),
      encoder: object_encoder(),
    },
  }
}

///|
/// Returns an object lens positioned at a root property.
pub fn object(key : String) -> ObjectLens {
  root().object(key)
}

///|
/// Returns an object lens positioned at a child property.
pub fn ObjectLens::object(self : ObjectLens, key : String) -> ObjectLens {
  ObjectLens::{
    lens: Lens::{
      pointer: self.lens.pointer.key(key),
      decoder: object_decoder(),
      encoder: object_encoder(),
    },
  }
}

///|
/// Returns a string lens positioned at a child property.
pub fn ObjectLens::string(self : ObjectLens, key : String) -> Lens[String] {
  Lens::{
    pointer: self.lens.pointer.key(key),
    decoder: string_decoder(),
    encoder: string_encoder(),
  }
}

///|
/// Returns a boolean lens positioned at a child property.
pub fn ObjectLens::bool(self : ObjectLens, key : String) -> Lens[Bool] {
  Lens::{
    pointer: self.lens.pointer.key(key),
    decoder: bool_decoder(),
    encoder: bool_encoder(),
  }
}

///|
/// Returns a finite-number lens positioned at a child property.
pub fn ObjectLens::number(self : ObjectLens, key : String) -> Lens[Double] {
  Lens::{
    pointer: self.lens.pointer.key(key),
    decoder: number_decoder(),
    encoder: number_encoder(),
  }
}

///|
/// Returns an integer lens that delegates conversion to `Double::to_int`.
pub fn ObjectLens::int(self : ObjectLens, key : String) -> Lens[Int] {
  Lens::{
    pointer: self.lens.pointer.key(key),
    decoder: int_decoder(),
    encoder: int_encoder(),
  }
}

///|
/// Returns a raw JSON lens positioned at a child property.
pub fn ObjectLens::json(self : ObjectLens, key : String) -> Lens[Json] {
  Lens::{
    pointer: self.lens.pointer.key(key),
    decoder: json_decoder(),
    encoder: json_encoder(),
  }
}

///|
/// Returns a lens that decodes every array item with this lens's decoder.
pub fn[T] Lens::array(self : Lens[T]) -> Lens[Array[T]] {
  Lens::{
    pointer: self.pointer,
    decoder: array_decoder(self.decoder),
    encoder: array_encoder(self.encoder),
  }
}

///|
/// Returns `None` for JSON `null` while preserving missing-property errors.
pub fn[T] Lens::nullable(self : Lens[T]) -> PresenceLens[T] {
  PresenceLens::{ lens: self, mode: Nullable }
}

///|
/// Returns `None` for a missing selected path while rejecting JSON `null`.
pub fn[T] Lens::optional(self : Lens[T]) -> PresenceLens[T] {
  PresenceLens::{ lens: self, mode: Optional }
}

///|
/// Returns `None` for either a missing selected path or JSON `null`.
///
/// `None` is omitted by default. Pass `encode_mode=Null` to write JSON `null` instead.
pub fn[T] Lens::nullish(
  self : Lens[T],
  encode_mode? : NullishEncodeMode,
) -> PresenceLens[T] {
  PresenceLens::{ lens: self, mode: Nullish(encode_mode.unwrap_or(Omit)) }
}

///|
/// Replaces the current presence policy with nullable semantics.
pub fn[T] PresenceLens::nullable(self : PresenceLens[T]) -> PresenceLens[T] {
  PresenceLens::{ lens: self.lens, mode: Nullable }
}

///|
/// Replaces the current presence policy with optional semantics.
pub fn[T] PresenceLens::optional(self : PresenceLens[T]) -> PresenceLens[T] {
  PresenceLens::{ lens: self.lens, mode: Optional }
}

///|
/// Replaces the current presence policy with nullish semantics.
pub fn[T] PresenceLens::nullish(
  self : PresenceLens[T],
  encode_mode? : NullishEncodeMode,
) -> PresenceLens[T] {
  PresenceLens::{ lens: self.lens, mode: Nullish(encode_mode.unwrap_or(Omit)) }
}

///|
/// Returns a lens that treats every array item as nullable.
pub fn[T] PresenceLens::array(self : PresenceLens[T]) -> Lens[Array[T?]] {
  Lens::{
    pointer: self.lens.pointer,
    decoder: array_decoder(nullable_decoder(self.lens.decoder)),
    encoder: array_encoder(nullable_encoder(self.lens.encoder)),
  }
}

///|
/// Appends this lens's pointer to an existing JSON path.
pub fn[T] Lens::add_to_json_path(
  self : Lens[T],
  path : @json.JsonPath,
) -> @json.JsonPath {
  self.pointer.add_to_json_path(path)
}

///|
/// Appends this lens's pointer to an existing JSON path.
pub fn[T] PresenceLens::add_to_json_path(
  self : PresenceLens[T],
  path : @json.JsonPath,
) -> @json.JsonPath {
  self.lens.pointer.add_to_json_path(path)
}

///|
/// Creates a standard JSON decode error at this lens's location under an existing path.
pub fn[T] Lens::json_decode_error(
  self : Lens[T],
  path : @json.JsonPath,
  message : String,
) -> @json.JsonDecodeError {
  self.pointer.json_decode_error(path, message)
}

///|
/// Creates a standard JSON decode error at this lens's location under an existing path.
pub fn[T] PresenceLens::json_decode_error(
  self : PresenceLens[T],
  path : @json.JsonPath,
  message : String,
) -> @json.JsonDecodeError {
  self.lens.pointer.json_decode_error(path, message)
}

///|
/// Reads this object lens from a JSON document.
pub fn ObjectLens::get(
  self : ObjectLens,
  document : Json,
) -> Map[String, Json] raise LensError {
  self.lens.get(document)
}

///|
/// Reads and decodes this lens from a JSON document.
pub fn[T] Lens::get(self : Lens[T], document : Json) -> T raise LensError {
  let selected = match lookup(document, self.pointer) {
    Some(value) => value
    None => raise missing_property(self.pointer)
  }
  self.decode_selected(selected)
}

///|
fn missing_property(pointer : Pointer) -> LensError {
  LensError(Issue::{ pointer, code: MissingProperty, message: None })
}

///|
fn[T] Lens::decode_selected(
  self : Lens[T],
  selected : Json,
) -> T raise LensError {
  self.decoder.decode(selected) catch {
    DecodeProblem(pointer, code, message) =>
      raise LensError(Issue::{
        pointer: self.pointer.append(pointer),
        code,
        message,
      })
  }
}

///|
/// Reads and decodes this presence-aware lens from a JSON document.
pub fn[T] PresenceLens::get(
  self : PresenceLens[T],
  document : Json,
) -> T? raise LensError {
  match self.mode {
    Optional => {
      let selected = match lookup(document, self.lens.pointer) {
        Some(value) => value
        None => return None
      }
      Some(self.lens.decode_selected(selected))
    }
    Nullable => {
      let selected = match lookup(document, self.lens.pointer) {
        Some(value) => value
        None => raise missing_property(self.lens.pointer)
      }
      match selected {
        Json::Null => None
        _ => Some(self.lens.decode_selected(selected))
      }
    }
    Nullish(_) => {
      let selected = match lookup(document, self.lens.pointer) {
        Some(value) => value
        None => return None
      }
      match selected {
        Json::Null => None
        _ => Some(self.lens.decode_selected(selected))
      }
    }
  }
}

///|
/// Reads this lens and translates a lens failure into a standard JSON decode error.
pub fn[T] Lens::get_or_json_decode_error(
  self : Lens[T],
  document : Json,
  path : @json.JsonPath,
) -> T raise @json.JsonDecodeError {
  self.get(document) catch {
    LensError(issue) =>
      raise issue.pointer.json_decode_error(path, repr(LensError(issue)))
  }
}

///|
/// Reads this lens and translates a lens failure into a standard JSON decode error.
pub fn[T] PresenceLens::get_or_json_decode_error(
  self : PresenceLens[T],
  document : Json,
  path : @json.JsonPath,
) -> T? raise @json.JsonDecodeError {
  self.get(document) catch {
    LensError(issue) =>
      raise issue.pointer.json_decode_error(path, repr(LensError(issue)))
  }
}