
exports.traverse = (graph, enterCallback, exitCallback) ->
    result = graph
    skip = false
    removed = 0
    context =
        path: []
        ancestors: []
        # skip traversing children of current node?
        skip: -> skip = true
        key: -> @path[@path.length - 1]
        parent: -> @ancestors[@ancestors.length - 1]
        remove: (node) ->
            if not node?
                throw new Error "You must specify the node to remove"
            parent = @parent()
            if Array.isArray parent
                index = parent.indexOf node
                parent.splice index, 1
                removed++
            else
                delete parent[@key()]
        insertAfter: (node) ->
            parent = @parent()
            if not Array.isArray parent
                throw new Error "Parent must be an array"
            parent.splice(Number(@key()) + 1, 0, node)
        replace: (value) ->
            if value is undefined
                throw new Error "You must specify a replacement value"
            parent = @parent()
            if parent?
                parent[@key()] = value
            else
                result = value
        previous: -> @parent()?[@key() - 1]
        next: -> @parent()?[@key() + 1]
        current: ->
            parent = @parent()
            if parent?
                return parent[@key()]
            else
                return result
    traverseNode = (node) ->
        if node? and typeof node is 'object'
            enterCallback?(node, context)
            if skip
                skip = false
            else
                # node may have been changed, in which case we have to get the new value
                while node isnt (newNode = context.current())
                    exitCallback?(node, context)
                    node = newNode
                    if node?
                        enterCallback?(node, context)
                    else
                        break
                if node? and typeof node is 'object'
                    context.ancestors.push node
                    if Array.isArray node
                        index = 0
                        while index < node.length
                            value = node[index]
                            context.path.push index
                            traverseNode value
                            context.path.pop()
                            index++
                            if removed > 0
                                index -= removed
                                removed = 0
                    else
                        for key, value of node
                            context.path.push key
                            traverseNode value
                            context.path.pop()
                    context.ancestors.pop()
            if node?
                exitCallback?(node, context)
    traverseNode graph
    return result

exports.test = ->
    graph =
        id: 'root'
        alpha: 1
        beta:
            id: 'beta'
            charlie: 2
            delta: 3
        echo:
            id: 'echo'
            foxtrot: 1
    # test basic call returns root graph
    throw new Error "traverse should have returned graph" unless graph is exports.traverse(graph, ->)
    # test that replacing root node returns new value
    throw new Error "traverse should have returned 2" unless 2 is exports.traverse(graph, (node, context) -> context.replace(2))
    # # test that traversal and replacement and skip works
    # result = []
    # exports.traverse graph, (node, context) ->
    #     result.push node.id
    #     result.push context.key()
    #     result.push context.parent()?.id
    #     if node.id is 'beta'
    #         context.replace
    #             id: 'foo'
    #             bar:
    #                 id: 'baz'
    #                 skipMe:
    #                     value: 1
    #     else if node.id is 'baz'
    #         context.skip()

    # expected = ["root", undefined, undefined, "beta", "beta", "root", "baz", "bar", "foo", "echo", "echo", "root"]
    # throw new Error "#{result} != #{expected}" unless JSON.stringify(result) is JSON.stringify(expected)
    # return
