///|
priv struct SourceDirectives {
  sources : Array[Source]
  included : Array[String]
  excluded : Map[String, Unit]
}

///|
fn quoted_content(input : String) -> String? {
  let value = trim(input)
  if value.length() < 2 {
    return None
  }
  if (value.has_prefix("\"") && value.has_suffix("\"")) ||
    (value.has_prefix("'") && value.has_suffix("'")) {
    Some(value[1:value.length() - 1].to_owned())
  } else {
    None
  }
}

///|
fn inline_source_content(params : String) -> String? raise CompileError {
  let value = trim(params)
  let marker = "inline("
  guard value.to_lower().find(marker) is Some(start) else { return None }
  let open = start + marker.length() - 1
  let close = find_function_end(value, open)
  quoted_content(value[open + 1:close].to_owned())
}

///|
fn parse_source_directives(
  nodes : ArrayView[CssNode],
  base : String,
) -> SourceDirectives raise CompileError {
  let result = { sources: [], included: [], excluded: Map([]) }
  for node in nodes {
    match node {
      AtRule(name="@source", params~, ..) => {
        let value = trim(params)
        let negated = value.has_prefix("not ")
        let target = if negated { trim(value[4:].to_owned()) } else { value }
        match inline_source_content(target) {
          Some(content) =>
            for raw in content.split(" ") {
              let candidate = trim(raw.to_owned())
              if candidate != "" {
                if negated {
                  result.excluded[candidate] = ()
                } else {
                  result.included.push(candidate)
                }
              }
            }
          None =>
            match quoted_content(target) {
              Some(pattern) => result.sources.push({ base, pattern, negated })
              None => raise InvalidCss("Invalid @source directive: \{params}")
            }
        }
      }
      // An imported file carries its own base for `@source` resolution.
      Context(values~, nodes~, ..) => {
        let nested = parse_source_directives(
          nodes,
          values.get("base").unwrap_or(base),
        )
        for source in nested.sources {
          result.sources.push(source)
        }
        for candidate in nested.included {
          result.included.push(candidate)
        }
        for candidate, _ in nested.excluded {
          result.excluded[candidate] = ()
        }
      }
      Rule(nodes~, ..) | AtRule(nodes=Some(nodes), ..) => {
        let nested = parse_source_directives(nodes, base)
        for source in nested.sources {
          result.sources.push(source)
        }
        for candidate in nested.included {
          result.included.push(candidate)
        }
        for candidate, _ in nested.excluded {
          result.excluded[candidate] = ()
        }
      }
      _ => ()
    }
  }
  result
}

///|
fn remove_source_nodes(nodes : ArrayView[CssNode]) -> (Array[CssNode], Bool) {
  let output : Array[CssNode] = []
  let mut changed = false
  for node in nodes {
    match node {
      AtRule(name="@source", ..) => changed = true
      Rule(selector~, nodes~, span~) => {
        let (children, child_changed) = remove_source_nodes(nodes)
        changed = changed || child_changed
        output.push(Rule(selector~, nodes=children, span~))
      }
      AtRule(name~, params~, nodes=Some(nodes), span~) => {
        let (children, child_changed) = remove_source_nodes(nodes)
        changed = changed || child_changed
        output.push(AtRule(name~, params~, nodes=Some(children), span~))
      }
      Context(values~, nodes=children, span~) => {
        let (inner, inner_changed) = remove_source_nodes(children)
        changed = changed || inner_changed
        output.push(Context(values~, nodes=inner, span~))
      }
      _ => output.push(node)
    }
  }
  (output, changed)
}