///|
/// Typed readings of an untyped tree.
///
/// The AST stores every declaration value as a generic component-value list.
/// That is what makes coverage total -- a vendor hack, a property added last
/// month and a value behind a flag all parse -- and it is also why asking "what
/// length is this" needs somewhere to live.
///
/// Here. Every function in this package is a **lens**: a pure function from a
/// `ComponentValue` to an optional typed reading, computed on demand and stored
/// nowhere. That is deliberately the opposite of lightningcss, which types five
/// hundred properties in the tree itself and therefore cannot represent what its
/// tables do not know. biome takes this approach too, layering `value_ext.rs`
/// over its untyped tree, and it is the one that keeps the two questions apart:
/// the parser decides what the text SAYS, and a lens decides what it MEANS.
///
/// Nothing here can fail loudly. A lens returns `None` when the value is not
/// the thing asked about, which is an answer rather than an error -- `as_length`
/// on the identifier `auto` is a perfectly ordinary question with a perfectly
/// ordinary negative answer.

// ------------------------------------------------------------------- numbers

///|
/// The number a value is, if it is one.
///
/// A percentage is deliberately NOT a number: `50%` and `50` mean different
/// things everywhere in CSS, and folding them here would make every caller
/// re-check which it had.
pub fn as_number(v : @ast.ComponentValue) -> Double? {
  match v {
    Num(n) => Some(n.value)
    _ => None
  }
}

///|
/// The integer a value is, if it is exactly one.
///
/// `is_int` rather than a floor: `2.0` is written as a number with a fractional
/// part, and a grammar asking for an integer should not silently accept it.
pub fn as_integer(v : @ast.ComponentValue) -> Int? {
  match v {
    Num(n) => if n.is_int { Some(n.value.to_int()) } else { None }
    _ => None
  }
}

///|
/// The percentage a value is, as its face value: `50%` gives 50, not 0.5.
///
/// Face value because that is what the source said, and a caller dividing by a
/// hundred is doing so for a reason this package cannot know.
pub fn as_percentage(v : @ast.ComponentValue) -> Double? {
  match v {
    Percentage(n) => Some(n.value)
    _ => None
  }
}

// ---------------------------------------------------------------- dimensions

///|
/// What kind of quantity a unit measures.
///
/// The grouping is the one CSS's own grammars use: a property takes a length,
/// or an angle, or a time -- never "a dimension".
pub(all) enum UnitKind {
  Length
  Angle
  Time
  Frequency
  Resolution
  Flex
  /// A unit no table here knows. Kept as a kind rather than a failure, because
  /// CSS gains units and a lens that refused an unknown one would be wrong
  /// every time it did.
  Unknown
} derive(Eq, Debug)

///|
/// A dimension: a number and the unit it was written with.
pub(all) struct Dimension {
  value : Double
  unit : String
  kind : UnitKind
} derive(Eq, Debug)

///|
/// The dimension a value is, if it is one.
pub fn as_dimension(v : @ast.ComponentValue) -> Dimension? {
  match v {
    Dimension(n, u) => Some({ value: n.value, unit: u, kind: unit_kind(u), })
    _ => None
  }
}

///|
/// The length a value is.
///
/// A bare `0` counts: CSS lets a zero length omit its unit, and a caller
/// asking for a length would otherwise have to special-case the one number that
/// is always allowed.
pub fn as_length(v : @ast.ComponentValue) -> Dimension? {
  match v {
    Dimension(n, u) =>
      if unit_kind(u) is Length {
        Some({ value: n.value, unit: u, kind: Length, })
      } else {
        None
      }
    Num(n) =>
      if n.value == 0.0 {
        Some({ value: 0.0, unit: "px", kind: Length, })
      } else {
        None
      }
    _ => None
  }
}

///|
pub fn as_angle(v : @ast.ComponentValue) -> Dimension? {
  as_dimension_of(v, Angle)
}

///|
pub fn as_time(v : @ast.ComponentValue) -> Dimension? {
  as_dimension_of(v, Time)
}

///|
pub fn as_resolution(v : @ast.ComponentValue) -> Dimension? {
  as_dimension_of(v, Resolution)
}

///|
fn as_dimension_of(v : @ast.ComponentValue, want : UnitKind) -> Dimension? {
  match as_dimension(v) {
    Some(d) => if d.kind == want { Some(d) } else { None }
    None => None
  }
}

///|
/// What a unit measures. Case-insensitive, as CSS units are.
pub fn unit_kind(unit : String) -> UnitKind {
  match unit.to_lower() {
    "px" | "cm" | "mm" | "q" | "in" | "pt" | "pc" => Length
    "em" | "rem" | "ex" | "rex" | "cap" | "rcap" => Length
    "ch" | "rch" | "ic" | "ric" | "lh" | "rlh" => Length
    "vw" | "vh" | "vi" | "vb" | "vmin" | "vmax" => Length
    "svw" | "svh" | "svi" | "svb" | "svmin" | "svmax" => Length
    "lvw" | "lvh" | "lvi" | "lvb" | "lvmin" | "lvmax" => Length
    "dvw" | "dvh" | "dvi" | "dvb" | "dvmin" | "dvmax" => Length
    "cqw" | "cqh" | "cqi" | "cqb" | "cqmin" | "cqmax" => Length
    "deg" | "grad" | "rad" | "turn" => Angle
    "s" | "ms" => Time
    "hz" | "khz" => Frequency
    "dpi" | "dpcm" | "dppx" | "x" => Resolution
    "fr" => Flex
    _ => Unknown
  }
}

// -------------------------------------------------------------------- colours

///|
/// A colour, as far as the syntax can say.
///
/// Syntactic rather than numeric, and that is the point. lightningcss quantises
/// every colour to `RGBA{u8,u8,u8,u8}`, which is right for a minifier and makes
/// `#fff`, `#ffffff` and `white` the same value -- so a formatter built on it
/// cannot give a stylesheet back the way it arrived. `to_rgba` below is
/// available for a caller that wants the numbers; the tree keeps the text.
pub(all) enum Color {
  /// `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`, digits as written.
  Hex(String)
  /// A named colour, `transparent`, or `currentColor`.
  Named(String)
  /// `rgb(...)`, `oklch(...)`, `color-mix(...)` -- the function and its
  /// arguments, uninterpreted.
  Function(String, Array[@ast.ComponentValue])
} derive(Eq, Debug)

///|
/// The colour a value is, if it looks like one.
///
/// "Looks like" is honest about the limit: this cannot know whether a caller's
/// property takes a colour, only whether the value could be one. An identifier
/// is a colour if it is a known name, because `red` is and `auto` is not.
pub fn as_color(v : @ast.ComponentValue) -> Color? {
  match v {
    Hex(d) => if is_hex_digits(d) { Some(Hex(d)) } else { None }
    Ident(name) =>
      if is_color_name(name.to_lower()) {
        Some(Named(name))
      } else {
        None
      }
    Function(f, args) =>
      if is_color_function(f.to_lower()) {
        Some(Function(f, args))
      } else {
        None
      }
    _ => None
  }
}

///|
/// A hex colour as its channels, if it is one this can resolve.
///
/// Only the hex form: resolving `oklch()` means implementing a colour space,
/// and resolving `red` means carrying the named-colour table's values. Both are
/// real work with real answers, and neither belongs behind a function that
/// looks this cheap.
pub fn to_rgba(c : Color) -> (Int, Int, Int, Int)? {
  match c {
    Hex(d) => {
      let n = d.length()
      if n == 3 || n == 4 {
        let r = hex1(d, 0)
        let g = hex1(d, 1)
        let b = hex1(d, 2)
        let a = if n == 4 { hex1(d, 3) } else { Some(15) }
        match (r, g, b, a) {
          (Some(r), Some(g), Some(b), Some(a)) =>
            Some((r * 17, g * 17, b * 17, a * 17))
          _ => None
        }
      } else if n == 6 || n == 8 {
        let r = hex2(d, 0)
        let g = hex2(d, 2)
        let b = hex2(d, 4)
        let a = if n == 8 { hex2(d, 6) } else { Some(255) }
        match (r, g, b, a) {
          (Some(r), Some(g), Some(b), Some(a)) => Some((r, g, b, a))
          _ => None
        }
      } else {
        None
      }
    }
    _ => None
  }
}

///|
fn hex1(s : String, i : Int) -> Int? {
  hex_value(s.unsafe_get(i).to_int().unsafe_to_char())
}

///|
fn hex2(s : String, i : Int) -> Int? {
  match (hex1(s, i), hex1(s, i + 1)) {
    (Some(a), Some(b)) => Some(a * 16 + b)
    _ => None
  }
}

///|
fn hex_value(c : Char) -> Int? {
  if c >= '0' && c <= '9' {
    Some(c.to_int() - 48)
  } else if c >= 'a' && c <= 'f' {
    Some(c.to_int() - 87)
  } else if c >= 'A' && c <= 'F' {
    Some(c.to_int() - 55)
  } else {
    None
  }
}

///|
fn is_hex_digits(d : String) -> Bool {
  let n = d.length()
  if !(n == 3 || n == 4 || n == 6 || n == 8) {
    return false
  }
  for c in d {
    if hex_value(c) is None {
      return false
    }
  }
  true
}

///|
/// The colour-valued functions. Names only: what they mean is a colour-space
/// question, and this package answers syntax questions.
fn is_color_function(f : String) -> Bool {
  match f {
    "rgb" | "rgba" | "hsl" | "hsla" | "hwb" => true
    "lab" | "lch" | "oklab" | "oklch" | "color" => true
    "color-mix" | "light-dark" | "device-cmyk" | "contrast-color" => true
    _ => false
  }
}

///|
/// Whether an identifier names a colour.
///
/// The CSS named colours, plus the three keywords that behave like one. Kept
/// as a list rather than derived, because there is no rule -- it is a list in
/// the specification too.
fn is_color_name(name : String) -> Bool {
  match name {
    "currentcolor" | "transparent" | "inherit" => true
    "black" | "silver" | "gray" | "grey" | "white" | "maroon" | "red" => true
    "purple" | "fuchsia" | "magenta" | "green" | "lime" | "olive" => true
    "yellow" | "navy" | "blue" | "teal" | "aqua" | "cyan" | "orange" => true
    "aliceblue" | "antiquewhite" | "aquamarine" | "azure" | "beige" => true
    "bisque" | "blanchedalmond" | "blueviolet" | "brown" | "burlywood" => true
    "cadetblue" | "chartreuse" | "chocolate" | "coral" | "cornflowerblue" =>
      true
    "cornsilk" | "crimson" | "darkblue" | "darkcyan" | "darkgoldenrod" => true
    "darkgray" | "darkgrey" | "darkgreen" | "darkkhaki" | "darkmagenta" => true
    "darkolivegreen" | "darkorange" | "darkorchid" | "darkred" => true
    "darksalmon" | "darkseagreen" | "darkslateblue" | "darkslategray" => true
    "darkslategrey" | "darkturquoise" | "darkviolet" | "deeppink" => true
    "deepskyblue" | "dimgray" | "dimgrey" | "dodgerblue" | "firebrick" => true
    "floralwhite" | "forestgreen" | "gainsboro" | "ghostwhite" | "gold" => true
    "goldenrod" | "greenyellow" | "honeydew" | "hotpink" | "indianred" => true
    "indigo" | "ivory" | "khaki" | "lavender" | "lavenderblush" => true
    "lawngreen" | "lemonchiffon" | "lightblue" | "lightcoral" => true
    "lightcyan" | "lightgoldenrodyellow" | "lightgray" | "lightgrey" => true
    "lightgreen" | "lightpink" | "lightsalmon" | "lightseagreen" => true
    "lightskyblue" | "lightslategray" | "lightslategrey" => true
    "lightsteelblue" | "lightyellow" | "limegreen" | "linen" => true
    "mediumaquamarine" | "mediumblue" | "mediumorchid" | "mediumpurple" => true
    "mediumseagreen" | "mediumslateblue" | "mediumspringgreen" => true
    "mediumturquoise" | "mediumvioletred" | "midnightblue" | "mintcream" => true
    "mistyrose" | "moccasin" | "navajowhite" | "oldlace" | "olivedrab" => true
    "orangered" | "orchid" | "palegoldenrod" | "palegreen" => true
    "paleturquoise" | "palevioletred" | "papayawhip" | "peachpuff" => true
    "peru" | "pink" | "plum" | "powderblue" | "rebeccapurple" => true
    "rosybrown" | "royalblue" | "saddlebrown" | "salmon" | "sandybrown" => true
    "seagreen" | "seashell" | "sienna" | "skyblue" | "slateblue" => true
    "slategray" | "slategrey" | "snow" | "springgreen" | "steelblue" => true
    "tan" | "thistle" | "tomato" | "turquoise" | "violet" | "wheat" => true
    "whitesmoke" | "yellowgreen" => true
    _ => false
  }
}

// ------------------------------------------------------------------ functions

///|
/// The arguments of a function call by this name, if that is what this is.
///
/// Case-insensitive, because CSS function names are.
pub fn as_function(
  v : @ast.ComponentValue,
  name : String,
) -> Array[@ast.ComponentValue]? {
  match v {
    Function(f, args) =>
      if f.to_lower() == name.to_lower() {
        Some(args)
      } else {
        None
      }
    _ => None
  }
}

///|
/// The custom property a `var()` reads, and its fallback.
pub fn as_var(v : @ast.ComponentValue) -> (String, Array[@ast.ComponentValue])? {
  match as_function(v, "var") {
    None => None
    Some(args) =>
      match args {
        [Ident(name), ..] =>
          if name.has_prefix("--") {
            let fallback : Array[@ast.ComponentValue] = []
            let mut seen_comma = false
            for a in args {
              if seen_comma {
                fallback.push(a)
              } else if a is Comma {
                seen_comma = true
              }
            }
            Some((name, fallback))
          } else {
            None
          }
        _ => None
      }
  }
}

///|
/// The url a value names, quoted or not -- both are the same node.
pub fn as_url(v : @ast.ComponentValue) -> String? {
  match v {
    Url(u) => Some(u)
    _ => None
  }
}

///|
/// The keyword a value is, lower-cased, if it is one of `allowed`.
///
/// The membership test is the caller's list, because "is this a keyword" has no
/// answer without knowing which property is being read.
pub fn as_keyword(v : @ast.ComponentValue, allowed : Array[String]) -> String? {
  match v {
    Ident(name) => {
      let lower = name.to_lower()
      for a in allowed {
        if a == lower {
          return Some(lower)
        }
      }
      None
    }
    _ => None
  }
}