
require:
   .util ->
      tuck, identify, next-id
      ID, FORKID, PROTOID
      VERSION, SOURCE, DIRECTORY
      methods, fields

provide:
   Patch, Tracks
   proxy, deprox, reprox
   object-proxy, array-proxy
   read-proxy, write-proxy
   setup-proxy



class Patch:
   constructor{@data = {=}} =
      pass
   _data{} = @data
   get{id} =
      @data[id] or= {=}
   get-for{obj} =
      if not obj: return null
      id = obj[ID]
      id and [@data[id] or= {=}]
   register{obj, key, fn} =
      patch = @get-for{obj}
      if patch:
         patch[key] = fn{patch[key]}
   shift{new-data = {=}} =
      {expr-value, @data} = {@data, new-data}
   list{} =
      items{@data}
   empty{} =
      return keys{@data}.length === 0


class Tracks:
   constructor{@data = {=}} =
      pass
   _data{} =
      @data
   get{id} =
      id and [@data[id] or= {=}]
   get-for{obj} =
      if not obj: return null
      id = obj[ID]
      id and [@data[id] or= {=}]
   register{obj, key, fn} =
      tracks = @get-for{obj}
      if tracks:
         tracks[key] = fn{tracks[key]}
   list{} =
      items{@data}
   empty{} =
      return keys{@data}.length === 0



proxy{match value, tracks, patch, prevent-dirty-reads = true} =
   null? or undefined? or Number? or String? or Symbol? or Boolean? ->
      value
   {^methods.proxy => Function?} ->
      deprox{value}[methods.proxy]{tracks, patch, prevent-dirty-reads}
   Array? ->
      array-proxy{deprox{value}, tracks, patch, prevent-dirty-reads}
   else ->
      object-proxy{deprox{value}, tracks, patch, prevent-dirty-reads}

deprox{x} =
   if x and x[SOURCE]:
      x[SOURCE]
   else:
      x

reprox{match value, tracks, patch} =
   null? or undefined? or Number? or String? or Symbol? or Boolean? ->
      value
   {^SOURCE => true? obj} ->
      proxy{obj, tracks or value[fields.tracks], patch or value[fields.patch]}
   else ->
      proxy{value, tracks, patch}


read-proxy{value, tracks} =
   proxy{value, tracks, null}

write-proxy{value, patch} =
   proxy{value, null, patch}


setup-proxy{self, obj, tracks, patch, prevent-dirty-reads} =

   identify{obj}

   self[fields.obj] = obj
   self[fields.tracks] = tracks
   self[fields.patch] = patch
   self[fields.prevent-dirty-reads] = prevent-dirty-reads

   if tracks: tracks[DIRECTORY] or= obj[DIRECTORY]
   if patch:  patch[DIRECTORY]  or= obj[DIRECTORY]

   id = obj[ID]

   self[methods.register-tracks]{key, fn} =
      let tracks = self[fields.tracks]
      if tracks:
         tracks.register{self, key, fn}

   self[methods.register-patch]{key, fn} =
      let patch = self[fields.patch]
      if patch:
         patch.register{self, key, fn}

   self[methods.check-dirty-read]{key, fn = [-> true]} =
      obj-patch = patch and patch.get-for{obj}
      if obj-patch
         \ and self[fields.prevent-dirty-reads]
         \ and obj-patch[key] and fn{obj-patch[key]}:
         throw E.invalid_access with
            '''Cannot access '{key}' after setting it.'''

   tuck{self, SOURCE, obj}
   tuck{self, ID, obj[ID]}
   tuck{self, VERSION, obj[VERSION]}


basic-object-proxy{obj, tracks, patch
                   prevent-dirty-reads = true} =
   self = this
   setup-proxy{self, obj, tracks, patch, prevent-dirty-reads}
   Object.prevent-extensions{self}

basic-get{name} =
   self = this
   self[methods.check-dirty-read]{name}
   self[methods.register-tracks]{name, -> true}
   proxy{self[fields.obj][name]
         self[fields.tracks]
         self[fields.patch]
         self[fields.prevent-dirty-reads]}

basic-set{name, new-value} =
   self = this
   self[methods.register-patch]{name, -> #update{deprox{new-value}}}


proxy-proto-cache = {=}

proxy-proto-for{obj} =
   orig-proto = Object.get-prototype-of{obj}
   pid = match orig-proto:
      null? or undefined? -> null
      when Object.has-own-property.call{orig-proto, PROTOID} ->
         orig-proto[PROTOID]
      else ->
         let pid = next-id{}
         tuck{orig-proto, PROTOID, pid}
         pid

   the-keys = keys{obj}.sort{}
   key = '{pid}\x01{the-keys.join{"\x00"}}'
   match proxy-proto-cache[key]:
      undefined? ->
         proto = Object.create{orig-proto}
         the-keys each match name ->
            R"^__"? ->
               Object.define-property{proto, name} with {
                  get{} = this[fields.obj][name]
                  enumerable = true
               }
            else ->
               Object.define-property{proto, name} with {
                  get{} = this._get{name}
                  set{value} = this._set{name, value}
                  enumerable = true
               }
         proto._get = basic-get
         proto._set = basic-set
         proxy-proto-cache[key] = proto
         proto
      proto ->
         proto

object-proxy{obj, tracks, patch, var prevent-dirty-reads = true} =
   p = proxy-proto-for{obj}
   basic-object-proxy.call{Object.create{p}, obj, tracks, patch, prevent-dirty-reads}



class array-proxy:

   constructor{obj, tracks, patch, prevent-dirty-reads = true} =
      setup-proxy{@, obj, tracks, patch, prevent-dirty-reads}

   _update-iter{match, i, j, stick-to-end = false} =
      undefined? ->
         {i, j, stick-to-end}
      {i2, j2} ->
         {Math.min{i, i2}, Math.max{j, j2}, stick-to-end}
      else ->
         {i, j, stick-to-end}

   ;; GETTERS

   get{i} =
      self[methods.check-dirty-read]{i}
      self[methods.register-tracks]{i, -> true}
      proxy{self[fields.obj][i]
            self[fields.tracks]
            self[fields.patch]
            self[fields.prevent-dirty-reads]}

   slice{i = 0, var j = null} =
      stick = if{j === null, [j = self[fields.obj].length; true], false}
      self[methods.register-tracks]{"@iter"} with prev ->
         @_update-iter{prev, i, j, stick}

      self[fields.obj].slice{i, j} each* x ->
         proxy{x
               self[fields.tracks]
               self[fields.patch]
               self[fields.prevent-dirty-reads]}

   [^Symbol.iterator]{} =
      proxies = self[fields.obj] each x ->
         proxy{x
               self[fields.tracks]
               self[fields.patch]
               self[fields.prevent-dirty-reads]}

      self[methods.register-tracks]{"@iter", -> {0, self[fields.obj].length, true}}

      proxies[Symbol.iterator]{}

   for-each{fn} =
      [@] each x -> fn{x}
      undefined

   index-of{deprox! elem} =
      self[methods.register-tracks]{"@iter", -> {0, self[fields.obj].length, true}}
      self[fields.obj].index-of{elem}

   map{fn} =
      [@] each x -> fn{x}

   join{sep} =
      [[@] each x -> x].join{sep}

   to-string{} =
      '\{{@join{", "}}\}'

   ;; SETTERS

   set{i, new-value} =
      @_splice{i, 1, {new-value}, false}

   remove{deprox! elem} =
      idx = self[fields.obj].index-of{elem}
      if idx !== -1:
         @splice{idx, 1}

   pop{n = 1} =
      @_splice{self[fields.obj].length - n, n, {}, false}
   push{*values} =
      @_splice{self[fields.obj].length, 0, values, .append}

   shift{n = 1} =
      @_splice{0, n, {}, false}
   unshift{*values} =
      @_splice{0, 0, values, .prepend}

   .splice{i, n, *values} =
      @_splice{i, n, values, false}

   replace{new-array} =
      @_splice{0, self[fields.obj].length, new-array, false}

   ._splice{i, n, values, pos = false} =
      emsg = 'Inconsistent splicing (this may happen if you modify'
         \ + ' the array more than once at the same place)'

      self[methods.register-patch]{"@splice"} with var d ->
         d or= Array{self[fields.obj].length + 1}
         match d[i]:
            === true ->
               throw E.splice{emsg}
            undefined? ->
               d[i] = {n, values}
            {n2, previous-values} when n == 0 or n2 == 0 ->
               d[i] = {Math.max{n, n2}, new-values} where new-values =
                  match pos:
                     .prepend -> values ++ previous-values
                     .append -> previous-values ++ values
                     else -> throw E.splice{emsg}
            else ->
               throw E.splice{emsg}
         1..[n - 1] each j ->
            match d[i + j]:
               undefined? ->
                  d[i + j] = true
               else ->
                  throw E.splice{emsg}
         d

Object.define-property{array-proxy.prototype, .length} with {
   .get{} =
      this[methods.check-dirty-read]{.length}
      this[methods.register-tracks]{"length", -> true}
      proxy{this[fields.obj].length
            this[fields.tracks]
            this[fields.patch]
            this[fields.prevent-dirty-reads]}
}

