
require:
   "./proxy" ->
      Patch, Tracks
      proxy, deprox, reprox
      read-proxy, write-proxy
   .iter ->
      iterate-properties
   .relevant ->
      intersect
   .directory ->
      Directory
   .util ->
      tuck, identify, fork-identify
      ID, FORKID, VERSION, DIRECTORY, CACHE
      fields
   .root ->
      Root

provide:
   Reactive, reactive-function
   args-key
   System, with-system, current-system

   State, DependentState, reactive-function2
   transact-all


arg-key{match x} =
   String? ->
      "S" + x
   null? or undefined? or Number? or Boolean? ->
      "P" + String{x}
   deprox! identify! fork-identify! x ->
      "O" + String{x[ID]} + ":" + String{x[FORKID]} ;; or ""}

args-key{args} =
   args.map{arg-key}.join{"\x00"}


get-replacement{match cached} =
   {=> replacement} ->
      get-replacement{replacement}
   else ->
      cached


class CachedResult:

   constructor{@reactive, @self, @args, @value, @deps, @version} =
      @children = {}
      @walk{@value}

   walk{identify! v} =
      iterate-properties{v} each {k, match obj, gen-patch, ignore-children = false} ->
         {^CACHE => cached} when cached ->
            @children.push with {k, v[ID], cached, gen-patch}
         when ignore-children ->
            pass
         else ->
            @walk{obj}
      undefined

   transform-patch-children{the-patch, new-patch, dir} =
      {children, @children} = {@children, {}}
      children each {k, vid, orig-cached and get-replacement! cached, gen-patch} ->
         if not cached.transform-patch{the-patch, new-patch, dir}:
            new-cached = cached.reactive.render{cached.self, cached.args, true}
            cached.replacement = new-cached
            new-cached.transform-patch-children{the-patch, new-patch, dir}
            gen-patch{new-patch, new-cached.value}
            @children.push with {k, vid, new-cached, gen-patch}
         elif orig-cached !== cached:
            gen-patch{new-patch, cached.value}
            @children.push with {k, vid, cached, gen-patch}
         else:
            @children.push with {k, vid, cached, gen-patch}

   transform-patch{the-patch, new-patch, dir and {version => match}} =
      == @version or when not @version ->
         @transform-patch-children{the-patch, new-patch, dir}
         true
      == [@version + 1] ->
         dirty = @deps and intersect{@deps, the-patch, dir}
         if dirty:
            false
         else:
            @transform-patch-children{the-patch, new-patch, dir}
            @version += 1
            true
      else ->
         false


class Reactive:

   constructor{@renderfn} =
      @argmap = {=}

   render{the-self, args, force = false} =
      key = args-key{{the-self} ++ args}
      cached =
         match @argmap[key]:
            undefined? or [== true is force] ->
               @render-cache-object{the-self, args}
            cached ->
               get-replacement{cached}
      @argmap[key] = cached
      cached

   render-cache-object{the-self, args} =
      deps = Tracks{} ;;{=}
      ;; pargs = args each deprox! arg -> read-proxy{arg, deps}
      pargs = args each arg -> reprox{arg, deps, null}
      self-prox = reprox{the-self, deps, null}
      {=> version = null} = deps[DIRECTORY]
      res = identify{@renderfn.apply{self-prox, pargs}}
      cached = CachedResult{@, the-self, args, res, deps, version}
      tuck{res, CACHE, cached}
      cached




system-stack = {}

with-system{sys, fn} =
   system-stack.unshift{sys}
   result = fn{}
   system-stack.shift{}
   result

current-system{} =
   system-stack[0]


class System:

   constructor{@origin, @render, @options = {=}} =
      identify{@}
      fork-identify{@}

      {clobber-patch => @clobber-patch = true
       action => @action = {x} -> x} = @options

      @patch-stack = {}
      @patch = Patch{}

      @bdir = Directory{}
      @basis = @bdir.acquire-copy{@origin}
      @basis.__SYSTEM = [@]
      @basis-proxy = proxy{@basis, null, @patch}

      @vdir = Directory{clobber-patch = @clobber-patch}

      rend = with-system{@, -> @render{@basis-proxy}}
      @value-root = CachedResult{null, null, null, r, null} where
         r = @vdir.acquire with
            Root{rend, @action, true}

   get{} =
      @value-root.value.root

   model{} =
      @basis-proxy

   push{} =
      @patch-stack.push with @patch.shift{}

   commit{} =
      @push{}
      {stack, @patch-stack} = {@patch-stack, {}}
      stack each patch ->
         @apply-patch{Patch{patch}}

   apply-patch{the-patch} =
      @bdir.patch{the-patch}
      new-patch = Patch{} ;; {=}
      with-system{@} with ->
         @value-root.transform-patch{the-patch, new-patch, @bdir}
      @vdir.patch{new-patch}

   transact{fn, Array! roots = {@basis}} =
      @push{}
      proxies = roots each root -> write-proxy{root, @patch}
      fn.apply{this, proxies}
      @commit{}


reactive-function{render} =
   r = Reactive{render}
   rval{*args} =
      system = current-system{}
      the-self = system.model{}
      r.render{the-self, args}.value
   rval.orig = render
   rval.reactive = r
   rval









class State:

   constructor{@origin, @options = {=}} =
      {clobber-patch => @clobber-patch = false} = @options
      @listeners = {}

      @patch-stack = {}
      @patch = Patch{}

      @dir = Directory{clobber-patch = @clobber-patch}
      @basis = @dir.acquire-copy{@origin}
      @basis-proxy = proxy{@basis, null, @patch}
      @patch.shift{}

      State.all.push{@}

   add-listener{listener} =
      @listeners.push with listener

   get{} =
      @basis

   model{} =
      @basis-proxy

   commit{} =
      ;; if @patch.empty{}: return false
      patch = Patch{@patch.shift{}}
      @dir.patch{patch}
      @listeners each listener ->
         listener{@model{}, patch}
      true

   transact{fn} =
      ;; if not @patch.empty{}:
      ;;    throw E.dirty{"There are uncommitted changes for this State."}
      fn{@model{}}
      @commit{}


State.all = {}


class DependentState < State:

   constructor{@input, @render, @options = {=}} =

      {clobber-patch => @clobber-patch = true
       action => @action = {x} -> x} = @options
      @listeners = {}

      @patch-stack = {}
      @patch = Patch{}

      @dir = Directory{clobber-patch = @clobber-patch}

      @compute{}

      @input.add-listener{@listen.bind{@}}

      State.all.push{@}

   compute{} =
      @_previous-input = @input.get{}
      rend = with-state{@, -> @render.call{null, @input.model{}}}
      r = @dir.acquire with
         Root{rend, @action, true}
      @basis = CachedResult{null, null, null, r, null}

   get{} =
      @basis.value.root

   model{} =
      proxy{@basis.value.root, null, @patch}

   listen{model, patch} =
      ;; if not @patch.empty{}:
      ;;    throw E.dirty{"There are uncommitted changes for this State."}
      if @_previous-input !== @input.get{}:
         @compute{}
      with-state{@} with ->
         @basis.transform-patch{patch, @patch, @input.dir}
      @commit{}


reactive-function2{render} =
   r = Reactive{render}
   rval{*args} =
      state = current-state{}
      the-self =
         if this === undefined or this === null:
            state??.input??.model??{}
         else:
            this
      r.render{the-self, args}.value
   rval.orig = render
   rval.reactive = r
   rval.force{*args} =
      state = current-state{}
      the-self = state??.input??.model??{}
      r.render{the-self, args, true}.value
   rval


state-stack = {}

with-state{sys, fn} =
   state-stack.unshift{sys}
   result = fn{}
   state-stack.shift{}
   result

current-state{} =
   state-stack[0]

var is-transaction = false

transact-all{fn} =
   if is-transaction:
      throw E.transaction.in-progress{"There is already a transaction in progress."}
   is-transaction = true
   try:
      fn{}
      State.all each state ->
         state.commit{}
   finally:
      is-transaction = false
