/*
 *  Copyright 2016 Anyware Services
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
/**
 * This class provides a TreePanel for content type structure
 * @private
 */
Ext.define('Ametys.plugins.cms.contenttype.ContentTypeTree', {
    extend: 'Ext.tree.Panel', 
    
    statics: 
    {
        /**
         * @property {Ext.XTemplate} _cTypeTooltipTpl The template used for content types' tooltip
         */
        CONTENT_TYPE_TOOLTIP_TPL: Ext.create ('Ext.XTemplate', [
                                         '{description}<br/>',
                                         '<div class="x-clear">',
                                         '</div>',
                                         '<tpl if="isPrivate == true">',
                                            "<span class='tooltip-ctype-type private ametysicon-lock81'>{{i18n PLUGINS_CMS_CONTENTTYPE_TREE_PRIVATE}}</span><br/>",
                                         '</tpl>',
                                         '<tpl if="isReferenceTable == true">',
                                            "<span class='tooltip-ctype-type reference-table ametysicon-list6 editor-tables1'>{{i18n PLUGINS_CMS_CONTENTTYPE_TREE_REFERENCE_TABLE}}</span><br/>",
                                         '</tpl>',
                                         '<tpl if="isAbstract == true">',
                                            "<span class='tooltip-ctype-type abstract ametysicon-text'>{{i18n PLUGINS_CMS_CONTENTTYPE_TREE_ABSTRACT}}</span><br/>",
                                         '</tpl>',
                                         '<tpl if="isMixin == true">',
                                            "<span class='tooltip-ctype-type mixin ametysicon-info28'>{{i18n PLUGINS_CMS_CONTENTTYPE_TREE_MIXIN}}</span><br/>",
                                         '</tpl>',
                                         '<tpl if="isSimple == true">',
                                            "<span class='tooltip-ctype-type simple ametysicon-document112'>{{i18n PLUGINS_CMS_CONTENTTYPE_TREE_SIMPLE}}</span><br/>",
                                         '</tpl>',
                                         '<tpl if="isMultilingual == true">',
                                            "<span class='tooltip-ctype-type multilingual ametysicon-translation'>{{i18n PLUGINS_CMS_CONTENTTYPE_TREE_MULTILINGUAL}}</span><br/>",
                                         '</tpl>',
                                         "<span class='tooltip-ctype-type {glyphOrigin}'>{origin}</span>",
                                         '</div>'
                                ])
    },
    
    scrollable: true,
    animate: true,
    cls : 'contenttype-hierarchie-tree',

    /**
     * @cfg {String} [plugin="cms"] The plugin for the store.
     */
    plugin: "cms",
    
    /** @cfg {String} rootLabel The root node label. Only if #cfg-rootVisible */

    /**
     * @cfg {Boolean/String} [excludePrivate=false] True to exclude private content types.
     */

    /**
     * @cfg {Boolean/String} [excludeReferenceTable=true] True to exclude reference tables.
     */
    
    /**
     * @cfg {Boolean/String} [includeReferenceTableOnly=false] True to only include reference table content types. 
     */
    
    /**
     * @cfg {Boolean/String} [excludeAbstract=false] True to exclude abstract content types.
     */
    
     /**
     * @cfg {Boolean/String} [excludeMixin=false] True to exclude mixin content types. By default excludeMixin is false.
     */
    
     /**
     * @cfg {Boolean/String} [includeMixinOnly=false] True to only include mixin content types. By default includeMixinOnly content type is false.
     */

      /**
      * @cfg {Boolean/String} [separateGlobal=false] True to separate global and local type under two different roots.
      */
    
    /** @cfg {String[]} strictContentTypes The strict content types ids  */
    
    /** @cfg {String[]} contentTypes The parent content types ids */
     
    initComponent: function ()
    {
        Ext.apply(this, {
            folderSort: false,
            root: {
                text: this.rootLabel,
                iconGlyph: "ametysicon-organization1",
                expanded: true,
                loaded: true
            },
            store: this.createTreeStore(),
            dockedItems: [
                  this._getFilterToolbarConfig(),
                  {
                      dock: 'top',
                      xtype: 'button',
                      ui: 'tool-hintmessage',
                      hidden: true,
                      itemId: 'no-result',
                      text: "{{i18n PLUGINS_CMS_CONTENTTYPE_TREE_FILTER_NO_MATCH}}" + "{{i18n PLUGINS_CMS_CONTENTTYPE_TREE_FILTER_NO_MATCH_ACTION}}",
                      scope: this,
                      handler: this._clearSearchFilter
                  }
            ]
        });
    
        this.on('itemmouseenter', this._createQtip, this);
        this.on('beforeselect', this._disabledNode, this);
        this.on('beforeitemmouseenter', this._disabledNode, this);
        this.on('load', this._onLoad, this);
        this.on('checkchange', this._onCheckChange, this);
        
        this.callParent();
    },

    constructor: function(config) 
    {
        config.rootVisible = config.rootVisible || false;
        config.rootLabel = config.rootLabel || "{{i18n PLUGINS_CMS_CONTENTTYPE_TREE_ROOT}}";
        
        this.rootVisible = config.rootVisible;
        this.excludeReferenceTable = config.excludeReferenceTable === undefined ? false : config.excludeReferenceTable;
        this.includeReferenceTableOnly = config.includeReferenceTableOnly === undefined ? false : config.includeReferenceTableOnly;
        this.excludeAbstract = config.excludeAbstract === undefined ? false : config.excludeAbstract;
        this.excludePrivate = config.excludePrivate === undefined ? false : config.excludePrivate;
        this.excludeMixin = config.excludeMixin === undefined ? false : config.excludeMixin;
        this.includeMixinOnly = config.includeMixinOnly === undefined ? false : config.includeMixinOnly;
        this.excludeMode = config.excludeMode || 'disabled';
        this.hierarchicalView = config.hierarchicalView === undefined ? true : config.hierarchicalView;
        
        this.separateGlobal = config.separateGlobal === undefined ? false : config.separateGlobal;

        
        this.strictContentTypes = config.strictContentTypes;
        this.contentTypes = config.contentTypes;
        
        this.checkMode = config.checkMode === undefined ? false : config.checkMode;
        this.values = config.values;
        
        this.callParent(arguments);
    },
    
    /**
     * @protected
     * Create the tree store
     * @param {Object} config The tree panel configuration
     * @return {Ext.data.TreeStore} The created tree store
     */
    createTreeStore: function (config)
    {
        var sorters = [];
        if (this.separateGlobal)
        {
            sorters.push({
                sorterFn: function(n1, n2) {
                    // sort of hack to be sure that root nodes are always
                    // sorted the same way, whatever the language
                    if (n1.getDepth() == 0 && n2.getDepth() == 0)
                    {
                        var global1 = n1.get('global');
                        var global2 = n2.get('global');
                        return global1 == global2 ? 0 : global1 > global2 ? 1 : -1;
                    }
                    return 0;
                }

            })
        }
        sorters.push({
                property: 'text',
                direction: 'ASC'
            });
        var store = Ext.create('Ext.data.TreeStore', {
            model: 'Ametys.plugins.cms.contenttype.ContentTypeTree.ContentTypeEntry',
            autoLoad: false,
            
            proxy: {
                type: 'ametys',
                plugin: this.plugin,
                role: 'org.ametys.cms.contenttype.ContentTypesTreeComponent',
                methodName: 'getContentTypes',
                reader: {
                    type: 'json'
                }
            },
            
            listeners: {
	                'beforeload': this._onBeforeLoad,
	                scope: this
            },
            
            sorters: sorters
        });
        
        return store;
    },
    
    /**
     * Function called before loading the store
     * @param {Ext.data.Store} store The store
     * @param {Ext.data.operation.Operation} operation The object that will be passed to the Proxy to load the store
     * @private
     */
    _onBeforeLoad: function(store, operation)
    {
        operation.setParams( Ext.apply(operation.getParams() || {}, {
            includeMixinOnly: this.includeMixinOnly,
            hierarchicalView: this.hierarchicalView,
            excludeReferenceTable: this.excludeReferenceTable,
            includeReferenceTableOnly: this.includeReferenceTableOnly,
            excludeMixin: this.excludeMixin,
            excludeAbstract: this.excludeAbstract,
            excludePrivate: this.excludePrivate,
            excludeMode: this.excludeMode,
            separateGlobal: this.separateGlobal,
            strictContentTypes: this.strictContentTypes,
            contentTypes: this.contentTypes
        }));
    },
    
    /**
     * @private
     * Function after tree loading
     * @param {Ext.data.TreeStore} store The tree store
     * @param {Ext.data.TreeModel[]} records The records
     * @param {Boolean} successful True if the operation was successful.
     * @param {Ext.data.Operation} operation The operation that triggered this load.
     * @param {Ext.data.NodeInterface} node The loaded node
     */
    _onLoad: function(store, records, successful, operation, node)
    {
        if (this.checkMode)   
        {
            this._setCheckboxes(node);
        }
    },
    
    /**
     * Iterate through the children nodes, and call #_setCheckbox
     * @param {Ext.data.Model} node The node to iterate
     * @private
     */
    _setCheckboxes: function(node)
    {
        var childNodes = node.childNodes;
        for (var i=0; i < childNodes.length; i++)
        {
            this._setCheckbox(childNodes[i]);
            this._setCheckboxes(childNodes[i]);
        }
    },
    
    /**
     * Put a checkbox on nodes that can be checked, with the corresponding state. Does not verify any rights. 
     * @param {Ext.data.Model} node The node 
     * @private
     */
    _setCheckbox: function(node)
    {
        if (!node.get('disabled'))
        {
            node.set('checked', Ext.Array.contains(this.values, node.get('contentTypeId')));
        }
        else
        {
            node.set('checked', false);
        }
    },
    
    /**
     * Verify if a node can be checked. If it can, his value is updated
     * @param {Ext.data.Model} node The node which state was updated
     * @param {Boolean} checked The new node state
     * @private
     */
    _onCheckChange: function(node, checked)
    {
        if (node.get('disabled'))
        {
            // cancel check
            node.set('checked', false); 
        }
        else if (checked === true && Ext.Array.contains(this.values, node.get('contentTypeId')) === false)
        {
            this.values.push(node.get('contentTypeId'));
        }
        else if (checked === false && Ext.Array.contains(this.values, node.get('contentTypeId')) === true)
        {
            Ext.Array.remove(this.values, node.get('contentTypeId'));
        }
    },

    /**
     * Load the root node
     */
    onRender: function(ct, position)
    {
        this.callParent(arguments);
        this.getRootNode().expand();
    },
    
    /**
     * @private
     * Destroy and create the node tooltip when the mouse enters the node
     * @param {Ext.view.View} view The tree view
     * @param {Ext.data.Model} node The tree node
     * @param {HTMLElement} el The node's element
     */
    _createQtip: function (view, node, el)
    {
        Ext.QuickTips.unregister(el);
        Ext.QuickTips.register(Ext.apply({target: el, id: el.id + '-tooltip'}, this._getTooltip(node)));
    },
    
    /**
     * Get the tooltip configuration
     * @param {Ext.data.Model} node The tree node
     * @returns {Object} The tooltip configuration. See Ametys.ui.fluent.Tooltip.
     * @private
     */
    _getTooltip: function(node)
    {
        if (node.get('label'))
        {
            var text = Ametys.plugins.cms.contenttype.ContentTypeTree.CONTENT_TYPE_TOOLTIP_TPL.applyTemplate ({
                description: node.get('description'), 
                'isPrivate': node.get('private'), 
                'isReferenceTable': node.get('reference-table'), 
                'isAbstract': node.get('abstract'),
                'isMixin': node.get('mixin'),
                'isSimple': node.get('simple'),
                'isMultilingual': node.get('multilingual'),
                'origin': node.get('origin'),
                'glyphOrigin': node.get('glyphOrigin')
            });
            
            return {
                title: node.get('label'),
                image: Ext.isEmpty(node.get('iconGlyph')) ? Ametys.CONTEXT_PATH + (node.get('iconLarge') != '' ? node.get('iconLarge') : node.get('iconMedium')) : null,
                glyphIcon: Ext.isEmpty(node.get('iconGlyph')) ? null : node.get('iconGlyph') + (!Ext.isEmpty(node.get('iconDecorator')) ? ' ' + node.get('iconDecorator') : ''),
                imageWidth: 48,
                imageHeight: 48,
                text: text,
                inribbon: false
            };
        }
        else
        {
            return {
                title: this.rootLabel,
                glyphIcon: "ametysicon-organization1",
                imageWidth: 48,
                imageHeight: 48,
                inribbon: false
            };
        }
    },
    
    /**
     * @private
     * Get the filter toolbar config
     * @return {Object} The filter toolbar config
     */
    _getFilterToolbarConfig: function()
    {
        return {
            dock: 'top',
            xtype: 'toolbar',
            layout: { 
                type: 'hbox',
                align: 'stretch'
            },
            border: false,
            defaultType: 'button',
            items: [{
                        // Filter input
                        xtype: 'textfield',
                        cls: 'ametys',
                        flex: 1,
                        maxWidth: 400,
                        itemId: 'plugins-filter-input',
                        emptyText: "{{i18n PLUGINS_CMS_CONTENTTYPE_TREE_FILTER}}",
                        minLength: 3,
                        minLengthText: "{{i18n PLUGINS_CMS_CONTENTTYPE_TREE_FILTER_INVALID}}",
                        msgTarget: 'qtip',
                        listeners: {change: Ext.Function.createBuffered(this._filter, 500, this)}
                    }, 
                    {
                        // Clear filter
                        tooltip: "{{i18n PLUGINS_CMS_CONTENTTYPE_TREE_CLEAR_FILTER}}",
                        handler: Ext.bind (this._clearSearchFilter, this),
                        iconCls: "ametysicon-eraser11"
                    },
                    {
                        tooltip: "{{i18n PLUGINS_CMS_CONTENTTYPE_TREE_FILTER_BY_CHECK}}",
                        enableToggle: true,
                        toggleHandler: Ext.bind(this._displayCheckedNodesOnly, this),
                        hidden: !this.checkMode,
                        iconCls: 'a-btn-glyph ametysicon-check51 size-16',
                        cls: 'a-btn-light'
                    }
            ]
        };
    },
    
        /**
     * @private
     * Filters the tree nodes by entered text.
     * @param {Ext.form.field.Text} field This text field
     */
    _filter: function(field)
    {
        this._filterField = field;
        this.getStore().clearFilter();
        var val = Ext.String.escapeRegex(field.getRawValue());
        
        if (val.length > 2)
        {
            this._regexFilter = new RegExp(val, 'i');
            
            if (this._checkFilter)
            {
                this.getStore().filter({
                    filterFn: Ext.bind(this._filterByTextAndChildrenCheckedNodes, this)
                });
            }
            else
            {
	            this.getStore().filter({
	                filterFn: Ext.bind(this._filterByTextAndChildren, this)
	            });
            }
        }
        else
        {
            this._regexFilter = null;
            if (this._checkFilter)
            {
                this.getStore().filter({
                    filterFn: Ext.bind(this._filterByTextAndChildrenCheckedNodes, this)
                });
            }
        }
        
        this.getDockedItems()[1].setVisible(!this.getStore().getCount()); // Display docked items if no node matched the search term
    },
    
    /**
     * @private
     * Filter function that check if a node in the tree should be visible or not.
     * @param {Ext.data.Model} record The record to check.
     * @return {boolean} True if the record should be visible.
     */
    _filterByTextAndChildren: function (record)
    {
        var minDepth = this.rootVisible ? 1 : 0;
        minDepth += this.separateGlobal ? 1 : 0;
        var isVisible = (this._regexFilter == null || this._regexFilter.test(record.data.text))
                        && record.getDepth() > minDepth;
        if (!isVisible)
        {
            // if the record does not match, we check if any child is visible. If at least one is, this record should not be hidden.
            // This is efficient because the data is in the store, and is not loaded in the view.
            for (var i = 0; !isVisible && i < record.childNodes.length; i++) {
                isVisible = this._filterByTextAndChildren(record.childNodes[i]);
            }
        }
        
        if (isVisible)
        {
            this.expandNode(record);
        }
        
        return isVisible; 
    },
    
    /**
     * Triggered by the button "filter by checked only".
     * @param {Ext.button.Button} button The button which triggered this method. Parameter not used.
     * @param {Boolean} state If true, filter by "checked only" 
     * @private
     */
    _displayCheckedNodesOnly: function(button, state)
    {
        this._checkFilter = state;
        var filterValue = "";
        if (this._filterField !== undefined)
        {
            filterValue = new String(this._filterField.getValue()).trim();
        }
        if (filterValue && filterValue.length < 3)
        {   
            filterValue = "";
        }
        
        if (filterValue === "" && state === false)
        {
            this._clearSearchFilter();
        }
        else if (state === true)
        {
            this.getStore().filter({
                filterFn: Ext.bind(this._filterByTextAndChildrenCheckedNodes, this)
            });
        }
        else if (filterValue != "")
        {
            this._filter(this._filterField);
        }
    },
    
    /**
     * @private
     * Filter function that check if a checked node in the tree should be visible or not.
     * @param {Ext.data.Model} record The record to check.
     * @return {boolean} True if the record should be visible.
     */
    _filterByTextAndChildrenCheckedNodes: function (record)
    {
        var minDepth = this.rootVisible ? 1 : 0;
        minDepth += this.separateGlobal ? 1 : 0;
        var isVisible = (this._regexFilter == null || this._regexFilter.test(record.data.text)) && record.data.checked 
                        && record.getDepth() > minDepth;
        if (!isVisible)
        {
            // if the record does not match, we check if any child is visible. If at least one is, this record should not be hidden.
            // This is efficient because the data is in the store, and is not loaded in the view.
            for (var i = 0; !isVisible && i < record.childNodes.length; i++) {
                isVisible = this._filterByTextAndChildrenCheckedNodes(record.childNodes[i]);
            }
        }
        
        if (isVisible)
        {
            this.expandNode(record);
        }
        
        return isVisible; 
    },
    
    /**
     * @private
     * Handler for the clear search
     */
    _clearSearchFilter: function ()
    {
        if (this._filterField)
        {
            this._filterField.reset();
        }
        
        this._regexFilter = null;
        this.getStore().clearFilter();
        this.getDockedItems()[1].setVisible(false);
        
        var selection = this.getSelectionModel().getSelection()[0];
        if (selection)
        {
            this.ensureVisible(selection.getPath());
        }
    },
    
    /**
     * @private
     * Disabled node
     * @param {Ext.selection.RowModel} rowModel The selection model
     * @param {Ext.data.Model} record The selected record
     * @return {Boolean}
     */
    _disabledNode: function(rowModel, record)
    {
        if (record.get('disabled'))
        {
            return false;
        }
        return true;
    }
    
});

Ext.define('Ametys.plugins.cms.contenttype.ContentTypeTree.ContentTypeEntry', { 
    extend: 'Ext.data.Model',
    
    fields: [
         {name: 'private', type:'boolean'},
         {name: 'abstract', type:'boolean'},
         {name: 'reference-table', type:'boolean'},
         {name: 'mixin', type:'boolean'},
         {name: 'simple', type:'boolean'},
         {name: 'global', type:'boolean'},
         {name: 'multilingual', type:'boolean'},
         {name: 'disabled', type:'boolean', defaultValue: false},
         {
            name: 'text',
            sortType: function(value) {
                return Ext.data.SortTypes.asNonAccentedUCString(value || "").toLowerCase(); 
            }
         },
         'label',
         'description',
         'contentTypeId',
         'iconSmall',
         'iconMedium',
         'iconLarge',
         'iconGlyph',
         'iconDecorator',
         'origin',
         'glyphOrigin',
         {
            name: 'icon',
            depends: ['iconGlyph', 'iconSmall'],
            calculate: function (data)
            {
                if (Ext.isEmpty(data.iconGlyph))
                {
                    return Ametys.CONTEXT_PATH + data.iconSmall;
                }
                return null;
            }
        },
        {
            name: 'iconCls',
            depends: ['iconGlyph', 'iconDecorator'],
            calculate: function (data)
            {
                if (!Ext.isEmpty(data.iconGlyph))
                {
                    return 'a-tree-glyph ' + data.iconGlyph + (!Ext.isEmpty(data.iconDecorator) ? ' ' + data.iconDecorator : '');
                }
                else
                {
                    return null;
                }
                
            }
        },
        {
            name: 'cls', 
            convert: function (value, node)
            {
                return (node.get('abstract') ? ' abstract' : '') + (node.get('reference-table') ? ' reference-table' : '') + (node.get('private') ? ' private' : '') + (node.get('disabled') ? ' disabled' : '');
            }
        }
    ]
});