/*
 *  Copyright 2024 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.
 */

/**
 * Field that displays a textarea in which it is possible to mention a user
 */
Ext.define('Ametys.form.field.TextAreaWithMentions', {
    extend: 'Ametys.form.AbstractFieldsWrapper',
    
    /** @cfg {String} triggerChar=@ The char that starts mentions */
    triggerChar: '@',
    /** @cfg {Number} minChars=2 The number of chars to type before starting search */
    minChars: 2,
    /** @cfg {Boolean} allowRepeat=true Can one user be mentioned several times */
    allowRepeat: true,
    /** @cfg {Boolean} showAvatars=true Should display user avatars in mentions */
    showAvatars: true,
    /** @cfg {Number} maxResults=10 The max number of results displayed in the combobox */
    maxResults: 10,
    /** @cfg {Function} renderMentionedUserFn=null The function to display mentioned user */
    renderMentionedUserFn: null,

    /** @cfg {Object} textareaConfig The configuration object for the textarea. Note that many configuration can be set directly here and will we broadcasted to underlying field (allowBlank...) */
    /** @cfg {Boolean} allowBlank This property is copied to underlying text fields. See {@link Ext.form.field.TextArea#cfg-allowBlank} */
    /** @cfg {Boolean} blankText This property is copied to underlying text fields. See {@link Ext.form.field.TextArea#cfg-blankText} */
    /** @cfg {String} emptyText This property is copied to underlying textarea. See {@link Ext.form.field.TextArea#cfg-emptyText} */
    /** @cfg {String} invalidText This property is copied to underlying text fields. See {@link Ext.form.field.Text#cfg-invalidText} */
    /** @cfg {RegExp} maskRe This property is copied to underlying text fields. See {@link Ext.form.field.TextArea#cfg-maskRe} */
    /** @cfg {Number} maxLength This property is copied to underlying text fields. See {@link Ext.form.field.TextArea#cfg-maxLength} */
    /** @cfg {String} maxLengthText This property is copied to underlying text fields. See {@link Ext.form.field.TextArea#cfg-maxLengthText} */
    /** @cfg {Number} minLength This property is copied to underlying text fields. See {@link Ext.form.field.TextArea#cfg-minLength} */
    /** @cfg {String} minLengthText This property is copied to underlying text fields. See {@link Ext.form.field.TextArea#cfg-minLengthText} */
    /** @cfg {RegExp} regex This property is copied to underlying text fields. See {@link Ext.form.field.TextArea#cfg-regex} */
    /** @cfg {String} regexText This property is copied to underlying text fields. See {@link Ext.form.field.TextArea#cfg-regexText} */
    /** @cfg {Boolean} selectOnFocus This property is copied to underlying text fields. See {@link Ext.form.field.TextArea#cfg-selectOnFocus} */
    /** @cfg {Number} size This property is copied to underlying text fields. See {@link Ext.form.field.TextArea#cfg-size} */
    /** @cfg {RegExp} stripCharsRe This property is copied to underlying text fields. See {@link Ext.form.field.TextArea#cfg-stripCharsRe} */
    
    displayField: 'fullname',
    valueField: 'value',
    
    alias: ['widget.ametystextarea.withmentions'],
    
    layout: 'fit',
    
    _currentDataQuery: '',
    
    constructor: function(config)
    {
        config.cls = Ext.Array.from(config.cls);
        config.cls.push('a-textarea-with-mentions');
        
        this.renderMentionedUserFn = config.renderMentionedUserFn || this._renderMentionedUser;
        
        this._overlay = Ext.create("Ext.Component", {
            cls: 'a-textarea-with-mentions-overlay'
        });
        
        this._overlayBg = Ext.create("Ext.Component", {
            cls: 'a-textarea-with-mentions-overlay-bg'
        });
        
        var textareaConfig = this.textareaConfig || {};
            textareaConfig.cls = 'a-textarea-with-mentions-textarea';
            textareaConfig.enableKeyEvents = true;
            textareaConfig.listeners  = {
                'keydown': this._onKeydown, 
                'keypress': this._onKeypress, 
                'click': this._onClick, 
                'change': this._onChange, 
                'scroll': {
                    fn: this._onTextareaScroll,
                    element: 'inputEl'
                },
                scope: this
            };

        var propertiesToCopy = this._getConfigPropertiesToCopy();
        this._copyPropIfDefined(textareaConfig, propertiesToCopy, config);
        
        this._textarea = Ext.create("Ext.form.field.TextArea", textareaConfig);
        
        this._combo = Ext.create("Ext.form.field.ComboBox", {
            valueField: 'value',
            displayField: 'displayName',
            cls: 'a-textarea-with-mentions-combo',
            minChars: 0,
            store: this.getStore(),
            listeners: {
                'focus': this._onComboFocus,
                'select': this._onComboSelect,
                scope: this
            }
        });
        this._combo.onFocusLeave = function() {
            // nothing
        };
        
        this.reset();
        
        config.items = [ this._textarea, this._combo, this._overlay, this._overlayBg ];
        
        this.callParent(arguments);
        
        this.on('blur', this._onBlur, this);
    },
    
    /**
     * @protected
     * Retrieves the name of the configuration properties to copy to the underlying field 
     * @return {String[]} The name of the properties
     */
    _getConfigPropertiesToCopy: function()
    {
        return ['allowBlank', 'blankText', 'emptyText', 'invalidText', 'maskRe', 'maxLength', 'maxLengthText', 'minLength', 'minLengthText', 'regex', 'regexText', 'selectOnFocus', 'size', 'stripCharsRe', 'fieldStyle'];
    },
    
    _onComboFocus: function()
    {
        this._textarea.focus();
    },
    
    _onComboSelect: function(combo, record)
    {
        if (record)
        {
            this._addMention(record);
            combo.reset();
        }
    },
    
    _onTextareaScroll: function()
    {
        if (this._overlay.el)
        {
            let scrollTop = this._textarea.inputEl.getScrollTop();
            this._overlay.el.setScrollTop(scrollTop);
            this._overlayBg.el.setScrollTop(scrollTop);
        }
    },
    
    getStore: function()
    {
        return Ext.create('Ext.data.Store', {
            model: 'Ametys.form.field.TextAreaWithMentions.Users',
            proxy: {
                type: 'ametys',
                role: "org.ametys.plugins.core.user.UserDAO",
                methodName: "searchUsersByContexts",
                methodArguments: ['contexts', 'limit', 'offset', 'searchCriteria', 'limitedToStoredUserData'],
                cancelOutdatedRequest: true,
                reader: {
                    type: 'json',
                    rootProperty: 'users'
                }
            },
            
            pageSize: this.maxResult,
            sortOnLoad: true,
            sorters: [{property: 'displayName', direction:'ASC'}],
            
            listeners: {
                beforeload: {fn: this._onStoreBeforeLoad, scope: this}
            }
        });
    },
    
    /**
     * Set the request parameters before loading the store.
     * @param {Ext.data.Store} store The store.
     * @param {Ext.data.operation.Operation} operation The Ext.data.Operation object that will be passed to the Proxy to load the Store.
     * @protected
     */
    _onStoreBeforeLoad: function(store, operation)
    {
        var proxy = operation.getProxy();
        
        proxy.setExtraParam('limit', this.maxResult);
        proxy.setExtraParam('offset', 0);
        proxy.setExtraParam('searchCriteria', operation.getParams().query);
    }, 
    
    _updateValues: function() 
    {
        function regexpEncode(str) 
        {
            return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
        }
        function encode(str)
        {
            return str.replaceAll('&', '&amp;').replaceAll('<', '&lt;')
        }
        
        let syntaxMessage = this._textarea.getValue();
        
        for (let mention of this._mentionsCollection)
        {
            let textSyntax = "@(" + mention.get(this.valueField) + ")";
            syntaxMessage = syntaxMessage.replace(new RegExp(regexpEncode(this.triggerChar + mention.get(this.displayField)), 'g'), textSyntax);
        }
        
        let mentionText = encode(syntaxMessage); //Encode the syntaxMessage

        for (let mention of this._mentionsCollection)
        {
            let textSyntax = "@(" + mention.get(this.valueField) + ")";
            var textHighlight = this.renderMentionedUserFn(mention, this.displayField, this.triggerChar);

            mentionText = mentionText.replace(new RegExp(regexpEncode(textSyntax), 'g'), textHighlight);
        }

        mentionText = mentionText.replace(/\n/g, '<br />'); //Replace the escape character for <br />
        mentionText = mentionText.replace(/ {2}/g, '&nbsp; '); //Replace the 2 preceding token to &nbsp;

        this._messageText = syntaxMessage;
        if (/<br\s*\/>\s*$/.test(mentionText))
        {
            mentionText += "&#160";
        }
        this._overlay.update("<div>" + mentionText + "</div>");
        this._overlayBg.update("<div>" + mentionText + "</div>");
        this._onTextareaScroll();
    },
    
    _renderMentionedUser: function(mention, displayField, prefixChar)
    {
        let currentUser = Ametys.getAppParameter('user');
        let isCurrentUser = currentUser.login === mention.get("login") && currentUser.population === mention.get("populationId");
        let highlightCls = isCurrentUser ? "current-user" : "";
        
        return '<strong class="' + highlightCls + '"><span>' + prefixChar + mention.get(displayField) + '</span></strong>';
    },
    
    _updateMentionsCollection: function() 
    {
        let inputText = this._textarea.getValue();
        
        this._mentionsCollection = this._mentionsCollection.filter(m => inputText.indexOf(m.get(this.displayField)) != -1); 
    },
    
    _addMention: function(record)
    {
        let currentMessage = this._textarea.getValue();
        let textareaInputEl = this._textarea.inputEl.dom;
        let caretStart = textareaInputEl.selectionStart;
        let shortestDistance = false;
        let bestLastIndex = false;
        
        // Using a regex to figure out positions
        let regex = new RegExp("\\" + this.triggerChar + this._currentDataQuery, "gi");

        while (regex.exec(currentMessage)) 
        {
            if (shortestDistance === false || Math.abs(regex.lastIndex - caretStart) < shortestDistance) 
            {
                shortestDistance = Math.abs(regex.lastIndex - caretStart);
                bestLastIndex = regex.lastIndex;
            }
        }

        let startCaretPosition = bestLastIndex - this._currentDataQuery.length - 1; //Set the start caret position (right before the @)
        let currentCaretPosition = bestLastIndex; //Set the current caret position (right after the end of the "mention")


        let start = currentMessage.substr(0, startCaretPosition);
        let end = currentMessage.substr(currentCaretPosition, currentMessage.length);
        let startEndIndex = (start + record.get(this.displayField)).length + 1;

        // See if there's the same mention in the list
        if (this._mentionsCollection.filter(e => e.get(this.valueField) == record.get(this.valueField)).length == 0)
        {
            this._mentionsCollection.push(record);//Add the mention to mentionsColletions
        }
        
        // Cleaning before inserting the value, otherwise auto-complete would be triggered with "old" inputbuffer
        this._resetBuffer();
        this._currentDataQuery = '';

        // Mentions and syntax message
        let updatedMessageText = start + this.triggerChar + record.get(this.displayField) + ' ' + end;
        this._textarea.setValue(updatedMessageText);
        this._updateValues();

        // Set correct focus and selection
        this._textarea.focus();
        this._textarea.selectText(startEndIndex, startEndIndex);
        // textareaInputEl.setSelectionRange(startEndIndex, startEndIndex);
    },
    
    getValue: function()
    {
        return this._messageText;
    },
    
    setValue: function(value)
    {
        let text = value.replaceAll("<br/>", "\n");
        
        let users = this._parseMentionnedUsers(value);
        if (users.length)
        {
            Ametys.data.ServerComm.callMethod({
                role: "org.ametys.plugins.core.user.UserDAO",
                methodName: "getUsersByUserIdentitiesAndContexts",
                parameters: [users, null, false],
                callback: {
                    handler: this._getUsersCB,
                    arguments: {
                        value: text
                    },
                    scope: this
                },
                waitMessage: {target: this},
                errorMessage: true
            });
        }
        else
        {
            this._textarea.setValue(text);
            this._updateValues();
        }
    },
    
    _getUsersCB: function(users, params)
    {
        let text = params.value;
        for (let user of users)
        {
            let record = new Ametys.form.field.TextAreaWithMentions.Users(user);
            let textToReplace = "@(" + record.get(this.valueField) + ")";
            let textSyntax = record.get(this.displayField);
            text = text.replaceAll(textToReplace, textSyntax);
            this._mentionsCollection.push(record);
        }
        this._textarea.setValue(text);
        this._updateValues();
    },
    
    _parseMentionnedUsers: function(value)
    {
        let users = [];
        const regex = /@\(([^()]+)\)/g;

        let match = regex.exec(value);
        while (match !== null) 
        {
            users.push(match[1]);
            match = regex.exec(value);
        }
        
        return users;
    },
    
    _resetBuffer: function()
    {
        this._inputBuffer = [];
    },
    
    _onKeydown: function(textarea, event)
    {
        // This also matches HOME/END on OSX which is CMD+LEFT, CMD+RIGHT
        if (event.getKey() === event.LEFT || event.getKey() === event.RIGHT || event.getKey() === event.HOME || event.getKey() === event.END || event.getKey() === event.ESC) 
        {
            // Defer execution to ensure carat pos has changed after HOME/END keys then call the resetBuffer function
            Ext.defer(this._resetBuffer, 1, this);
            this._combo.collapse();
            return;
        }
        
        if (event.getKey() === event.SPACE) 
        {
            this._resetBuffer();
            return;
        }

        //If the key pressed was the backspace
        if (event.getKey() === event.BACKSPACE) 
        {
            this._inputBuffer = this._inputBuffer.slice(0, -1 + this._inputBuffer.length);
            return;
        }
        
        if (!this._combo.isExpanded)
        {
            return;
        }
        
        let picked = this._combo.getPicker();
        
        switch (event.getKey()) {
            case event.UP: //If the key pressed was UP or DOWN
                if (picked.highlightedItem && picked.highlightedItem.previousSibling)
                {
                    picked.highlightItem(picked.highlightedItem.previousSibling);
                }
                else
                {
                    picked.highlightItem(picked.listEl.last().dom);
                }
                event.preventDefault();
                return false;
            case event.DOWN:
                if (picked.highlightedItem && picked.highlightedItem.nextSibling)
                {
                    picked.highlightItem(picked.highlightedItem.nextSibling);
                }
                else
                {
                    picked.highlightItem(picked.listEl.first().dom);
                }
                event.preventDefault();
                return false;
            case event.RETURN: //If the key pressed was RETURN or TAB
            case event.TAB:
                let r = picked.getRecord(picked.highlightedItem);
                this._combo.select(r, true);
                this._combo.fireEvent('select', this._combo, r);
                event.preventDefault();
                return false;
        }        

        return true;
    },
    
    reset: function()
    {
        this._textarea.reset();
        this._combo.reset();
        this._mentionsCollection = [];
        this._resetBuffer();
        this._currentDataQuery = '';
        this._updateValues();        
    },
    
    _onKeypress: function(textarea, event)
    {
        this._inputBuffer.push(event.getChar()); //Push the value pressed into inputBuffer
    },
    
    _onClick: function()
    {
        this._resetBuffer();
    },
    
    _onBlur: function()
    {
        this._combo.collapse();
    },
    
    _onChange: function()
    {
        this._updateValues();
        this._updateMentionsCollection();
        
        let triggerCharIndex = this._inputBuffer.lastIndexOf(this.triggerChar); //Returns the last match of the triggerChar in the inputBuffer
        if (triggerCharIndex > -1) 
        { 
            //If the triggerChar is present in the inputBuffer array
            this._currentDataQuery = this._inputBuffer.slice(triggerCharIndex + 1).join(''); //Gets the currentDataQuery
            this._currentDataQuery = this._currentDataQuery.replace(/\s+$/,""); //Deletes the whitespaces
            if (this._currentDataQuery.length >= this.minChars)
            {
                this._combo.doQuery(this._currentDataQuery);
            }
        }
        else
        {
            this._combo.collapse();
        }
    }
});

Ext.define("Ametys.form.field.TextAreaWithMentions.Users", {
    extend: 'Ext.data.Model',

    idProperty: 'value',

    fields: [
             {name: 'firstname', type: 'string'},
             {name: 'lastname', type: 'string'},
             {name: 'login', type: 'string'},
             {name: 'populationId', type: 'string'},
             {name: 'populationLabel', type: 'string'},
             {
                 name: 'value', 
                 type: 'string',
                 calculate: function(data)
                 {
                     return data.login + '#' + data.populationId;
                 }
              },
             {name: 'fullname', type: 'string'},
             {name: 'sortablename', type: 'string'},
             {
                 name: 'displayName',
                 type: 'string',
                 calculate: function (data)
                 {
                     return Ametys.helper.Users.renderUser(data.login, data.populationLabel, data.sortablename);
                 }
             }
    ]
});