///|
/// The kind of a codec finding. Constraint findings come from Smithy value validation.
pub(all) enum CodecCode {
  Constraint(@validation.FindingCode)
  InvalidJson
  DuplicateName
  KindMismatch
  UnknownMember
  MissingMember
  MemberCollision
  UnknownTag
  Arity
  AmbiguousForm
  AdapterFailure
  DuplicateKey
  IncompatibleValue
  UnrepresentableValue
  MissingVersion
  InvalidVersion
  UnsupportedVersion
  VersionConflict
  LimitExceeded
} derive(Eq, Debug)

///|
pub fn CodecCode::to_string(self : CodecCode) -> String {
  match self {
    Constraint(code) => code.to_string()
    InvalidJson => "InvalidJson"
    DuplicateName => "DuplicateName"
    KindMismatch => "KindMismatch"
    UnknownMember => "UnknownMember"
    MissingMember => "MissingMember"
    MemberCollision => "MemberCollision"
    UnknownTag => "UnknownTag"
    Arity => "Arity"
    AmbiguousForm => "AmbiguousForm"
    AdapterFailure => "AdapterFailure"
    DuplicateKey => "DuplicateKey"
    IncompatibleValue => "IncompatibleValue"
    UnrepresentableValue => "UnrepresentableValue"
    MissingVersion => "MissingVersion"
    InvalidVersion => "InvalidVersion"
    UnsupportedVersion => "UnsupportedVersion"
    VersionConflict => "VersionConflict"
    LimitExceeded => "LimitExceeded"
  }
}

///|
pub impl Show for CodecCode with fn output(self, logger) {
  logger.write_string(self.to_string())
}

///|
/// One finding. The path is the original input location when reading, and the logical location when
/// encoding a value.
pub(all) struct CodecFinding {
  code : CodecCode
  path : @validation.InstancePath
  shape : @smithy.ShapeId
  message : String
} derive(Eq, Debug)

///|
/// The result of a check. A value is valid only when the check is complete and has no findings.
pub(all) struct CodecResult {
  findings : Array[CodecFinding]
  complete : Bool
} derive(Eq, Debug)

///|
pub fn CodecResult::is_valid(self : CodecResult) -> Bool {
  self.complete && self.findings.is_empty()
}

///|
pub impl ToJson for CodecFinding with fn to_json(self) {
  Json::object({
    "code": Json::string(self.code.to_string()),
    "severity": Json::string("error"),
    "path": self.path.to_json(),
    "pointer": Json::string(self.path.pointer()),
    "shape": Json::string(self.shape.to_string()),
    "message": Json::string(self.message),
  })
}

///|
pub impl ToJson for CodecResult with fn to_json(self) {
  Json::object({
    "valid": Json::boolean(self.is_valid()),
    "complete": Json::boolean(self.complete),
    "findings": Json::array(self.findings.map(f => ToJson::to_json(f))),
  })
}

///|
/// A refused check, decode or encode. The result holds the findings; no value or output exists.
pub(all) suberror CodecFailure {
  CodecFailure(CodecResult)
} derive(Debug)

///|
/// Findings and the shared work meter of one request.
priv struct Request {
  limits : CodecLimits
  meter : @validation.WorkMeter
  findings : Array[CodecFinding]
  mut complete : Bool
}

///|
fn Request::new(limits : CodecLimits) -> Request {
  {
    limits,
    meter: @validation.WorkMeter::new(limits.work) catch {
      _ => panic()
    },
    findings: [],
    complete: true,
  }
}

///|
fn Request::stop(
  self : Request,
  path : @validation.InstancePath,
  shape : @smithy.ShapeId,
  reason : String,
) -> Unit {
  if !self.complete {
    return
  }
  self.complete = false
  self.findings.push({
    code: LimitExceeded,
    path,
    shape,
    message: "I reached the " + reason + " limit before completing the request.",
  })
}

///|
fn Request::charge(
  self : Request,
  path : @validation.InstancePath,
  shape : @smithy.ShapeId,
  units~ : Int32 = 1,
) -> Bool {
  if !self.complete {
    return false
  }
  if !self.meter.charge(units) {
    self.stop(path, shape, "work")
    return false
  }
  true
}

///|
fn Request::emit(
  self : Request,
  code : CodecCode,
  path : @validation.InstancePath,
  shape : @smithy.ShapeId,
  message : String,
) -> Unit {
  if !self.complete {
    return
  }
  if self.findings.length() >= self.limits.findings - 1 {
    self.stop(path, shape, "findings")
    return
  }
  self.findings.push({ code, path, shape, message, })
}

///|
fn Request::result(self : Request) -> CodecResult {
  { findings: self.findings.copy(), complete: self.complete, }
}