var animationState = {};
animationState.reset = function (engine) {
    if ('string' === typeof engine) {
        this.defaultEngine = engine;
    }
    this.defaults = {};
    this.displayList = [];
    this.displayDict = {};
    this.start = null;
    this.time = 0;
    this.timeline = [];
    this.timelineIndex = 0;
    this.requestID = null;
    this.paused = false;
    this.displayEngine = 'undefined' === typeof engine ? this.defaultEngine : engine;
}

function addActions(frame, timeline) {
    var keyframe = keyframes[frame];
    var len = keyframe.length;
    for (var i = 0; i < len; ++i) {
        var action = keyframe[i];
        loopOver(action, timeline);
    }
}

function animateList(now) {
    if (animationState.paused) {
        return;
    }
    if (animationState.start == null) {
        animationState.start = now - animationState.time;
    }
    animationState.time = now - animationState.start;
    var stillAnimating = false;
    for (var index = animationState.timelineIndex; index < animationState.timeline.length; ++index) {
        var animation = animationState.timeline[index];
        if (animation.time > animationState.time) {
            stillAnimating = true;
            break;
        }
        if (animation.time + animation.duration < animationState.time) {
            if (animation.finalized) {
                continue;
            }
            animation.finalized = true;
        }
        stillAnimating = true;
        var actions = animation.actions;
        for (var aIndex = 0; aIndex < actions.length; ++aIndex) {
            var action = actions[aIndex];
            var hasDraw = 'draw' in action;
            var hasRef = 'ref' in action;
            var displayIndex;
            if (hasDraw) {
                var ref = hasRef ? action.ref : "anonymous_" + index + "_" + aIndex;
                assert('string' == typeof(ref));
                if (ref in animationState.displayDict) {
                    displayIndex = animationState.displayDict[ref];
                } else {
                    assert('string' == typeof(action.draw));
                    var draw = (new Function("return " + action.draw))();
                    assert('object' == typeof(draw));
                    var paint;
                    if ('paint' in action) {
                        assert('string' == typeof(action.paint));
                        paint = (new Function("return " + action.paint))();
                        assert('object' == typeof(paint) && !isArray(paint));
                    } else {
                        paint = animationState.defaults.paint;
                    }
                    displayIndex = animationState.displayList.length;
                    animationState.displayList.push( { "ref":ref, "draw":draw, "paint":paint,
                        "drawSpec":action.draw, "paintSpec":action.paint,
                        "drawCopied":false, "paintCopied":false,
                        "drawDirty":true, "paintDirty":true, "once":false } );
                    animationState.displayDict[ref] = displayIndex;
                }
            } else if (hasRef) {
                assert('string' == typeof(action.ref));
                displayIndex = animationState.displayDict[action.ref];
            } else {
                assert(actions.length == 1);
                for (var prop in action) {
                    if ('paint' == prop) {
                        assert('string' == typeof(action[prop]));
                        var obj = (new Function("return " + action[prop]))();
                        assert('object' == typeof(obj) && !isArray(obj));
                        animationState.defaults[prop] = obj;
                    } else {
                        animationState.defaults[prop] = action[prop];
                    }
                }
                continue;
            }
            var targetSpec = 'target' in action ? action.target : animationState.defaults.target;
            assert(targetSpec);
            assert('string' == typeof(targetSpec));
            assert(displayIndex < animationState.displayList.length);
            var display = animationState.displayList[displayIndex];
            var modDraw = targetSpec.startsWith('draw');
            assert(modDraw || targetSpec.startsWith('paint'));
            var modType = modDraw ? "draw" : "paint";
            var copied = modDraw ? display.drawCopied : action.paintCopied;
            if (!copied) {
                var copy;
                if (!modDraw || display.drawSpec.startsWith("text")) {
                    copy = {};
                    var original = modDraw ? display.draw : display.paint;
                    for (var p in original) {
                        copy[p] = original[p];
                    }
                } else if (display.drawSpec.startsWith("paths")) {
                    copy = [];
                    for (var i = 0; i < display.draw.length; ++i) {
                        var curves = display.draw[i];
                        var curve = Object.keys(curves)[0];
                        copy[i] = {};
                        copy[i][curve] = curves[curve].slice(0);  // clone the array of curves
                    }
                } else {
                    assert(display.drawSpec.startsWith("pictures"));
                    copy = [];
                    for (var i = 0; i < display.draw.length; ++i) {
                        var entry = display.draw[i];
                        copy[i] = { "draw":entry.draw, "paint":entry.paint };
                    }
                }
                display[modType] = copy;
                display[modType + "Copied"] = true;
            }
            var targetField, targetObject, fieldOffset;
            if (targetSpec.endsWith("]")) {
                fieldOffset = targetSpec.lastIndexOf("[");
                assert(fieldOffset >= 0);
                targetField = targetSpec.substring(fieldOffset + 1, targetSpec.length - 1);
                var arrayIndex = +targetField;
                if (!isNaN(arrayIndex) && targetField.length > 0) {
                    targetField = arrayIndex;
                }

            } else {
                fieldOffset = targetSpec.lastIndexOf(".");
                if (fieldOffset >= 0) {
                    targetField = targetSpec.substring(fieldOffset + 1, targetSpec.length);
                } else {
                    targetObject = display;
                    targetField = targetSpec;
                }
            }
            if (fieldOffset >= 0) {
                var sub = targetSpec.substring(0, fieldOffset);
                targetObject = (new Function('display', "return display." + sub))(display);
            }
            assert(null != targetObject[targetField]);
            if (!('start' in action) || action.start < animation.time) {
                for (var p in animationState.defaults) {
                    if ('draw' == p || 'paint' == p || 'ref' == p) {
                        continue;
                    }
                    assert('range' == p || 'target' == p || 'formula' == p || 'params' == p);
                    if (!(p in action)) {
                        action[p] = animationState.defaults[p];
                    }
                }
                if ('number' == typeof(action.formula)) {
                    targetObject[targetField] = action.formula;
                    action.once = true;
                }
                action.start = animation.time;
            }
            if (action.once) {
                continue;
            }
            var value = Math.min(1, (animationState.time - animation.time) / animation.duration);
            var scaled = action.range[0] + (action.range[1] - action.range[0]) * value;
            if ('params' in action) {
                if (!('func' in action)) {
                    if (isArray(action.params)) {
                        action.funcParams = [];
                        var len = action.params.length;
                        for (var i = 0; i < len; ++i) {
                            action.funcParams[i] = 'target' == action.params[i]
                                ? targetObject[targetField]
                                : (new Function("return " + action.params[i]))();
                        }
                    } else {
                        action.funcParams = 'target' == action.params
                                ? targetObject[targetField]
                                : (new Function("return " + action.params))();
                    }
                    assert('formula' in action && 'string' == typeof(action.formula));
                    // evaluate inline function to get value
                    action.func = new Function('value', 'params', "return " + action.formula);
                }
                scaled = action.func(scaled, action.funcParams);
            }
            if (targetObject[targetField] != scaled) {
                if (modDraw) {
                    display.drawDirty = true;
                } else {
                    display.paintDirty = true;
                }
                targetObject[targetField] = scaled;
            }
        }
    }
    displayBackend(animationState.displayEngine, animationState.displayList);

    if (stillAnimating) {
        animationState.requestID = requestAnimationFrame(animateList);
    }
}

function flattenPaint(paint) {
    if (!paint.paint) {
        return;
    }
    var parent = paints[paint.paint];
    flattenPaint(parent);
    for (var prop in parent) {
        if (!(prop in paint)) {
            paint[prop] = parent[prop];
        }
    }
    paint.paint = null;
}

function init(engine, keyframe) {
    animationState.reset(engine);
    setupPaint();
    setupBackend(animationState.displayEngine);
    keyframeInit(keyframe);
}

function keyframeInit(frame) {
    animationState.reset();
    addActions("_default", animationState.timeline);
    addActions(frame, animationState.timeline);
    for (var index = 0; index < animationState.timeline.length; ++index) {
        animationState.timeline[index].position = index;
    }
    animationState.timeline.sort(function(a, b) {
        if (a.time == b.time) {
            return a.position - b.position;
        }
        return a.time - b.time;
    });
    keyframeBackendInit(animationState.displayEngine, animationState.displayList,
            keyframes[frame][0]);
    animationState.requestID = requestAnimationFrame(animateList);
}

function loopAddProp(action, propName) {
    var funcStr = "";
    var prop = action[propName];
    if ('draw' != propName && isArray(prop)) {
        funcStr += '[';
        for (var index = 0; index < prop.length; ++index) {
            funcStr += loopAddProp(prop, index);
            if (index + 1 < prop.length) {
                funcStr += ", ";
            }
        }
        funcStr += ']';
        return funcStr;
    }
    assert("object" != typeof(prop));
    var useString = "string" == typeof(prop) && isAlpha(prop.charCodeAt(0));
    if (useString) {
        funcStr += "'";
    }
    funcStr += prop;
    if (useString) {
        funcStr += "'";
    }
    return funcStr;
}

function loopOver(rec, timeline) {
    var funcStr = "";
    if (rec.for) {
        funcStr += "for (" + rec.for[0] + "; " + rec.for[1] + "; " + rec.for[2] + ") {\n";
    }
    funcStr += "    var time = " + ('time' in rec ? rec.time : 0) + ";\n";
    funcStr += "    var duration = " + ('duration' in rec ? rec.duration : 0) + ";\n";
    funcStr += "    var actions = [];\n";
    var len = rec.actions.length;
    for (var i = 0; i < len; ++i) {
        funcStr += "    var action" + i + " = {\n";
        var action = rec.actions[i];
        for (var p in action) {
            funcStr += "        '" + p + "':";
            funcStr += loopAddProp(action, p);
            funcStr += ",\n";
        }
        funcStr = funcStr.substring(0, funcStr.length - 2);
        funcStr += "\n    };\n";
        funcStr += "    actions.push(action" + i + ");\n";
    }
    funcStr += "    timeline.push( { 'time':time, 'duration':duration, 'actions':actions,"
            + "'finalized':false } );\n";
    if (rec.for) {
        funcStr += "}\n";
    }
    var func = new Function('rec', 'timeline', funcStr);
    func(rec, timeline);
}

function setupPaint() {
    for (var prop in paints) {
        flattenPaint(paints[prop]);
    }
}