// Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
// This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
// The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
// The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
// Code distributed by Google as part of the polymer project is also
// subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt

var esprima = require("js-libs/esprima").esprima;
var Path = require("js-libs/polymer-expressions/path-parser").Path;

(function (global) {
    'use strict';

    // TODO(rafaelw): Implement simple LRU.
    var expressionParseCache = Object.create(null);

    function getExpression(expressionText) {
        var expression = expressionParseCache[expressionText];
        if (!expression) {
            var delegate = new ASTDelegate();
            esprima.parse(expressionText, delegate);
            expression = new Expression(delegate);
            expressionParseCache[expressionText] = expression;
        }
        return expression;
    }

    function Literal(value) {
        this.value = value;
        this.valueFn_ = undefined;
    }

    Literal.prototype = {
        valueFn: function () {
            if (!this.valueFn_) {
                var value = this.value;
                this.valueFn_ = function () {
                    return value;
                }
            }

            return this.valueFn_;
        }
    }

    function IdentPath(name) {
        this.name = name;
        this.path = Path.get(name);
    }

    IdentPath.prototype = {
        valueFn: function () {
            if (!this.valueFn_) {
                var name = this.name;
                var path = this.path;
                this.valueFn_ = function (model, observer, changedModel) {
                    if (observer)
                        observer.addPath(model, path);

                    if (changedModel) {
                        var result = path.getValueFrom(changedModel);
                        if (result !== undefined) {
                            return result;
                        }
                    }

                    return path.getValueFrom(model);
                }
            }

            return this.valueFn_;
        },

        setValue: function (model, newValue) {
            if (this.path.length == 1) {
                model = findScope(model, this.path[0]);
            }

            return this.path.setValueFrom(model, newValue);
        }
    };

    function MemberExpression(object, property, accessor) {
        this.computed = accessor == '[';

        this.dynamicDeps = typeof object == 'function' ||
                           object.dynamicDeps ||
                           (this.computed && !(property instanceof Literal));

        this.simplePath =
            !this.dynamicDeps &&
            (property instanceof IdentPath || property instanceof Literal) &&
            (object instanceof MemberExpression || object instanceof IdentPath);

        this.object = this.simplePath ? object : getFn(object);
        this.property = !this.computed || this.simplePath ?
            property : getFn(property);
    }

    MemberExpression.prototype = {
        get fullPath() {
            if (!this.fullPath_) {

                var parts = this.object instanceof MemberExpression ?
                    this.object.fullPath.slice() : [this.object.name];
                parts.push(this.property instanceof IdentPath ?
                    this.property.name : this.property.value);
                this.fullPath_ = Path.get(parts);
            }

            return this.fullPath_;
        },

        valueFn: function () {
            if (!this.valueFn_) {
                var object = this.object;

                if (this.simplePath) {
                    var path = this.fullPath;

                    this.valueFn_ = function (model, observer) {
                        if (observer)
                            observer.addPath(model, path);

                        return path.getValueFrom(model);
                    };
                } else if (!this.computed) {
                    var path = Path.get(this.property.name);

                    this.valueFn_ = function (model, observer, filterRegistry) {
                        var context = object(model, observer, filterRegistry);

                        if (observer)
                            observer.addPath(context, path);

                        return path.getValueFrom(context);
                    }
                } else {
                    // Computed property.
                    var property = this.property;

                    this.valueFn_ = function (model, observer, filterRegistry) {
                        var context = object(model, observer, filterRegistry);
                        var propName = property(model, observer, filterRegistry);
                        if (observer)
                            observer.addPath(context, [propName]);

                        return context ? context[propName] : undefined;
                    };
                }
            }
            return this.valueFn_;
        },

        setValue: function (model, newValue) {
            if (this.simplePath) {
                this.fullPath.setValueFrom(model, newValue);
                return newValue;
            }

            var object = this.object(model);
            var propName = this.property instanceof IdentPath ? this.property.name :
                this.property(model);
            return object[propName] = newValue;
        }
    };

    function Filter(name, args) {
        this.name = name;
        this.args = [];
        for (var i = 0; i < args.length; i++) {
            this.args[i] = getFn(args[i]);
        }
    }

    Filter.prototype = {
        transform: function (model, observer, filterRegistry, toModelDirection,
                            initialArgs) {
            var fn = filterRegistry[this.name];
            var context = model;
            if (fn) {
                context = undefined;
            } else {
                fn = context[this.name];
                if (!fn) {
                    console.error('Cannot find function or filter: ' + this.name);
                    return;
                }
            }

            // If toModelDirection is falsey, then the "normal" (dom-bound) direction
            // is used. Otherwise, it looks for a 'toModel' property function on the
            // object.
            if (toModelDirection) {
                fn = fn.toModel;
            } else if (typeof fn.toView == 'function') {
                fn = fn.toView;
            }

            if (typeof fn != 'function') {
                console.error('Cannot find function or filter: ' + this.name);
                return;
            }

            var args = initialArgs || [];
            for (var i = 0; i < this.args.length; i++) {
                args.push(getFn(this.args[i])(model, observer, filterRegistry));
            }

            return fn.apply(context, args);
        }
    };

    function notImplemented() { throw Error('Not Implemented'); }

    var unaryOperators = {
        '+': function (v) { return +v; },
        '-': function (v) { return -v; },
        '!': function (v) { return !v; }
    };

    var binaryOperators = {
        '+': function (l, r) { return l + r; },
        '-': function (l, r) { return l - r; },
        '*': function (l, r) { return l * r; },
        '/': function (l, r) { return l / r; },
        '%': function (l, r) { return l % r; },
        '<': function (l, r) { return l < r; },
        '>': function (l, r) { return l > r; },
        '<=': function (l, r) { return l <= r; },
        '>=': function (l, r) { return l >= r; },
        '==': function (l, r) { return l == r; },
        '!=': function (l, r) { return l != r; },
        '===': function (l, r) { return l === r; },
        '!==': function (l, r) { return l !== r; },
        '&&': function (l, r) { return l && r; },
        '||': function (l, r) { return l || r; },
    };

    function getFn(arg) {
        return typeof arg == 'function' ? arg : arg.valueFn();
    }

    function ASTDelegate() {
        this.expression = null;
        this.filters = [];
        this.deps = {};
        this.currentPath = undefined;
        this.scopeIdent = undefined;
        this.indexIdent = undefined;
        this.dynamicDeps = false;
    }

    ASTDelegate.prototype = {
        createUnaryExpression: function (op, argument) {
            if (!unaryOperators[op])
                throw Error('Disallowed operator: ' + op);

            argument = getFn(argument);

            return function (model, observer, filterRegistry) {
                return unaryOperators[op](argument(model, observer, filterRegistry));
            };
        },

        createBinaryExpression: function (op, left, right) {
            if (!binaryOperators[op])
                throw Error('Disallowed operator: ' + op);

            left = getFn(left);
            right = getFn(right);

            switch (op) {
                case '||':
                    this.dynamicDeps = true;
                    return function (model, observer, filterRegistry) {
                        return left(model, observer, filterRegistry) ||
                            right(model, observer, filterRegistry);
                    };
                case '&&':
                    this.dynamicDeps = true;
                    return function (model, observer, filterRegistry) {
                        return left(model, observer, filterRegistry) &&
                            right(model, observer, filterRegistry);
                    };
            }

            return function (model, observer, filterRegistry) {
                return binaryOperators[op](left(model, observer, filterRegistry),
                                           right(model, observer, filterRegistry));
            };
        },

        createConditionalExpression: function (test, consequent, alternate) {
            test = getFn(test);
            consequent = getFn(consequent);
            alternate = getFn(alternate);

            this.dynamicDeps = true;

            return function (model, observer, filterRegistry) {
                return test(model, observer, filterRegistry) ?
                    consequent(model, observer, filterRegistry) :
                    alternate(model, observer, filterRegistry);
            }
        },

        createIdentifier: function (name) {
            var ident = new IdentPath(name);
            ident.type = 'Identifier';
            return ident;
        },

        createMemberExpression: function (accessor, object, property) {
            var ex = new MemberExpression(object, property, accessor);
            if (ex.dynamicDeps)
                this.dynamicDeps = true;
            return ex;
        },

        createCallExpression: function (expression, args) {
            if (!(expression instanceof IdentPath))
                throw Error('Only identifier function invocations are allowed');

            var filter = new Filter(expression.name, args);

            return function (model, observer, filterRegistry) {
                return filter.transform(model, observer, filterRegistry, false);
            };
        },

        createLiteral: function (token) {
            return new Literal(token.value);
        },

        createArrayExpression: function (elements) {
            for (var i = 0; i < elements.length; i++)
                elements[i] = getFn(elements[i]);

            return function (model, observer, filterRegistry) {
                var arr = []
                for (var i = 0; i < elements.length; i++)
                    arr.push(elements[i](model, observer, filterRegistry));
                return arr;
            }
        },

        createProperty: function (kind, key, value) {
            return {
                key: key instanceof IdentPath ? key.name : key.value,
                value: value
            };
        },

        createObjectExpression: function (properties) {
            for (var i = 0; i < properties.length; i++)
                properties[i].value = getFn(properties[i].value);

            return function (model, observer, filterRegistry) {
                var obj = {};
                for (var i = 0; i < properties.length; i++)
                    obj[properties[i].key] =
                        properties[i].value(model, observer, filterRegistry);
                return obj;
            }
        },

        createFilter: function (name, args) {
            this.filters.push(new Filter(name, args));
        },

        createAsExpression: function (expression, scopeIdent) {
            this.expression = expression;
            this.scopeIdent = scopeIdent;
        },

        createInExpression: function (scopeIdent, indexIdent, expression) {
            this.expression = expression;
            this.scopeIdent = scopeIdent;
            this.indexIdent = indexIdent;
        },

        createTopLevel: function (expression) {
            this.expression = expression;
        },

        createThisExpression: notImplemented
    }

    function Expression(delegate) {
        this.scopeIdent = delegate.scopeIdent;
        this.indexIdent = delegate.indexIdent;

        if (!delegate.expression)
            throw Error('No expression found.');

        this.expression = delegate.expression;
        getFn(this.expression); // forces enumeration of path dependencies

        this.filters = delegate.filters;
        this.dynamicDeps = delegate.dynamicDeps;
    }

    Expression.prototype = {
        getValue: function (model, isBackConvert, changedModel, observer) {
            var value = getFn(this.expression)(model.context, observer, changedModel);
            for (var i = 0; i < this.filters.length; i++) {
                value = this.filters[i].transform(model.context, observer, model.context, isBackConvert, [value]);
            }

            return value;
        },

        setValue: function (model, newValue, filterRegistry) {
            var count = this.filters ? this.filters.length : 0;
            while (count-- > 0) {
                newValue = this.filters[count].transform(model, undefined,
                    filterRegistry, true, [newValue]);
            }

            if (this.expression.setValue)
                return this.expression.setValue(model, newValue);
        }
    }

    /**
     * Converts a style property name to a css property name. For example:
     * "WebkitUserSelect" to "-webkit-user-select"
     */
    function convertStylePropertyName(name) {
        return String(name).replace(/[A-Z]/g, function (c) {
            return '-' + c.toLowerCase();
        });
    }

    var parentScopeName = '@' + Math.random().toString(36).slice(2);

    // Single ident paths must bind directly to the appropriate scope object.
    // I.e. Pushed values in two-bindings need to be assigned to the actual model
    // object.
    function findScope(model, prop) {
        while (model[parentScopeName] &&
               !Object.prototype.hasOwnProperty.call(model, prop)) {
            model = model[parentScopeName];
        }

        return model;
    }

    function isLiteralExpression(pathString) {
        switch (pathString) {
            case '':
                return false;

            case 'false':
            case 'null':
            case 'true':
                return true;
        }

        if (!isNaN(Number(pathString)))
            return true;

        return false;
    };

    function PolymerExpressions() { }

    PolymerExpressions.prototype = {
        // "built-in" filters
        styleObject: function (value) {
            var parts = [];
            for (var key in value) {
                parts.push(convertStylePropertyName(key) + ': ' + value[key]);
            }
            return parts.join('; ');
        },

        tokenList: function (value) {
            var tokens = [];
            for (var key in value) {
                if (value[key])
                    tokens.push(key);
            }
            return tokens.join(' ');
        },

        // binding delegate API
        prepareInstancePositionChanged: function (template) {
            var indexIdent = template.polymerExpressionIndexIdent_;
            if (!indexIdent)
                return;

            return function (templateInstance, index) {
                templateInstance.model[indexIdent] = index;
            };
        },

        prepareInstanceModel: function (template) {
            var scopeName = template.polymerExpressionScopeIdent_;
            if (!scopeName)
                return;

            var parentScope = template.templateInstance ?
                template.templateInstance.model :
                template.model;

            var indexName = template.polymerExpressionIndexIdent_;

            return function (model) {
                return createScopeObject(parentScope, model, scopeName, indexName);
            };
        }
    };

    var createScopeObject = ('__proto__' in {}) ?
      function (parentScope, model, scopeName, indexName) {
          var scope = {};
          scope[scopeName] = model;
          scope[indexName] = undefined;
          scope[parentScopeName] = parentScope;
          scope.__proto__ = parentScope;
          return scope;
      } :
      function (parentScope, model, scopeName, indexName) {
          var scope = Object.create(parentScope);
          Object.defineProperty(scope, scopeName,
              { value: model, configurable: true, writable: true });
          Object.defineProperty(scope, indexName,
              { value: undefined, configurable: true, writable: true });
          Object.defineProperty(scope, parentScopeName,
              { value: parentScope, configurable: true, writable: true });
          return scope;
      };

    global.PolymerExpressions = PolymerExpressions;
    PolymerExpressions.getExpression = getExpression;
})(module.exports);