///|
/// Text ingest for local Object Contract fixtures.

///|
pub(all) suberror ContractIngestError {
  ParseFailed(String)
  MissingField(String)
  InvalidField(String, String)
} derive(Debug)

///|
pub fn parse_contract_registry_text(
  text : String,
) -> ContractRegistry raise ContractIngestError {
  let trimmed = text[:].trim()
  if trimmed.has_prefix("{") {
    parse_contract_registry_json(text)
  } else {
    parse_contract_registry_yaml(text)
  }
}

///|
pub fn parse_contract_registry_json(
  text : String,
) -> ContractRegistry raise ContractIngestError {
  let parsed = @json.parse(text[:]) catch {
    err => raise ContractIngestError::ParseFailed(err.to_string())
  }
  guard parsed is { "objects": Array(objects_json), .. } else {
    raise ContractIngestError::MissingField("objects")
  }
  let registry = ContractRegistry::new()
  for object_json in objects_json {
    registry.register(parse_contract_object_json(object_json))
  }
  registry
}

///|
pub fn parse_contract_registry_yaml(
  text : String,
) -> ContractRegistry raise ContractIngestError {
  let registry = ContractRegistry::new()
  for block in parse_key_value_blocks(text) {
    registry.register(parse_contract_object_fields(block))
  }
  registry
}

///|
fn parse_contract_object_json(
  json : Json,
) -> ObjectContract raise ContractIngestError {
  let id = json_required_string(json, "id")
  let object = ObjectContract::make(
    id~,
    kind=parse_object_kind(json_required_string(json, "kind"), "kind"),
    schema=parse_schema(json_required_string(json, "schema"), "schema"),
    timing={
      period_ns: json_optional_int64(json, "period_ns"),
      max_age_ns: json_optional_int64(json, "max_age_ns"),
      deadline_ns: json_optional_int64(json, "deadline_ns"),
      valid_for_ns: json_optional_int64(json, "valid_for_ns"),
      clock_requirement: parse_clock_requirement(
        json_optional_string(json, "clock_requirement"),
        "clock_requirement",
      ),
    },
    safety={
      authority: parse_authority(json_optional_string(json, "authority")),
      min_payload_digest: json_optional_int(json, "min_payload_digest"),
      max_payload_digest: json_optional_int(json, "max_payload_digest"),
      timeout_action: json_optional_string(json, "timeout_action").unwrap_or(
        "hold_last",
      ),
    },
  )
  let mut out = object
  match json {
    { "bindings": Array(bindings_json), .. } =>
      for binding_json in bindings_json {
        out = out.with_binding(parse_binding_json(id, binding_json))
      }
    _ => ()
  }
  out
}

///|
fn parse_binding_json(
  object_id : String,
  json : Json,
) -> ObjectBinding raise ContractIngestError {
  {
    object_id: json_optional_string(json, "object_id").unwrap_or(object_id),
    binding_id: json_required_string(json, "binding_id"),
    medium_id: json_optional_string(json, "medium_id").unwrap_or(""),
    label: json_required_string(json, "label"),
    direction: parse_direction(
      json_optional_string(json, "direction"),
      "direction",
    ),
    kind: parse_binding_kind(json_optional_string(json, "kind")),
    index: json_optional_uint(json, "index"),
    subindex: json_optional_byte(json, "subindex"),
    channel: json_optional_string(json, "channel").unwrap_or(""),
    key: json_optional_string(json, "key").unwrap_or(""),
    authority: parse_authority(json_optional_string(json, "authority")),
  }
}

///|
fn parse_contract_object_fields(
  fields : Array[(String, String)],
) -> ObjectContract raise ContractIngestError {
  let id = required_field(fields, "object")
  let object = ObjectContract::make(
    id~,
    kind=parse_object_kind(required_field(fields, "kind"), "kind"),
    schema=parse_schema(required_field(fields, "schema"), "schema"),
    timing={
      period_ns: optional_int64_field(fields, "period_ns"),
      max_age_ns: optional_int64_field(fields, "max_age_ns"),
      deadline_ns: optional_int64_field(fields, "deadline_ns"),
      valid_for_ns: optional_int64_field(fields, "valid_for_ns"),
      clock_requirement: parse_clock_requirement(
        optional_field(fields, "clock_requirement"),
        "clock_requirement",
      ),
    },
    safety={
      authority: parse_authority(optional_field(fields, "authority")),
      min_payload_digest: optional_int_field(fields, "min_payload_digest"),
      max_payload_digest: optional_int_field(fields, "max_payload_digest"),
      timeout_action: optional_field(fields, "timeout_action").unwrap_or(
        "hold_last",
      ),
    },
  )
  let mut out = object
  for field in fields {
    let (key, value) = field
    if key == "binding" {
      out = out.with_binding(parse_binding_row(id, value))
    }
  }
  out
}

///|
fn parse_binding_row(
  object_id : String,
  value : String,
) -> ObjectBinding raise ContractIngestError {
  let parts = value.split(",").to_array()
  guard parts.length() >= 2 else {
    raise ContractIngestError::InvalidField("binding", value)
  }
  let binding_id = clean_value(parts[0].trim().to_owned())
  let label = clean_value(parts[1].trim().to_owned())
  let medium_id = if parts.length() > 2 {
    clean_value(parts[2].trim().to_owned())
  } else {
    ""
  }
  let direction = if parts.length() > 3 {
    parse_direction(Some(clean_value(parts[3].trim().to_owned())), "binding")
  } else {
    None
  }
  let authority = if parts.length() > 4 {
    parse_authority(Some(clean_value(parts[4].trim().to_owned())))
  } else {
    Any
  }
  let kind = if parts.length() > 5 {
    parse_binding_kind(Some(clean_value(parts[5].trim().to_owned())))
  } else {
    TraceLabel
  }
  let binding_object_id = binding_named_option(parts, "object_id").unwrap_or(
    if parts.length() > 6 &&
      !clean_value(parts[6].trim().to_owned()).contains("=") {
      clean_value(parts[6].trim().to_owned())
    } else {
      object_id
    },
  )
  let index = match binding_named_option(parts, "index") {
    Some(raw) => Some(parse_text_uint(raw, "index"))
    None => None
  }
  let subindex = match binding_named_option(parts, "subindex") {
    Some(raw) => Some(parse_text_byte(raw, "subindex"))
    None => None
  }
  let channel = binding_named_option(parts, "channel").unwrap_or("")
  let key = binding_named_option(parts, "key").unwrap_or("")
  {
    object_id: binding_object_id,
    binding_id,
    medium_id,
    label,
    direction,
    kind,
    index,
    subindex,
    channel,
    key,
    authority,
  }
}

///|
fn binding_named_option(parts : Array[StringView], name : String) -> String? {
  let mut found : String? = None
  for i in 6.. ObjectKind raise ContractIngestError {
  match normalized(value) {
    "signal" => Signal
    "command" => Command
    "query" => Query
    "event" => Event
    "lifecycle" => Lifecycle
    other => raise ContractIngestError::InvalidField(field, other)
  }
}

///|
fn parse_schema(
  value : String,
  field : String,
) -> Schema raise ContractIngestError {
  match normalized(value) {
    "bool" => Bool
    "i8" => I8
    "i32" => I32
    "u16" => U16
    "state" => State
    "text" => Text
    other => raise ContractIngestError::InvalidField(field, other)
  }
}

///|
fn parse_authority(value : String?) -> Authority {
  match value {
    None => Any
    Some(raw) =>
      match normalized(raw) {
        "" | "any" => Any
        "read_only" | "readonly" | "read-only" => ReadOnly
        other => Role(other)
      }
  }
}

///|
fn parse_clock_requirement(
  value : String?,
  field : String,
) -> ObjectClockRequirement raise ContractIngestError {
  match value {
    None => AnyClock
    Some(raw) =>
      match normalized(raw) {
        "any" | "unspecified" => AnyClock
        "physical" | "physical-clock" | "physical_clock" => PhysicalClock
        other => raise ContractIngestError::InvalidField(field, other)
      }
  }
}

///|
fn parse_binding_kind(value : String?) -> BindingKind raise ContractIngestError {
  match value {
    None => TraceLabel
    Some(raw) =>
      match normalized(raw) {
        "" | "trace_label" | "trace-label" | "trace" => TraceLabel
        "ethercat_pdo" | "ethercat-pdo" => EtherCatPdo
        "coe_sdo" | "coe-sdo" => CoeSdo
        "canopen_sdo" | "canopen-sdo" => CanopenSdo
        "canopen_pdo" | "canopen-pdo" => CanopenPdo
        "zenoh_key" | "zenoh-key" => ZenohKey
        "modbus_register" | "modbus-register" => ModbusRegister
        other => raise ContractIngestError::InvalidField("kind", other)
      }
  }
}

///|
fn parse_direction(
  value : String?,
  field : String,
) -> @trace.TraceDirection? raise ContractIngestError {
  match value {
    None => None
    Some(raw) =>
      match normalized(raw) {
        "" | "any" => None
        "tx" => Some(@trace.Tx)
        "rx" => Some(@trace.Rx)
        "fault" => Some(@trace.Fault)
        "tick" => Some(@trace.Tick)
        other => raise ContractIngestError::InvalidField(field, other)
      }
  }
}

///|
fn parse_key_value_blocks(
  text : String,
) -> Array[Array[(String, String)]] raise ContractIngestError {
  let blocks : Array[Array[(String, String)]] = []
  let mut current : Array[(String, String)] = []
  for raw_line in text.split("\n") {
    let line = raw_line.trim().to_owned()
    if line == "" || line.has_prefix("#") {
      ()
    } else if line == "---" {
      if !current.is_empty() {
        blocks.push(current)
        current = []
      }
    } else {
      current.push(parse_key_value_line(line))
    }
  }
  if !current.is_empty() {
    blocks.push(current)
  }
  blocks
}

///|
fn parse_key_value_line(
  line : String,
) -> (String, String) raise ContractIngestError {
  let parts = if line.contains(":") {
    line.split(":").to_array()
  } else {
    line.split("=").to_array()
  }
  guard parts.length() == 2 else {
    raise ContractIngestError::ParseFailed("invalid line: " + line)
  }
  (
    normalized(parts[0].trim().to_owned()),
    clean_value(parts[1].trim().to_owned()),
  )
}

///|
fn required_field(
  fields : Array[(String, String)],
  name : String,
) -> String raise ContractIngestError {
  match optional_field(fields, name) {
    Some(value) => value
    None => raise ContractIngestError::MissingField(name)
  }
}

///|
fn optional_field(fields : Array[(String, String)], name : String) -> String? {
  let mut found : String? = None
  for field in fields {
    let (key, value) = field
    if key == name {
      found = Some(value)
    }
  }
  found
}

///|
fn optional_int_field(
  fields : Array[(String, String)],
  name : String,
) -> Int? raise ContractIngestError {
  match optional_field(fields, name) {
    Some(value) => Some(parse_text_int(value, name))
    None => None
  }
}

///|
fn optional_int64_field(
  fields : Array[(String, String)],
  name : String,
) -> Int64? raise ContractIngestError {
  match optional_int_field(fields, name) {
    Some(value) => Some(Int64::from_int(value))
    None => None
  }
}

///|
fn json_required_string(
  json : Json,
  field : String,
) -> String raise ContractIngestError {
  match json_optional_string(json, field) {
    Some(value) => value
    None => raise ContractIngestError::MissingField(field)
  }
}

///|
fn json_optional_string(
  json : Json,
  field : String,
) -> String? raise ContractIngestError {
  match (field, json) {
    ("id", { "id": value, .. }) => Some(decode_json_string(value, field))
    ("kind", { "kind": value, .. }) => Some(decode_json_string(value, field))
    ("schema", { "schema": value, .. }) =>
      Some(decode_json_string(value, field))
    ("clock_requirement", { "clock_requirement": value, .. }) =>
      Some(decode_json_string(value, field))
    ("authority", { "authority": value, .. }) =>
      Some(decode_json_string(value, field))
    ("timeout_action", { "timeout_action": value, .. }) =>
      Some(decode_json_string(value, field))
    ("binding_id", { "binding_id": value, .. }) =>
      Some(decode_json_string(value, field))
    ("object_id", { "object_id": value, .. }) =>
      Some(decode_json_string(value, field))
    ("medium_id", { "medium_id": value, .. }) =>
      Some(decode_json_string(value, field))
    ("label", { "label": value, .. }) => Some(decode_json_string(value, field))
    ("direction", { "direction": value, .. }) =>
      Some(decode_json_string(value, field))
    ("channel", { "channel": value, .. }) =>
      Some(decode_json_string(value, field))
    ("key", { "key": value, .. }) => Some(decode_json_string(value, field))
    _ => None
  }
}

///|
fn json_optional_int(
  json : Json,
  field : String,
) -> Int? raise ContractIngestError {
  match (field, json) {
    ("min_payload_digest", { "min_payload_digest": value, .. }) =>
      Some(decode_json_int(value, field))
    ("max_payload_digest", { "max_payload_digest": value, .. }) =>
      Some(decode_json_int(value, field))
    _ => None
  }
}

///|
fn json_optional_int64(
  json : Json,
  field : String,
) -> Int64? raise ContractIngestError {
  match (field, json) {
    ("period_ns", { "period_ns": value, .. }) =>
      Some(decode_json_int64(value, field))
    ("max_age_ns", { "max_age_ns": value, .. }) =>
      Some(decode_json_int64(value, field))
    ("deadline_ns", { "deadline_ns": value, .. }) =>
      Some(decode_json_int64(value, field))
    ("valid_for_ns", { "valid_for_ns": value, .. }) =>
      Some(decode_json_int64(value, field))
    _ => None
  }
}

///|
fn json_optional_uint(
  json : Json,
  field : String,
) -> UInt? raise ContractIngestError {
  match (field, json) {
    ("index", { "index": value, .. }) => Some(decode_json_uint(value, field))
    _ => None
  }
}

///|
fn json_optional_byte(
  json : Json,
  field : String,
) -> Byte? raise ContractIngestError {
  match (field, json) {
    ("subindex", { "subindex": value, .. }) =>
      Some(decode_json_byte(value, field))
    _ => None
  }
}

///|
fn decode_json_string(
  json : Json,
  field : String,
) -> String raise ContractIngestError {
  @json.from_json(json) catch {
    _ => raise ContractIngestError::InvalidField(field, json.stringify())
  }
}

///|
fn decode_json_int(
  json : Json,
  field : String,
) -> Int raise ContractIngestError {
  @json.from_json(json) catch {
    _ => raise ContractIngestError::InvalidField(field, json.stringify())
  }
}

///|
fn decode_json_int64(
  json : Json,
  field : String,
) -> Int64 raise ContractIngestError {
  Int64::from_int(decode_json_int(json, field))
}

///|
fn decode_json_uint(
  json : Json,
  field : String,
) -> UInt raise ContractIngestError {
  let value = decode_json_int(json, field)
  if value < 0 {
    raise ContractIngestError::InvalidField(field, value.to_string())
  }
  value.reinterpret_as_uint()
}

///|
fn decode_json_byte(
  json : Json,
  field : String,
) -> Byte raise ContractIngestError {
  let value = decode_json_int(json, field)
  if value < 0 || value > 255 {
    raise ContractIngestError::InvalidField(field, value.to_string())
  }
  value.to_byte()
}

///|
fn parse_text_int(
  value : String,
  field : String,
) -> Int raise ContractIngestError {
  let body = clean_value(value)
  @string.parse_int(body[:]) catch {
    _ => raise ContractIngestError::InvalidField(field, body)
  }
}

///|
fn parse_text_uint(
  value : String,
  field : String,
) -> UInt raise ContractIngestError {
  let parsed = parse_text_int(value, field)
  if parsed < 0 {
    raise ContractIngestError::InvalidField(field, parsed.to_string())
  }
  parsed.reinterpret_as_uint()
}

///|
fn parse_text_byte(
  value : String,
  field : String,
) -> Byte raise ContractIngestError {
  let parsed = parse_text_int(value, field)
  if parsed < 0 || parsed > 255 {
    raise ContractIngestError::InvalidField(field, parsed.to_string())
  }
  parsed.to_byte()
}

///|
fn clean_value(value : String) -> String {
  let trimmed = value[:].trim().to_owned()
  let without_comma = if trimmed.has_suffix(",") {
    trimmed[:trimmed.length() - 1].to_owned()
  } else {
    trimmed
  }
  if without_comma.length() >= 2 &&
    (
      (without_comma.has_prefix("\"") && without_comma.has_suffix("\"")) ||
      (without_comma.has_prefix("'") && without_comma.has_suffix("'"))
    ) {
    without_comma[1:without_comma.length() - 1].to_owned()
  } else {
    without_comma
  }
}

///|
fn normalized(value : String) -> String {
  clean_value(value).to_lower()
}