///|
/// CSS Cascade Algorithm
/// Resolves multiple declarations into final cascaded values

///|
/// Cascaded values for an element
/// Maps property names to their winning declaration
pub struct CascadedValues {
  /// Map from property name to winning declaration
  values : Map[String, Declaration]
}

///|
pub fn CascadedValues::new() -> CascadedValues {
  { values: Map([]) }
}

///|
/// Get the cascaded value for a property
pub fn CascadedValues::get(
  self : CascadedValues,
  property : String,
) -> Declaration? {
  self.values.get(property)
}

///|
/// Get the raw value string for a property
pub fn CascadedValues::get_value(
  self : CascadedValues,
  property : String,
) -> String? {
  match self.values.get(property) {
    Some(decl) =>
      match decl.value {
        Value(v) => Some(v)
        _ => None
      }
    None => None
  }
}

///|
/// Check if a property has a cascaded value
pub fn CascadedValues::has(self : CascadedValues, property : String) -> Bool {
  self.values.contains(property)
}

///|
/// Get all property names that have cascaded values
pub fn CascadedValues::properties(self : CascadedValues) -> Array[String] {
  let result : Array[String] = []
  self.values.each(fn(k, _v) { result.push(k) })
  result
}

///|
/// Iterate cascaded declarations without allocating an intermediate key array.
pub fn CascadedValues::each(
  self : CascadedValues,
  f : (String, Declaration) -> Unit,
) -> Unit {
  self.values.each(f)
}

///|
/// Cascade a list of declarations into final values
/// Declarations should be for a single element
pub fn cascade(declarations : Array[Declaration]) -> CascadedValues {
  let result = CascadedValues::new()
  for decl in declarations {
    accumulate_cascaded_declaration(result, decl)
  }
  result
}

///|
fn accumulate_cascaded_declaration(
  result : CascadedValues,
  decl : Declaration,
) -> Unit {
  match result.values.get(decl.property) {
    Some(current) =>
      if compare_declarations(decl, current) > 0 {
        result.values.set(decl.property, decl)
      }
    None => result.values.set(decl.property, decl)
  }
}

///|
fn accumulate_inline_declaration(
  result : CascadedValues,
  decl : Declaration,
  source_order_offset : Int,
) -> Unit {
  accumulate_cascaded_declaration(result, {
    property: decl.property,
    value: decl.value,
    origin: Author,
    importance: decl.importance,
    specificity: { a: 1000, b: 0, c: 0 },
    // Inline styles are authored after every stylesheet, so their source order
    // must sort above all stylesheet declarations. Otherwise an inline longhand
    // (e.g. border-top-width) would be applied before a stylesheet shorthand
    // (e.g. border), and the shorthand's four-side expansion would clobber it
    // at compute time even though the inline declaration wins the cascade.
    source_order: source_order_offset + decl.source_order,
  })
}

///|
/// Collect declarations from matching rules for an element
/// This is a helper to build the input for cascade()
pub(all) struct RuleMatch {
  /// The matched selector's specificity
  specificity : @selector.Specificity
  /// Declarations from the rule
  declarations : Array[Declaration]
  /// Source order of the rule
  source_order : Int
}

///|
/// A CSS rule: selector + declarations
pub struct CSSRule {
  /// Selector text (for debugging)
  selector_text : String
  /// Parsed selector
  selector : @selector.ComplexSelector
  /// Declarations in this rule
  declarations : Array[Declaration]
  /// Source order
  source_order : Int
  /// Media query (if this rule is inside @media)
  media_query : @media.MediaQueryList?
}

///|
/// One keyframe block inside an `@keyframes` rule: a set of offsets (the
/// `from` / `to` / `` selectors, normalized to [0, 1]) sharing the
/// same declarations.
pub(all) struct KeyframeBlock {
  /// Offsets in [0, 1] this block applies to (e.g. `0%, 50%` => [0.0, 0.5]).
  offsets : Array[Double]
  /// Declarations declared for these offsets.
  declarations : Array[Declaration]
}

///|
/// A parsed `@keyframes` at-rule.
pub(all) struct KeyframesRule {
  /// The animation name (the identifier after `@keyframes`).
  name : String
  /// Keyframe blocks in source order.
  blocks : Array[KeyframeBlock]
}

///|
/// A parsed `@import` rule. Fetching and recursively parsing the referenced
/// stylesheet remains the embedding application's responsibility.
pub(all) struct ImportRule {
  href : String
  media : String
}

///|
/// A stylesheet: collection of rules
pub struct Stylesheet {
  /// `@import` rules in source order.
  imports : Array[ImportRule]
  /// Rules in source order
  rules : Array[CSSRule]
  /// `@keyframes` rules in source order.
  keyframes : Array[KeyframesRule]
  /// Origin of this stylesheet
  origin : Origin
  /// Lazily-built selector index for fast match candidate lookup. Invalidated
  /// to `None` on every rule mutation; rebuilt on first lookup.
  mut cached_index : SelectorIndex?
}

///|
pub fn Stylesheet::new(origin : Origin) -> Stylesheet {
  { imports: [], rules: [], keyframes: [], origin, cached_index: None }
}

///|
pub fn Stylesheet::add_import(
  self : Stylesheet,
  import_rule : ImportRule,
) -> Unit {
  self.imports.push(import_rule)
}

///|
/// Add a parsed `@keyframes` rule to the stylesheet.
pub fn Stylesheet::add_keyframes(
  self : Stylesheet,
  rule : KeyframesRule,
) -> Unit {
  self.keyframes.push(rule)
}

///|
/// Find the last `@keyframes` rule with the given name (later rules win, per
/// the CSS cascade). Returns None if no such rule exists.
pub fn Stylesheet::find_keyframes(
  self : Stylesheet,
  name : String,
) -> KeyframesRule? {
  let mut found : KeyframesRule? = None
  for kf in self.keyframes {
    if kf.name == name {
      found = Some(kf)
    }
  }
  found
}

///|
/// Add a rule to the stylesheet
pub fn Stylesheet::add_rule(
  self : Stylesheet,
  selector_text : String,
  selector : @selector.ComplexSelector,
  declarations : Array[Declaration],
) -> Unit {
  let source_order = self.rules.length()
  let rule : CSSRule = {
    selector_text,
    selector,
    declarations,
    source_order,
    media_query: None,
  }
  self.rules.push(rule)
  self.cached_index = None
}

///|
/// Add a rule with a media query to the stylesheet
pub fn Stylesheet::add_rule_with_media(
  self : Stylesheet,
  selector_text : String,
  selector : @selector.ComplexSelector,
  declarations : Array[Declaration],
  media_query : @media.MediaQueryList,
) -> Unit {
  let source_order = self.rules.length()
  let rule : CSSRule = {
    selector_text,
    selector,
    declarations,
    source_order,
    media_query: Some(media_query),
  }
  self.rules.push(rule)
  self.cached_index = None
}

///|
/// Return (and lazily build) the selector index for this stylesheet.
/// Subsequent calls on an unmutated stylesheet reuse the cached value.
fn Stylesheet::get_or_build_index(self : Stylesheet) -> SelectorIndex {
  match self.cached_index {
    Some(idx) => idx
    None => {
      let idx = SelectorIndex::from_stylesheet(self)
      self.cached_index = Some(idx)
      idx
    }
  }
}

///|
/// Match all rules against an element and return matched rules
pub fn Stylesheet::match_element(
  self : Stylesheet,
  element : @selector.Element,
) -> Array[RuleMatch] {
  self.match_element_with_media(element, None)
}

///|
/// Match all rules against an element with media query evaluation. Uses the
/// stylesheet's cached selector index to prune rule candidates — large
/// stylesheets see ~30× speedup vs scanning every rule.
pub fn Stylesheet::match_element_with_media(
  self : Stylesheet,
  element : @selector.Element,
  media_env : @media.MediaEnvironment?,
) -> Array[RuleMatch] {
  let matches : Array[RuleMatch] = []
  let idx = self.get_or_build_index()
  for rule_idx in idx.get_candidates(element) {
    push_rule_match(
      matches,
      self.rules[rule_idx],
      element,
      self.origin,
      media_env,
    )
  }
  matches
}

///|
/// If `rule` matches `element` under `media_env`, append its `RuleMatch`
/// (with origin annotation) to `matches`. Shared by Stylesheet and
/// IndexedStylesheet.
fn push_rule_match(
  matches : Array[RuleMatch],
  rule : CSSRule,
  element : @selector.Element,
  origin : Origin,
  media_env : @media.MediaEnvironment?,
) -> Unit {
  let media_matches = match (rule.media_query, media_env) {
    (Some(mq), Some(env)) => mq.evaluate(env)
    (Some(_), None) => true
    (None, _) => true
  }
  if !media_matches {
    return
  }
  if !@selector.matches_complex(element, rule.selector) {
    return
  }
  let specificity = @selector.complex_specificity(rule.selector)
  // Combine rule source_order with declaration source_order to preserve
  // declaration order within rules (important for vendor prefix fallbacks like
  // "display: -webkit-flex; display: flex;")
  let decls : Array[Declaration] = []
  for decl in rule.declarations {
    decls.push({
      property: decl.property,
      value: decl.value,
      origin,
      importance: decl.importance,
      specificity,
      source_order: rule.source_order * 10000 + decl.source_order,
    })
  }
  matches.push({
    specificity,
    declarations: decls,
    source_order: rule.source_order,
  })
}

///|
/// Apply one CSS rule against an element: if media gate passes and selector
/// matches, accumulate every declaration into `result`. Returns the largest
/// source order assigned by this rule, or `prev_max` if no declarations were
/// applied.
fn cascade_rule_into(
  result : CascadedValues,
  rule : CSSRule,
  element : @selector.Element,
  origin : Origin,
  media_env : @media.MediaEnvironment?,
  source_order_offset : Int,
  prev_max : Int,
  matches : (@selector.Element, @selector.ComplexSelector) -> Bool,
  declaration_is_valid : (String, PropertyValue) -> Bool,
) -> Int {
  let media_matches = match (rule.media_query, media_env) {
    (Some(mq), Some(env)) => mq.evaluate(env)
    (Some(_), None) => true
    (None, _) => true
  }
  if !media_matches {
    return prev_max
  }
  if !matches(element, rule.selector) {
    return prev_max
  }
  let specificity = @selector.complex_specificity(rule.selector)
  let mut max_so = prev_max
  for decl in rule.declarations {
    if !declaration_is_valid(decl.property, decl.value) {
      continue
    }
    let adjusted_source_order = source_order_offset +
      rule.source_order * 10000 +
      decl.source_order
    if adjusted_source_order > max_so {
      max_so = adjusted_source_order
    }
    accumulate_cascaded_declaration(result, {
      property: decl.property,
      value: decl.value,
      origin,
      importance: decl.importance,
      specificity,
      source_order: adjusted_source_order,
    })
  }
  max_so
}

///|
/// Iterate stylesheet rules, accumulating declarations from rules whose
/// selector passes `matches`. Returns the maximum source order assigned so
/// the caller can advance the cross-stylesheet offset.
fn cascade_stylesheet_into(
  result : CascadedValues,
  stylesheet : Stylesheet,
  element : @selector.Element,
  media_env : @media.MediaEnvironment?,
  source_order_offset : Int,
  matches : (@selector.Element, @selector.ComplexSelector) -> Bool,
  declaration_is_valid : (String, PropertyValue) -> Bool,
) -> Int {
  let mut max_so = source_order_offset - 1
  let idx = stylesheet.get_or_build_index()
  for rule_idx in idx.get_candidates(element) {
    max_so = cascade_rule_into(
      result,
      stylesheet.rules[rule_idx],
      element,
      stylesheet.origin,
      media_env,
      source_order_offset,
      max_so,
      matches,
      declaration_is_valid,
    )
  }
  max_so
}

///|
fn cascade_stylesheet_matches_into(
  result : CascadedValues,
  stylesheet : Stylesheet,
  element : @selector.Element,
  media_env : @media.MediaEnvironment?,
  source_order_offset : Int,
) -> Int {
  cascade_stylesheet_into(
    result,
    stylesheet,
    element,
    media_env,
    source_order_offset,
    @selector.matches_complex,
    fn(_property, _value) { true },
  )
}

///|
fn cascade_validated_stylesheet_matches_into(
  result : CascadedValues,
  stylesheet : Stylesheet,
  element : @selector.Element,
  media_env : @media.MediaEnvironment?,
  source_order_offset : Int,
  declaration_is_valid : (String, PropertyValue) -> Bool,
) -> Int {
  cascade_stylesheet_into(
    result, stylesheet, element, media_env, source_order_offset, @selector.matches_complex,
    declaration_is_valid,
  )
}

///|
/// Cascade styles for an element from multiple stylesheets
pub fn cascade_element(
  element : @selector.Element,
  stylesheets : Array[Stylesheet],
  inline_style : Array[Declaration],
) -> CascadedValues {
  cascade_element_with_media(element, stylesheets, inline_style, None)
}

///|
/// Cascade styles for an element with media query evaluation
pub fn cascade_element_with_media(
  element : @selector.Element,
  stylesheets : Array[Stylesheet],
  inline_style : Array[Declaration],
  media_env : @media.MediaEnvironment?,
) -> CascadedValues {
  let result = CascadedValues::new()
  let mut source_order_offset = 0

  // Collect matches from all stylesheets
  for stylesheet in stylesheets {
    let max_source_order_in_sheet = cascade_stylesheet_matches_into(
      result, stylesheet, element, media_env, source_order_offset,
    )
    if max_source_order_in_sheet >= source_order_offset {
      source_order_offset = max_source_order_in_sheet + 1
    }
  }

  for decl in inline_style {
    accumulate_inline_declaration(result, decl, source_order_offset)
  }
  result
}

///|
/// Cascade styles while allowing an embedding language to reject declarations
/// for extension properties before they can win the cascade.
pub fn cascade_element_with_media_and_validator(
  element : @selector.Element,
  stylesheets : Array[Stylesheet],
  inline_style : Array[Declaration],
  media_env : @media.MediaEnvironment?,
  declaration_is_valid : (String, PropertyValue) -> Bool,
) -> CascadedValues {
  let result = CascadedValues::new()
  let mut source_order_offset = 0
  for stylesheet in stylesheets {
    let max_source_order_in_sheet = cascade_validated_stylesheet_matches_into(
      result, stylesheet, element, media_env, source_order_offset, declaration_is_valid,
    )
    if max_source_order_in_sheet >= source_order_offset {
      source_order_offset = max_source_order_in_sheet + 1
    }
  }
  for decl in inline_style {
    if declaration_is_valid(decl.property, decl.value) {
      accumulate_inline_declaration(result, decl, source_order_offset)
    }
  }
  result
}