///|
/// Public factory used by generated adapters when rejecting a typed value.
pub fn value_error(path : String, reason : String) -> SbeError {
  InvalidValue(path, reason)
}

///|
fn code_quote(text : String) -> String {
  let result = StringBuilder()
  result.write_char('"')
  for char in text {
    match char {
      '"' => result.write_string("\\\"")
      '\\' => result.write_string("\\\\")
      '\n' => result.write_string("\\n")
      '\r' => result.write_string("\\r")
      '\t' => result.write_string("\\t")
      _ =>
        if char.to_int() < 32 {
          result.write_string("\\u{\{char.to_int().to_string(radix=16)}}")
        } else {
          result.write_char(char)
        }
    }
  }
  result.write_char('"')
  result.to_string()
}

///|
fn type_identifier(name : String) -> String raise SbeError {
  let out = StringBuilder()
  let mut upper = true
  for char in name {
    if char.is_ascii_alphabetic() || char.is_ascii_digit() {
      out.write_char(if upper { char.to_ascii_uppercase() } else { char })
      upper = false
    } else {
      upper = true
    }
  }
  let result = out.to_string()
  if result.is_empty() || result.iter().to_array()[0].is_ascii_digit() {
    raise Unsupported(
      "schema name cannot be converted to a MoonBit type: \{name}",
    )
  }
  result
}

///|
fn field_identifier(name : String) -> String raise SbeError {
  let out = StringBuilder()
  let mut previous_lower = false
  for char in name {
    if char.is_ascii_uppercase() {
      if previous_lower {
        out.write_char('_')
      }
      out.write_char(char.to_ascii_lowercase())
      previous_lower = false
    } else if char.is_ascii_lowercase() || char.is_ascii_digit() {
      out.write_char(char)
      previous_lower = true
    } else {
      out.write_char('_')
      previous_lower = false
    }
  }
  let result = out.to_string()
  if result.is_empty() || result.iter().to_array()[0].is_ascii_digit() {
    raise Unsupported("schema name cannot be converted to a field: \{name}")
  }
  // Every generated field has a prefix, avoiding language keywords and method
  // collisions without silently changing wire names.
  "f_\{result}"
}

///|
priv struct GenField {
  wire_name : String
  name : String
  type_name : String
  to_value : (String) -> String
  from_value : (String) -> String
}

///|
priv struct Generator {
  schema : Schema
  output : StringBuilder
  emitted : Map[String, String]
}

///|
fn Generator::claim(
  self : Generator,
  generated : String,
  original : String,
) -> Bool raise SbeError {
  match generated {
    "String"
    | "Bytes"
    | "Json"
    | "Unit"
    | "Bool"
    | "Int"
    | "Int32"
    | "Int64"
    | "UInt"
    | "UInt32"
    | "UInt64"
    | "Float"
    | "Double"
    | "Array"
    | "Map"
    | "Option"
    | "Result" =>
      raise Unsupported(
        "generated type conflicts with MoonBit built-in: \{generated}",
      )
    _ => ()
  }
  match self.emitted.get(generated) {
    Some(previous) if previous != original =>
      raise InvalidSchema("generated name collision: \{generated}", 0)
    Some(_) => false
    None => {
      self.emitted[generated] = original
      true
    }
  }
}

///|
fn optional_adapter(field : GenField) -> GenField {
  {
    ..field,
    type_name: "\{field.type_name}?",
    to_value: source => {
      "match \{source} { Some(item) => \{(field.to_value)("item")}; None => @sbe.Absent }"
    },
    from_value: source => {
      "match \{source} { @sbe.Absent => None; item => Some(\{(field.from_value)("item")}) }"
    },
  }
}

///|
fn array_adapter(field : GenField) -> GenField {
  {
    ..field,
    type_name: "Array[\{field.type_name}]",
    to_value: source => {
      "@sbe.Sequence(\{source}.map((item) => \{(field.to_value)("item")}))"
    },
    from_value: source => {
      "\{source}.as_sequence().map((item) => \{(field.from_value)("item")})"
    },
  }
}

///|
fn scalar_adapter(
  primitive : Primitive,
  wire_name : String,
) -> GenField raise SbeError {
  let (type_name, value_case, getter) = if primitive == Char {
    ("String", "Text", "as_text")
  } else if primitive.is_float() {
    ("Double", "Floating", "as_float")
  } else if primitive.is_signed() {
    ("Int64", "Signed", "as_signed")
  } else {
    ("UInt64", "Unsigned", "as_unsigned")
  }
  {
    wire_name,
    name: field_identifier(wire_name),
    type_name,
    to_value: source => "@sbe.\{value_case}(\{source})",
    from_value: source => "\{source}.\{getter}()",
  }
}