/**
* src: @definejs/panel/modules/Panel.js
* pkg: @definejs/panel@1.0.0
*/
define('Panel', function (require, module, exports) { 
    const $ = require('jquery');
    const $Object = require('Object');
    const Emitter = require('Emitter');
    const Template = require('Template');
    const AppModule = require('AppModule');
    
    const Container = module.require('Container');
    const Meta = module.require('Meta');
    const Params = module.require('Params');
    
    const mapper = new Map();
    const id$panel = {};                      //
    
    //因为 View 类的原因，这里用 ES5 的构造函数的写法，而不用 ES6 的 class 的写法。
    
    /**
    * 构造器。
    */
    function Panel(container, config) {
        config = $Object.deepAssign({}, exports.defaults, config);
    
        const meta = Meta.create(config, {
            'moudle': null,                 //如果非空，则是由 Panel.define() 创建的，此时 container='[data-panel="XXX"]'。
            'container': container,         //
            'tplContainer': container,      //
            '$emitter': new Emitter(),      //供外部用的事件管理器。
            'emitter': new Emitter(this),   //内部使用的事件管理器。
            '$': $(container),              //当前实例关联的 DOM 节点对应的 jQuery 实例。
            'this': this,                   //方便内部使用。
        });
    
        mapper.set(this, meta);
    
        //对外暴露的属性。
        Object.assign(this, {
            'container': container,
            'id': meta.id,
            '$': meta.$,
        });
    
    }
    
    //实例成员。
    Object.assign(Panel.prototype, {
        /**
        * 构造实例时传入的 container 参数。
        */
        container: '',
    
        /**
        * 当前实例的 id。
        */
        id: '',
    
        /**
        * 当前实例关联的 DOM 节点对应的 jQuery 实例。
        * 即 $(container) 的快捷方式。
        */
        $: null,
    
        /**
        * 当前实例关联的 module 对象。
        * 业务层只有使用 KISP.panel() 或 KISP.view() 创建实例时，此值才存在。
        */
        module: null,
    
    
        /**
        * 渲染。
        * 触发事件:
        *   首次渲染时，首先会触发 `init` 事件，即该事件只会触发一次。
        *   每次渲染时，都会依次触发 `before-render`、`render`、`after-render` 事件。
        */
        render(...args) {
            let meta = mapper.get(this);
            let emitter = meta.emitter;
    
            meta.renderArgs = args; //保存 render 参数，用于 refresh()。
    
            //首次 render。
            if (!meta.rendered) {
                meta.rendered = true;    //要放在此处。
                emitter.fire('init');
            }
    
            emitter.fire('before-render', args);
            emitter.fire('render', args);
    
    
            //指定了要自动显示。
            if (meta.show) {
                this.show();
            }
    
            emitter.fire('after-render', args);
        },
    
        /**
        * 显示本组件。
        * 触发事件: `show`。
        */
        show(...args) {
            let meta = mapper.get(this);
    
            meta.$.show(...args);
            meta.visible = true;
    
            //外面可能会用到事件返回值。
            return meta.emitter.fire('show');
    
        },
    
        /**
        * 隐藏本组件。
        * 触发事件: `hide`。
        */
        hide(...args) {
            let meta = mapper.get(this);
    
            meta.$.hide(...args);
            meta.visible = false;
    
            //外面可能会用到事件返回值。
            return meta.emitter.fire('hide');
        },
    
        /**
        * 切换显示或隐藏本组件。
        */
        toggle(needShow) {
            let meta = mapper.get(this);
    
            //重载 toggle(); 
            //未指定参数，则根据原有状态进行切换。
            if (arguments.length == 0) {
                meta.visible ? this.hide() : this.show();
            }
            else {
                needShow ? this.show() : this.hide();
            }
    
            //返回更改后的可见状态。
            return meta.visible;
        },
    
        /**
        * 设置模板填充的规则，为模板填充进行预处理。
        */
        template(process) {
            let meta = mapper.get(this);
            let tpl = meta.tpl;
    
            if (!tpl) {
                tpl = meta.tpl = new Template(meta.tplContainer);
            }
    
            if (process) {
                tpl.process(...arguments);
            }
    
            //返回给外面，可能要用到。
            //通过 panel.template() 即可取得 tpl。
            return tpl;
        },
    
        /**
        * 对本组件进行模板填充。
        * 触发事件: `fill`。
        * @param {Object|Array} 要填充的数据，可以是对象或数组。
        * @param {function} [fn] 当要填充的数据是一个数组时，需要进行迭代转换的处理函数。
        *   调用该函数，可以把一个数组转换成一个新的数组。
        */
        fill(data, fn) {
            let meta = mapper.get(this);
    
            this.template(); //先确保 meta.tpl 存在。
            meta.tpl.render(data, fn);
    
            //外面可能会用到事件返回值。
            return meta.emitter.fire('fill', [data]);
        },
    
        /**
        * 刷新。
        * 即使用最近一次的渲染参数重新进行渲染。
        * 触发事件: `refresh`。
        */
        refresh() {
            let meta = mapper.get(this);
            let args = meta.renderArgs;
    
            this.render(...args);
    
            //外面可能会用到事件返回值。
            return meta.emitter.fire('refresh', args);
        },
    
        /**
        * 重置。
        * 触发事件: `reset`。
        */
        reset(...args) {
            let meta = mapper.get(this);
    
            //外面可能会用到事件返回值。
            return meta.emitter.fire('reset', args);
        },
    
        /**
        * 关闭。
        * 触发事件: `close`。
        */
        close(...args) {
            let meta = mapper.get(this);
    
            //外面可能会用到事件返回值。
            return meta.emitter.fire('close', args);
        },
    
        /**
        * 获取一个状态，该状态表示本组件是否为显示状态。
        */
        visible() {
            let meta = mapper.get(this);
            return meta.visible;
        },
    
        /**
        * 获取一个状态，该状态表示本组件是否已渲染过。
        */
        rendered() {
            let meta = mapper.get(this);
            return meta.rendered;
        },
    
        /**
        * 触发外部的事件。
        */
        fire(...args) {
            let meta = mapper.get(this);
    
            //外面可能会用到事件返回值。
            return meta.$emitter.fire(...args);
        },
    
        /**
        * 批量绑定(委托)事件到 panel.$ 对象的多个元素上。
        * 该方法可以批量绑定一个或多个不同的(委托)事件到多个元素上。
        * 该方法是以事件为组长、选择器为组员进行绑定的。
        * 已重载 $on(name$selector$fn);            //绑定多个(委托)事件到多个元素上。
        * 已重载 $on(name$fn);                     //绑定多个事件到当前元素上。
        
        * 已重载 $on(name, selector$fn);           //绑定单个(委托)事件到多个元素上。
        * 已重载 $on(name, fn);                    //绑定单个事件到当前元素上。
        
        * 已重载 $on(name, sample, selector$fn);   //绑定单个(委托)事件到多个元素上，这些元素的选择器有共同的填充模板。 此时 sample 中的 `{value}` 会给 selector$fn 中的 selector 填充。
        * 已重载 $on(name, selector, fn);          //绑定单个(委托)事件到单个元素上。
        *   
        *   name: '',           //事件名。 如 `click`。
        *   selector$fn: {      //选择器对应的事件处理器。
        *       '#id-0': fn,    //
        *       '#id-1': fn,    //
        *   },
        *
        * 例如，绑定多个(委托)事件到多个元素上：
        *   $on({
        *       'click': {
        *           '#id-0': fn,
        *           '#id-1': fn,
        *       },
        *       'keyup': {
        *           '#id-0': fn,
        *           '#id-1': fn,
        *       },
        *   });
        * 例如，绑定选择器有共同模板的多个元素：
        *   $on('click', '[data-cmd="{value}"]', {
        *       'print': fn,
        *       'top': fn,
        *   });
        *   等价于：
        *   $on('click', {
        *       '[data-cmd="print"]': fn,
        *       '[data-cmd="top"]': fn,
        *   });
        */
        $on(name, sample, selector$fn) {
            let name$selector$fn = Params.normalize(name, sample, selector$fn);
    
            if (!name$selector$fn) {
                return;
            }
    
            //统一形式后再处理。
            let meta = mapper.get(this);
    
            $Object.each(name$selector$fn, function (name, selector$fn) {
                if (!selector$fn) {
                    return;
                }
    
                //重载 $on(name$fn); 
                //如 $on({ 'click': fn, });
                if (typeof selector$fn == 'function') {
                    meta.$.on(name, selector$fn); //此时的 selector$fn 就是 fn。
                    return;
                }
    
                $Object.each(selector$fn, function (selector, fn) {
                    meta.$.on(name, selector, fn);
                });
            });
        },
    
        /**
        * 对 panel.$ 对象中的多个元素进行多个(委托)事件的绑定。
        * 该方法可以对一个或多个元素批量绑定多个(委托)事件。
        * 该方法是以选择器为组长、事件为组员进行绑定的。
        * 已重载 $bind(selector$name$fn);    //对多个元素绑定多个(委托)事件。
        * 已重载 $bind(selector, name$fn);   //对单个元素上绑定多个(委托)事件。
        *   
        *   selector: '',       //要绑定的元素或其选择器。
        *   name$fn: {          //事件名对应的处理器函数。
        *       'click': fn0,   //
        *       'keyup': fn1,   //
        *   },
        *
        * 例如，对多个元素绑定多个(委托)事件：
        *   $on({
        *       '#id-0': {
        *           'click': fn10,
        *           'keyup': fn11,
        *       },
        *       '#id-1': {
        *           'click': fn20,
        *           'keyup': fn21,
        *       },
        *   });
        */
        $bind(selector, name$fn) {
            let selector$name$fn = null;
    
            if (typeof selector == 'string') {
                //重载 $bind(selector, name$fn);
                //单个元素，多个事件的情况。
                selector$name$fn = { [selector]: name$fn, };
            }
            else if (typeof selector == 'object') {
                //重载 $bind(selector$name$fn);
                //多个元素，多个事件的情况。
                selector$name$fn = selector;
            }
            else {
                throw new Error(`无法识别参数 selector 的类型。`);
            }
    
            if (!selector$name$fn) {
                return;
            }
    
            //统一形式后再处理。
            let meta = mapper.get(this);
    
            $Object.each(selector$name$fn, function (selector, name$fn) {
                if (!name$fn) {
                    return;
                }
    
                $Object.each(name$fn, function (name, fn) {
                    meta.$.on(name, selector, fn);
                });
            });
    
        },
    
    
        /**
        * 包装一个新对象，使其拥有当前 Panel 实例的部分成员和新对象的成员。
        * @param {Object} [obj] 要需要包装的对象。 
            如果不指定，则只包装当前实例对象。
        * @return {Object} 返回一个由当前实例的部分成员和要包装对象的成员组成的新对象。
        * @example
            var panel = KISP.create('Panel');
            var obj = panel.wrap();
            obj.show();
        
            var obj1 = panel.wrap({ a: 100 });
            console.log(obj1.a);
        */
        wrap(obj) {
            let meta = mapper.get(this);
            let panel = meta.panel;
    
            if (panel) {
                return panel;
            }
    
            obj = obj || {};
            panel = meta.panel = {};
    
            let keys = new Set([
                ...Reflect.ownKeys(meta.this),          //当前实例的成员。
                ...Reflect.ownKeys(Panel.prototype),    //原型上的成员。
            ]);
    
            //忽略的成员。
            keys.delete('constructor');
            keys.delete('fire');
            keys.delete('wrap');
    
            keys = [...keys];
    
            //拷贝实例原有的成员，忽略的成员除外。
            keys.forEach((key) => {
                let value = meta.this[key];
    
                //实例方法静态化。
                if (typeof value == 'function') {
                    value = value.bind(meta.this);
                }
    
                panel[key] = value;
            });
    
            let $emitter = meta.$emitter;
    
            //重写事件绑定，让事件绑定到外部的事件管理器上，而不是内部使用的 emitter。
            Object.assign(panel, obj, {
                'on': $emitter.on.bind($emitter),
                'off': $emitter.off.bind($emitter),
            });
    
            return panel;
        },
    
        /**
        * 传播指定模块的事件列表。
        * 用于透传子模块的事件给父级。
        */
        propagate(M, names) {
            let meta = mapper.get(this);
    
            names.forEach(function (name) {
                M.on(name, function (...args) {
                    meta.this.fire(name, args);
                });
            });
        },
    
        /**
        * 设置指定的属性。
        * 已重载 set(obj);         //批量设置。
        * 已重载 set(key, value);  //单个设置。
        * @param {string} key 要设置的属性的名称。 
        *   目前支持的字段有：'show'、'rendered'、'$'、'container'、'visible'、'template'。
        * @param value 要设置的属性的值，可以是任何类型。
        */
        set(key, value) {
            var meta = mapper.get(this);
    
            //重载 set({...}); 
            //批量设置的情况。
            if ($Object.isPlain(key)) {
                $Object.each(key, function (key, value) {
                    meta.this.set(key, value);
                });
                return;
            }
    
            //重载 set(key, value); 单个设置的情况。
            switch (key) {
                case 'show':
    
                //提供一个重置的机会，以便可以再次触发 init。 
                //这是高级用法，针对特殊场景。
                //场景：在 set('$') 更新容器后，原 `init` 事件中绑定的逻辑，如果用到了 panel.$.on() 之类的，则会失效。
                //因此在 set('$') 后再调一下 set('rendered', false)，可以让 `init` 事件有机会再次触发。
                case 'rendered':
                    meta[key] = value;
                    break;
    
                //更新容器。
                case '$':
                case 'container':
                    Container.set(meta, value);
                    break;
    
                //允许设置可见性的初始状态，以便在不调用 render() 的前提下直接调用 show() 或 hide()。
                case 'visible':
                    meta.visible = !!value;
                    break;
    
                //设置新的模板容器，这样可以把指定的子部分当成模板进行填充，而不影响其它部分。
                case 'template':
                    if (meta.tpl) {
                        throw new Error(`当前实例中已创建了模板实例，无法再修改模板实例所关联的 DOM 容器。`);
                    }
    
                    meta.tplContainer = meta.$.find(value);
                    break;
                default:
                    throw new Error(`目前不支持设置属性: ${key}`);
            }
    
        },
    
        /**
        * 销毁本组件。
        */
        destroy() {
            let meta = mapper.get(this);
            meta.emitter.destroy();
            meta.$emitter.destroy();
            meta.$.off();
    
            mapper.delete(this);
        },
    
        /**
        * 绑定事件到内部的事件管理器。
        * 注意，该方法在通过 wrap() 导出后，会给重写。
        */
        on(...args) {
            let meta = mapper.get(this);
            meta.emitter.on(...args);
        },
    });
    
    //静态成员。
    Object.assign(Panel, {
        defaults: require('Panel.defaults'),
        
        /**
        * 提供一种按标准方法定义面板的方式。
        * 参数 options 是留给内部模块 View 扩展使用的。
        *   options = {
        *       constructor: Panel, //要使用的构造器，Panel 或 View。
        *       defaults: {},       //要使用的默认配置，为 Panel 或 View 对应的配置。
        *   };
        */
        define(id, factory, options) {
            options = options || {
                'constructor': Panel,
                'defaults': exports.defaults,
            };
    
    
            AppModule.define(id, function ($require, $module, $exports) {
                id = $module.id;    //此 id 才是完整的 id。 外面的那个可能是个模板 id。
    
                let container = Container.get(id, options.defaults);    //如 `[data-panel="/Users/Main"]`。
                let panel = new options.constructor(container);         //如 new Panel(`[data-panel="/Users/Main"]`)。
                let meta = mapper.get(panel);                           //获取 panel 对应的元数据。
    
                //指示此 panel 由 Panel.define() 创建的。
                meta.module = panel.module = $module;
    
                //注意，参数中的 factory 并不是真正的工厂函数，本函数体才是。
                //因此，参数中的 factory 的返回值 $exports 只是一个部分的导出对象。 
                $exports = factory($require, $module, panel);
    
                //把部分的导出对象跟 panel 实例合并成一个新的导出对象，
                //让新的导出对象拥有 panel 实例的大部分成员，以及 factory 中导出的成员。
                $exports = panel.wrap($exports);
    
                id$panel[id] = panel;
    
                //业务层拿到的是 $module.exports === $exports。
                //在 factory 函数体内，panel !== $module.exports，它们是包装前与后的关系。
                return $exports;
            });
    
        },
    
        /**
        * 更新容器。
        * 已重载 update(id);   //更新单个。
        * 已重载 update(ids);  //更新多个。
        * 参数 options 是留给内部模块 View 扩展使用的。
        */
        update(ids, options) {
            ids = Array.isArray(ids) ? ids : [ids];
    
            options = options || {
                'defaults': exports.defaults,
            };
    
            ids.forEach(function (id) {
                let panel = id$panel[id];
                let container = Container.get(id, options.defaults);  //如 `[data-panel="/Users/Main"]`。
    
                if (!panel) {
                    console.warn(`不存在 ${container} 的 Panel 实例。`);
                    return;
                }
    
                panel.set('container', container);
            });
        },
    });
    
    module.exports = exports = Panel;
});