///|
fn zero_default(value : @smithy.NodeValue, target : @smithy.Shape?) -> Bool {
  guard target is Some(target) else { return false }
  match (target.body, value) {
    (Simple(Boolean), Boolean(false)) => true
    (
      Simple(Byte | Short | Integer | Long | Float | Double)
      | IntEnum(_),
      Number(n),
    ) => n == @smithy.NodeNumber::parse("0").unwrap()
    _ => false
  }
}

///|
fn target(model : @smithy.Model, shape : @smithy.Shape) -> @smithy.Shape? {
  match shape.body {
    Member(target~) => model.shapes.get(target)
    _ => None
  }
}

///|
fn default_changes(
  d : Differences,
  change : ShapeChange,
  events : Array[ValidationEvent],
) -> Unit {
  let a = change.old_shape
  let b = change.new_shape
  let old_value = a.trait_value("smithy.api#default")
  let new_value = b.trait_value("smithy.api#default")
  if old_value == new_value {
    return
  }
  let member_shape = b.body is Member(..)
  match (old_value, new_value) {
    (Some(old_value), None) => {
      let target_default = target(d.new_model, b).bind(s => {
        s.trait_value("smithy.api#default")
      })
      if member_shape &&
        old_value == Null &&
        (target_default == None || target_default == Some(Null)) {
        return
      }
      emit(
        events, "ChangedDefault", "ERROR", b, "@default trait was removed. This will break previously generated code.",
      )
    }
    (None, Some(value)) =>
      if !member_shape {
        emit(
          events,
          "ChangedDefault",
          "ERROR",
          b,
          "Adding the @default trait to a root-level shape will break previously generated code. Added @default: " +
          json(value),
        )
      } else if !has(b, "addedDefault") && value != Null {
        emit(
          events,
          "ChangedDefault",
          "ERROR",
          b,
          "Adding the @default trait to a member without also adding the @addedDefault trait will break previously generated code. Added @default: " +
          json(value),
        )
      }
    (Some(old_value), Some(new_value)) =>
      if !member_shape {
        emit(
          events,
          "ChangedDefault",
          "ERROR",
          b,
          "Changing the @default value of a root-level shape will break previously generated code. Old value: " +
          json(old_value) +
          ". New value: " +
          json(new_value),
        )
      } else {
        let old_zero = zero_default(old_value, target(d.old_model, a))
        let new_zero = zero_default(new_value, target(d.new_model, b))
        if old_zero == new_zero {
          emit(
            events,
            "ChangedDefault",
            "DANGER",
            b,
            "Changing the @default value of a member is dangerous and could break previously generated code or lead to subtle errors. Do this only when strictly necessary. Old value: " +
            json(old_value) +
            ". New value: " +
            json(new_value),
          )
        } else {
          emit(
            events,
            "ChangedDefault",
            "ERROR",
            b,
            "The @default trait of this member changed from " +
            (if old_zero {
              "the zero value of the target shape, "
            } else {
              "something other than the zero value of the target shape, "
            }) +
            tick(json(old_value)) +
            ", to " +
            (if new_zero {
              "the zero value, "
            } else {
              "a value that is not the zero value, "
            }) +
            tick(json(new_value)) +
            ". This will break previously generated code.",
          )
        }
      }
    _ => ()
  }
}

///|
fn nullable(model : @smithy.Model, member_shape : @smithy.Shape) -> Bool {
  guard model.shapes.get(member_shape.id.root()) is Some(container) else {
    return false
  }
  match container.body {
    Structure(_) =>
      has(member_shape, "clientOptional") ||
      has(container, "input") ||
      (
        !has(member_shape, "required") &&
        (
          member_shape.trait_value("smithy.api#default") == None ||
          member_shape.trait_value("smithy.api#default") == Some(Null)
        )
      )
    List(_) => !container.is_legacy_set() && has(container, "sparse")
    Map(..) =>
      member_shape.id.member_name() != Some("key") && has(container, "sparse")
    _ => false
  }
}

///|
fn nullability_changes(
  d : Differences,
  events : Array[ValidationEvent],
) -> Unit {
  let candidates : Map[@smithy.ShapeId, ShapeChange] = Map([])
  for change in d.changed {
    if change.old_shape.body is Member(..) &&
      change.new_shape.body is Member(..) {
      candidates[change.new_shape.id] = change
    }
    if change.old_shape.body is Structure(_) &&
      change.new_shape.body is Structure(ids) &&
      has(change.old_shape, "input") != has(change.new_shape, "input") {
      for id in ids {
        if d.old_model.shapes.get(id) is Some(a) &&
          d.new_model.shapes.get(id) is Some(b) {
          candidates[id] = shape_change(a, b)
        }
      }
    }
  }
  for _, change in candidates {
    let a = change.old_shape
    let b = change.new_shape
    let was_nullable = nullable(d.old_model, a)
    if was_nullable == nullable(d.new_model, b) {
      continue
    }
    let prefix = "Member " +
      tick(b.id.member_name().unwrap()) +
      " changed from " +
      (if was_nullable {
        "nullable to non-nullable"
      } else {
        "non-nullable to nullable"
      })
    let old_input = d.old_model.shapes
      .get(a.id.root())
      .map(s => has(s, "input"))
      .unwrap_or(false)
    let new_input = d.new_model.shapes
      .get(b.id.root())
      .map(s => has(s, "input"))
      .unwrap_or(false)
    let initial = events.length()
    let add = fn(suffix : String, severity : String, message : String) {
      emit(
        events,
        "ChangedNullability." + suffix,
        severity,
        a,
        prefix + ": " + message,
      )
    }
    if old_input && !new_input {
      add(
        "RemovedInputTrait",
        "ERROR",
        "The `@input` trait was removed from " + b.id.root().to_string(),
      )
    } else if !old_input && new_input {
      add(
        "AddedInputTrait",
        "DANGER",
        "The `@input` trait was added to " + b.id.root().to_string(),
      )
    } else if !old_input && !new_input {
      if !has(a, "clientOptional") &&
        has(b, "clientOptional") &&
        has(a, "required") &&
        has(b, "required") {
        add(
          "AddedClientOptionalTrait", "ERROR", "The `@clientOptional` trait was added to a `@required` member.",
        )
      } else if has(a, "clientOptional") &&
        !has(b, "clientOptional") &&
        has(a, "required") &&
        has(b, "required") {
        add(
          "RemovedClientOptionalTrait", "ERROR", "The `@clientOptional` trait was removed from a `@required` member.",
        )
      } else if !has(a, "required") &&
        has(b, "required") &&
        !has(b, "clientOptional") {
        let target = target(d.new_model, b)
        if target is Some({ body: Structure(_) | Union(_), .. }) {
          add(
            "AddedRequiredTrait.StructureOrUnion",
            "WARNING",
            "The `@required` trait was added to a member that targets a " +
            target.map(kind).unwrap() +
            ". This is backward compatible in generators that always treat structures and unions as optional (e.g., AWS generators)",
          )
        } else {
          add(
            "AddedRequiredTrait", "ERROR", "The `@required` trait was added to a member.",
          )
        }
      }
      if !has(a, "default") &&
        has(b, "default") &&
        !(has(a, "required") && !has(b, "required")) {
        add(
          "AddedDefaultTrait", "ERROR", "The `@default` trait was added to a member that was not previously `@required`.",
        )
      }
      if has(a, "required") &&
        !has(b, "required") &&
        !has(b, "default") &&
        !has(a, "clientOptional") {
        let target = target(d.new_model, b)
        if target is Some({ body: Structure(_) | Union(_), .. }) {
          add(
            "RemovedRequiredTrait.StructureOrUnion",
            "WARNING",
            "The @required trait was removed from a member that targets a " +
            target.map(kind).unwrap() +
            ". This is backward compatible in generators that always treat structures and unions as optional (e.g., AWS generators)",
          )
        } else {
          add(
            "RemovedRequiredTrait", "ERROR", "The @required trait was removed and not replaced with the @default trait and @addedDefault trait.",
          )
        }
      }
    }
    match
      (a.trait_value("smithy.api#default"), b.trait_value("smithy.api#default")) {
      (Some(old_value), Some(new_value)) =>
        if old_value != Null && new_value == Null {
          add(
            "AddedNullDefault",
            "ERROR",
            "The `@default` trait was changed from " +
            tick(json(old_value)) +
            " to `null`.",
          )
        } else if old_value == Null && new_value != Null {
          add(
            "RemovedNullDefault",
            "ERROR",
            "The `@default` trait was changed from `null` to " +
            tick(json(new_value)) +
            ".",
          )
        }
      _ => ()
    }
    if events.length() == initial {
      emit(events, "ChangedNullability", "ERROR", a, prefix)
    }
  }
}