/* eslint-disable max-len */
/**
 * This mixin provides classes with a `responsiveConfig` config that allows instances to
 * conditionally control config properties. This mixin is typically applied to the
 * `Ext.Component` and `Ext.Widget` classes via the `Ext.Responsive` override:
 *
 *      Ext.application({
 *          requires: [
 *              'Ext.Responsive'
 *          ],
 *
 *          // ...
 *      });
 *
 * Once this mixin is applied, config properties can be declaratively managed based on
 * current device characteristics (such as viewport width):
 *
 *      Ext.create({
 *          xtype: 'panel',
 *          title: 'Some Title',
 *
 *          layout: {
 *              type: 'hbox
 *          },
 *
 *          responsiveConfig: {
 *              'width < 800': {
 *                  layout: {
 *                      vertical: true
 *                  }
 *              },
 *              'width >= 800': {
 *                  layout: {
 *                      vertical: false
 *                  }
 *              }
 *          }
 *      });
 *
 * To use this mixin on other classes, you can mix it in directly:
 *
 *      Ext.define('ResponsiveClass', {
 *          mixin: [
 *              'Ext.mixin.Responsive'
 *          ],
 *
 *          responsiveConfig: {
 *              portrait: {
 *              },
 *
 *              landscape: {
 *              }
 *          }
 *      });
 *
 * For details see `{@link #responsiveConfig}`.
 * @since 5.0.0
 */
Ext.define('Ext.mixin.Responsive', function(Responsive) { return { // eslint-disable-line brace-style, max-len
    extend: 'Ext.Mixin',
 
    mixinConfig: {
        id: 'responsive',
 
        after: {
            destroy: 'destroy'
        }
    },
 
    config: {
        /**
         * @cfg {Object} responsiveConfig
         * This object consists of keys that represent the conditions on which configs
         * will be applied. For example:
         *
         *      responsiveConfig: {
         *          landscape: {
         *              region: 'west'
         *          },
         *          portrait: {
         *              region: 'north'
         *          }
         *      }
         *
         * In this case the keys ("landscape" and "portrait") are the criteria (or "rules")
         * and the object to their right contains the configs that will apply when that
         * rule is true.
         *
         * These rules can be any valid JavaScript expression but the following values
         * are considered in scope:
         *
         *  * `landscape` - True if the device orientation is landscape (always `true` on
         *   desktop devices).
         *  * `portrait` - True if the device orientation is portrait (always `false` on
         *   desktop devices).
         *  * `tall` - True if `width` < `height` regardless of device type.
         *  * `wide` - True if `width` > `height` regardless of device type.
         *  * `width` - The width of the viewport in pixels.
         *  * `height` - The height of the viewport in pixels.
         *  * `platform` - An object containing various booleans describing the platform
         *  (see `{@link Ext#platformTags Ext.platformTags}`). The properties of this
         *  object are also available implicitly (without "platform." prefix) but this
         *  sub-object may be useful to resolve ambiguity (for example, if one of the
         *  `{@link #responsiveFormulas}` overlaps and hides any of these properties).
         *  Previous to Ext JS 5.1, the `platformTags` were only available using this
         *  prefix.
         *
         * A more complex example:
         *
         *      responsiveConfig: {
         *          'desktop || width > 800': {
         *              region: 'west'
         *          },
         *
         *          '!(desktop || width > 800)': {
         *              region: 'north'
         *          }
         *      }
         *
         * **NOTE**: If multiple rules set a single config (like above), it is important
         * that the rules be mutually exclusive. That is, only one rule should set each
         * config. If multiple rules are actively setting a single config, the order of
         * these (and therefore the config's value) is unspecified.
         *
         * For a config to participate as a `responsiveConfig` it must have a "setter"
         * method. In the above example, a "setRegion" method must exist.
         *
         * @since 5.0.0
         */
        responsiveConfig: {
            $value: undefined,
 
            merge: function(newValue, oldValue, target, mixinClass) {
                var marker, entry, ret, rule;
                
                if (!newValue) {
                    return oldValue;
                }
 
                ret = oldValue ? Ext.Object.chain(oldValue) : {};
                marker = Responsive.context;
 
                for (rule in newValue) {
                    if (!mixinClass || !(rule in ret)) {
                        entry = newValue[rule];
 
                        // If we are called to merge A to B and then B to C we must not
                        // keep pushing the fn/config wrapper around things.
                        if (entry.fn !== marker) {
                            entry = {
                                fn: marker, // created on first evaluation of this rule
                                config: entry
                            };
                        }
 
                        ret[rule] = entry;
                    }
                }
 
                return ret;
            }
        },
 
        /**
         * @cfg {Object} responsiveFormulas
         * It is common when using `responsiveConfig` to have recurring expressions that
         * make for complex configurations. Using `responsiveFormulas` allows you to cut
         * down on this repetition by adding new properties to the "scope" for the rules
         * in a `responsiveConfig`.
         *
         * For example:
         *
         *      Ext.define('MyApp.view.main.Main', {
         *          extend: 'Ext.container.Container',
         *
         *          mixins: [
         *              'Ext.mixin.Responsive'
         *          ],
         *
         *          responsiveFormulas: {
         *              small: 'width < 600',
         *
         *              medium: 'width >= 600 && width < 800',
         *
         *              large: 'width >= 800',
         *
         *              tuesday: function(context) {
         *                  return (new Date()).getDay() === 2;
         *              }
         *          }
         *      });
         *
         * With the above declaration, any `responsiveConfig` can now use these values
         * like so:
         *
         *      responsiveConfig: {
         *          small: {
         *              hidden: true
         *          },
         *          'medium && !tuesday': {
         *              hidden: false,
         *              region: 'north'
         *          },
         *          large: {
         *              hidden: false,
         *              region: 'west'
         *          }
         *      }
         *
         * @since 5.0.1
         */
        responsiveFormulas: {
            $value: null,
 
            merge: function(newValue, oldValue, target, mixinClass) {
                return this.mergeNew(newValue, oldValue, target, mixinClass);
            }
        }
    },
 
    /**
     * This method removes this instance from the Responsive collection.
     */
    destroy: function() {
        Responsive.unregister(this);
        
        // No callParent() here, it's a mixin
    },
 
    privates: {
        statics: {
            /**
             * @property {Boolean} active
             * @static
             * @private
             */
            active: false,
 
            /**
             * @property {Object} all
             * The collection of all `Responsive` instances. These are the instances that
             * will be notified when dynamic conditions change.
             * @static
             * @private
             */
            all: {},
 
            _configNames: [
                'responsiveConfig',
                'responsiveFormulas'
            ],
 
            /**
             * @property {Object} context
             * This object holds the various context values passed to the rule evaluation
             * functions.
             * @static
             * @private
             */
            context: Ext.Object.chain(Ext.platformTags),
 
            /**
             * @property {Number} count
             * The number of instances in the `all` collection.
             * @static
             * @private
             */
            count: 0,
 
            /**
             * @property {Number} nextId
             * The seed value used to assign `Responsive` instances a unique id for keying
             * in the `all` collection.
             * @static
             * @private
             */
            nextId: 0,
 
            /**
             * Activates event listeners for all `Responsive` instances. This method is
             * called when the first instance is registered.
             * @private
             */
            activate: function() {
                Responsive.active = true;
                Responsive.updateContext();
                Ext.on('resize', Responsive.onResize, Responsive);
            },
 
            /**
             * Deactivates event listeners. This method is called when the last instance
             * is destroyed.
             * @private
             */
            deactivate: function() {
                Responsive.active = false;
                Ext.un('resize', Responsive.onResize, Responsive);
            },
 
            /**
             * Updates all registered the `Responsive` instances (found in the `all`
             * collection).
             * @private
             */
            notify: function() {
                var all = Responsive.all,
                    context = Responsive.context,
                    timer = Responsive.timer,
                    id;
 
                if (timer) {
                    Responsive.timer = Ext.unasap(timer);
                }
 
                Responsive.updateContext();
 
                Ext.suspendLayouts();
 
                Ext.fireEvent('beforeresponsiveupdate', context);
 
                for (id in all) {
                    all[id].setupResponsiveContext();
                }
 
                Ext.fireEvent('beginresponsiveupdate', context);
 
                for (id in all) {
                    all[id].updateResponsiveState();
                }
 
                Ext.fireEvent('responsiveupdate', context);
 
                Ext.resumeLayouts(true);
            },
 
            /**
             * Handler of the window resize event. Schedules a timer so that we eventually
             * call `notify`.
             * @private
             */
            onResize: function() {
                if (!Responsive.timer) {
                    Responsive.timer = Ext.asap(Responsive.onTimer);
                }
            },
 
            /**
             * This method is the timer handler. When called this removes the timer and
             * calls `notify`.
             * @private
             */
            onTimer: function() {
                Responsive.timer = null;
                Responsive.notify();
            },
 
            register: function(responder) {
                var id = responder.$responsiveId;
 
                if (!id) {
                    responder.$responsiveId = id = ++Responsive.nextId;
 
                    Responsive.all[id] = responder;
 
                    if (++Responsive.count === 1) {
                        Responsive.activate();
                    }
                }
            },
 
            unregister: function(responder) {
                var id = responder.$responsiveId;
 
                if (id in Responsive.all) {
                    responder.$responsiveId = null;
 
                    delete Responsive.all[id];
 
                    if (--Responsive.count === 0) {
                        Responsive.deactivate();
                    }
                }
            },
 
            /**
             * Updates the `context` object base on the current environment.
             * @private
             */
            updateContext: function() {
                var El = Ext.Element,
                    width = El.getViewportWidth(),
                    height = El.getViewportHeight(),
                    context = Responsive.context;
 
                context.width = width;
                context.height = height;
                context.tall = width < height;
                context.wide = !context.tall;
 
                context.landscape = context.portrait = false;
                
                if (!context.platform) {
                    context.platform = Ext.platformTags;
                }
 
                context[Ext.dom.Element.getOrientation()] = true;
            }
        }, // private static
 
        //--------------------------------------------------------------------------
 
        /**
         * This class system hook method is called at the tail end of the mixin process.
         * We need to see if the `targetClass` has already got a `responsiveConfig` and
         * if so, we must add its value to the real config.
         * @param {Ext.Class} targetClass 
         * @private
         */
        afterClassMixedIn: function(targetClass) {
            var proto = targetClass.prototype,
                responsiveConfig = proto.responsiveConfig,
                responsiveFormulas = proto.responsiveFormulas,
                config;
 
            if (responsiveConfig || responsiveFormulas) {
                config = {};
 
                if (responsiveConfig) {
                    delete proto.responsiveConfig;
                    config.responsiveConfig = responsiveConfig;
                }
 
                if (responsiveFormulas) {
                    delete proto.responsiveFormulas;
                    config.responsiveFormulas = responsiveFormulas;
                }
 
                targetClass.getConfigurator().add(config);
            }
 
            targetClass.addConfigTransform('transformResponsiveConfig', 10);
        },
 
        // The reason this method exists is so to convince the config system to put the
        // "responsiveConfig" and "responsiveFormulas" in the initList. This needs to be
        // done so that the initGetter is setup prior to calling transformResponsiveConfig
        // when we need to call the getters.
 
        applyResponsiveConfig: function(rules) {
            var rule;
            
            for (rule in rules) {
                rules[rule].fn = Ext.createRuleFn(rule);
            }
 
            return rules;
        },
 
        applyResponsiveFormulas: function(formulas) {
            var fn, name, ret;
 
            if (formulas) {
                ret = {};
 
                for (name in formulas) {
                    if (Ext.isString(fn = formulas[name])) {
                        fn = Ext.createRuleFn(fn);
                    }
                    
                    ret[name] = fn;
                }
            }
 
            return ret;
        },
 
        /**
         * Evaluates and returns the configs based on the `responsiveConfig`. This
         * method relies on the state being captured by the `updateContext` method.
         * @private
         */
        getResponsiveState: function() {
            var context = Responsive.context,
                rules = this.getResponsiveConfig(),
                entry, ret, rule;
 
            if (rules) {
                ret = {};
 
                for (rule in rules) {
                    entry = rules[rule];
                    
                    if (entry.fn.call(this, context)) {
                        Ext.merge(ret, entry.config);
                    }
                }
            }
 
            return ret;
        },
 
        setupResponsiveContext: function() {
            var formulas = this.getResponsiveFormulas(),
                context = Responsive.context,
                name;
 
            if (formulas) {
                for (name in formulas) {
                    context[name] = formulas[name].call(this, context);
                }
            }
        },
 
        /**
         * This config system hook method is called just prior to processing the specified
         * "instanceConfig". This hook returns the instanceConfig that will actually be
         * processed by the config system.
         * @param {Object} instanceConfig The user-supplied instance config object.
         * @param {Ext.Configurator} configurator 
         * @private
         */
        transformResponsiveConfig: function(instanceConfig, configurator) {
            var me = this,
                ret = instanceConfig,
                state;
 
            // Since we are called immediately prior to the Configurator looking at the
            // instanceConfig, that incoming value has not yet been merged on to
            // "this.config". We need to call getResponsiveConfig and getResponsiveFormulas
            // and still get all that merged goodness, so we have to do the merge here.
 
            // TODO make sure tests cover these on class body
            // TODO make sure tests cover that we only register() if needed
            if (configurator.hoistConfigs(me, instanceConfig, Responsive._configNames)) {
                Responsive.register(me);
 
                // For updates this is done in bulk prior to updating all of the responsive
                // objects, but for instantiation, we have to do this for ourselves now.
                me.setupResponsiveContext();
 
                // Now we can merge the current responsive state with the incoming config.
                // The responsiveConfig takes priority.
                state = me.getResponsiveState();
 
                if (state) {
                    ret = Ext.merge({}, instanceConfig, state);
 
                    // We don't want these to remain since we've already handled them.
                    delete ret.responsiveConfig;
                    delete ret.responsiveFormulas;
                }
            }
 
            return ret;
        },
 
        /**
         * Evaluates and applies the `responsiveConfig` to this instance. This is called
         * by `notify` automatically.
         * @private
         */
        updateResponsiveState: function() {
            var config = this.getResponsiveState();
            
            this.setConfig(config);
        }
    } // private
};
});