///|
pub(all) struct PaintPropertyChange {
  node_path : String
  node_id : String
  property : String
  before : String?
  after : String?
}

///|
pub(all) struct PaintLayoutShift {
  node_path : String
  node_id : String
  dx : Double
  dy : Double
  dw : Double
  dh : Double
}

///|
pub(all) struct PaintTreeDiff {
  changes : Array[PaintPropertyChange]
  layout_shifts : Array[PaintLayoutShift]
}

///|
priv struct FlatPaintNode {
  path : String
  node : @model.PaintNode
}

///|
pub fn diff_trees(
  before : @model.PaintNode,
  after : @model.PaintNode,
) -> PaintTreeDiff {
  let before_nodes : Array[FlatPaintNode] = []
  let after_nodes : Array[FlatPaintNode] = []
  let before_by_path : Map[String, FlatPaintNode] = {}
  let after_by_path : Map[String, FlatPaintNode] = {}
  collect_flat_nodes(before, before.tag + "[0]", before_nodes, before_by_path)
  collect_flat_nodes(after, after.tag + "[0]", after_nodes, after_by_path)

  let changes : Array[PaintPropertyChange] = []
  let layout_shifts : Array[PaintLayoutShift] = []

  for flat in before_nodes {
    match after_by_path.get(flat.path) {
      Some(other) => compare_nodes(flat, other, changes, layout_shifts)
      None =>
        changes.push({
          node_path: flat.path,
          node_id: flat.node.id,
          property: "node_presence",
          before: Some("present"),
          after: None,
        })
    }
  }

  for flat in after_nodes {
    if before_by_path.get(flat.path) is None {
      changes.push({
        node_path: flat.path,
        node_id: flat.node.id,
        property: "node_presence",
        before: None,
        after: Some("present"),
      })
    }
  }

  { changes, layout_shifts }
}

///|
fn collect_flat_nodes(
  node : @model.PaintNode,
  path : String,
  flat_nodes : Array[FlatPaintNode],
  by_path : Map[String, FlatPaintNode],
) -> Unit {
  let flat = { path, node }
  flat_nodes.push(flat)
  by_path.set(path, flat)
  for i, child in node.children {
    collect_flat_nodes(
      child,
      path + "/" + child.tag + "[" + i.to_string() + "]",
      flat_nodes,
      by_path,
    )
  }
}

///|
fn compare_nodes(
  before : FlatPaintNode,
  after : FlatPaintNode,
  changes : Array[PaintPropertyChange],
  layout_shifts : Array[PaintLayoutShift],
) -> Unit {
  let before_node = before.node
  let after_node = after.node

  if before_node.x != after_node.x ||
    before_node.y != after_node.y ||
    before_node.width != after_node.width ||
    before_node.height != after_node.height {
    layout_shifts.push({
      node_path: before.path,
      node_id: after_node.id,
      dx: after_node.x - before_node.x,
      dy: after_node.y - before_node.y,
      dw: after_node.width - before_node.width,
      dh: after_node.height - before_node.height,
    })
  }

  compare_string_property(
    before.path,
    before_node.id,
    "id",
    before_node.id,
    after_node.id,
    changes,
  )
  compare_string_property(
    before.path,
    before_node.id,
    "tag",
    before_node.tag,
    after_node.tag,
    changes,
  )
  compare_optional_string_property(
    before.path,
    before_node.id,
    "text",
    before_node.text,
    after_node.text,
    changes,
  )
  compare_optional_string_property(
    before.path,
    before_node.id,
    "src",
    before_node.src,
    after_node.src,
    changes,
  )
  compare_double_property(
    before.path,
    before_node.id,
    "scroll_width",
    before_node.scroll_width,
    after_node.scroll_width,
    changes,
  )
  compare_double_property(
    before.path,
    before_node.id,
    "scroll_height",
    before_node.scroll_height,
    after_node.scroll_height,
    changes,
  )
  compare_string_property(
    before.path,
    before_node.id,
    "clip",
    before_node.clip.to_string(),
    after_node.clip.to_string(),
    changes,
  )
  compare_string_property(
    before.path,
    before_node.id,
    "overflow_x",
    overflow_to_string(before_node.overflow_x),
    overflow_to_string(after_node.overflow_x),
    changes,
  )
  compare_string_property(
    before.path,
    before_node.id,
    "overflow_y",
    overflow_to_string(before_node.overflow_y),
    overflow_to_string(after_node.overflow_y),
    changes,
  )
  compare_paint_properties(
    before.path,
    before_node.id,
    before_node.paint,
    after_node.paint,
    changes,
  )
}

///|
fn compare_paint_properties(
  path : String,
  node_id : String,
  before : @model.PaintProperties,
  after : @model.PaintProperties,
  changes : Array[PaintPropertyChange],
) -> Unit {
  compare_string_property(
    path,
    node_id,
    "z_index",
    before.z_index.to_string(),
    after.z_index.to_string(),
    changes,
  )
  compare_string_property(
    path,
    node_id,
    "visibility",
    before.visibility.to_string(),
    after.visibility.to_string(),
    changes,
  )
  compare_string_property(
    path,
    node_id,
    "pointer_events",
    before.pointer_events.to_string(),
    after.pointer_events.to_string(),
    changes,
  )
  compare_double_property(
    path,
    node_id,
    "opacity",
    before.opacity,
    after.opacity,
    changes,
  )
  compare_string_property(
    path,
    node_id,
    "color",
    before.color.to_string(),
    after.color.to_string(),
    changes,
  )
  compare_string_property(
    path,
    node_id,
    "background_color",
    before.background_color.to_string(),
    after.background_color.to_string(),
    changes,
  )
  compare_string_property(
    path,
    node_id,
    "background_image",
    before.background_image.to_string(),
    after.background_image.to_string(),
    changes,
  )
  compare_double_property(
    path,
    node_id,
    "font_size",
    before.font_size,
    after.font_size,
    changes,
  )
  compare_double_property(
    path,
    node_id,
    "line_height",
    before.line_height,
    after.line_height,
    changes,
  )
  compare_double_property(
    path,
    node_id,
    "letter_spacing",
    before.letter_spacing,
    after.letter_spacing,
    changes,
  )
  compare_double_property(
    path,
    node_id,
    "word_spacing",
    before.word_spacing,
    after.word_spacing,
    changes,
  )
  compare_double_property(
    path,
    node_id,
    "font_weight",
    before.font_weight,
    after.font_weight,
    changes,
  )
  compare_bool_property(
    path,
    node_id,
    "is_bold",
    before.is_bold,
    after.is_bold,
    changes,
  )
  compare_string_property(
    path,
    node_id,
    "font_family",
    before.font_family,
    after.font_family,
    changes,
  )
  compare_double_property(
    path,
    node_id,
    "border_top_left_radius",
    before.border_top_left_radius,
    after.border_top_left_radius,
    changes,
  )
  compare_double_property(
    path,
    node_id,
    "border_top_right_radius",
    before.border_top_right_radius,
    after.border_top_right_radius,
    changes,
  )
  compare_double_property(
    path,
    node_id,
    "border_bottom_right_radius",
    before.border_bottom_right_radius,
    after.border_bottom_right_radius,
    changes,
  )
  compare_double_property(
    path,
    node_id,
    "border_bottom_left_radius",
    before.border_bottom_left_radius,
    after.border_bottom_left_radius,
    changes,
  )
  compare_bool_property(
    path,
    node_id,
    "text_decoration_underline",
    before.text_decoration_underline,
    after.text_decoration_underline,
    changes,
  )
  compare_bool_property(
    path,
    node_id,
    "text_decoration_line_through",
    before.text_decoration_line_through,
    after.text_decoration_line_through,
    changes,
  )
  compare_bool_property(
    path,
    node_id,
    "text_decoration_overline",
    before.text_decoration_overline,
    after.text_decoration_overline,
    changes,
  )
  compare_double_property(
    path,
    node_id,
    "border_top_width",
    before.border_top_width,
    after.border_top_width,
    changes,
  )
  compare_double_property(
    path,
    node_id,
    "border_right_width",
    before.border_right_width,
    after.border_right_width,
    changes,
  )
  compare_double_property(
    path,
    node_id,
    "border_bottom_width",
    before.border_bottom_width,
    after.border_bottom_width,
    changes,
  )
  compare_double_property(
    path,
    node_id,
    "border_left_width",
    before.border_left_width,
    after.border_left_width,
    changes,
  )
  compare_string_property(
    path,
    node_id,
    "border_top_color",
    before.border_top_color.to_string(),
    after.border_top_color.to_string(),
    changes,
  )
  compare_string_property(
    path,
    node_id,
    "border_right_color",
    before.border_right_color.to_string(),
    after.border_right_color.to_string(),
    changes,
  )
  compare_string_property(
    path,
    node_id,
    "border_bottom_color",
    before.border_bottom_color.to_string(),
    after.border_bottom_color.to_string(),
    changes,
  )
  compare_string_property(
    path,
    node_id,
    "border_left_color",
    before.border_left_color.to_string(),
    after.border_left_color.to_string(),
    changes,
  )
}

///|
fn compare_bool_property(
  path : String,
  node_id : String,
  property : String,
  before : Bool,
  after : Bool,
  changes : Array[PaintPropertyChange],
) -> Unit {
  if before != after {
    changes.push({
      node_path: path,
      node_id,
      property,
      before: Some(before.to_string()),
      after: Some(after.to_string()),
    })
  }
}

///|
fn compare_double_property(
  path : String,
  node_id : String,
  property : String,
  before : Double,
  after : Double,
  changes : Array[PaintPropertyChange],
) -> Unit {
  if before != after {
    changes.push({
      node_path: path,
      node_id,
      property,
      before: Some(before.to_string()),
      after: Some(after.to_string()),
    })
  }
}

///|
fn compare_string_property(
  path : String,
  node_id : String,
  property : String,
  before : String,
  after : String,
  changes : Array[PaintPropertyChange],
) -> Unit {
  if before != after {
    changes.push({
      node_path: path,
      node_id,
      property,
      before: Some(before),
      after: Some(after),
    })
  }
}

///|
fn compare_optional_string_property(
  path : String,
  node_id : String,
  property : String,
  before : String?,
  after : String?,
  changes : Array[PaintPropertyChange],
) -> Unit {
  if before != after {
    changes.push({ node_path: path, node_id, property, before, after })
  }
}

///|
fn overflow_to_string(value : @types.Overflow) -> String {
  match value {
    @types.Visible => "visible"
    @types.Hidden => "hidden"
    @types.Clip => "clip"
    @types.Scroll => "scroll"
    @types.Auto => "auto"
  }
}