///|
pub(all) struct Point {
  x : Double
  y : Double
} derive(Eq, Debug, ToJson)

///|
pub(all) struct Size {
  width : Double
  height : Double
} derive(Eq, Debug, ToJson)

///|
pub(all) struct Rect {
  origin : Point
  size : Size
} derive(Eq, Debug, ToJson)

///|
pub(all) struct Insets {
  top : Double
  right : Double
  bottom : Double
  left : Double
} derive(Eq, Debug, ToJson)

///|
pub(all) struct Constraints {
  min : Size
  max : Size
} derive(Eq, Debug, ToJson)

///|
pub(all) enum Axis {
  Horizontal
  Vertical
} derive(Eq, Debug, ToJson)

///|
pub fn Point::new(x~ : Double, y~ : Double) -> Point {
  { x, y }
}

///|
pub fn Size::new(width~ : Double, height~ : Double) -> Size {
  { width, height }
}

///|
pub fn Rect::new(
  x~ : Double,
  y~ : Double,
  width~ : Double,
  height~ : Double,
) -> Rect {
  { origin: Point::new(x~, y~), size: Size::new(width~, height~) }
}

///|
pub fn Insets::all(value : Double) -> Insets {
  { top: value, right: value, bottom: value, left: value }
}

///|
pub fn Insets::symmetric(
  horizontal? : Double = 0.0,
  vertical? : Double = 0.0,
) -> Insets {
  { top: vertical, right: horizontal, bottom: vertical, left: horizontal }
}

///|
pub fn Constraints::tight(size : Size) -> Constraints {
  { min: size, max: size }
}

///|
pub fn Constraints::loose(max : Size) -> Constraints {
  { min: Size::new(width=0.0, height=0.0), max }
}

///|
pub fn Constraints::unbounded() -> Constraints {
  {
    min: Size::new(width=0.0, height=0.0),
    max: Size::new(width=1.0e9, height=1.0e9),
  }
}

///|
pub fn Constraints::is_tight(self : Constraints) -> Bool {
  self.min == self.max
}

///|
pub fn Constraints::loosen(self : Constraints) -> Constraints {
  { min: Size::new(width=0.0, height=0.0), max: self.max }
}

///|
pub fn Constraints::tighten(
  self : Constraints,
  width? : Double,
  height? : Double,
) -> Constraints {
  let min_width = width.unwrap_or(self.min.width)
  let max_width = width.unwrap_or(self.max.width)
  let min_height = height.unwrap_or(self.min.height)
  let max_height = height.unwrap_or(self.max.height)
  {
    min: Size::new(
      width=clamp_double(min_width, self.min.width, self.max.width),
      height=clamp_double(min_height, self.min.height, self.max.height),
    ),
    max: Size::new(
      width=clamp_double(max_width, self.min.width, self.max.width),
      height=clamp_double(max_height, self.min.height, self.max.height),
    ),
  }
}

///|
pub fn Constraints::constrain(self : Constraints, size : Size) -> Size {
  size.clamp(self)
}

///|
pub fn Size::clamp(self : Size, constraints : Constraints) -> Size {
  {
    width: clamp_double(
      self.width,
      constraints.min.width,
      constraints.max.width,
    ),
    height: clamp_double(
      self.height,
      constraints.min.height,
      constraints.max.height,
    ),
  }
}

///|
pub fn Rect::contains(self : Rect, point : Point) -> Bool {
  point.x >= self.origin.x &&
  point.y >= self.origin.y &&
  point.x <= self.origin.x + self.size.width &&
  point.y <= self.origin.y + self.size.height
}

///|
pub fn Rect::is_empty(self : Rect) -> Bool {
  self.size.width <= 0.0 || self.size.height <= 0.0
}

///|
pub fn Rect::area(self : Rect) -> Double {
  if self.is_empty() {
    0.0
  } else {
    self.size.width * self.size.height
  }
}

///|
pub fn Rect::max_x(self : Rect) -> Double {
  self.origin.x + self.size.width
}

///|
pub fn Rect::max_y(self : Rect) -> Double {
  self.origin.y + self.size.height
}

///|
pub fn Rect::union(self : Rect, other : Rect) -> Rect {
  if self.is_empty() {
    other
  } else if other.is_empty() {
    self
  } else {
    let min_x = self.origin.x.min(other.origin.x)
    let min_y = self.origin.y.min(other.origin.y)
    let max_x = self.max_x().max(other.max_x())
    let max_y = self.max_y().max(other.max_y())
    Rect::new(x=min_x, y=min_y, width=max_x - min_x, height=max_y - min_y)
  }
}

///|
pub fn Rect::intersection(self : Rect, other : Rect) -> Rect? {
  let min_x = self.origin.x.max(other.origin.x)
  let min_y = self.origin.y.max(other.origin.y)
  let max_x = self.max_x().min(other.max_x())
  let max_y = self.max_y().min(other.max_y())
  if max_x <= min_x || max_y <= min_y {
    None
  } else {
    Some(Rect::new(x=min_x, y=min_y, width=max_x - min_x, height=max_y - min_y))
  }
}

///|
pub fn Rect::inflate(self : Rect, value : Double) -> Rect {
  Rect::new(
    x=self.origin.x - value,
    y=self.origin.y - value,
    width=self.size.width + value * 2.0,
    height=self.size.height + value * 2.0,
  )
}

///|
pub fn Rect::clamp_to(self : Rect, bounds : Rect) -> Rect? {
  self.intersection(bounds)
}

///|
pub fn Rect::inset(self : Rect, insets : Insets) -> Rect {
  Rect::new(
    x=self.origin.x + insets.left,
    y=self.origin.y + insets.top,
    width=max_double(0.0, self.size.width - insets.left - insets.right),
    height=max_double(0.0, self.size.height - insets.top - insets.bottom),
  )
}

///|
pub fn Rect::offset(
  self : Rect,
  dx? : Double = 0.0,
  dy? : Double = 0.0,
) -> Rect {
  Rect::new(
    x=self.origin.x + dx,
    y=self.origin.y + dy,
    width=self.size.width,
    height=self.size.height,
  )
}

///|
pub fn Constraints::deflate(self : Constraints, insets : Insets) -> Constraints {
  let horizontal = insets.left + insets.right
  let vertical = insets.top + insets.bottom
  {
    min: Size::new(
      width=max_double(0.0, self.min.width - horizontal),
      height=max_double(0.0, self.min.height - vertical),
    ),
    max: Size::new(
      width=max_double(0.0, self.max.width - horizontal),
      height=max_double(0.0, self.max.height - vertical),
    ),
  }
}

///|
pub fn Size::inflate(self : Size, insets : Insets) -> Size {
  Size::new(
    width=self.width + insets.left + insets.right,
    height=self.height + insets.top + insets.bottom,
  )
}

///|
fn clamp_double(value : Double, min : Double, max : Double) -> Double {
  if value < min {
    min
  } else if value > max {
    max
  } else {
    value
  }
}

///|
fn max_double(a : Double, b : Double) -> Double {
  if a > b {
    a
  } else {
    b
  }
}

///|
fn min_double(a : Double, b : Double) -> Double {
  if a < b {
    a
  } else {
    b
  }
}

///|
pub fn min_int(a : Int, b : Int) -> Int {
  if a < b {
    a
  } else {
    b
  }
}

///|
pub fn max_int(a : Int, b : Int) -> Int {
  if a > b {
    a
  } else {
    b
  }
}

///|
pub fn clamp_int(value : Int, min : Int, max : Int) -> Int {
  if value < min {
    min
  } else if value > max {
    max
  } else {
    value
  }
}