///|
pub fn parse_brand_document(text : String) -> BrandDocumentResult {
  let trimmed = text.trim().to_owned()
  if trimmed == "" {
    return {
      value: None,
      diagnostics: DiagnosticBag::empty().push(
        Diagnostic::error("$", "document is empty", None),
      ),
    }
  }
  if trimmed[0] == '{' || trimmed[0] == '[' {
    let parsed = parse_json(text)
    match parsed.value {
      Some(value) if !parsed.diagnostics.has_errors() => build_brand_book(value)
      _ => { value: None, diagnostics: parsed.diagnostics }
    }
  } else {
    let parsed = parse_yaml(text)
    match parsed.value {
      Some(value) if !parsed.diagnostics.has_errors() => build_brand_book(value)
      _ => { value: None, diagnostics: parsed.diagnostics }
    }
  }
}

///|
pub fn parse_brand_document_as(
  text : String,
  format : DocumentFormat,
) -> BrandDocumentResult {
  match format {
    Auto => parse_brand_document(text)
    Json => {
      let parsed = parse_json(text)
      match parsed.value {
        Some(value) if !parsed.diagnostics.has_errors() =>
          build_brand_book(value)
        _ => { value: None, diagnostics: parsed.diagnostics }
      }
    }
    Yaml => {
      let parsed = parse_yaml(text)
      match parsed.value {
        Some(value) if !parsed.diagnostics.has_errors() =>
          build_brand_book(value)
        _ => { value: None, diagnostics: parsed.diagnostics }
      }
    }
  }
}

///|
fn build_brand_book(root : JsonValue) -> BrandDocumentResult {
  let diagnostics : Array[Diagnostic] = []
  let object = match get_object(Some(root), "$", diagnostics) {
    Some(value) => value
    None => return { value: None, diagnostics: { items: diagnostics } }
  }
  let name = get_required_string(object, "name", "name", diagnostics)
  let tagline = get_optional_string(object, "tagline", "")
  let voice_object = match object.object_field("voice") {
    None => None
    Some(value) => get_object(Some(value), "voice", diagnostics)
  }
  let voice = match voice_object {
    Some(value) =>
      Voice::{
        tone: get_optional_string(value, "tone", ""),
        writing_principles: string_array(
          value.object_field("writing_principles"),
          "voice.writing_principles",
          diagnostics,
        ),
      }
    None => Voice::{ tone: "", writing_principles: [] }
  }
  let palette_object = get_object(
    object.object_field("palette"),
    "palette",
    diagnostics,
  )
  let palette = match palette_object {
    Some(value) =>
      Palette::{
        colors: build_colors(value.object_field("colors"), diagnostics),
        gradients: build_gradients(value.object_field("gradients"), diagnostics),
      }
    None => Palette::{ colors: [], gradients: [] }
  }
  let assets_object = get_object(
    object.object_field("assets"),
    "assets",
    diagnostics,
  )
  let assets = match assets_object {
    Some(value) =>
      AssetSet::{
        primary_logo: build_logo(
          value.object_field("primary_logo"),
          "assets.primary_logo",
          diagnostics,
        ),
        alternate_logos: build_logos(
          value.object_field("alternate_logos"),
          "assets.alternate_logos",
          diagnostics,
        ),
        imagery_style: get_optional_string(value, "imagery_style", ""),
      }
    None =>
      AssetSet::{
        primary_logo: default_logo(),
        alternate_logos: [],
        imagery_style: "",
      }
  }
  let typography_object = get_object(
    object.object_field("typography"),
    "typography",
    diagnostics,
  )
  let typography = match typography_object {
    Some(value) =>
      Typography::{
        families: build_fonts(value.object_field("families"), diagnostics),
        scale: build_scale(value.object_field("scale"), diagnostics),
      }
    None => Typography::{ families: [], scale: [] }
  }
  let metadata_object = object.object_field("metadata")
  let metadata = match metadata_object {
    Some(value) =>
      match value {
        JObject(_) =>
          Meta::{
            source: get_optional_string(value, "source", ""),
            maintainer: get_optional_string(value, "maintainer", ""),
            license: get_optional_string(value, "license", "Apache-2.0"),
          }
        _ => {
          add_document_error(diagnostics, "metadata", "expected an object")
          Meta::{ source: "", maintainer: "", license: "Apache-2.0" }
        }
      }
    None => Meta::{ source: "", maintainer: "", license: "Apache-2.0" }
  }
  let components = build_components(
    object.object_field("components"),
    diagnostics,
  )
  let forbidden = build_misuse_rules(
    object.object_field("forbidden"),
    diagnostics,
  )
  let book = BrandBook::{
    name,
    tagline,
    voice,
    assets,
    palette,
    typography,
    components,
    forbidden,
    metadata,
  }
  if diagnostics.any(fn(item) { item.severity == Error }) {
    { value: None, diagnostics: { items: diagnostics } }
  } else {
    { value: Some(book), diagnostics: { items: diagnostics } }
  }
}

///|
fn string_array(
  value : JsonValue?,
  path : String,
  diagnostics : Array[Diagnostic],
) -> Array[String] {
  let result : Array[String] = []
  for item in get_array(value, path, diagnostics) {
    match item {
      JString(text) => result.push(text)
      _ => add_document_error(diagnostics, path, "array items must be strings")
    }
  }
  result
}

///|
fn build_colors(
  value : JsonValue?,
  diagnostics : Array[Diagnostic],
) -> Array[ColorToken] {
  let result : Array[ColorToken] = []
  for i, item in get_array(value, "palette.colors", diagnostics) {
    match item {
      JObject(_) => {
        let name = get_required_string(
          item,
          "name",
          "palette.colors[" + i.to_string() + "].name",
          diagnostics,
        )
        let color = get_required_string(
          item,
          "value",
          "palette.colors[" + i.to_string() + "].value",
          diagnostics,
        )
        let role = get_optional_string(item, "role", "")
        if !is_hex_color(color) {
          add_document_error(
            diagnostics,
            "palette.colors[" + i.to_string() + "].value",
            "expected six-digit hex color",
          )
        }
        result.push(ColorToken::hex(name, color, role~))
      }
      _ =>
        add_document_error(
          diagnostics,
          "palette.colors[" + i.to_string() + "].",
          "expected an object",
        )
    }
  }
  result
}

///|
fn build_gradients(
  value : JsonValue?,
  diagnostics : Array[Diagnostic],
) -> Array[GradientToken] {
  let result : Array[GradientToken] = []
  for i, item in get_array(value, "palette.gradients", diagnostics) {
    match item {
      JObject(_) =>
        result.push(GradientToken::{
          name: get_required_string(
            item,
            "name",
            "palette.gradients[" + i.to_string() + "].name",
            diagnostics,
          ),
          stops: string_array(
            item.object_field("stops"),
            "palette.gradients[" + i.to_string() + "].stops",
            diagnostics,
          ),
          angle: get_int(
            item,
            "angle",
            "palette.gradients[" + i.to_string() + "].angle",
            diagnostics,
            true,
          ),
          usage: get_optional_string(item, "usage", ""),
        })
      _ =>
        add_document_error(
          diagnostics,
          "palette.gradients[" + i.to_string() + "]",
          "expected an object",
        )
    }
  }
  result
}

///|
fn build_logo(
  value : JsonValue?,
  path : String,
  diagnostics : Array[Diagnostic],
) -> Logo {
  let object = get_object(value, path, diagnostics)
  match object {
    Some(item) =>
      Logo::{
        name: get_required_string(item, "name", path + ".name", diagnostics),
        path: get_required_string(item, "path", path + ".path", diagnostics),
        min_width_px: get_int(
          item,
          "min_width_px",
          path + ".min_width_px",
          diagnostics,
          true,
        ),
        clear_space: Pixels(
          get_int(
            item,
            "clear_space_px",
            path + ".clear_space_px",
            diagnostics,
            true,
          ),
        ),
        background: parse_background(
          get_optional_string(item, "background", "light_or_dark"),
        ),
      }
    None => default_logo()
  }
}

///|
fn build_logos(
  value : JsonValue?,
  path : String,
  diagnostics : Array[Diagnostic],
) -> Array[Logo] {
  let result : Array[Logo] = []
  for i, item in get_array(value, path, diagnostics) {
    result.push(
      build_logo(Some(item), path + "[" + i.to_string() + "]", diagnostics),
    )
  }
  result
}

///|
fn build_fonts(
  value : JsonValue?,
  diagnostics : Array[Diagnostic],
) -> Array[FontToken] {
  let result : Array[FontToken] = []
  for i, item in get_array(value, "typography.families", diagnostics) {
    match item {
      JObject(_) =>
        result.push(FontToken::{
          name: get_required_string(
            item,
            "name",
            "typography.families[" + i.to_string() + "].name",
            diagnostics,
          ),
          fallback: get_optional_string(
            item, "fallback", "system-ui, sans-serif",
          ),
          usage: get_optional_string(item, "usage", ""),
        })
      _ =>
        add_document_error(
          diagnostics,
          "typography.families[" + i.to_string() + "]",
          "expected an object",
        )
    }
  }
  result
}

///|
fn build_scale(
  value : JsonValue?,
  diagnostics : Array[Diagnostic],
) -> Array[TypeStep] {
  let result : Array[TypeStep] = []
  for i, item in get_array(value, "typography.scale", diagnostics) {
    match item {
      JObject(_) =>
        result.push(TypeStep::{
          name: get_required_string(
            item,
            "name",
            "typography.scale[" + i.to_string() + "].name",
            diagnostics,
          ),
          size_px: get_int(
            item,
            "size_px",
            "typography.scale[" + i.to_string() + "].size_px",
            diagnostics,
            true,
          ),
          line_height_px: get_int(
            item,
            "line_height_px",
            "typography.scale[" + i.to_string() + "].line_height_px",
            diagnostics,
            true,
          ),
        })
      _ =>
        add_document_error(
          diagnostics,
          "typography.scale[" + i.to_string() + "]",
          "expected an object",
        )
    }
  }
  result
}

///|
fn build_components(
  value : JsonValue?,
  diagnostics : Array[Diagnostic],
) -> Array[ComponentSpec] {
  let result : Array[ComponentSpec] = []
  for i, item in get_array(value, "components", diagnostics) {
    match item {
      JObject(_) =>
        result.push(ComponentSpec::{
          name: get_required_string(
            item,
            "name",
            "components[" + i.to_string() + "].name",
            diagnostics,
          ),
          purpose: get_optional_string(item, "purpose", ""),
          anatomy: string_array(
            item.object_field("anatomy"),
            "components[" + i.to_string() + "].anatomy",
            diagnostics,
          ),
          tokens: string_array(
            item.object_field("tokens"),
            "components[" + i.to_string() + "].tokens",
            diagnostics,
          ),
          states: string_array(
            item.object_field("states"),
            "components[" + i.to_string() + "].states",
            diagnostics,
          ),
        })
      _ =>
        add_document_error(
          diagnostics,
          "components[" + i.to_string() + "]",
          "expected an object",
        )
    }
  }
  result
}

///|
fn build_misuse_rules(
  value : JsonValue?,
  diagnostics : Array[Diagnostic],
) -> Array[MisuseRule] {
  let result : Array[MisuseRule] = []
  for i, item in get_array(value, "forbidden", diagnostics) {
    match item {
      JObject(_) =>
        result.push(MisuseRule::{
          title: get_required_string(
            item,
            "title",
            "forbidden[" + i.to_string() + "].title",
            diagnostics,
          ),
          reason: get_optional_string(item, "reason", ""),
          replacement: get_optional_string(item, "replacement", ""),
        })
      _ =>
        add_document_error(
          diagnostics,
          "forbidden[" + i.to_string() + "]",
          "expected an object",
        )
    }
  }
  result
}

///|
fn parse_background(text : String) -> BackgroundRule {
  match normalize_identifier(text) {
    "light-only" => LightOnly
    "dark-only" => DarkOnly
    "monochrome" => Monochrome
    _ => LightOrDark
  }
}

///|
fn default_logo() -> Logo {
  Logo::{
    name: "invalid",
    path: "",
    min_width_px: 0,
    clear_space: Pixels(0),
    background: LightOrDark,
  }
}