/**
 * This class manages a formula defined for an `Ext.app.ViewModel`.
 *
 * ## Formula Basics
 *
 * Formulas in a `ViewModel` can be defined as simply as just a function:
 *
 *      formulas: {
 *          xy: function (get) { return get('x') * get('y'); }
 *      }
 *
 * When you need to be more explicit, "xy" can become an object. The following means the
 * same thing as above:
 *
 *      formulas: {
 *          xy: {
 *              get: function (get) { return get('x') * get('y'); }
 *          }
 *      }
 *
 * ### Data Dependencies
 *
 * One of the important aspects of a `ViewModel` is notification of change. In order to
 * manage this, a `ViewModel` *must* know the dependencies between data. In the above case
 * this is accomplished by **parsing the text of the function**. While this is convenient
 * and reduces the maintenance/risk that would come from explicitly listing dependencies
 * separately, there are some rules to be aware of:
 *
 *   * All dependencies are resolved by matching the binding statements in the getter function.
 *   * If you need to use these values in other ways, cache them as a `var` (following
 *     the first rule to capture the value) and use that `var`.
 *
 * In the above formulas, the "xy" formula depends on "x" and "y" in the `ViewModel`. As
 * these values change, the formula is called to produce the correct value for "xy". This
 * in turn can be used by other formulas. For example:
 *
 *      formulas: {
 *          xy: function (get) {  // "get" is arbitrary but a good convention
 *              return get('x') * get('y');
 *          },
 *
 *          xyz: function (get) {
 *              return get('xy') * get('z');
 *          }
 *      }
 *
 * In the above, "xyz" depends on "xy" and "z" values in the `ViewModel`.
 *
 * ### The Getter Method
 *
 * The argument passed to the formula is a function that allows you to retrieve
 * the matched bind statements.
 *
 *      formulas: {
 *          foo: function (get) {
 *              return get('theUser.address.city');
 *          }
 *      }
 *
 * In the above, the dependency is resolved to `theUser.address.city`. The formula will not
 * be triggered until the value for `city` is present.
 *
 * ### Capturing Values
 *
 * If values need to be used repeatedly, you can use a `var` as long as the Rules are not
 * broken.
 *
 *      formulas: {
 *          x2y2: function (get) {
 *              // These are still "visible" as "get('x')" and "get('y')" so this is OK:
 *              var x = get('x'),
 *                  y = get('y');
 *
 *              return x * x * y * y;
 *          }
 *      }
 *
 * ## Explicit Binding
 *
 * While function parsing is convenient, there are times it is not the best solution. In
 * these cases, an explicit `bind` can be given. To revisit the previous example with an
 * explicit binding:
 *
 *      formulas: {
 *          zip: {
 *              bind: '{foo.bar.zip}',
 *
 *              get: function (zip) {
 *                  // NOTE: the only thing we get is what our bind produces.
 *                  return zip * 2;
 *              }
 *          }
 *      }
 *
 * In this case we have given the formula an explicit `bind` value so it will no longer
 * parse the `get` function. Instead, it will call `{@link Ext.app.ViewModel#bind}` with
 * the value of the `bind` property and pass the produced value to `get` whenever it
 * changes.
 *
 * ## Settable Formulas
 *
 * When a formula is "reversible" it can be given a `set` method to allow it to participate
 * in two-way binding. For example:
 *
 *      formulas: {
 *             fullName: {
 *                 get: function (get) {
 *                     var ret = get('firstName') || '';
 *
 *                     if (get('lastName')) {
 *                         ret += ' ' +  get('lastName');
 *                     }
 *
 *                     return ret;
 *                 },
 *
 *                 set: function (value) {
 *                     var space = value.indexOf(' '),
 *                         split = (space < 0) ? value.length : space;
 *
 *                     this.set({
 *                         firstName: value.substring(0, split),
 *                         lastName: value.substring(split + 1)
 *                     });
 *                 }
 *             }
 *         }
 *
 * When the `set` method is called the `this` reference is the `Ext.app.ViewModel` so it
 * just calls its `{@link Ext.app.ViewModel#method-set set method}`.
 *
 * ## Single Run Formulas
 *
 * If a formula only needs to produce an initial value, it can be marked as `single`.
 *
 *      formulas: {
 *          xy: {
 *              single: true,
 *
 *              get: function (get) {
 *                  return get('x') * get('y');
 *              }
 *          }
 *      }
 *
 * This formulas `get` method will be called with `x` and `y` once and then its binding
 * to these properties will be destroyed. This means the `get` method (and hence the value
 * of `xy`) will only be executed/calculated once.
 */
Ext.define('Ext.app.bind.Formula', {
    extend: 'Ext.util.Schedulable',
 
    requires: [
        'Ext.util.LruCache'
    ],
 
    statics: {
        getFormulaParser: function(name) {
            var cache = this.formulaCache,
                parser, s;
 
            if (!cache) {
                cache = this.formulaCache = new Ext.util.LruCache({
                    maxSize: 20
                });
            }
 
            parser = cache.get(name);
            
            if (!parser) {
                // Unescaped: [^\.a-z0-9_]NAMEHERE\(\s*(['"])(.*?)\1\s*\)
                s = '[^\\.a-z0-9_]' + Ext.String.escapeRegex(name) +
                    '\\(\\s*([\'"])(.*?)\\1\\s*\\)';
                parser = new RegExp(s, 'gi');
                cache.add(name, parser);
            }
            
            return parser;
        }
    },
 
    isFormula: true,
 
    calculation: null,
 
    explicit: false,
 
    /**
     * @cfg {Object} [bind]
     * An explicit bind request to produce data to provide the `get` function. If this is
     * specified, the result of this bind is the first argument to `get`. If not given,
     * then `get` receives a getter function that can retrieve bind expressions. For details
     * on what can be specified for this property see `{@link Ext.app.ViewModel#bind}`.
     * @since 5.0.0
     */
 
    /**
     * @cfg {Function} get
     * The function to call to calculate the formula's value. The `get` method executes
     * with a `this` pointer of the `ViewModel` and receives a getter function or the result of
     * a configured `bind`.
     * @since 5.0.0
     */
 
    /**
     * @cfg {Function} [set]
     * If provided this method allows a formula to be set. This method is typically called
     * when `{@link Ext.app.bind.Binding#setValue}` is called. The `set` method executes
     * with a `this` pointer of the `ViewModel`. Whatever values need to be updated can
     * be set by calling `{@link Ext.app.ViewModel#set}`.
     * @since 5.0.0
     */
    set: null,
 
    /**
     * @cfg {Boolean} [single=false]
     * This option instructs the binding to call its `destroy` method immediately after
     * delivering the initial value.
     * @since 5.0.0
     */
    single: false,
 
    /* eslint-disable-next-line no-useless-escape */
    fnKeywordArgumentNamesRe: /^function\s*[^\(]*\(\s*([^,\)\s]+)/,
 
    fnKeywordRe: /^\s*function/,
 
    /* eslint-disable-next-line no-useless-escape */
    replaceParenRe: /[\(\)]/g,
 
    constructor: function(stub, formula) {
        var me = this,
            owner = stub.owner,
            bindTo, expressions, getter, options;
 
        me.owner = owner;
        me.stub = stub;
 
        me.callParent();
 
        if (formula instanceof Function) {
            me.get = getter = formula;
        }
        else {
            me.get = getter = formula.get;
            me.set = formula.set;
            expressions = formula.bind;
 
            if (formula.single) {
                me.single = formula.single;
            }
 
            if (expressions) {
                bindTo = expressions.bindTo;
 
                if (bindTo) {
                    options = Ext.apply({}, expressions);
                    delete options.bindTo;
                    expressions = bindTo;
                }
            }
        }
 
        //<debug>
        if (!getter) {
            Ext.raise('Must specify a getter method for a formula');
        }
        //</debug>
 
        if (expressions) {
            me.explicit = true;
        }
        else {
            expressions = getter.$expressions || me.parseFormula(getter);
        }
 
        me.binding = owner.bind(expressions, me.onChange, me, options);
    },
 
    destroy: function() {
        var me = this,
            binding = me.binding,
            stub = me.stub;
 
        if (binding) {
            binding.destroy();
            me.binding = null;
        }
 
        if (stub) {
            stub.formula = null;
        }
 
        me.callParent();
 
        // Save for last because this is used to remove us from the Scheduler
        me.getterFn = me.owner = null;
    },
 
    getFullName: function() {
        return this.fullName ||
               (this.fullName = this.stub.getFullName() + '=' + this.callParent() + ')');
    },
 
    getRawValue: function() {
        return this.calculation;
    },
 
    onChange: function() {
        if (!this.scheduled) {
            this.schedule();
        }
    },
 
    parseFormula: function(formula) {
        var str = Ext.Function.toCode(formula),
            defaultProp = 'get',
            expressions = {
                $literal: true
            },
            match, getterProp, formulaRe, expr;
 
        if (this.fnKeywordRe.test(str)) {
            match = this.fnKeywordArgumentNamesRe.exec(str);
            
            if (match) {
                getterProp = match[1];
            }
        }
        else {
            match = str.split('=>')[0];
            
            if (match) {
                match = Ext.String.trim(match.replace(this.replaceParenRe, '')).split(',');
                getterProp = match[0];
            }
        }
 
        getterProp = getterProp || defaultProp;
        formulaRe = Ext.app.bind.Formula.getFormulaParser(getterProp);
 
        while ((match = formulaRe.exec(str))) {
            expr = match[2];
            expressions[expr] = expr;
        }
 
        expressions.$literal = true;
 
        // We store the parse results on the function object because we might reuse the
        // formula function (typically when a ViewModel class is created a 2nd+ time).
        formula.$expressions = expressions;
 
        return expressions;
    },
 
    react: function() {
        var me = this,
            owner = me.owner,
            data = me.binding.lastValue,
            arg;
 
        if (me.explicit) {
            arg = data;
        }
        else {
            arg = owner.getFormulaFn(data);
        }
        
        me.settingValue = true;
        me.stub.set(me.calculation = me.get.call(owner, arg));
        me.settingValue = false;
 
        if (me.single) {
            me.destroy();
        }
    },
 
    setValue: function(value) {
        this.set.call(this.stub.owner, value);
    },
 
    privates: {
        getScheduler: function() {
            var owner = this.owner;
            
            return owner && owner.getScheduler();
        },
        
        sort: function() {
            var me = this,
                binding = me.binding;
 
            // Our binding may be single:true
            if (!binding.destroyed) {
                me.scheduler.sortItem(binding);
            }
 
            // Schedulable#sort === emptyFn
            // me.callParent();
        }
    }
});