/*
 *  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 form directories and forms.
 * @private
 */
Ext.define('Ametys.plugins.forms.tree.FormDirectoriesTree', {
    extend: 'Ext.tree.Panel',
    
    statics: {
        /**
         * Function to render form's title in result grid
         * @param {Object} title The data title
         * @param {Object} metaData A collection of metadata about the current cell
         * @param {Ext.data.Model} record The record
         * @private
         */
        renderTitle: function(title, metaData, record)
        {
            var data = record.data;
            if (data.isForm && !data.isConfigured)
            {
                return '<span title="{{i18n PLUGINS_FORMS_TREE_WARNING_NOT_CONFIGURED}}" >' + Ext.String.escapeHtml(title) + '</span>';
            }
            return Ext.String.escapeHtml(title);
        },
        
        /**
         * Page renderer
         * @param {Object} value The data value
         * @param {Object} metaData A collection of metadata about the current cell
         * @param {Ext.data.Model} record The record
         * @return {String} The html representation of the page
         */
        renderPage: function(value, metaData, record)
        {
            if (record.get('isForm') == true)
            {
                var html = "";
                for (var index in value)
                {
                    var val = value[index];
                    var toolId = val.isPage ? 'uitool-page' : 'uitool-sitemappage';
                    var title = val.isPage ? val.title : '{{i18n plugin.web:PLUGINS_WEB_SITEMAP_TREE_ROOT_LABEL}}';
                    html += '<a href="javascript:(function(){Ametys.tool.ToolsManager.openTool(\'' + toolId + '\', {id:\'' + val.id + '\'});})()">' + Ext.String.escapeHtml(title) + '</a> <br/>';
                }
                return html;
            }
        }
    },
    
    /**
     * @cfg {Boolean} onlyDirectories true to only have the directories
     */
    /**
     * @cfg {String} profile The profile ('read_access' or 'write_access') filter. By default, if null, the server will treat the request as 'read_access'
     */
    
    /**
     * @private
     * @property {Boolean} _isComputingRootNode To prevent multiple requests to refresh the root node.
     */
    _isComputingRootNode: false,
    
    constructor: function(config)
    {
        config.store = this._createStore(config);
        
        config.allowEdition = Ext.isString(config.allowEdition) 
            ? config.allowEdition == "true" 
            : (config.allowEdition || false);
            
        if (config.onlyDirectories !== true)
        {
            config.dockedItems = [this._getToolbarCfg(config)];
        }
        
        var plugins = config.plugins;
        config.plugins = Ext.isArray(plugins) ? plugins : (plugins == null ? [] : [plugins]);
        
        if (config.allowEdition)
        {
            config.plugins.push({
                ptype: 'cellediting',
                clicksToEdit: 1,
                editAfterSelect: true,
                listeners: {
                    'beforeedit': this._onBeforeEdit,
                    'edit': this._onEdit,
                    scope: this
                }
            });
        }
        
        config.viewConfig = config.viewConfig || {};
        Ext.apply(config.viewConfig, {
            plugins: {
                ptype: 'ametystreeviewdragdrop',
                containerScroll: true,
                appendOnly: true,
                sortOnDrop: true,
                expandDelay: 500,
                setAmetysDragInfos: Ext.bind(this.getDragInfo, this),
                setAmetysDropZoneInfos: Ext.bind(this.getDropInfo, this)
            }
        });
        
        this.callParent(arguments);
        
        // expand root node
        var tree = this,
            store = this.getStore(),
            root = store.getRoot();
        store.load({
            callback: function() {
                tree.expandNode(root);
            }}
        );
        
        Ametys.message.MessageBus.on(Ametys.message.Message.CREATED, this._onCreatedMessage, this);
        Ametys.message.MessageBus.on(Ametys.message.Message.MODIFIED, this._onUpdatedMessage, this);
        Ametys.message.MessageBus.on(Ametys.message.Message.MOVED, this._onMovedMessage, this);
    },
    
    onDestroy: function()
    {
        Ametys.message.MessageBus.unAll(this);
        this.callParent(arguments);
    },
    
    /**
     * Refresh the whole tree
     * @param {Function} [callback] function to call after refreshing
     */
    initRootNodeParameter: function (callback)
    {
        if (this._isComputingRootNode)
        {
            return;
        }
        
        this._isComputingRootNode = true;
        var siteName = Ametys.getAppParameter("siteName");
        Ametys.data.ServerComm.callMethod({
            role: "org.ametys.plugins.forms.dao.FormDirectoryDAO",
            methodName: "getRootProperties",
            parameters: [siteName],
            callback: {
                scope: this,
                handler: this._getRootNodeCb,
                arguments: {
                    callback: callback
                }
            },
            errorMessage: {
                category: this.self.getName(),
                msg: "{{i18n DAOS_FORM_ROOT_ERROR}}"
            },
            waitMessage: true
        });
    },
    
    /**
     * @private
     * Callback function after retrieving root node
     * @param {Object} response The server response
     * @param {Object} args the callback arguments
     */
    _getRootNodeCb: function (response, args)
    {
        this._isComputingRootNode = false;
        this.setRootNode({
            name: response.title,
            canWrite: response.canWrite,
            canEdit: response.canEdit,
            canRename: response.canRename,
            canEditRight: response.canEditRight,
            expanded: this._showEmptyDirectories != null, // only load when we know if current user has right on empty directories or not
            text: "{{i18n PLUGINS_FORMS_UITOOL_FORMS_ROOT_NAME}}"
        });

        if (Ext.isFunction (args.callback))
        {
            this.store.getRoot().on('expand', function() { args.callback (response.id); }, this, { single: true });
        }
    },
    
    /**
     * @private
     * Creates the forms directory store
     * @param {Object} config The configuration
     * @param {Boolean} [config.onlyDirectories=false] See {@link #cfg-onlyDirectories}
     * @param {String} [config.profile=null] See {@link #cfg-profile}
     * @return {Ext.data.Store} The form store
     */
    _createStore: function(config)
    {
        var onlyDirectories = config.onlyDirectories === true,
            onlyConfiguredForm = config.onlyConfiguredForm === true
            profile = config.profile,
            siteName = Ametys.getAppParameter("siteName");
            
        var store = Ext.create('Ext.data.TreeStore', {
            model: 'Ametys.plugins.forms.tree.FormsTree.FormDirectoryEntry',
            sorters: [
                {property: 'text', direction:'ASC'}
            ],
            
            proxy: {
                type: 'ametys',
                plugin: 'forms',
                url: 'forms/list.json',
                reader: {
                    type: 'json',
                    rootProperty: 'forms'
                },
                extraParams: {
                    onlyDirectories: onlyDirectories,
                    onlyConfiguredForm: onlyConfiguredForm,
                    profile: profile,
                    siteName : siteName
                }
            },
            autoLoad: true,
            
            folderSort: true,
            root: {
                id: 'root',
                isForm: false,
                expanded: true,
                text: "{{i18n PLUGINS_FORMS_UITOOL_FORMS_ROOT_NAME}}"
            }
        });
        
        return store;
    },
    
    /**
     * @private
     * Gets the toolbar configuration
     * @param {Object} config The configuration
     * @return {Object} The toolbar configuration
     */
    _getToolbarCfg: function(config)
    {
        return {
            dock: 'top',
            xtype: 'toolbar',
            layout: {
                type: 'hbox',
                align: 'stretch'
            },
            defaultType: 'button',
            items: [{
                // Search input
                xtype: 'textfield',
                cls: 'ametys',
                flex: 1,
                maxWidth: 300,
                itemId: 'search-filter-input',
                emptyText: "{{i18n PLUGINS_FORMS_TREE_FILTER_EMPTY_TEXT}}",
                enableKeyEvents: true,
                minLength: 3,
                minLengthText: "{{i18n PLUGINS_FORMS_TREE_FILTER_MIN_LENGTH_INVALID}}",
                msgTarget: 'qtip',
                listeners: {change: Ext.Function.createBuffered(this._searchFilter, 500, this)}
            }]
        }
    },
    
    /**
     * @private
     * Filters forms by input field value.
     * @param {Ext.form.Field} field The field
     */
    _searchFilter: function (field)
    {
        var value = Ext.String.trim(field.getValue());
        if (this._filterValue == value)
        {
            // Do nothing
            return;
        }
        
        this._filterValue = value;
        
        if (value.length > 2)
        {
            this.expandAll();
            this._addSearchFilter();
        }
        else
        {
            this._removeSearchFilter();
        }
    },
    
    /**
     * @private
     * Add filter on title on the store
     */
    _addSearchFilter: function()
    {
        var id = 'search',
            filterFactory = this._availableFilters[id],
            filterFn = filterFactory(this._filterValue),
            filterCfg = {
                id: id,
                filterFn: filterFn
            };
        this.getStore().removeFilter(id);
        this.getStore().addFilter(filterCfg);
    },
    
    /**
     * @private
     * Remove the filter on title on the store
     */
    _removeSearchFilter: function()
    {
        this._filterValue = null;
        this.getStore().removeFilter('search');
    },
    
    
    /**
     * @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() {
                    var title = Ext.String.deemphasize(record.get('text')).toLowerCase(),
                        search = Ext.String.deemphasize(searchedValue).toLowerCase();
                    return new RegExp(".*(" + search + ").*").test(title);
                }
                
                var isDirectory = !record.get('isForm'),
                    show = isDirectory || isLike();
                return show;
            }
        }
    },
    
    /**
     * @private
     * This event is thrown by the getDragData to add the 'source' of the drag.
     * @param {Object} item The default drag data that will be transmitted. You have to add a 'source' item in it: 
     * @param {Ametys.relation.RelationPoint} item.source The source (in the relation way) of the drag operation. 
     */
    getDragInfo: function(item)
    {
        var targets = item.records
            .filter(hasRight)
            .map(this.getMessageTargetConfiguration)
            .filter(function(cfg) {return cfg != null;});
            
        function hasRight(record)
        {
            if (record.get('isForm'))
            {
                return record.get('canWrite');
            }
            else
            {
                return record.get('canEdit');
            }
        }
    
        if (targets.length > 0)
        {
            item.source = {
                relationTypes: [Ametys.relation.Relation.MOVE], 
                targets: targets
            };
        }
    },
    
    /**
     * @private
     * This event is thrown before the beforeDrop event and create the target of the drop operation relation.
     * @param {Ext.data.Model[]} targetRecords The target records of the drop operation.
     * @param {Object} item The default drag data that will be transmitted. You have to add a 'target' item in it: 
     * @param {Object} item.target The target (in the relation way) of the drop operation. A Ametys.relation.RelationPoint config.   
     */ 
    getDropInfo: function(targetRecords, item)
    {
        var targets = targetRecords
            .map(this.getMessageTargetConfiguration)
            .filter(function(cfg) {return cfg != null});

        if (targets.length > 0)
        {
            item.target = {
                relationTypes: [Ametys.relation.Relation.MOVE], 
                targets: targets
            };
        }
    },
    
    /**
     * @private
     * Gets the configuration for creating message target
     * @param {Ext.data.Model} record The tree record to convert to its Ametys.message.MessageTarget configuration
     * @return {Object} The configuration to create a Ametys.message.MessageTarget. Can be null, if the record is null or not relevant to be a messagetarget.
     */
    getMessageTargetConfiguration: function(record)
    {
        if (record == null)
        {
            return null;
        }
        else if (record.get('isForm'))
        {
            return {
                id: Ametys.message.MessageTarget.FORM_TARGET,
                parameters: {
                    id: record.getId()
                }
            };
        }
        else
        {
            return {
                id: Ametys.message.MessageTarget.FORM_DIRECTORY,
                parameters: {
                    id: record.getId(),
                    name: record.get('text'),
                    canWrite: record.get('canWrite'),
                    canEdit: record.get('canEdit'),
                    canRename: record.get('canRename'),
                    canEditRight: record.get('canEditRight')
                }
            };
        }
    },
    
    /**
     * @private
     * Add filter on empty directories on the store
     */
    _addEmptyDirectoriesFilter: function()
    {
        var id = 'emptyDirectories',
            filterFn = this._availableFilters[id],
            filterCfg = {
                id: id,
                filterFn: filterFn
            };
        this.getStore().removeFilter(id);
        this.getStore().addFilter(filterCfg);
    },
    
    /**
     * @private
     * Listener called before cell editing 
     * @param {Ext.grid.plugin.CellEditing} editor The cell editor
     * @param {Object} context An editing context event with the following properties:
     * @param {Ext.grid.Panel}         context.grid The owning grid Panel.
     * @param {Ext.data.Model}         context.record The record being edited.
     * @param {String}                 context.field The name of the field being edited.
     * @param {Mixed}                  context.value The field's current value.
     * @param {HTMLElement}            context.row The grid row element.
     * @param {Ext.grid.column.Column} context.column The {@link Ext.grid.column.Column} being edited.
     * @param {Number}                 context.rowIdx The index of the row being edited.
     * @param {Number}                 context.colIdx The index of the column being edited.
     * @param {Boolean}                context.cancel Set this to `true` to cancel the edit or return false from your handler.
     */
    _onBeforeEdit: function(editor, context)
    {
        var record = context.record;
        return record && !record.isRoot() && (record.get("isForm") && record.get('canWrite') || !record.get("isForm") && record.get('canRename'));
    },
    
    /**
     * @private
     * Listener called after cell editing
     * @param {Ext.grid.plugin.CellEditing} editor The cell editor
     * @param {Object} context An editing context with the following properties:
     * @param {Ext.grid.Panel}         context.grid The owning grid Panel.
     * @param {Ext.data.Model}         context.record The record being edited.
     * @param {String}                 context.field The name of the field being edited.
     * @param {Mixed}                  context.value The field's current value.
     * @param {HTMLElement}            context.row The grid row element.
     * @param {Ext.grid.column.Column} context.column The {@link Ext.grid.column.Column} being edited.
     * @param {Number}                 context.rowIdx The index of the row being edited.
     * @param {Number}                 context.colIdx The index of the column being edited.
     * @param {Mixed}                  context.originalValue The original value before being edited.
     */
    _onEdit: function(editor, context)
    {
        var record = context.record,
            id = record.getId(),
            newName = context.value,
            oldName = context.originalValue;
        
        if (newName != oldName)
        {
            if(record.getData().isForm)
            {
                Ametys.plugins.forms.dao.FormDAO.renameForm([id, newName], renameCb, { ignoreCallbackOnError: false })
            }
            else
            {
                Ametys.plugins.forms.dao.FormDirectoryDAO.renameFormDirectory([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 create messages
     * @param {Ametys.message.Message} message The creation message.
     * @private
     */
    _onCreatedMessage: function(message)
    {
        var formTarget = message.getTarget(Ametys.message.MessageTarget.FORM_TARGET);
        var formDirectoryTarget = message.getTarget(Ametys.message.MessageTarget.FORM_DIRECTORY);
        var editingPlugin = this.editingPlugin;
        if (formTarget)
        {
            this._onFormCreatedOrMoved(formTarget, beginEdit);
        }
        else if (formDirectoryTarget)
        {
            this._onFormCreatedOrMoved(formDirectoryTarget, beginEdit);
        }
        
        function beginEdit(created)
        {
            // FIXME startEdit is deprecated, but I was not able to found the "new" way to do it
            if (editingPlugin)
            {
                editingPlugin.startEdit(created, 0);
            }
        }
    },
    
    /**
     * Listener on moved messages
     * @param {Ametys.message.Message} message The moved message.
     * @private
     */
    _onMovedMessage : function(message)
    {
        var formTarget = message.getTarget(Ametys.message.MessageTarget.FORM_TARGET);
        var formDirectoryTarget = message.getTarget(Ametys.message.MessageTarget.FORM_DIRECTORY);
        if (formTarget)
        {
            this._onFormCreatedOrMoved(formTarget, Ext.emptyFn);
        }
        else if (formDirectoryTarget)
        {
            this._onFormCreatedOrMoved(formDirectoryTarget, Ext.emptyFn);
        }
    },
    
    /**
     * Called when a Form or Form directory is created
     * @param {Ametys.message.MessageTarget} target The Form message target
     * @param {Function} cb The callback
     * @private
     */
    _onFormCreatedOrMoved: function(target, cb)
    {
        var store = this.getStore();
        var selModel = this.getSelectionModel();
        var createdId = target.getParameters().id;
        var node = store.getNodeById(target.getParameters().parentId);
        if (node == null)
        {
            node = this.getRootNode();
        }
        
        node.set('hasChildren', true);
        node.set('expanded', true);
        
        store.load({
            node: node,
            callback: function() {
                selModel.select(node);
                var created = store.getById(createdId);
                if (created)
                {
                    selModel.select(created);
                    cb(created);
                }
            }
        });
    },
    
    /**
     * Listener on update messages
     * @param {Ametys.message.Message} message The edition message.
     * @private
     */
    _onUpdatedMessage: function(message)
    {
        var formTarget = message.getTarget(Ametys.message.MessageTarget.FORM_TARGET),
            formDirectoryTarget = message.getTarget(Ametys.message.MessageTarget.FORM_DIRECTORY),
            target = null,
            newName;
        
        if (formTarget)
        {
            target = formTarget;
            newName = formTarget.getParameters().title;
        }
        else if (formDirectoryTarget)
        {
            target = formDirectoryTarget;
            newName = formDirectoryTarget.getParameters().name;
        }
        
        if (target)
        {
            var store = this.getStore(),
                node = store.getNodeById(target.getParameters().id);
            
            if (node)
            {
                node.beginEdit();
                node.set('text', newName);
                node.endEdit();
                node.commit();
                store.sort();
            }
        }
    },
    
    /**
     * Reloads parent node
     * @param {Ametys.message.MessageTarget} target The message target
     * @param {Boolean} keepSelection true to keep old selection
     */
    reloadParent: function(id, keepSelection)
    {
        var 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.emptyFn});
            
            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);
            }
        }
    },
    
    /**
     * Refresh tree and select the last selected node
     * @param cb the callback function
     */
    refresh: function(cb)
    {
        var selection = this.getSelectionModel().getSelection();
        var node = selection.length > 0 ? selection[0] : null;
        var selModel = this.getSelectionModel();
            
        this.getStore().load({
            scope: this,
            callback: function () {
                if (node != null)
                {
                    var path = node.getPath("name");
                    this.expandPath(path, "name", null, function (successful, node) {
                        if (successful)
                        {
                            selModel.select(node);
                            this.getView().focusNode(node);
                        }
                        cb()
                    })
                }
                else
                {
                    cb()
                }
            }
        });
    }
});