///|
using @cirru {type Cirru}

///|
/// coord of on virtual dom
pub(all) enum RespoCoord {
  Key(RespoIndexKey)
  Comp(String)
}

///|
pub impl Show for RespoCoord with output(self, logger) -> Unit {
  match self {
    Key(key) => logger.write_string("(Key " + key.to_string() + ")")
    Comp(comp) => logger.write_string("(Comp " + comp + ")")
  }
}

///|
/// Diff/patch for virtual DOM, note that children are not included in this type
enum DomChange[T, G] {
  ReplaceElement(
    coord~ : @immut/vector.Vector[RespoCoord],
    dom_path~ : @immut/vector.Vector[UInt],
    node~ : RespoNode[T, G]
  )
  ModifyChildren(
    coord~ : @immut/vector.Vector[RespoCoord],
    dom_path~ : @immut/vector.Vector[UInt],
    operations~ : Array[ChildDomOp[T, G]]
  )
  ModifyAttrs(
    coord~ : @immut/vector.Vector[RespoCoord],
    dom_path~ : @immut/vector.Vector[UInt],
    set~ : Map[String, String],
    unset~ : @hashset.HashSet[String]
  )
  ModifyStyle(
    coord~ : @immut/vector.Vector[RespoCoord],
    dom_path~ : @immut/vector.Vector[UInt],
    set~ : Map[String, String],
    unset~ : @hashset.HashSet[String]
  )
  ModifyEvent(
    coord~ : @immut/vector.Vector[RespoCoord],
    dom_path~ : @immut/vector.Vector[UInt],
    add~ : @hashset.HashSet[RespoEventType],
    remove~ : @hashset.HashSet[RespoEventType]
  )
  Effect(
    coord~ : @immut/vector.Vector[RespoCoord],
    dom_path~ : @immut/vector.Vector[UInt],
    effect_type~ : RespoEffectType,
    skip_indexes~ : @hashset.HashSet[Int]
  )
}

///|
pub impl[T, G] Show for DomChange[T, G] with output(self, logger) {
  logger.write_string(self.to_cirru().to_string())
}

///|
pub fn[T, G] DomChange::to_cirru(self : DomChange[T, G]) -> Cirru {
  match self {
    Effect(coord~, dom_path~, effect_type~, skip_indexes~) =>
      List([
        Leaf("::effect"),
        effect_type.to_cirru(),
        coord_path_to_cirru(coord),
        dom_path_to_cirru(dom_path),
        indexes_to_cirru(skip_indexes),
      ])
    ReplaceElement(coord~, dom_path~, node~) =>
      List([
        Leaf("::replace-element"),
        coord_path_to_cirru(coord),
        dom_path_to_cirru(dom_path),
        node.to_cirru(),
      ])
    ModifyChildren(coord~, dom_path~, operations~) =>
      List([
        Leaf("::modify-children"),
        coord_path_to_cirru(coord),
        dom_path_to_cirru(dom_path),
        List(operations.map(fn(x) { x.to_cirru() })),
      ])
    ModifyAttrs(coord~, dom_path~, set~, unset~) => {
      let set_data : Array[Cirru] = []
      for pair in set {
        let (key, value) = pair
        set_data.push(List([Leaf(key), Leaf(value)]))
      }
      let unset_data : Array[Cirru] = []
      for key in unset {
        unset_data.push(Leaf(key))
      }
      List([
        Leaf("::modify-attrs"),
        coord_path_to_cirru(coord),
        dom_path_to_cirru(dom_path),
        List([List(set_data), List(unset_data)]),
      ])
    }
    ModifyStyle(coord~, dom_path~, set~, unset~) => {
      let set_data : Array[Cirru] = []
      for pair in set {
        let (key, value) = pair
        set_data.push(List([Leaf(key), Leaf(value)]))
      }
      let unset_data : Array[Cirru] = []
      for key in unset {
        unset_data.push(Leaf(key))
      }
      List([
        Leaf("::modify-style"),
        coord_path_to_cirru(coord),
        dom_path_to_cirru(dom_path),
        List([List(set_data), List(unset_data)]),
      ])
    }
    ModifyEvent(coord~, dom_path~, add~, remove~) => {
      let add_data : Array[Cirru] = []
      for event in add {
        add_data.push(event.to_cirru())
      }
      let remove_data : Array[Cirru] = []
      for event in remove {
        remove_data.push(event.to_cirru())
      }
      List([
        Leaf("::modify-event"),
        coord_path_to_cirru(coord),
        dom_path_to_cirru(dom_path),
        List([List(add_data), List(remove_data)]),
      ])
    }
  }
}

///|
enum ChildDomOp[T, G] {
  InsertAfter(UInt, RespoIndexKey, RespoNode[T, G])
  RemoveAt(UInt)
  Append(RespoIndexKey, RespoNode[T, G])
  Prepend(RespoIndexKey, RespoNode[T, G])
  NestedEffect(
    nested_coord~ : @immut/vector.Vector[RespoCoord],
    nested_dom_path~ : @immut/vector.Vector[UInt],
    effect_type~ : RespoEffectType,
    skip_indexes~ : @hashset.HashSet[Int]
  )
}

///|
pub fn[T, G] ChildDomOp::to_cirru(self : ChildDomOp[T, G]) -> Cirru {
  match self {
    InsertAfter(index, key, node) =>
      List([
        Leaf("::insert-after"),
        Leaf(index.to_string()),
        key.to_cirru(),
        node.to_cirru(),
      ])
    RemoveAt(index) => List([Leaf("::remove-at"), Leaf(index.to_string())])
    Append(key, node) =>
      List([Leaf("::append"), key.to_cirru(), node.to_cirru()])
    Prepend(key, node) =>
      List([Leaf("::prepend"), key.to_cirru(), node.to_cirru()])
    NestedEffect(nested_coord~, nested_dom_path~, effect_type~, skip_indexes~) =>
      List([
        Leaf("::nested-effect"),
        coord_path_to_cirru(nested_coord),
        dom_path_to_cirru(nested_dom_path),
        effect_type.to_cirru(),
        indexes_to_cirru(skip_indexes),
      ])
  }
}

///|
pub fn[T, G] DomChange::get_dom_path(
  self : DomChange[T, G],
) -> @immut/vector.Vector[UInt] {
  match self {
    ReplaceElement(dom_path~, ..) => dom_path
    ModifyChildren(dom_path~, ..) => dom_path
    ModifyAttrs(dom_path~, ..) => dom_path
    ModifyStyle(dom_path~, ..) => dom_path
    ModifyEvent(dom_path~, ..) => dom_path
    Effect(dom_path~, ..) => dom_path
  }
}

///|
pub fn RespoCoord::to_cirru(self : RespoCoord) -> Cirru {
  match self {
    Key(key) => Leaf(key.to_string())
    Comp(comp) => List([Leaf("::Comp"), Leaf(comp)])
  }
}

///|
pub fn coord_path_to_cirru(coord : @immut/vector.Vector[RespoCoord]) -> Cirru {
  let items = coord.map(fn(x) { x.to_cirru() })
  List(items.to_array())
}

///|
pub fn dom_path_to_cirru(dom_path : @immut/vector.Vector[UInt]) -> Cirru {
  let items = dom_path.map(fn(x) { Cirru::Leaf(x.to_string()) })
  List(items.to_array())
}

///|
/// turn @hashset.T[Int] to Cirru
pub fn indexes_to_cirru(xs : @hashset.HashSet[Int]) -> Cirru {
  let items : Array[Cirru] = []
  for x in xs {
    items.push(Leaf(x.to_string()))
  }
  List(items)
}