//
// Copyright (c) 2016, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   8 Mar 2016  Andy Frank  Creation
//

using concurrent
using dom
using graphics

**
** DomListener monitors the DOM and invokes callbacks when modifications occur.
**
** DomListener works by registering a global
** [MutationObserver]`dom::MutationObserver` on the 'body' tag and collects
** all 'childList' events for his subtree.  All mutation events are queued and
** processed on a [reqAnimationFrame]`dom::Win.reqAnimationFrame`.  Registered
** nodes are held with weak references, and will be garbage collected when out
** of scope.
**
@Js class DomListener
{
  static DomListener cur()
  {
    r := Actor.locals["domkit.DomListener"] as DomListener
    if (r == null) Actor.locals["domkit.DomListener"] = r = DomListener()
    return r
  }

  ** Private ctor.
  private new make()
  {
    this.observer = MutationObserver() |recs| { checkMutations.addAll(recs) }
    this.observer.observe(Win.cur.doc.body, ["childList":true, "subtree":true])
    reqCheck
  }

  ** Request callback when target node is mounted into document.
  Void onMount(Elem target, |Elem| f)
  {
    DomState state := map.get(target) ?: DomState()
    state.onMount = f
    map.set(target, state)
  }

  ** Request callback when target node is unmounted from document.
  Void onUnmount(Elem target, |Elem| f)
  {
    DomState state := map.get(target) ?: DomState()
    state.onUnmount = f
    map.set(target, state)
  }

  ** Request callback when target node size has changed.
  Void onResize(Elem target, |Elem| f)
  {
    DomState state := map.get(target) ?: DomState()
    state.onResize = f
    map.set(target, state)
  }

  ** Request check callback.
  private Void reqCheck()
  {
    Win.cur.reqAnimationFrame |->| { onCheck }
  }

  ** Callback to check elements.
  private Void onCheck()
  {
    try
    {
      // throttle checks
      nowTicks := Duration.nowTicks
      if (lastTicks != null && nowTicks-lastTicks < checkFreq) return
      this.lastTicks = nowTicks

      // debug
      // start := Duration.now

      // check mount/unmount
      checkMutations.each |r,i|
      {
        checkState.clear
        r.added.each |e| { findRegNodes(e, checkState) }
        checkState.each |e|
        {
          DomState s := map[e]
          s.fireMount(e)
          mounted[e.hash] = e
        }

        checkState.clear
        r.removed.each |e| { findRegNodes(e, checkState) }
        checkState.each |e|
        {
          DomState s := map[e]
          s.fireUnmount(e)
          mounted.remove(e.hash)
        }
      }

      // make sure we cleanup refs
      checkMutations.clear
      checkState.clear

      // check for resize events
      mounted.each |e|
      {
        DomState s := map[e]
        if (s.onResize != null)
        {
          s.newSize = e.size
          if (s.lastSize == null) s.lastSize = s.newSize
          if (s.lastSize != s.newSize) s.fireResize(e)
          s.lastSize = s.newSize
        }
      }

      // debug
      // dur := Duration.now - start
      // echo("# DomListener.onCheck [${dur.toMillis}ms]")
    }
    catch (Err err) { err.trace }
    finally
    {
      reqCheck
    }
  }

  ** Walk subtree to find all registered nodes.
  private Void findRegNodes(Elem elem, Elem[] list)
  {
    if (map.has(elem)) list.add(elem)
    elem.children.each |c| { findRegNodes(c, list) }
  }

  private Int checkFreq := 1sec.ticks
  private Int? lastTicks

  private MutationObserver observer
  private WeakMap map := WeakMap()
  private Int:Elem mounted := [:]

  private MutationRec[] checkMutations := [,]
  private Elem[] checkState := [,]
}

**************************************************************************
** DomState
**************************************************************************

@Js internal class DomState
{
  Func? onMount   := null
  Func? onUnmount := null
  Func? onResize  := null

  Size? lastSize
  Size? newSize

  Void fireMount(Elem elem)
  {
    if (mounted) return
    mounted = true
    unmounted = false
    try { onMount?.call(elem) }
    catch (Err err) { err.trace }
  }

  Void fireUnmount(Elem elem)
  {
    if (unmounted) return
    mounted = false
    unmounted = true
    try { onUnmount?.call(elem) }
    catch (Err err) { err.trace }
  }

  Void fireResize(Elem elem)
  {
    try { onResize?.call(elem) }
    catch (Err err) { err.trace }
  }

  private Bool mounted   := false
  private Bool unmounted := true
}