///|
/// Expanded `flex` shorthand. `basis` is the specified flex-basis text.
#derive(Eq, Debug)
pub(all) struct FlexShorthand {
  grow : Double
  shrink : Double
  basis : String
}

///|
pub extend FlexShorthand with Eq::{equal, not_equal}

///|
pub extend FlexShorthand with @debug.Debug::{to_repr}

///|
fn flex_factor(token : String) -> Double? {
  //  without a unit
  let mut seen_digit = false
  let mut seen_dot = false
  for c in token {
    if c >= '0' && c <= '9' {
      seen_digit = true
    } else if c == '.' && !seen_dot {
      seen_dot = true
    } else {
      return None
    }
  }
  if !seen_digit {
    return None
  }
  Some(@string.parse_double(token)) catch {
    _ => None
  }
}

///|
fn flex_basis_token(token : String) -> Bool {
  if token == "auto" || token == "content" || token.contains("(") {
    return true
  }
  // : digits, optional fraction, then a unit or %
  let mut i = 0
  let mut digits = 0
  while i < token.length() {
    let c = token[i]
    if (c >= '0' && c <= '9') || c == '.' {
      digits += 1
      i += 1
    } else {
      break
    }
  }
  if digits == 0 || i == token.length() {
    return false
  }
  let unit = token.unsafe_substring(start=i, end=token.length())
  if unit == "%" {
    return true
  }
  for c in unit {
    if !(c >= 'a' && c <= 'z') {
      return false
    }
  }
  true
}

///|
/// Whitespace-separated tokens at parenthesis depth 0, so `calc(a + b)` stays whole.
fn top_level_tokens(value : String) -> Array[String] {
  let tokens : Array[String] = []
  let current = StringBuilder()
  let mut depth = 0
  for c in value {
    if c == '(' {
      depth += 1
    } else if c == ')' && depth > 0 {
      depth -= 1
    }
    if depth == 0 && (c == ' ' || c == '\t' || c == '\n') {
      if current.to_string() != "" {
        tokens.push(current.to_string())
        current.reset()
      }
    } else {
      current.write_char(c)
    }
  }
  if current.to_string() != "" {
    tokens.push(current.to_string())
  }
  tokens
}

///|
/// css-flexbox-1 §7.2: `none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]`.
/// The factors are contiguous; the basis may come before or after them. A
/// unitless zero is a factor unless two factors already precede it. Omitted
/// grow and shrink are 1; an omitted basis is 0 (serialized as `0%`).
/// Returns None for an invalid value. CSS-wide keywords are not handled here.
pub fn parse_flex_shorthand(value : String) -> FlexShorthand? {
  let v = value.trim().to_lower()
  if v == "none" {
    return Some({ grow: 0.0, shrink: 0.0, basis: "auto", })
  }
  if v == "auto" {
    return Some({ grow: 1.0, shrink: 1.0, basis: "auto", })
  }
  let factors : Array[Double] = []
  let mut basis : String? = None
  let mut group_closed = false
  for token in top_level_tokens(v.to_owned()) {
    let factor = flex_factor(token)
    let zero_basis = token == "0" && factors.length() == 2
    match factor {
      Some(f) if !zero_basis && !(group_closed && factors.length() > 0) => {
        if factors.length() == 2 {
          return None
        }
        factors.push(f)
      }
      _ =>
        if basis is None && (zero_basis || flex_basis_token(token)) {
          basis = Some(token)
          if factors.length() > 0 {
            group_closed = true
          }
        } else {
          return None
        }
    }
  }
  if factors.is_empty() && basis is None {
    return None
  }
  Some({
    grow: if factors.length() > 0 {
      factors[0]
    } else {
      1.0
    },
    shrink: if factors.length() > 1 {
      factors[1]
    } else {
      1.0
    },
    basis: basis.unwrap_or("0%"),
  })
}

///|
/// `flex-flow: <'flex-direction'> || <'flex-wrap'>`. An omitted component is
/// its initial value (row, nowrap). Returns None for an invalid value.
pub fn parse_flex_flow(value : String) -> (FlexDirection, FlexWrap)? {
  let mut direction : FlexDirection? = None
  let mut wrap : FlexWrap? = None
  let tokens = top_level_tokens(value.trim().to_lower().to_owned())
  if tokens.is_empty() {
    return None
  }
  for token in tokens {
    let d : FlexDirection? = match token {
      "row" => Some(Row)
      "row-reverse" => Some(RowReverse)
      "column" => Some(Column)
      "column-reverse" => Some(ColumnReverse)
      _ => None
    }
    let w : FlexWrap? = match token {
      "nowrap" => Some(NoWrap)
      "wrap" => Some(Wrap)
      "wrap-reverse" => Some(WrapReverse)
      _ => None
    }
    match (d, w) {
      (Some(_), _) if direction is None => direction = d
      (_, Some(_)) if wrap is None => wrap = w
      _ => return None
    }
  }
  Some((direction.unwrap_or(Row), wrap.unwrap_or(NoWrap)))
}