///|
/// A validated SVG path-data string, shared by clipping and motion paths.
pub struct PathData {
  data : String
}

///|
fn path_space(c : UInt16) -> Bool {
  c == ' ' || c == '\t' || c == '\n' || c == '\r'
}

///|
fn path_digit(c : UInt16) -> Bool {
  c >= '0' && c <= '9'
}

///|
fn path_arity(c : UInt16) -> Int32 {
  match c {
    'M' | 'm' | 'L' | 'l' | 'T' | 't' => 2
    'H' | 'h' | 'V' | 'v' => 1
    'C' | 'c' => 6
    'S' | 's' | 'Q' | 'q' => 4
    'A' | 'a' => 7
    'Z' | 'z' => 0
    _ => -1
  }
}

///|
fn path_number_end(text : String, start : Int32) -> Int32? {
  let mut i = start
  if i < text.length() && (text[i] == '+' || text[i] == '-') {
    i += 1
  }
  let mut digits = 0
  while i < text.length() && path_digit(text[i]) {
    digits += 1
    i += 1
  }
  if i < text.length() && text[i] == '.' {
    i += 1
    while i < text.length() && path_digit(text[i]) {
      digits += 1
      i += 1
    }
  }
  if digits == 0 {
    return None
  }
  if i < text.length() && (text[i] == 'e' || text[i] == 'E') {
    i += 1
    if i < text.length() && (text[i] == '+' || text[i] == '-') {
      i += 1
    }
    let exponent = i
    while i < text.length() && path_digit(text[i]) {
      i += 1
    }
    if i == exponent {
      return None
    }
  }
  Some(i)
}

///|
fn valid_path_data(text : String) -> Bool {
  let mut i = 0
  let mut first = true
  let mut command : UInt16 = ' '
  let mut count = 0
  let mut arity = -1
  while i < text.length() {
    while i < text.length() && path_space(text[i]) {
      i += 1
    }
    if i == text.length() {
      break
    }
    let next_arity = path_arity(text[i])
    if next_arity >= 0 {
      if first && text[i] != 'M' && text[i] != 'm' {
        return false
      }
      if arity > 0 && (count == 0 || count % arity != 0) {
        return false
      }
      command = text[i]
      arity = next_arity
      count = 0
      first = false
      i += 1
      continue
    }
    if first || arity <= 0 {
      return false
    }
    if text[i] == ',' {
      if count == 0 {
        return false
      }
      i += 1
      while i < text.length() && path_space(text[i]) {
        i += 1
      }
      if i == text.length() || text[i] == ',' || path_arity(text[i]) >= 0 {
        return false
      }
    }
    let is_arc = command == 'A' || command == 'a'
    let parameter = count % arity
    let end = if is_arc && (parameter == 3 || parameter == 4) {
      if text[i] != '0' && text[i] != '1' {
        return false
      }
      i + 1
    } else {
      match path_number_end(text, i) {
        Some(end) => end
        None => return false
      }
    }
    let number = @string.parse_double(text[i:end]) catch { _ => return false }
    if number.is_nan() || number.is_pos_inf() || number.is_neg_inf() {
      return false
    }
    if is_arc && parameter < 2 && number < 0.0 {
      return false
    }
    i = end
    count += 1
  }
  !first && (arity == 0 || (count > 0 && count % arity == 0))
}

///|
pub fn path_data(value : String) -> PathData {
  let data = value.trim().to_owned()
  if valid_path_data(data) {
    { data, }
  } else {
    {
      data: issue_css(
        "STYLE_PATH_DATA", "Invalid SVG path command, parameter count, number or arc flag",
      ),
    }
  }
}

///|
pub fn PathData::to_string(self : PathData) -> String {
  self.data
}

///|
pub(all) enum FillRule {
  Nonzero
  Evenodd
} derive(Eq, Debug)

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

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

///|
fn FillRule::css(self : FillRule) -> String {
  match self {
    Nonzero => "nonzero"
    Evenodd => "evenodd"
  }
}

///|
pub(all) enum GeometryBox {
  BorderBox
  PaddingBox
  ContentBox
  MarginBox
  FillBox
  StrokeBox
  ViewBox
} derive(Eq, Debug)

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

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

///|
pub fn GeometryBox::to_css(self : GeometryBox) -> String {
  match self {
    BorderBox => "border-box"
    PaddingBox => "padding-box"
    ContentBox => "content-box"
    MarginBox => "margin-box"
    FillBox => "fill-box"
    StrokeBox => "stroke-box"
    ViewBox => "view-box"
  }
}

///|
pub struct BasicShape {
  expression : Expression
}

///|
pub fn BasicShape::expression(self : BasicShape) -> Expression {
  self.expression
}

///|
pub fn BasicShape::to_css(self : BasicShape) -> String {
  self.expression.to_css()
}

///|
pub fn BasicShape::in_box(self : BasicShape, box : GeometryBox) -> BasicShape {
  if self.expression.category == "shape-with-box" ||
    self.expression.category == "geometry-box" {
    return {
      expression: invalid_expression(
        "STYLE_SHAPE_BOX", "A shape accepts only one geometry box",
      ),
    }
  }
  {
    expression: group_expression(
      "shape-with-box",
      "",
      [self.expression, literal_expression(box.to_css(), "geometry-box")],
      " ",
      "",
    ),
  }
}

///|
pub fn geometry_box(box : GeometryBox) -> BasicShape {
  { expression: literal_expression(box.to_css(), "geometry-box"), }
}

///|
pub fn[X : LengthPercentageMathInput, Y : LengthPercentageMathInput] ellipse_shape(
  x : X,
  y : Y,
  center~ : GradientPosition = gradient_center(),
) -> BasicShape {
  {
    expression: group_expression(
      "shape",
      "ellipse(",
      [
        x.length_percentage_math().expression.constrain("nonnegative"),
        y.length_percentage_math().expression.constrain("nonnegative"),
        literal_expression("at", "keyword"),
        center.expression(),
      ],
      " ",
      ")",
    ),
  }
}

///|
pub fn[R : LengthPercentageMathInput] circle_shape(
  radius : R,
  center~ : GradientPosition = gradient_center(),
) -> BasicShape {
  {
    expression: group_expression(
      "shape",
      "circle(",
      [
        radius.length_percentage_math().expression.constrain("nonnegative"),
        literal_expression("at", "keyword"),
        center.expression(),
      ],
      " ",
      ")",
    ),
  }
}

///|
pub fn inset_shape(
  offsets : Array[MathExpr[LengthPercentage]],
  radius? : MathExpr[LengthPercentage],
) -> BasicShape {
  let values = offsets.map(value => value.expression)
  if offsets.is_empty() || offsets.length() > 4 {
    values.push(
      invalid_expression(
        "STYLE_SHAPE_INSET", "Inset requires one to four offsets",
      ),
    )
  }
  if radius is Some(value) {
    values.push(literal_expression("round", "keyword"))
    values.push(value.expression.constrain("nonnegative"))
  }
  { expression: group_expression("shape", "inset(", values, " ", ")"), }
}

///|
pub fn polygon_shape(
  points : Array[GradientPosition],
  fill~ : FillRule = Nonzero,
) -> BasicShape {
  let values = [literal_expression(fill.css(), "fill-rule")]
  for point in points {
    values.push(point.expression())
  }
  if points.length() < 3 {
    values.push(
      invalid_expression(
        "STYLE_SHAPE_POLYGON", "Polygon requires at least three points",
      ),
    )
  }
  { expression: group_expression("shape", "polygon(", values, ", ", ")"), }
}

///|
pub fn path_shape(path : PathData, fill~ : FillRule = Nonzero) -> BasicShape {
  {
    expression: literal_expression(
      "path(" + fill.css() + ", " + quoted_css_string(path.data, "path") + ")",
      "shape",
    ),
  }
}