/*
 *  Copyright 2021 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.
 */
 
 /**
 * Provides a Tree for displaying the components of a form.
 * @private
 */
Ext.define('Ametys.plugins.forms.tree.FormTree', {
    extend: 'Ext.tree.Panel',
    
    /**
     * @property {String} _formId The id of the current root of the tree
     * @private
     */
    _formId : null,
    
    /**
     * @cfg {Object[]} activeIndicators The current active indicators
     */
    /**
     * @property {Object[]} _activeIndicators The active indicators
     * @private
     */
    _activeIndicators: {},
        
    /**
     * @property {Object} _availableFilters The object containing the available filter functions (or filter function factories if they depend on another argument)
     * @private
     */
    _availableFilters: {
        'search': function(searchedValue) {
            return function(record){
                function isLike(rec) {
                    var title = Ext.String.deemphasize(rec.get('text')).toLowerCase(),
                        search = Ext.String.deemphasize(searchedValue).toLowerCase();
                    var test = new RegExp(".*(" + search + ").*").test(title);
                    
                    var needExpand = rec.get('type') == 'page' && !rec.isExpanded();
                    if (needExpand) {
                        rec.expand();
                    }
                    
                    return test || rec.childNodes.filter(isLike).length > 0;
                }
                
                var type = record.get('type');
                if (type == 'root')
                {
                    // Always show root
                    return true;
                }
                else
                {
                    return isLike(record);
                }
            }
        }
    },
    
    constructor: function(config)
    {
        this._indicators = config.indicators || [];
        this._activeIndicators = this._getDefaultActiveIndicators();
        
        config.store = this._createStore(config);
        
        //header of the tree
        config.dockedItems = [this._getToolbarCfg()];
        
        var plugins = config.plugins;
        config.plugins = Ext.isArray(plugins) ? plugins : (plugins == null ? [] : [plugins]);
        config.plugins.push({
            ptype: 'cellediting',
            clicksToEdit: 1,
            editAfterSelect: true,
            listeners: {
                'beforeedit': this._onBeforeEdit,
                'edit': this._onEdit,
                scope: this
            }
        });
        
        this.callParent(arguments);
        
        var me = this;
        var view = this.view.lockedView || this.view;
        
        var tpl = new Ext.XTemplate(view.cellTpl.html.replace('</div></td>', 
            '<tpl for="values.column.getItemId().startsWith(\'tree\') ? this.getActiveIndicators() : []">' // startsWith(\'tree\') to apply to the main column only
                + '<tpl if="this.matchIndicator(values, parent.record)">' 
                    + '{% out.push(this.applyIndicator(values, parent.record)) %}'
                 + '</tpl>' 
            + '</tpl>'
            + '</div></td>'), 
            {
                priority: view.cellTpl.priority,
                getActiveIndicators: function() {
                    // We could directly return me activeIndicators; but the order would change following the order of clicking on the checkbox
                    return me._indicators.map(function(i) { return i.id;})
                                         .filter(function(i) { return me._activeIndicators[i]});
                },
                matchIndicator: function(id, record) {
                    return Ext.Array.findBy(me._indicators, function(v) { return v.id == id;}).matchFn.call(me, record);
                },
                applyIndicator: function(id, record) {
                    return Ext.Array.findBy(me._indicators, function(v) { return v.id == id;}).applyFn.call(me, record);
                }
            });
        if (this.view.lockedView)
        {
            this.view.lockedView.cellTpl = tpl;
        }
        else
        {
            this.view.cellTpl = tpl;
        }
        
        this.on('afterrender', this._afterRender, this);
        
        Ametys.message.MessageBus.on(Ametys.message.Message.CREATED, this._onMessageCreated, this);
        Ametys.message.MessageBus.on(Ametys.message.Message.MODIFIED, this._onUpdatedMessage, this);
    },
    
    /**
     * After render tree, check or uncheck indicators
     * @private
     */
    _afterRender: function()
    {
        var me = this;
        var indicatorMenu = this.down('toolbar > button[itemId="indicators-menu"]');
        if (indicatorMenu)
        {
            indicatorMenu.getMenu().items.each(function(item) {
                item.setChecked(me._activeIndicators[item.name], true);
            });
        }
    },
    
    applyState: function (state)
    {
        //this.callParent(arguments) do not call the parent because the columns size do not be recorded in the state 
        this._activeIndicators = {}
        Object.assign(this._activeIndicators, this._getDefaultActiveIndicators())
        Object.assign(this._activeIndicators, state.activeIndicators)
    },
    
    getState: function ()
    {
        var state = {}; //this.callParent(arguments) do not call the parent because the columns size do not be recorded in the state 
        // save indicators 
        state.activeIndicators = this._activeIndicators
        return state;
    },
    
    /**
     * Get default active indicators
     * @private
     */
    _getDefaultActiveIndicators: function()
    {
        var defaultIndicators = {};
        for (var i in this._indicators)
        {
            defaultIndicators[this._indicators[i].id] = this._indicators[i].defaultValue || false;
        }
        
        return defaultIndicators; 
    },
    
    onDestroy: function()
    {
        Ametys.message.MessageBus.unAll(this);
        this.callParent(arguments);
    },
    
     /**
     * Creates the form store
     * @param {Object} config The configuration
     * @param {String} [config.profile=null] See {@link #cfg-profile}
     * @return {Ext.data.Store} The form store
     * @private
     */
    _createStore: function(config)
    {
        var store = Ext.create('Ext.data.TreeStore', {
            model: 'Ametys.plugins.forms.tree.FormsTree.FormEntry',
            asynchronousLoad: false,
            
            proxy: {
                type: 'ametys',
                plugin: 'forms',
                url: 'forms/structure.json',
                reader: {
                    type: 'json',
                    rootProperty: 'pages'
                }
            },
            root: {
                id: 'root',
                isForm: true,
                text: "{{i18n PLUGINS_FORMS_UITOOL_FORMS_ROOT_NAME}}"
            }
        });
        
        return store;
    },
    
    /**
     * Init the root node parameters
     * @param {Function} callback the callback function after getting form properties
     * @param {String} formId the form id
     */
    initRootNodeParameter: function (callback, formId)
    {
        this._formId = formId;
        Ametys.data.ServerComm.callMethod({
            role: "org.ametys.plugins.forms.dao.FormDAO",
            methodName: "getFormProperties",
            parameters: [formId, false, false],
            callback: {
                scope: this,
                handler: this._getRootNodeCb,
                arguments: {
                    callback: callback
                }
            },
            errorMessage: {
                category: this.self.getName(),
                msg: "{{i18n DAOS_FORM_ROOT_ERROR}}"
            },
            waitMessage: {target: this}
        });
    },
    
    /**
     * Gets the toolbar configuration
     * @return {Object} The toolbar configuration
     * @private
     */
    _getToolbarCfg: function()
    {
        var toolbarCfg = {
            dock: 'top',
            xtype: 'toolbar',
            layout: {
                type: 'hbox',
                align: 'stretch'
            },
            defaultType: 'button',
            items: [{
                    // Filter input
                    xtype: 'textfield',
                    cls: 'ametys',
                    flex: 1,
                    maxWidth: 300,
                    itemId: 'search-filter-input',
                    emptyText: "{{i18n PLUGINS_FORMS_TREE_FILTER}}",
                    minLength: 3,
                    minLengthText: "{{i18n PLUGINS_FORMS_TREE_FILTER_INVALID}}",
                    msgTarget: 'qtip',
                    listeners: {change: Ext.Function.createBuffered(this._searchFilter, 500, this)},
                    style: {
                        marginRight: '0px'
                    }
                }, 
                {
                    // Clear filter
                    tooltip: "{{i18n PLUGINS_FORMS_TREE_CLEAR_FILTER}}",
                    handler: Ext.bind (this._clearSearchFilter, this),
                    iconCls: 'a-btn-glyph ametysicon-eraser11 size-16',
                    cls: 'a-btn-light'
                },
                {
                    xtype: 'tbspacer',
                    flex: 0.0001
                },
                {
                    // Collapse all
                    tooltip: "{{i18n PLUGINS_FORMS_TREE_TREE_COLLAPSE_ALL}}",
                    handler: Ext.bind (this._collapseNode, this, [], false),
                    iconCls: 'a-btn-glyph ametysicon-minus-sign4 size-16',
                    cls: 'a-btn-light'
                }, 
                {
                    // Refresh node
                    tooltip: "{{i18n PLUGINS_FORMS_TREE_REFRESH_NODE}}",
                    handler: Ext.bind (this._refreshCurrentNode, this, [], false),
                    iconCls: 'a-btn-glyph ametysicon-arrow123 size-16',
                    cls: 'a-btn-light'
                }
            ]
        }
        
        var menuItems = [];
        var me = this;
        Ext.Array.forEach(this._indicators, function(indicator) {
            menuItems.push({
                xtype: 'menucheckitem',
                iconCls: indicator.iconGlyph,
                text: indicator.label,
                tooltip: indicator.description,
                name: indicator.id,
                itemId: indicator.id,
                value: indicator.defaultValue,
                checkHandler: me._selectIndicator,
                scope: me
            })
        });
        
        toolbarCfg.items.push({
            tooltip: "{{i18n PLUGINS_FORMS_TREE_INDICATORS_TOOTIP}}",
            iconCls: 'a-btn-glyph ametysicon-puzzle33 size-16',
            cls: 'a-btn-light',
            itemId: 'indicators-menu',
            menu: {
                xtype: 'menu',
                items: menuItems
            }
        });
        
        return toolbarCfg;
    },
    
    /**
     * Listener when an indicator is checked/unchecked
     * @param {Ext.menu.CheckItem} item the item
     * @param {Boolean} checked the checked status
     * @private
     */
    _selectIndicator: function(item, checked)
    {
        var oldValue = this._activeIndicators[item.name];
        var hasChanges = oldValue !== checked;
        this._activeIndicators[item.name] = checked;
        
        if (hasChanges)
        {
            this.saveState();
            this.view.refresh();
        }
    },
    
    /**
     * Callback function after retrieving root node
     * @param {Object} response The server response
     * @param {Object} args the callback arguments
     * @private
     */
    _getRootNodeCb: function (response, args)
    {
        this.setRootNode({
            expanded: true,
            type: 'root',
            text: response.title,
            id: response.id,
            isConfigured: response.isConfigured
        });

        if (Ext.isFunction (args.callback))
        {
            this.store.getRoot().on('expand', function() { args.callback (response.id); }, this, { single: true });
        }
    },
    
    /**
     * Listener before edit
     * @param {Ext.grid.plugin.CellEditing} editor the cell editor
     * @param {Object} context The editing context
     * @private
     */
    _onBeforeEdit: function(editor, context)
    {
        var record = context.record;
        return record && !record.isRoot() && record.get('canWrite');
    },
    
    /**
     * Listener called after cell editing
     * @param {Ext.grid.plugin.CellEditing} editor The cell editor
     * @param {Object} context The editing context
     * @private
     */
    _onEdit: function(editor, context)
    {
        var record = context.record,
            id = record.getId(),
            newName = context.value,
            oldName = context.originalValue;
        
        if (newName != oldName)
        {
            if(record.getData().type == "page")
            {
                Ametys.plugins.forms.dao.FormPageDAO.renamePage([id, newName], renameCb, { ignoreCallbackOnError: false })
            }
            
            if(record.getData().type == "question")
            {
                Ametys.plugins.forms.dao.FormQuestionDAO.renameQuestion([id, newName], renameCb, { ignoreCallbackOnError: false })
            }
            
            function renameCb(response)
            {
                if (!response || !response.id)
                {
                    // edit failed
                    record.beginEdit();
                    record.set('text', oldName);
                    record.endEdit();
                    record.commit();
                }
            }
        }
    },
    
    /**
     * Listener on creation message.
     * @param {Ametys.message.Message} message The creation message.
     * @private
     */
    _onMessageCreated: function(message)
    {
        var target = message.getTarget(Ametys.message.MessageTarget.FORM_PAGE),
            questionTarget = message.getTarget(Ametys.message.MessageTarget.FORM_QUESTION);
            
        var editingPlugin = this.editingPlugin;
        
        if (target)
        {
            this.onFormComponentCreated(target.getParameters().id, beginEdit);
        }
        if ( questionTarget)
        {
            this.onFormComponentCreated(questionTarget.getParameters().id, beginEdit);
        }
        function beginEdit(created)
        {
            // FIXME startEdit is deprecated, but I was not able to found the "new" way to do it
            editingPlugin.startEdit(created, 0);
        }
    },
    
    /**
     * Listener on form component created
     * @param {Ametys.message.MessageTarget} target The message target
     * @param {Function} cb The callback
     * @private
     */
    onFormComponentCreated: function(createdId, cb)
    {
        var tree = this,
            store = tree.getStore(),
            selModel = tree.getSelectionModel(),
            record = store.getById(createdId);
            selModel.deselectAll();
        
        if (record)
        {
            selModel.select(record);
        }
        else
        {
            Ametys.plugins.forms.dao.FormPageDAO.getIdsOfPath([createdId], Ext.bind(expandAndSelectUntil, this));            
        }
        
        function expandAndSelectUntil(pathIds)
        {
            pathIds.splice(0, 0, this._formId);
            loadNextPathEl([store.getById(this._formId)]);
            
            function loadNextPathEl(amongRecords)
            {
                if (pathIds.length)
                {
                    var pathId = pathIds.shift();
                    var childNode = Ext.Array.findBy(amongRecords, function(childRecord) {
                        return childRecord.getId() == pathId;
                    });
                    if (childNode)
                    {
                        store.load({
                            node: childNode, 
                            callback: function(records) {
                                expand(childNode, records);
                            }
                        });
                    }
                }
                else
                {
                    var created = store.getById(createdId);
                    if (created)
                    {
                        selModel.select(created);
                        created.expand();
                        cb(created);
                    }
                }
            }
            
            function expand(loadedNode, records)
            {
                tree.expandNode(loadedNode, false, function() {
                    loadNextPathEl(records);
                });
            }
        }
    },
    
    /**
     * Refresh the page node
     * @param {String} pageId The page id.
     * @private
     */
    refreshPageNode: function(pageId)
    {
        var tree = this,
            store = tree.getStore(),
            record = store.getById(pageId);
            
        if (record)
        {
            this._refreshNode(record);
        }
    },
    
    /**
     * Listener on update messages
     * @param {Ametys.message.Message} message The edition message.
     * @private
     */
    _onUpdatedMessage: function(message)
    {
        var questionTarget = message.getTarget(Ametys.message.MessageTarget.FORM_QUESTION),
            pageTarget = message.getTarget(Ametys.message.MessageTarget.FORM_PAGE),
            formTarget = message.getTarget(Ametys.message.MessageTarget.FORM_TARGET);
        
        if (questionTarget)
        {
            this.reloadParent(questionTarget, true, Ext.bind(this._refreshIsConfiguedAttribute, this));
        }
        else if (pageTarget || formTarget)
        {
            this._setLocalName(pageTarget || formTarget);
        }
    },
    
    /**
     * Set the isConfigured attribute to the page
     * @param {String} pageId the page id
     * @private
     */
    _refreshIsConfiguedAttribute: function(pageId)
    {
        var parentPageNode = this.getStore().getNodeById(pageId);
        var isConfigured = this._isConfiguredPage(parentPageNode);
        parentPageNode.set("isConfigured", isConfigured);
        
        this._refreshNode(parentPageNode)
    },
    
    /**
     * Return true if the page node is well configured
     * @param {Object} pageNode the page node
     * @private
     */
    _isConfiguredPage(pageNode)
    {
        var isConfigured = true;
        for(var i in pageNode.childNodes)
        {
            var child = pageNode.childNodes[i];
            if (!child.get("isConfigured"))
            {
                isConfigured = false;
            }
        }
        
        return isConfigured;
    },
    
    /**
     * Set node local name
     * @param {Ametys.message.MessageTarget} target The message target
     * @private
     */
    _setLocalName: function(target)
    {
        var store = this.getStore(),
            node = store.getNodeById(target.getParameters().id);
        
        if (node)
        {
            var newName = target.getParameters().title;
            node.beginEdit();
            node.set('text', newName);
            node.endEdit();
            node.commit();
        }
    },
        
    /**
     * Reloads parent node
     * @param {Ametys.message.MessageTarget} target The message target
     * @param {Boolean} keepSelection true to keep old selection
     * @public
     */
    reloadParent: function(target, keepSelection, cb)
    {
        var id = target.getParameters().id,
            store = this.getStore(),
            node = store.getNodeById(id),
            parentId = node && node.get('parentId'),
            parentNode = store.getNodeById(parentId);
        if (parentNode)
        {
            var tree = this;
            var oldSelIds = tree.getSelection()
                    .map(function(record) {
                        return record.getId();
                    });
            
            store.load({node: parentNode, callback: keepSelection ? keepSelectionFn : Ext.bind(cb, this, [parentId])});
            
            function keepSelectionFn()
            {
                var selModel = tree.getSelectionModel();
                var oldSel = oldSelIds
                    .map(function(id) {
                        return store.getNodeById(id);
                    })
                    .filter(function(record) { return record != null });
                selModel.deselectAll();
                selModel.select(oldSel);
                cb(parentId);
            }
        }
    },
    
    /**
     * This listener is called on 'keyup' event on filter input field.
     * Filters the tree by text input.
     * @param {Ext.form.Field} field The field
     * @private
     */
    _searchFilter: function (field)
    {
        var value = new String(field.getValue()).trim();
        this._filterField = field;
        
        if (this._filterValue == value)
        {
            // Do nothing
            return;
        }
        
        this._filterValue = value;
        
        if (value.length > 2)
        {   
            this._addSearchFilter (value);
        }
        else
        {
            this._clearFilter();
        }
    },
    
    /**
     * Add filter on title on the store
     * @private
     */
    _addSearchFilter: function(value)
    {
        var id = 'search',
            filterFactory = this._availableFilters[id],
            filterFn = filterFactory(value),
            filterCfg = {
                id: id,
                filterFn: filterFn
            };
        this.getStore().removeFilter(id);
        this.getStore().addFilter(filterCfg);
    },
    
    /**
     * Clear the filter search if exists
     * @private
     */
    _clearSearchFilter: function()
    {
        this._clearFilter();
        
        if (this._filterField)
        {
            this._filterField.reset();
        }
        
        this._filterValue = null;
        
        var selection = this.getSelectionModel().getSelection()[0];
        if (selection)
        {
            this.ensureVisible(selection.getPath('name'), {field: 'name'});
        }
    },
    
    /**
     * Clear all filters
     * @private
     */
    _clearFilter: function ()
    {
        this.getStore().clearFilter();
        
        var selection = this.getSelectionModel().getSelection()[0];
        if (selection)
        {
            this.ensureVisible(selection.getPath());
        }
    },
    
    /**
     * Collapse recursively the children of the node, then select the collapsed node.
     * @param {Ext.data.NodeInterface} [node] The node the collapse. Can be null to collapse the whole tree.
     * @private
     */
    _collapseNode: function (node)
    {
        node = node || this.getRootNode();
        node.collapseChildren(true);
        this.getSelectionModel().select(node);
    },
    
    /**
     * This function reload the current node
     * @private
     */
    _refreshCurrentNode: function ()
    {
        var selection = this.getSelectionModel().getSelection();
        node = selection.length > 0 ? selection[0] : null;
        
        // Workaround - Refresh selection in case node is not existing anymore (deleted by another user for example).
        this.getSelectionModel().deselect(node, true);
        this.getSelectionModel().select(node);
            
        if (node != null)
        {
            this._refreshNode(node);
        }
    },
    
    /**
     * This function reload the given node
     * @private
     */
    _refreshNode: function(node)
    {
        // Set leaf to false, to allow children to be added during the load. Leaf will be set to true again if needed after the load.
        node.set('leaf', false);
        
        var me = this;
        this.getStore().load({
            node: node,
            callback: function () {
                me._updateNodeUI(node);
                
                if (me._filterValue && me._filterValue.length > 2)
                {
                    me._addSearchFilter (this._filterValue);
                }
                else
                {
                    Ext.defer(this.expandNode, 200, this, [node]);
                }
            },
            scope: this
        });
    },
    
    /**
     * Updates the node UI (icon, text, ...)
     * @param {Ext.data.NodeInterface} node the node
     * @private
     */
    _updateNodeUI: function(node)
    {
        this.view.refreshNode(node);
    }
});