// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// What a reader will accept and what a writer will send.
///
/// Every default here is the Rust `fory` crate's `Config::default()`, because
/// that is the reference implementation the specification names first, and
/// because a default nobody else shares is a default an interop test finds.
/// Where this package has no counterpart for one of its knobs — the schema
/// version check, the type-meta cache — the knob is absent rather than inert.
pub(all) struct Config {
  /// Whether the header byte carries the cross-language flag.
  ///
  /// The xlang format is the one implemented here, so this is `true`; a writer
  /// asked for `false` writes a header without the flag, and a reader asked for
  /// `true` refuses one.
  xlang : Bool
  /// Whether values that are written more than once, or that take part in a
  /// cycle, are sent once and pointed at afterwards.
  ///
  /// Off by default, as in the Rust crate: reference tracking costs a byte per
  /// tracked value and a table on both sides, which a graph without sharing
  /// should not pay for. Turn it on and mark the shared nodes with `shared`.
  track_ref : Bool
  /// Whether the bytes of a string are checked as UTF-8 before they become a
  /// `String`.
  ///
  /// On by default, as in the Rust crate. Turning it off is a promise that the
  /// peer is trusted, not a way to read a string that is not one.
  check_string : Bool
  /// How deeply values may nest before a message is refused.
  ///
  /// Five is the Rust crate's `max_dyn_depth`. A message of ten thousand open
  /// lists is a denial of service rather than a message, and the depth is the
  /// one number that stops it before the stack does.
  depth : Int32
  /// How many fields a struct may declare before a message is refused.
  ///
  /// Five hundred and twelve is the Rust crate's `max_type_fields`. The count
  /// is written by the message and read before the fields are, so it is the
  /// cheapest place to stop a message that claims more fields than it has.
  fields : Int32
  /// How many bytes of variable-length payload one message may declare before
  /// it is refused: strings, binaries, dense arrays and container bodies added
  /// up as they are read.
  ///
  /// A hundred and twenty-eight mebibytes is the Rust crate's
  /// `max_graph_memory_bytes`. Declared lengths are already bounded by the
  /// bytes actually present, so this gate is the one that bounds a message
  /// made of many small declarations whose total is a large allocation.
  graph : Int32
}

///|
/// The preset every entry point takes when it is not given one.
///
/// Named `config` so that an argument reads `config=config` and an update reads
/// `{ ..config, track_ref: true }`.
pub let config : Config = {
  xlang: true,
  track_ref: false,
  check_string: true,
  depth: 5,
  fields: 512,
  graph: 134_217_728,
}

///|
/// The same preset, one argument at a time.
///
/// For when several parts differ at once and a record update would mean
/// spelling the ones that did not change.
pub fn Config::new(
  xlang~ : Bool = config.xlang,
  track_ref~ : Bool = config.track_ref,
  check_string~ : Bool = config.check_string,
  depth~ : Int32 = config.depth,
  fields~ : Int32 = config.fields,
  graph~ : Int32 = config.graph,
) -> Config {
  { xlang, track_ref, check_string, depth, fields, graph, }
}

///|
/// Which of the two ways of giving a configuration wins when both are given.
///
/// `Extra` — the flat arguments — is the default, because a caller who took the
/// trouble to write `track_ref=true` on one call means it on that call.
pub(all) enum Wins {
  /// The record wins: the flat arguments are ignored where they clash.
  Base
  /// The flat arguments win. The default.
  Extra
} derive(Eq, Debug)

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

///|
pub extend Wins with Debug::{to_repr}

///|
/// What to do when the same setting is given twice, once in the record and once
/// as a flat argument.
pub(all) enum OnClash[T] {
  /// Take one of them silently: the one `wins` says.
  Ignore
  /// Refuse the call.
  Panic
  /// Let the caller decide. The pair is (the record's, the flat one's).
  Handle((T, T) -> T)
}

///|
/// The flat mirrors of a `Config`: each one is `None` when the caller did not
/// give it, which is what tells a setting given once from a setting given twice.
priv struct Patch {
  xlang : Bool?
  track_ref : Bool?
  check_string : Bool?
  depth : Int32?
  fields : Int32?
  graph : Int32?
}

///|
/// What `OnClash::Panic` raises: a setting given twice where the caller asked
/// to hear about it.
pub(all) suberror Clashed {
  /// The name of the setting, and the two values it was given.
  Clashed(name~ : String)
} derive(Eq, Debug)

///|
pub impl Show for Clashed with fn output(self, logger) {
  match self {
    Clashed(name~) => logger.write_string("Clashed(name=\{name})")
  }
}

///|
pub extend Clashed with Debug::{to_repr}

///|
pub extend Clashed with Show::{to_string, output}

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

///|
/// Whether a flat mirror contradicts the record it was given beside.
fn clashes(base : Config, patch : Patch) -> Bool {
  match patch.xlang {
    Some(v) => if v != base.xlang { return true }
    None => ()
  }
  match patch.track_ref {
    Some(v) => if v != base.track_ref { return true }
    None => ()
  }
  match patch.check_string {
    Some(v) => if v != base.check_string { return true }
    None => ()
  }
  match patch.depth {
    Some(v) => if v != base.depth { return true }
    None => ()
  }
  match patch.fields {
    Some(v) => if v != base.fields { return true }
    None => ()
  }
  match patch.graph {
    Some(v) => if v != base.graph { return true }
    None => ()
  }
  false
}

///|
/// The two as one, in the direction `wins` names: `Extra` takes the flat mirror,
/// `Base` keeps the record.
fn apply(base : Config, patch : Patch, wins : Wins) -> Config {
  {
    xlang: if wins == Extra {
      match patch.xlang {
        Some(v) => v
        None => base.xlang
      }
    } else {
      base.xlang
    },
    track_ref: if wins == Extra {
      match patch.track_ref {
        Some(v) => v
        None => base.track_ref
      }
    } else {
      base.track_ref
    },
    check_string: if wins == Extra {
      match patch.check_string {
        Some(v) => v
        None => base.check_string
      }
    } else {
      base.check_string
    },
    depth: if wins == Extra {
      match patch.depth {
        Some(v) => v
        None => base.depth
      }
    } else {
      base.depth
    },
    fields: if wins == Extra {
      match patch.fields {
        Some(v) => v
        None => base.fields
      }
    } else {
      base.fields
    },
    graph: if wins == Extra {
      match patch.graph {
        Some(v) => v
        None => base.graph
      }
    } else {
      base.graph
    },
  }
}

///|
/// The configuration one call runs under.
///
/// The library preset is beaten by the record, and the record by this call's
/// flat mirrors — unless `wins` says otherwise. A flat mirror that contradicts
/// the record beside it is what `clash` arbitrates: taken silently by `wins`,
/// refused, or handed to the caller's function as (the record, the two as one).
pub fn resolve(
  config : Config?,
  xlang : Bool?,
  track_ref : Bool?,
  check_string : Bool?,
  depth : Int32?,
  fields : Int32?,
  graph : Int32?,
  wins : Wins?,
  clash : OnClash[Config]?,
) -> Config raise {
  let base = match config {
    Some(given) => given
    None => Config::new()
  }
  let patch : Patch = { xlang, track_ref, check_string, depth, fields, graph, }
  let wins = match wins {
    Some(given) => given
    None => Extra
  }
  let merged = apply(base, patch, wins)
  match clash {
    Some(Ignore) => merged
    Some(Panic) =>
      if clashes(base, patch) {
        raise Clashed::Clashed(name="config")
      } else {
        merged
      }
    Some(Handle(arbitrate)) => arbitrate(base, merged)
    None => merged
  }
}