///|
priv struct ImportSpec {
  id : String
  layer : String?
  supports : String?
  media : String?
  reference : Bool
  theme_only : Bool
}

///|
fn consume_import_function(
  input : String,
  name : String,
) -> (String, String)? raise CompileError {
  let prefix = "\{name}("
  if !input.to_lower().has_prefix(prefix) {
    return None
  }
  let close = find_function_end(input, name.length())
  Some(
    (
      input[name.length() + 1:close].to_owned(),
      trim(input[close + 1:].to_owned()),
    ),
  )
}

///|
fn parse_import_spec(params : String) -> ImportSpec? raise CompileError {
  let value = trim(params)
  if value.has_prefix("url(") {
    return None
  }
  if value.length() < 2 {
    return None
  }
  let quote = value[0]
  if quote != '\'' && quote != '"' {
    return None
  }
  for i in 1.. {
            layer = Some(content)
            rest = tail
            progressed = true
          }
          None => ()
        }
        match consume_import_function(rest, "supports") {
          Some((content, tail)) => {
            supports = Some(content)
            rest = tail
            progressed = true
          }
          None => ()
        }
        match consume_import_function(rest, "theme") {
          Some((content, tail)) =>
            if trim(content).split(" ").contains("reference") {
              reference = true
              theme_only = true
              rest = tail
              progressed = true
            }
          None => ()
        }
        // `source(...)` controls automatic content detection for the imported
        // sheet. This surface takes candidates explicitly, so the directive is
        // consumed and discarded rather than emitted as a media condition.
        match consume_import_function(rest, "source") {
          Some((_, tail)) => {
            rest = tail
            progressed = true
          }
          None => ()
        }
      }
      // `@import "…" reference` is the shorthand of `@reference "…"`.
      if rest.to_lower() == "reference" {
        reference = true
        rest = ""
      }
      return Some({
        id,
        layer,
        supports,
        reference,
        theme_only,
        media: if rest == "" {
          None
        } else {
          Some(rest)
        },
      })
    }
  }
  None
}

///|
fn wrap_import_conditions(
  nodes : Array[CssNode],
  spec : ImportSpec,
  span : SourceSpan,
) -> Array[CssNode] {
  let mut root = nodes
  match spec.layer {
    Some(layer) =>
      root = [AtRule(name="@layer", params=layer, nodes=Some(root), span~)]
    None => ()
  }
  match spec.media {
    Some(media) =>
      root = [AtRule(name="@media", params=media, nodes=Some(root), span~)]
    None => ()
  }
  match spec.supports {
    Some(supports) => {
      let condition = if supports.has_prefix("(") {
        supports
      } else {
        "(\{supports})"
      }
      root = [
        AtRule(name="@supports", params=condition, nodes=Some(root), span~),
      ]
    }
    None => ()
  }
  root
}

///|
fn is_import_at_rule(name : String) -> Bool {
  let lowered = name.to_lower()
  lowered == "@import" || lowered == "@reference"
}

///|
fn contains_import(nodes : ArrayView[CssNode]) -> Bool {
  for node in nodes {
    match node {
      // At-rule names are case insensitive in CSS, so `@IMPORT` counts too.
      AtRule(name~, nodes=None, ..) =>
        if is_import_at_rule(name) {
          return true
        }
      Rule(nodes~, ..) | AtRule(nodes=Some(nodes), ..) | Context(nodes~, ..) =>
        if contains_import(nodes) {
          return true
        }
      _ => ()
    }
  }
  false
}

///|
fn mark_reference_theme(nodes : ArrayView[CssNode]) -> Array[CssNode] {
  nodes.map(fn(node) {
    match node {
      AtRule(name="@theme", params~, nodes~, span~) =>
        AtRule(
          name="@theme",
          params=if trim(params) == "" {
            "reference"
          } else {
            "\{params} reference"
          },
          nodes~,
          span~,
        )
      _ => node
    }
  })
}

///|
/// Whether an imported stylesheet only declares theme blocks.
fn only_theme_nodes(nodes : ArrayView[CssNode]) -> Bool {
  for node in nodes {
    match node {
      AtRule(name="@theme", ..) | Comment(..) => ()
      Context(nodes=children, ..) =>
        if !only_theme_nodes(children) {
          return false
        }
      _ => return false
    }
  }
  true
}

///|
/// Keep only the parts of a referenced stylesheet that outlive the import.
fn keep_reference_nodes(nodes : ArrayView[CssNode]) -> Array[CssNode] {
  let output : Array[CssNode] = []
  for node in nodes {
    match node {
      AtRule(name="@theme" | "@source", ..) => output.push(node)
      Context(values~, nodes=children, span~) => {
        let inner = keep_reference_nodes(children)
        if !inner.is_empty() {
          output.push(Context(values~, nodes=inner, span~))
        }
      }
      _ => ()
    }
  }
  output
}

///|
async fn resolve_imports(
  nodes : ArrayView[CssNode],
  loader : &StylesheetLoader,
  base : String,
  stack : Array[String],
) -> Array[CssNode] raise CompileError {
  let output : Array[CssNode] = []
  for node in nodes {
    match node {
      AtRule(name~, params~, nodes=None, span~) =>
        if !is_import_at_rule(name) {
          output.push(node)
        } else {
          match parse_import_spec(params) {
            None => output.push(node)
            Some(spec) => {
              let loaded = loader.load(spec.id, base)
              if stack.contains(loaded.path) {
                let cycle = stack.copy()
                cycle.push(loaded.path)
                raise ImportCycle(cycle)
              }
              let imported = parse_css(loaded.content)
              let next_stack = stack.copy()
              next_stack.push(loaded.path)
              let resolved = resolve_imports(
                imported,
                loader,
                loaded.base,
                next_stack,
              )
              let is_reference = spec.reference ||
                name.to_lower() == "@reference"
              if spec.theme_only && !only_theme_nodes(resolved) {
                raise InvalidCss(
                  "Files imported with `@import \"\u2026\" theme(reference)` must only contain `@theme` blocks.",
                )
              }
              // A referenced stylesheet only contributes theme variables.
              let resolved = if is_reference {
                keep_reference_nodes(mark_reference_theme(resolved))
              } else {
                resolved
              }
              // The imported file's own directory becomes the base of every
              // `@source` it declares.
              let scoped : Array[CssNode] = [
                Context(values={ "base": loaded.base }, nodes=resolved, span~),
              ]
              for child in wrap_import_conditions(scoped, spec, span) {
                output.push(child)
              }
            }
          }
        }
      Rule(selector~, nodes~, span~) => {
        let children = resolve_imports(nodes, loader, base, stack)
        output.push(Rule(selector~, nodes=children, span~))
      }
      AtRule(name~, params~, nodes=Some(nodes), span~) => {
        let children = resolve_imports(nodes, loader, base, stack)
        output.push(AtRule(name~, params~, nodes=Some(children), span~))
      }
      _ => output.push(node)
    }
  }
  output
}

///|
/// Synchronous counterpart of `resolve_imports`, driving a `SyncStylesheetLoader`.
///
/// Behaves identically to `resolve_imports`; kept in lockstep with it. Used by
/// `compile_sync` so hosts without an async runtime (e.g. wasm-gc) can still
/// resolve in-memory `@import`s.
fn resolve_imports_sync(
  nodes : ArrayView[CssNode],
  loader : &SyncStylesheetLoader,
  base : String,
  stack : Array[String],
) -> Array[CssNode] raise CompileError {
  let output : Array[CssNode] = []
  for node in nodes {
    match node {
      AtRule(name~, params~, nodes=None, span~) =>
        if !is_import_at_rule(name) {
          output.push(node)
        } else {
          match parse_import_spec(params) {
            None => output.push(node)
            Some(spec) => {
              let loaded = loader.load(spec.id, base)
              if stack.contains(loaded.path) {
                let cycle = stack.copy()
                cycle.push(loaded.path)
                raise ImportCycle(cycle)
              }
              let imported = parse_css(loaded.content)
              let next_stack = stack.copy()
              next_stack.push(loaded.path)
              let resolved = resolve_imports_sync(
                imported,
                loader,
                loaded.base,
                next_stack,
              )
              let is_reference = spec.reference ||
                name.to_lower() == "@reference"
              if spec.theme_only && !only_theme_nodes(resolved) {
                raise InvalidCss(
                  "Files imported with `@import \"…\" theme(reference)` must only contain `@theme` blocks.",
                )
              }
              let resolved = if is_reference {
                keep_reference_nodes(mark_reference_theme(resolved))
              } else {
                resolved
              }
              let scoped : Array[CssNode] = [
                Context(values={ "base": loaded.base }, nodes=resolved, span~),
              ]
              for child in wrap_import_conditions(scoped, spec, span) {
                output.push(child)
              }
            }
          }
        }
      Rule(selector~, nodes~, span~) => {
        let children = resolve_imports_sync(nodes, loader, base, stack)
        output.push(Rule(selector~, nodes=children, span~))
      }
      AtRule(name~, params~, nodes=Some(nodes), span~) => {
        let children = resolve_imports_sync(nodes, loader, base, stack)
        output.push(AtRule(name~, params~, nodes=Some(children), span~))
      }
      _ => output.push(node)
    }
  }
  output
}