

let
    observerMap = new Map()
    nextCheckMap = new Map()
    changedObjects = new Map()

global.observerMap = observerMap

const
    clone = (object, properties) ->
        if properties?
            return {}
                for key of properties
                    [key]: object[key]
        else
            return {}
                for key, value of object
                    [key]: value
    getChanges = (oldValue, newValue, properties) ->
        let changes = null
        let change = (type, name, oldValue, object) ->
            changes ?= []
            changes.push({type,name,oldValue,object})
            # console.log('change', type, name)
        let checkForChange = (property) ->
            # we only check add/delete on plain old javascript objects
            if newValue.constructor is Object
                if oldValue.hasOwnProperty(name)
                    let oldPropertyValue = oldValue[name]
                    if not newValue.hasOwnProperty(name)
                        if oldPropertyValue isnt undefined
                            change("delete", name, oldPropertyValue, newValue)
                    else
                        let newPropertyValue = newValue[name]
                        # must use Object.is because NaN != NaN
                        if not Object.is(newPropertyValue, oldPropertyValue)
                            change("update", name, oldPropertyValue, newValue)
                else if newValue.hasOwnProperty(name)
                    change("add", name, undefined, newValue)
            else
                # for everything else, we only check current property values
                let oldPropertyValue = oldValue[name]
                let newPropertyValue = newValue[name]
                # must use Object.is because NaN != NaN
                if not Object.is(newPropertyValue, oldPropertyValue)
                    change("update", name, oldPropertyValue, newValue)

        if properties?
            for name of properties
                checkForChange(name)
        else
            for name of oldValue
                checkForChange(name)
            for name of newValue
                if not oldValue.hasOwnProperty(name)
                    checkForChange(name)

        return changes

export const
    # edge is of type [object,object] and if present is used for topologically sorting
    observe = (object, callback, property, edge) ->
        # do not observe frozen objects... they never change
        if Object.isFrozen(object)
            return
        # console.log('+observe', property)
        let meta = observerMap.get(object)
        if not meta?
            meta =
                object: object
                properties: {}
                all: 0
                clone: clone(object, property ? {[property]:0} : null)
                callbacks: []
            observerMap.set(object, meta)
        if property?
            meta.properties[property] ?= 0
            meta.properties[property]++
        else
            meta.all++
        meta.callbacks.push(callback)
    unobserve = (object, callback, property) ->
        # console.log('-unobserve', object, callback)
        let meta = observerMap.get(object)
        if meta?
            meta.callbacks.remove(callback)
            if meta.callbacks.length is 0
                # remove no longer observed objects
                observerMap.delete(object)
            if property?
                meta.properties[property]--
                if meta.properties[property] is 0
                    delete meta.properties[property]
            else
                meta.all--
    nextCheck = (fn) ->
        # console.log('++++ nextCheck ============================')
        nextCheckMap.set(fn, fn)
    changed = (obj) ->
        for object in arguments
            # console.log('changed ' + JSON.stringify(object))
            changedObjects.set(object, object)
    checkForChanges = (source) ->
        # console.time('sync')
        # console.trace('sync ' + source)
        let changes
        let maxCycles = 100
        # we have to run multiple cycles in case callbacks cause further change propagation
        for let i = 0; i < maxCycles; i++
            # console.log('-------- check for changes ' + i + ' ' + forceAll + ' ' + changedObjects.size + ' ' + nextCheckMap.size)
            # traverse all objects and find changes
            let changeCount = 0
            let check = (meta) ->
                let properties = meta.all > 0 ? null : meta.properties
                changes = getChanges(meta.clone, meta.object, properties)
                if changes?
                    changeCount++
                    # console.log(changes)
                    meta.clone = clone(meta.object, properties)
                    for callback in meta.callbacks.slice(0) # SLICE IS IMPORTANT! callbacks could otherwise be modified during operation
                        callback(changes)

            # observerMap.forEach(check)
            let metas = Array.from(observerMap.values())
            metas.reverse()
            metas.forEach(check)

            #   then call all nextChecks.
            let pendingChecks = nextCheckMap.size # this and it's use before quitting fixes where we were missing some pending changes
            let currentCheckMap = nextCheckMap
            nextCheckMap = new Map()

            currentCheckMap.forEach(
                (callback) ->
                    callback()
            )
            currentCheckMap.clear()

            if changeCount is 0 and pendingChecks is 0
                # console.log('loops: ' + i)
                # console.timeEnd('sync')
                return


        # we have hit max cycles, indicates a circular dependency error
        throw new Error("Circular Object.observe dependency")

    test = ->
        let object =
            a: 1
            b:
                c: 2
                d: 3
        let changes
        let handler = (c) ->
            changes = c
        observe(object, handler)
        object.a = 2
        delete object.b
        object.c = 5
        changed(object)
        checkForChanges()
        assert JSON.stringify(changes) is JSON.stringify([{"type":"update","name":"a","oldValue":1,"object":{"a":2,"c":5}},{"type":"delete","name":"b","oldValue":{"c":2,"d":3},"object":{"a":2,"c":5}},{"type":"add","name":"c","object":{"a":2,"c":5}}])
        unobserve(object, handler)

