///////////////////////////
// JAVASCRIPT RULE ENGINE
///////////////////////////
/*global define*/
define([
    'lodash.noconflict'
], function (_) {
    'use strict';

    function Rules(options) {
        var self = this,
            rules = [],
            facts = options && options.facts || {},
            chain = {},
            inFire = false,
            queueFire = false;

        this.rules = rules;
        this.facts = facts;

        function fire() {
            // if a rule fire asserts more facts, queue fire until all of the
            // current rules are processed.
            if (inFire) {
                queueFire = true;
            } else {
                inFire = true;
                try {
                    _.each(rules, function (rule) {
                        var result = rule.condition(facts);
                        // only fire a rule if its condition result changes
                        if (result && result !== chain[rule.id]) {
                            self.fireRule(rule);
                        }
                        chain[rule.id] = result;
                    });
                } finally {
                    inFire = false;
                }

                var shouldFire = queueFire;
                queueFire = false;
                if (shouldFire) {
                    fire();
                }
            }
        }

        this.fireRule = function (rule) {
            rule.fire(this, facts);
        };

        this.add = function (newRules) {
            rules = rules.concat(newRules);
            _.each(rules, function (rule, index) {
                rule.id = index;
            });
            return this;
        };

        this.fact = function (name, value) {
            if (facts[name] !== value) {
                var oldFacts = _.clone(facts);  // probably needs to be a deep clone
                facts[name] = value;
                try {
                    fire();
                    return true;
                } catch (e) {
                    // if asserting a fact throws, reset state
                    facts = oldFacts;
                    this.facts = oldFacts;
                    throw e;
                }
            }
            return false;
        };

        // add the rules passed into the construct. we use ".add" since it
        // id's the rules, which we need for tracking them in "chain".
        if (options && options.rules) {
            this.add(options.rules);
        }
    }

    function resolveLeft(facts, left) {
        return typeof(left) === 'function' ? left(facts) : facts[left];
    }

    function resolveRight(facts, right) {
        return typeof(right) === 'function' ? right(facts) : right;
    }

    Rules.fact = function (name) {
        return function (facts) {
            return facts[name];
        };
    };

    Rules.and = function (f1, f2) {
        return function (facts) {
            return f1(facts) && f2(facts);
        };
    };

    Rules.or = function (f1, f2) {
        return function (facts) {
            return f1(facts) || f2(facts);
        };
    };

    Rules.eq = function (fact, value) {
        return function (facts) {
            return resolveLeft(facts, fact) === resolveRight(facts, value);
        };
    };

    Rules.neq = function (fact, value) {
        return function (facts) {
            return resolveLeft(facts, fact) !== resolveRight(facts, value);
        };
    };

    Rules.gt = function (fact, value) {
        return function (facts) {
            return facts[fact] !== undefined && facts[fact] > value;
        };
    };

    Rules.lt = function (fact, value) {
        return function (facts) {
            return facts[fact] !== undefined && facts[fact] < value;
        };
    };

    Rules.gte = function (fact, value) {
        return function (facts) {
            return facts[fact] !== undefined && facts[fact] >= value;
        };
    };

    Rules.lte = function (fact, value) {
        return function (facts) {
            return facts[fact] !== undefined && facts[fact] <= value;
        };
    };

    Rules.setFact = function (name, value) {
        return function (rules) {
            rules.fact(name, value);
        };
    };

    return Rules;
});

///////////////////////////////
// HTML5 Video Rules
///////////////////////////////

var rules = new Rules({
    facts: {
        readyState: 0,
        duration: null,
        lastDuration: null,
        progressAmount: 0,
        event: null
    }
});

rules.add([
    {
        name: 'duration change',
        condition: function (facts) {
            return facts.duration !== facts.lastDuration;
        },
        fire: function (facts) {
            console.log('durationchange ' + facts.duration);
            rules.fact('lastDuration', facts.duration);
        }
    },
    {
        name: 'readyState gt 0',
        condition: rules.gt('readyState', 0),
        fire: function () {
            console.log('loadedmetadata');
        }
    },
    {
        name: 'readyState gt 1',
        condition: rules.gt('readyState', 1),
        fire: function () {
            console.log('loadeddata');
        }
    },
    {
        name: 'readyState gt 2',
        condition: rules.gt('readyState', 2),
        fire: function () {
            console.log('canplay');
        }
    },
    {
        name: 'readyState gt 3',
        condition: rules.gt('readyState', 3),
        fire: function () {
            console.log('canplaythrough');
        }
    },
    {
        name: 'duration set',
        condition: rules.gt('duration', 0),
        fire: rules.setFact('readyState', 1)
    },
    {
        name: 'progress',
        condition: rules.eq('event', 'progress'),
        fire: rules.setFact('readyState', 2)
    },
    {
        name: 'canplay',
        condition: rules.eq('event', 'canplay'),
        fire: rules.setFact('readyState', 3)
    },
    {
        name: 'canplaythrough',
        condition: rules.eq('event', 'canplaythrough'),
        fire: rules.setFact('readyState', 4)
    },
    {
        name: 'progressAmount >= duration',
        condition: function (facts) {
            return facts.progressAmount !== undefined && facts.duration !== undefined && facts.duration > 0 &&
                    facts.progressAmount >= facts.duration;
        },
        fire: rules.setFact('readyState', 4)
    }
]);

// TEST, DURATION 1 should fire durationchange and loadedmetadata
rules.fact('duration', 1);
// TEST, SHOULD ONLY FIRE DURATION CHANGE
rules.fact('duration', 2);
// TEST, SHOULD ONLY FIRE DURATION CHANGE
rules.fact('duration', 3);
// TEST, SHOULD FIRE LOADEDDATA, CANPLAY, CANPLAYTHROUGH
rules.fact('progressAmount', 3);