///|
/// Parse a value inside CSS math functions and resolve it to a dimension
/// Handles recursively nested calc(), min(), max(), clamp()
fn parse_css_value_to_px(value : String, ctx : ComputeContext) -> Double? {
  let v = value.trim()
  if v.is_empty() {
    return None
  }

  // Handle nested calc()
  if v.has_prefix("calc(") && v.has_suffix(")") {
    let inner = view_to_string(
      v.view(start_offset=5, end_offset=v.length() - 1),
    )
    return parse_simple_calc_expr(inner, ctx)
  }

  // Handle nested min()
  if v.has_prefix("min(") && v.has_suffix(")") {
    let inner = view_to_string(
      v.view(start_offset=4, end_offset=v.length() - 1),
    )
    let args = split_css_args(inner)
    let mut min_val : Double? = None
    for arg in args {
      match parse_css_value_to_px(arg, ctx) {
        Some(val) =>
          match min_val {
            None => min_val = Some(val)
            Some(current) => if val < current { min_val = Some(val) }
          }
        None => return None
      }
    }
    return min_val
  }

  // Handle nested max()
  if v.has_prefix("max(") && v.has_suffix(")") {
    let inner = view_to_string(
      v.view(start_offset=4, end_offset=v.length() - 1),
    )
    let args = split_css_args(inner)
    let mut max_val : Double? = None
    for arg in args {
      match parse_css_value_to_px(arg, ctx) {
        Some(val) =>
          match max_val {
            None => max_val = Some(val)
            Some(current) => if val > current { max_val = Some(val) }
          }
        None => return None
      }
    }
    return max_val
  }

  // Handle nested clamp()
  if v.has_prefix("clamp(") && v.has_suffix(")") {
    let inner = view_to_string(
      v.view(start_offset=6, end_offset=v.length() - 1),
    )
    let args = split_css_args(inner)
    if args.length() != 3 {
      return None
    }
    let min_px = parse_css_value_to_px(args[0], ctx)
    let val_px = parse_css_value_to_px(args[1], ctx)
    let max_px = parse_css_value_to_px(args[2], ctx)
    match (min_px, val_px, max_px) {
      (Some(min_v), Some(val_v), Some(max_v)) =>
        // clamp(min, val, max) == max(min, min(val, max))
        return Some(@types.apply_math_op(Clamp, [min_v, val_v, max_v]))
      _ => return None
    }
  }

  // Parse simple dimension value
  let dim = resolve_dimension(v.to_owned(), ctx)
  match dim {
    Length(px) => Some(px)
    Percent(_) => None // Cannot simplify percentages
    Calc(_, _) => None // Mixed calc needs a layout basis
    MathFn(_, _) => None // min/max/clamp need a layout basis
    Auto => None
    MinContent => None
    MaxContent => None
    FitContent(_) => None
  }
}

///|
/// Split CSS function arguments by comma (handling nested parentheses)
fn split_css_args(input : String) -> Array[String] {
  let result : Array[String] = []
  let mut current = StringBuilder::new()
  let mut paren_depth = 0
  for i = 0; i < input.length(); i = i + 1 {
    let c = input[i].to_int().unsafe_to_char()
    if c == '(' {
      paren_depth += 1
      current.write_char(c)
    } else if c == ')' {
      paren_depth -= 1
      current.write_char(c)
    } else if c == ',' && paren_depth == 0 {
      let s = current.to_string().trim().to_owned()
      if !s.is_empty() {
        result.push(s)
      }
      current = StringBuilder::new()
    } else {
      current.write_char(c)
    }
  }
  let s = current.to_string().trim().to_owned()
  if !s.is_empty() {
    result.push(s)
  }
  result
}

///|
/// Parse a simple calc expression (handles + - * / with px values)
fn is_calc_signed_number_start(
  expr : String,
  index : Int,
  token_start : Int,
) -> Bool {
  if index != token_start || index + 1 >= expr.length() {
    return false
  }
  let sign = expr[index].to_int().unsafe_to_char()
  if sign != '+' && sign != '-' {
    return false
  }
  let next = expr[index + 1].to_int().unsafe_to_char()
  (next >= '0' && next <= '9') || next == '.'
}

///|
fn parse_simple_calc_expr(expr : String, ctx : ComputeContext) -> Double? {
  let mut result : Double = 0.0
  let mut current_op : Char = '+'
  let mut i = 0
  let mut token_start = 0
  while i <= expr.length() {
    let c = if i < expr.length() {
      expr[i].to_int().unsafe_to_char()
    } else {
      ' '
    }
    let signed_number_start = if i < expr.length() {
      is_calc_signed_number_start(expr, i, token_start)
    } else {
      false
    }
    if (c == '+' || c == '-' || c == '*' || c == '/' || i == expr.length()) &&
      !signed_number_start {
      if i > token_start {
        let token = expr.unsafe_substring(start=token_start, end=i).trim()
        if !token.is_empty() {
          match parse_css_value_to_px(token.to_owned(), ctx) {
            Some(val) =>
              match current_op {
                '+' => result = result + val
                '-' => result = result - val
                '*' => result = result * val
                '/' => result = result / val
                _ => ()
              }
            None => {
              // Try parsing as unitless number for * and /
              let n = @string.parse_double(token.to_owned()) catch {
                _ => return None
              }
              match current_op {
                '*' => result = result * n
                '/' => result = result / n
                '+' | '-' =>
                  // Unitless number for + or - is invalid unless 0
                  if n != 0.0 {
                    return None
                  }
                _ => ()
              }
            }
          }
        }
      }
      if i < expr.length() {
        current_op = c
      }
      token_start = i + 1
    }
    i = i + 1
  }
  Some(result)
}

///|
/// Reduce a single min()/max()/clamp() argument to a linear (px, percent_ratio)
/// form. Returns None for intrinsic/auto values that cannot be reduced.
fn math_arg_lp(arg : String, ctx : ComputeContext) -> (Double, Double)? {
  // A bare unitless number is only a valid length argument when it is 0; reject
  // non-zero unitless numbers so the result matches the parser layer.
  let trimmed = arg.trim().to_owned()
  if is_pure_number(trimmed) {
    let n = @string.parse_double(trimmed) catch { _ => return None }
    return if n == 0.0 { Some((0.0, 0.0)) } else { None }
  }
  match resolve_dimension(arg, ctx) {
    Length(px) => Some((px, 0.0))
    Percent(p) => Some((0.0, p))
    Calc(px, pct) => Some((px, pct))
    _ => None
  }
}

///|
/// Collapse a linear (px, percent_ratio) form back to the most specific
/// Dimension. Mirrors parse_calc_string's pure-vs-mixed classification.
fn lp_to_dimension(px : Double, pct : Double) -> @types.Dimension {
  if pct.abs() < 0.0001 {
    Length(px)
  } else if px.abs() < 0.0001 {
    Percent(pct)
  } else {
    Calc(px, pct)
  }
}

///|
/// Parse min(), max(), clamp() CSS functions.
///
/// Each argument is reduced to a linear (px, percent_ratio) form so that
/// percentage arguments are preserved instead of collapsing to Auto
/// (WPT css/css-values/calc-in-max.html).
/// - single argument: returned as-is (Length/Percent/Calc).
/// - all pure-px arguments: compared by px -> Length.
/// - all pure-percent arguments: the containing-block basis is positive, so
///   ordering by ratio matches ordering by resolved length -> Percent.
/// - mixed px + percent arguments cannot be ordered without a layout basis and
///   are carried as a MathFn dimension, resolved at layout time.
fn parse_css_math_function(
  expr : String,
  ctx : ComputeContext,
) -> @types.Dimension? {
  let v = expr.trim()
  let (op, prefix_len) = if v.has_prefix("min(") {
    (@types.MathOp::Min, 4)
  } else if v.has_prefix("max(") {
    (Max, 4)
  } else if v.has_prefix("clamp(") {
    (Clamp, 6)
  } else {
    return None
  }
  if !v.has_suffix(")") {
    return None
  }
  let inner = view_to_string(
    v.view(start_offset=prefix_len, end_offset=v.length() - 1),
  )
  let raw_args = split_css_args(inner)
  if raw_args.is_empty() {
    return None
  }
  if op == Clamp && raw_args.length() != 3 {
    return None
  }
  let lps : Array[(Double, Double)] = []
  for arg in raw_args {
    match math_arg_lp(arg, ctx) {
      Some(lp) => lps.push(lp)
      None => return None
    }
  }
  // Single min()/max() argument resolves to that argument verbatim.
  if lps.length() == 1 {
    let (px, pct) = lps[0]
    return Some(lp_to_dimension(px, pct))
  }
  let mut all_px = true
  let mut all_pct = true
  for lp in lps {
    if lp.1 != 0.0 {
      all_px = false
    }
    if lp.0 != 0.0 {
      all_pct = false
    }
  }
  if all_px {
    Some(Length(@types.apply_math_op(op, lps.map(fn(lp) { lp.0 }))))
  } else if all_pct {
    Some(Percent(@types.apply_math_op(op, lps.map(fn(lp) { lp.1 }))))
  } else {
    // Mixed length + percentage: defer to layout via MathFn.
    Some(MathFn(op, lps))
  }
}

///|
/// Internal calc() value: either a dimensionless number or a length/percentage
/// linear form (`px` plus `pct` in percentage points, e.g. 50.0 for 50%).
priv struct CalcVal {
  px : Double
  pct : Double
  num : Double
  is_num : Bool
}

///|
fn calc_num(n : Double) -> CalcVal {
  { px: 0.0, pct: 0.0, num: n, is_num: true }
}

///|
fn calc_lp(px : Double, pct : Double) -> CalcVal {
  { px, pct, num: 0.0, is_num: false }
}

///|
/// Tokens for the calc() expression grammar.
priv enum CalcTok {
  CNum(Double, String)
  COp(Char)
  CLP
  CRP
  CFun(String)
  CComma
}

///|
fn calc_char_at(s : String, i : Int) -> Char {
  s[i].to_int().unsafe_to_char()
}

///|
/// Tokenize a calc() inner expression. `+`/`-` are treated as binary operators
/// unless an operand is expected (start, after another operator, after `(` or
/// `,`), in which case they are the sign of the following number.
fn calc_tokenize(s : String) -> Array[CalcTok]? {
  let toks : Array[CalcTok] = []
  let n = s.length()
  let mut i = 0
  let mut expect_operand = true
  while i < n {
    let c = calc_char_at(s, i)
    if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
      i = i + 1
      continue
    }
    if c == '(' {
      toks.push(CLP)
      expect_operand = true
      i = i + 1
      continue
    }
    if c == ')' {
      toks.push(CRP)
      expect_operand = false
      i = i + 1
      continue
    }
    if c == ',' {
      toks.push(CComma)
      expect_operand = true
      i = i + 1
      continue
    }
    if c == '*' || c == '/' {
      toks.push(COp(c))
      expect_operand = true
      i = i + 1
      continue
    }
    let is_sign = c == '+' || c == '-'
    if is_sign && !expect_operand {
      toks.push(COp(c))
      expect_operand = true
      i = i + 1
      continue
    }
    let is_digit = c >= '0' && c <= '9'
    if is_digit || c == '.' || (is_sign && expect_operand) {
      let start = i
      if is_sign {
        i = i + 1
      }
      while i < n {
        let d = calc_char_at(s, i)
        if (d >= '0' && d <= '9') || d == '.' {
          i = i + 1
        } else {
          break
        }
      }
      if i < n {
        let e = calc_char_at(s, i)
        if e == 'e' || e == 'E' {
          // Only an exponent if `e` is followed by [+-]?digit; otherwise the
          // `e` begins a unit (e.g. the "em"/"ex" in "20em", "2ex").
          let mut j = i + 1
          if j < n {
            let sgn = calc_char_at(s, j)
            if sgn == '+' || sgn == '-' {
              j = j + 1
            }
          }
          if j < n && calc_char_at(s, j) >= '0' && calc_char_at(s, j) <= '9' {
            i = j + 1
            while i < n {
              let d = calc_char_at(s, i)
              if d >= '0' && d <= '9' {
                i = i + 1
              } else {
                break
              }
            }
          }
        }
      }
      let num_str = s.unsafe_substring(start~, end=i)
      let value = @string.parse_double(num_str) catch { _ => return None }
      let ustart = i
      while i < n {
        let d = calc_char_at(s, i)
        if (d >= 'a' && d <= 'z') || (d >= 'A' && d <= 'Z') || d == '%' {
          i = i + 1
        } else {
          break
        }
      }
      let unit = s.unsafe_substring(start=ustart, end=i).to_lower()
      toks.push(CNum(value, unit))
      expect_operand = false
      continue
    }
    if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') {
      let start = i
      while i < n {
        let d = calc_char_at(s, i)
        if (d >= 'a' && d <= 'z') || (d >= 'A' && d <= 'Z') {
          i = i + 1
        } else {
          break
        }
      }
      let name = s.unsafe_substring(start~, end=i).to_lower()
      toks.push(CFun(name))
      expect_operand = true
      continue
    }
    return None
  }
  Some(toks)
}

///|
/// Resolve a numeric token with its unit into a CalcVal.
fn calc_unit_value(
  value : Double,
  unit : String,
  ctx : ComputeContext,
) -> CalcVal? {
  if unit == "" {
    return Some(calc_num(value))
  }
  if unit == "%" {
    return Some(calc_lp(0.0, value))
  }
  match resolve_dimension("\{value}\{unit}", ctx) {
    Length(px) => Some(calc_lp(px, 0.0))
    Percent(p) => Some(calc_lp(0.0, p * 100.0))
    _ => None
  }
}

///|
/// Resolve a CalcVal to pixels for min()/max()/clamp() comparisons, resolving
/// percentages against a width reference.
fn calc_to_px(cv : CalcVal, ctx : ComputeContext) -> Double {
  if cv.is_num {
    cv.num
  } else {
    cv.px + cv.pct / 100.0 * mixed_calc_width_reference(ctx)
  }
}

///|
fn calc_add(a : CalcVal, b : CalcVal, sign : Double) -> CalcVal? {
  if a.is_num && b.is_num {
    Some(calc_num(a.num + sign * b.num))
  } else if !a.is_num && !b.is_num {
    Some(calc_lp(a.px + sign * b.px, a.pct + sign * b.pct))
  } else {
    None
  }
}

///|
fn calc_mul(a : CalcVal, b : CalcVal) -> CalcVal? {
  if a.is_num && b.is_num {
    Some(calc_num(a.num * b.num))
  } else if a.is_num {
    Some(calc_lp(b.px * a.num, b.pct * a.num))
  } else if b.is_num {
    Some(calc_lp(a.px * b.num, a.pct * b.num))
  } else {
    None
  }
}

///|
fn calc_div(a : CalcVal, b : CalcVal) -> CalcVal? {
  if !b.is_num || b.num == 0.0 {
    return None
  }
  if a.is_num {
    Some(calc_num(a.num / b.num))
  } else {
    Some(calc_lp(a.px / b.num, a.pct / b.num))
  }
}

///|
/// primary := number | '(' sum ')' | fn '(' sum (',' sum)* ')'
fn calc_parse_primary(
  toks : Array[CalcTok],
  pos : Int,
  ctx : ComputeContext,
) -> (CalcVal, Int)? {
  if pos >= toks.length() {
    return None
  }
  match toks[pos] {
    CNum(v, unit) =>
      match calc_unit_value(v, unit, ctx) {
        Some(cv) => Some((cv, pos + 1))
        None => None
      }
    CLP =>
      match calc_parse_sum(toks, pos + 1, ctx) {
        Some((v, p2)) =>
          if p2 < toks.length() &&
            (match toks[p2] {
              CRP => true
              _ => false
            }) {
            Some((v, p2 + 1))
          } else {
            None
          }
        None => None
      }
    CFun(name) => {
      if pos + 1 >= toks.length() {
        return None
      }
      match toks[pos + 1] {
        CLP => ()
        _ => return None
      }
      let args : Array[CalcVal] = []
      let mut p = pos + 2
      let mut closed = false
      while !closed {
        match calc_parse_sum(toks, p, ctx) {
          Some((a, np)) => {
            args.push(a)
            if np >= toks.length() {
              return None
            }
            match toks[np] {
              CComma => p = np + 1
              CRP => {
                p = np + 1
                closed = true
              }
              _ => return None
            }
          }
          None => return None
        }
      }
      match name {
        "calc" => if args.length() == 1 { Some((args[0], p)) } else { None }
        "min" => {
          if args.is_empty() {
            return None
          }
          let mut m = calc_to_px(args[0], ctx)
          for k = 1; k < args.length(); k = k + 1 {
            let x = calc_to_px(args[k], ctx)
            if x < m {
              m = x
            }
          }
          Some((calc_lp(m, 0.0), p))
        }
        "max" => {
          if args.is_empty() {
            return None
          }
          let mut m = calc_to_px(args[0], ctx)
          for k = 1; k < args.length(); k = k + 1 {
            let x = calc_to_px(args[k], ctx)
            if x > m {
              m = x
            }
          }
          Some((calc_lp(m, 0.0), p))
        }
        "clamp" => {
          if args.length() != 3 {
            return None
          }
          let lo = calc_to_px(args[0], ctx)
          let mid = calc_to_px(args[1], ctx)
          let hi = calc_to_px(args[2], ctx)
          let mut r = mid
          if r < lo {
            r = lo
          }
          if r > hi {
            r = hi
          }
          Some((calc_lp(r, 0.0), p))
        }
        _ => None
      }
    }
    _ => None
  }
}

///|
/// product := primary (('*' | '/') primary)*
fn calc_parse_product(
  toks : Array[CalcTok],
  pos : Int,
  ctx : ComputeContext,
) -> (CalcVal, Int)? {
  match calc_parse_primary(toks, pos, ctx) {
    Some((first, p1)) => {
      let mut acc = first
      let mut p = p1
      let mut stop = false
      while !stop && p < toks.length() {
        let op = match toks[p] {
          COp('*') => '*'
          COp('/') => '/'
          _ => ' '
        }
        if op == ' ' {
          stop = true
        } else {
          match calc_parse_primary(toks, p + 1, ctx) {
            Some((rhs, p2)) => {
              let combined = if op == '*' {
                calc_mul(acc, rhs)
              } else {
                calc_div(acc, rhs)
              }
              match combined {
                Some(v) => {
                  acc = v
                  p = p2
                }
                None => return None
              }
            }
            None => return None
          }
        }
      }
      Some((acc, p))
    }
    None => None
  }
}

///|
/// sum := product (('+' | '-') product)*
fn calc_parse_sum(
  toks : Array[CalcTok],
  pos : Int,
  ctx : ComputeContext,
) -> (CalcVal, Int)? {
  match calc_parse_product(toks, pos, ctx) {
    Some((first, p1)) => {
      let mut acc = first
      let mut p = p1
      let mut stop = false
      while !stop && p < toks.length() {
        let sign = match toks[p] {
          COp('+') => 1.0
          COp('-') => -1.0
          _ => 0.0
        }
        if sign == 0.0 {
          stop = true
        } else {
          match calc_parse_product(toks, p + 1, ctx) {
            Some((rhs, p2)) =>
              match calc_add(acc, rhs, sign) {
                Some(v) => {
                  acc = v
                  p = p2
                }
                None => return None
              }
            None => return None
          }
        }
      }
      Some((acc, p))
    }
    None => None
  }
}

///|
/// Precedence-aware calc() evaluator. Returns (px, percent_ratio) or None if
/// the expression is malformed or not fully understood (callers then fall back
/// to the legacy left-to-right evaluator).
fn eval_calc(expr : String, ctx : ComputeContext) -> (Double, Double)? {
  let v = expr.trim()
  if !v.has_prefix("calc(") || !v.has_suffix(")") {
    return None
  }
  let inner = view_to_string(v.view(start_offset=5, end_offset=v.length() - 1))
  let toks = match calc_tokenize(inner) {
    Some(t) => t
    None => return None
  }
  if toks.is_empty() {
    return None
  }
  match calc_parse_sum(toks, 0, ctx) {
    Some((result, p)) =>
      // Trailing tokens or a bare (dimensionless) result is a parse failure.
      if p != toks.length() || result.is_num {
        None
      } else {
        Some((result.px, result.pct / 100.0))
      }
    None => None
  }
}

///|
/// Parse calc() terms and return (length_px, percent_ratio); percent_ratio is
/// 0.5 for 50%. Delegates to the precedence-aware recursive-descent evaluator.
/// Returns None for malformed or semantically invalid calc() (e.g. a bare
/// number, length × length, or division by zero), so the property falls back to
/// its initial value rather than a silently wrong length.
fn parse_calc_terms(expr : String, ctx : ComputeContext) -> (Double, Double)? {
  eval_calc(expr, ctx)
}

///|
/// Extract first percentage token from calc() expression as ratio.
/// Used as a fallback when mixed calc contains multiplicative terms.
fn extract_first_percent_ratio(expr : String) -> Double? {
  let mut i = 0
  while i < expr.length() {
    if expr[i].to_int().unsafe_to_char() == '%' {
      let mut start = i
      while start > 0 {
        let c = expr[start - 1].to_int().unsafe_to_char()
        if (c >= '0' && c <= '9') || c == '.' {
          start = start - 1
          continue
        }
        if c == '+' || c == '-' {
          start = start - 1
        }
        break
      }
      let num = expr.unsafe_substring(start~, end=i).trim()
      if !num.is_empty() {
        let n = @string.parse_double(num.to_owned()) catch { _ => return None }
        return Some(n / 100.0)
      }
    }
    i = i + 1
  }
  None
}

///|
/// Parse a calc() expression from a string and try to simplify it
/// Returns Some(dimension) if simplification is possible, None otherwise
fn parse_calc_string(expr : String, ctx : ComputeContext) -> @types.Dimension? {
  match parse_calc_terms(expr, ctx) {
    Some((result_px, result_percent)) =>
      if result_percent.abs() < 0.0001 {
        Some(Length(result_px))
      } else if result_px.abs() < 0.0001 {
        Some(Percent(result_percent))
      } else {
        Some(Calc(result_px, result_percent))
      }
    None => None
  }
}

///|
/// Choose a width reference for mixed calc() fallback.
/// Prefer parent's definite width when available; otherwise use viewport width.
fn mixed_calc_width_reference(ctx : ComputeContext) -> Double {
  match ctx.parent_style {
    Some(parent) =>
      match parent.width {
        Length(w) => w
        Percent(p) => ctx.viewport_width * p
        _ => ctx.viewport_width
      }
    None => ctx.viewport_width
  }
}

///|
/// Fallback for mixed calc() on properties that need concrete dimensions.
/// For expressions like calc(50% - 10px), resolve using a width reference.
fn resolve_dimension_with_calc_percent_fallback(
  value : String,
  ctx : ComputeContext,
) -> @types.Dimension {
  let dim = resolve_dimension(value, ctx)
  match dim {
    Auto =>
      if value.trim().has_prefix("calc(") {
        match parse_calc_terms(value, ctx) {
          Some((length_px, percent_ratio)) =>
            if percent_ratio.abs() >= 0.0001 {
              let percent_for_resolution = if value.contains("*") ||
                value.contains("/") {
                match extract_first_percent_ratio(value) {
                  Some(p) => p
                  None => percent_ratio
                }
              } else {
                percent_ratio
              }
              if length_px.abs() >= 0.0001 {
                // Preserve mixed calc() losslessly (px + percent) so layout
                // can resolve it against the correct containing-block basis,
                // e.g. table column widths, as required by CSS Values.
                Calc(length_px, percent_for_resolution)
              } else {
                let basis = mixed_calc_width_reference(ctx)
                Length(basis * percent_for_resolution + length_px)
              }
            } else if length_px.abs() >= 0.0001 {
              Length(length_px)
            } else {
              Length(0.0)
            }
          None => dim
        }
      } else {
        dim
      }
    _ => dim
  }
}

///|
/// Compatibility fallback for mixed calc() in min-size properties.
/// keep current Dimension model and pick axis-specific term when mixed.
fn resolve_dimension_with_mixed_calc_fallback(
  property : String,
  value : String,
  ctx : ComputeContext,
) -> @types.Dimension {
  let dim = resolve_dimension(value, ctx)
  match dim {
    Auto =>
      if value.trim().has_prefix("calc(") {
        match parse_calc_terms(value, ctx) {
          Some((length_px, percent_ratio)) =>
            if length_px.abs() >= 0.0001 && percent_ratio.abs() >= 0.0001 {
              // Preserve mixed calc() losslessly; resolved at layout time
              // against the property's containing-block basis.
              ignore(property)
              Calc(length_px, percent_ratio)
            } else {
              dim
            }
          None => dim
        }
      } else {
        dim
      }
    _ => dim
  }
}

///|
/// Resolve custom properties in an arbitrary CSS property value.
///
/// Returns `None` when the value contains malformed `var()` syntax, refers to
/// a missing property without a fallback, or reaches a custom-property cycle.
pub fn resolve_custom_property_value(
  value : String,
  custom_properties : Map[String, String],
) -> String? {
  resolve_custom_property_value_inner(value, custom_properties, [], 0)
}

///|
fn resolve_custom_property_value_inner(
  value : String,
  custom_properties : Map[String, String],
  stack : Array[String],
  depth : Int,
) -> String? {
  if depth > 32 {
    return None
  }
  let start = match value.find("var(") {
    Some(start) => start
    None => return Some(value)
  }
  let open = start + 3
  let mut close = -1
  let mut paren_depth = 0
  for i in open..
        resolve_custom_property_value_inner(
          fallback,
          custom_properties,
          stack,
          depth + 1,
        )
      None => None
    }
  } else {
    match custom_properties.get(name) {
      Some(custom_value) => {
        let next_stack = stack.copy()
        next_stack.push(name)
        match
          resolve_custom_property_value_inner(
            custom_value,
            custom_properties,
            next_stack,
            depth + 1,
          ) {
          Some(value) => Some(value)
          None =>
            match fallback {
              Some(fallback) =>
                resolve_custom_property_value_inner(
                  fallback,
                  custom_properties,
                  stack,
                  depth + 1,
                )
              None => None
            }
        }
      }
      None =>
        match fallback {
          Some(fallback) =>
            resolve_custom_property_value_inner(
              fallback,
              custom_properties,
              stack,
              depth + 1,
            )
          None => None
        }
    }
  }
  match replacement {
    Some(replacement) => {
      let before = view_to_string(value.view(end_offset=start))
      let after = view_to_string(value.view(start_offset=close + 1))
      resolve_custom_property_value_inner(
        before + replacement + after,
        custom_properties,
        stack,
        depth + 1,
      )
    }
    None => None
  }
}

///|
fn resolve_all_vars(value : String, ctx : ComputeContext) -> String {
  match resolve_custom_property_value(value, ctx.custom_properties) {
    Some(value) => value
    None => ""
  }
}

///|
fn strip_css_ascii_whitespace(value : String) -> String {
  let sb = StringBuilder::new()
  for c in value.iter() {
    if !is_css_ascii_whitespace(c) {
      sb.write_char(c)
    }
  }
  sb.to_string()
}

///|
fn is_css_ascii_whitespace(c : Char) -> Bool {
  c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\u000C'
}

///|
fn is_css_ident_char(c : Char) -> Bool {
  (c >= 'a' && c <= 'z') ||
  (c >= 'A' && c <= 'Z') ||
  (c >= '0' && c <= '9') ||
  c == '-' ||
  c == '_'
}

///|
fn css_math_function_prefix_len(value : String, index : Int) -> Int {
  if index > 0 {
    let prev = value[index - 1].to_int().unsafe_to_char()
    if is_css_ident_char(prev) {
      return 0
    }
  }
  let tail = view_to_string(value.view(start_offset=index))
  if tail.has_prefix("calc(") {
    5
  } else if tail.has_prefix("min(") {
    4
  } else if tail.has_prefix("max(") {
    4
  } else if tail.has_prefix("clamp(") {
    6
  } else {
    0
  }
}

///|
fn find_matching_function_end(value : String, index : Int) -> Int {
  let mut depth = 0
  let mut i = index
  while i < value.length() {
    let c = value[i].to_int().unsafe_to_char()
    if c == '(' {
      depth = depth + 1
    } else if c == ')' {
      depth = depth - 1
      if depth == 0 {
        return i
      }
    }
    i = i + 1
  }
  -1
}

///|
fn dimension_to_grid_token(dim : @types.Dimension) -> String? {
  match dim {
    Length(v) => Some(v.to_string() + "px")
    Percent(p) => Some((p * 100.0).to_string() + "%")
    _ => None
  }
}

///|
fn normalize_grid_template_math_functions(
  value : String,
  ctx : ComputeContext,
) -> String {
  let out = StringBuilder::new()
  let mut i = 0
  while i < value.length() {
    let prefix_len = css_math_function_prefix_len(value, i)
    if prefix_len > 0 {
      let end = find_matching_function_end(value, i + prefix_len - 1)
      if end > i {
        let expr = view_to_string(
          value.view(start_offset=i, end_offset=end + 1),
        )
        match dimension_to_grid_token(resolve_dimension(expr, ctx)) {
          Some(token) => {
            out.write_string(token)
            i = end + 1
            continue
          }
          None => ()
        }
      }
    }
    out.write_char(value[i].to_int().unsafe_to_char())
    i = i + 1
  }
  out.to_string()
}

///|
/// Resolve dimension with relative unit conversion

///|
/// Split a "" string into its numeric value and unit. Returns
/// None when there is no leading number. `e`/`E` only begins an exponent when
/// followed by [+-]?digit, so unit strings like "em"/"ex" are not mis-scanned.
fn split_dimension(v : String) -> (Double, String)? {
  let n = v.length()
  if n == 0 {
    return None
  }
  let mut i = 0
  let c0 = v[0].to_int().unsafe_to_char()
  if c0 == '+' || c0 == '-' {
    i = i + 1
  }
  let digits_start = i
  while i < n {
    let c = v[i].to_int().unsafe_to_char()
    if (c >= '0' && c <= '9') || c == '.' {
      i = i + 1
    } else {
      break
    }
  }
  if i < n {
    let e = v[i].to_int().unsafe_to_char()
    if e == 'e' || e == 'E' {
      let mut j = i + 1
      if j < n {
        let s = v[j].to_int().unsafe_to_char()
        if s == '+' || s == '-' {
          j = j + 1
        }
      }
      if j < n &&
        v[j].to_int().unsafe_to_char() >= '0' &&
        v[j].to_int().unsafe_to_char() <= '9' {
        i = j + 1
        while i < n {
          let d = v[i].to_int().unsafe_to_char()
          if d >= '0' && d <= '9' {
            i = i + 1
          } else {
            break
          }
        }
      }
    }
  }
  if i == digits_start {
    return None
  }
  let num = @string.parse_double(v.unsafe_substring(start=0, end=i)) catch {
    _ => return None
  }
  Some((num, v.unsafe_substring(start=i, end=n)))
}

///|
fn resolve_dimension(value : String, ctx : ComputeContext) -> @types.Dimension {
  // First resolve any var() functions
  let raw_v = resolve_all_vars(value, ctx).trim()
  // CSS units and keywords are ASCII case-insensitive. var() names are already
  // resolved above, so lowercasing what remains (a number+unit or a keyword) is
  // safe for the matching below.
  let v = strip_css_ascii_whitespace(raw_v.to_owned()).to_lower()
  if v == "auto" || v == "none" {
    return Auto
  }

  // Handle intrinsic sizing keywords
  if v == "min-content" {
    return MinContent
  }
  if v == "max-content" {
    return MaxContent
  }
  if v == "fit-content" {
    // fit-content without argument is equivalent to fit-content(max-content)
    return FitContent(1.0e10)
  }

  // Handle calc() expressions
  if view_starts_with(raw_v, "calc(") {
    match parse_calc_string(raw_v.to_owned(), ctx) {
      Some(dim) => return dim
      None => return Auto // Fallback for complex calc
    }
  }

  // Handle min(), max(), clamp() CSS math functions
  if view_starts_with(raw_v, "min(") ||
    view_starts_with(raw_v, "max(") ||
    view_starts_with(raw_v, "clamp(") {
    match parse_css_math_function(raw_v.to_owned(), ctx) {
      Some(dim) => return dim
      None => return Auto // Fallback for complex expressions
    }
  }

  // Extract the unit once and dispatch by equality. The string suffix tests
  // this replaces cost ~190ns each; for the common px/% values the old cascade
  // ran 20+ of them before falling through. ch/ex keep the historical 0.5em
  // approximation for generic fonts.
  match split_dimension(v) {
    Some((n, unit)) => {
      let resolved : @types.Dimension? = match unit {
        "px" => Some(Length(n))
        "%" => Some(Percent(n / 100.0))
        "rem" => Some(Length(n * ctx.root_font_size))
        "em" => Some(Length(n * ctx.font_size))
        "vw" | "dvw" | "svw" | "lvw" | "vi" =>
          Some(Length(n * ctx.viewport_width / 100.0))
        "vh" | "dvh" | "svh" | "lvh" | "vb" =>
          Some(Length(n * ctx.viewport_height / 100.0))
        "vmin" => {
          let base = if ctx.viewport_width < ctx.viewport_height {
            ctx.viewport_width
          } else {
            ctx.viewport_height
          }
          Some(Length(n * base / 100.0))
        }
        "vmax" => {
          let base = if ctx.viewport_width > ctx.viewport_height {
            ctx.viewport_width
          } else {
            ctx.viewport_height
          }
          Some(Length(n * base / 100.0))
        }
        "ch" => Some(Length(n * ctx.font_size * ch_unit_ratio(ctx)))
        "ex" => Some(Length(n * ctx.font_size * 0.5))
        "pt" => Some(Length(n * (96.0 / 72.0)))
        "pc" => Some(Length(n * 16.0))
        "in" => Some(Length(n * 96.0))
        "cm" => Some(Length(n * (96.0 / 2.54)))
        "mm" => Some(Length(n * (96.0 / 25.4)))
        "q" => Some(Length(n * (96.0 / 101.6)))
        _ => None
      }
      match resolved {
        Some(d) => return d
        None => ()
      }
    }
    None => ()
  }

  // Fall back for unitless, fit-content(...) and unknown units.
  parse_dimension(v.to_string())
}

///|
/// Resolve an authored CSS length-percentage against the unit environment while
/// preserving any percentage component for the caller's property-specific
/// basis. Intrinsic sizing keywords and invalid values are rejected.
pub fn resolve_length_dimension(
  value : String,
  ctx : ComputeContext,
) -> @types.Dimension? {
  match resolve_dimension(value, ctx) {
    Length(_) | Percent(_) | Calc(_, _) | MathFn(_, _) as dimension =>
      Some(dimension)
    Auto | MinContent | MaxContent | FitContent(_) => None
  }
}

///|
/// Check if a string is a pure number (no units)
fn is_pure_number(s : String) -> Bool {
  let v = s.trim()
  if v.is_empty() {
    return false
  }
  // Try to parse as double - will fail if it has units
  let _ = @string.parse_double(v.to_owned()) catch { _ => return false }
  // Make sure it doesn't have any unit suffixes that parse_double might accept
  for i = 0; i < v.length(); i = i + 1 {
    let c = v[i]
    if !(c == '0' ||
      c == '1' ||
      c == '2' ||
      c == '3' ||
      c == '4' ||
      c == '5' ||
      c == '6' ||
      c == '7' ||
      c == '8' ||
      c == '9' ||
      c == '.' ||
      c == '-' ||
      c == '+') {
      return false
    }
  }
  true
}