/*
 *  Copyright 2025 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 code editor.
 * Requires CodeMirror (version 3.14) to be loaded.
 * See http://codemirror.net/ to have documentation on it.
 */
Ext.define('Ametys.form.field.CodeAdvanced', {
    extend: 'Ametys.form.AbstractField',
    alias: ['widget.codeadvanced'],
    
    _editorInitialized: 0,
    
    layout: 'fit',
    
    border: true, // The border should be on the underlying component but the color would be wrong... let's hope noboy needs a label

    /** 
     * @cfg {String} [mode="html"] The CodeMirror mode
     */
    /**
     * @property {String} The CodeMirror mode. See #cfg-mode
     */
    _mode: null,

    constructor: function(config)
    {
        config.cls = Ext.Array.from(config.cls);
        config.cls.push('ametys-form-field-code-advanced');
        
        config.items = [
            { 
                xtype: 'component',
                name: 'code'
            }
        ];
        
        config._mode = config.mode || 'html';
        
        this.callParent(arguments);

        this.on('resize', this._onResize, this);
        this.on('initialize', this._init, this);
        this.addStateEvents('change');
    },
    
    afterComponentLayout: function(width, height, oldWidth, oldWeight)
    {
        this.callParent(arguments);
        
        // Creates the tinymce editor
        if (this._editorInitialized == 0)
        {
            this._editorInitialized = 1;
            
            this._createEditor();
        }
    },
    
    _createEditor: function()
    {
        var me = this;
        
        // Check if Monaco is already loaded globally
        if (window.monaco) 
        {
            me._createEditor2();
        }
        // Load Monaco's AMD loader dynamically to avoid conflicts with CodeMirror
        else if (!window.monacoRequire) 
        {
            Ametys.loadScript(Ametys.getPluginResourcesPrefix("monaco-editor") + '/min/vs/loader.js', function () {
                // Store Monaco's require in a separate namespace
                window.monacoRequire = window.require;
                
                // Configure and load Monaco
                window.monacoRequire.config({ 
                    paths: { 'vs': Ametys.getPluginResourcesPrefix("monaco-editor") + '/min/vs' }
                });
                
                window.monacoRequire(['vs/editor/editor.main'], function() {
                    // Disable some keybindings that conflict with browser shortcuts
                    monaco.editor.addKeybindingRules([
                        /* NOT WORKING {
                            keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.Shift | monaco.KeyCode.KeyR,
                            command: null
                        },*/
                        {
                            keybinding: monaco.KeyCode.F12,
                            command: null
                        }
                    ]);
                    
                    // validation settings
                    monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
                         noSemanticValidation: false,   // Activate semantic validation
                         noSyntaxValidation: false,    // Activate syntax validation
                    });
                    monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
                         noSemanticValidation: false,   // Activate semantic validation
                         noSyntaxValidation: false,    // Activate syntax validation
                    });

                    monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
                        target: monaco.languages.typescript.ScriptTarget.Latest,    // Compile for the last ES version. At this time, monaco supports for ES2020 and java for ES2024
                        lib: ["es2020"],                                            // Disable "window/dom"
                        allowNonTsExtensions: true                                  // Allow JS files
                    });
                    monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
                        target: monaco.languages.typescript.ScriptTarget.Latest,    // Compile for the last ES version. At this time, monaco supports for ES2020 and java for ES2024
                        lib: ["es2020"],                                            // Disable "window/dom"
                        allowNonTsExtensions: true                                  // Allow JS files
                    });
                        
                    me._createEditor2();
                });
            }, function() {
                Ametys.log.Error("Code", "Failed to load Monaco editor library.");
            });
        } else {
            // Monaco loader already exists, just load the editor
            window.monacoRequire(['vs/editor/editor.main'], function() {
                me._createEditor2();
            });
        }
    },
    
    /**
     * Create the Monaco editor instance
     * @private
     */
    _createEditor2: function()
    {
        let me = this;
        
        let v = me.initialConfig.value || '';
        
        this._monaco = monaco.editor.create(this.query("[name=code]")[0].getEl().dom, {
            value: v,
            // automaticLayout: true,
            scrollBeyondLastLine: false,
            minimap: { enabled: false },
            language: this._mode,
            readOnly: this._readOnly,
            fixedOverflowWidgets: true,  // let popups overflow outside the editor
            lineHeight: 17,
            fontSize: 12,
            insertSpaces: true,
            contextmenu: false,
            suggest: {
                showWords:false, // Disable suggestions based on words present in the document
            }
        });
        this._previousValue = v;
        
        // Add onFocus event listener
        this._monaco.onDidFocusEditorText(function() {
            me.onFocus();
        });
        
        // Add onBlur event listener
        this._monaco.onDidBlurEditorText(function() {
            me.onBlur();
        });
        
        this._monaco.onDidChangeModelContent(Ext.bind(this._onChange, this));        
        
        this._editorInitialized = 2;
        me.fireEvent('initialize', me);
    },
    
    _onResize: function(component, width, height, oldWidth, oldHeight, eOpts)
    {
        if (this._monaco)
        {
            this._monaco.layout({ width: width - 2, height: height - 2}); // -2 to account for borders (that extjs ignores)
        }
    },
    
    /**
     * Returns the parameter(s) that would be included in a standard form submit for this field. Typically this will be
     * an object with a single name-value pair, the name being this field's {@link #method-getName name} and the value being
     * its current stringified value. More advanced field implementations may return more than one name-value pair.
     *
     * Note that the values returned from this method are not guaranteed to have been successfully {@link #validate validated}.
     *
     * @return {Object} A mapping of submit parameter names to values; each value should be a string, or an array of
     * strings if that particular name has multiple values. It can also return null if there are no parameters to be
     * submitted.
     */
    getSubmitData: function() 
    {
        var me = this,
            data = null;
        if ((!me.disabled || me.submitDisabledValue) && me.submitValue && !me.isFileUpload()) 
        {
            data = {};
            data[me.getName()] = me.getSubmitValue();
        }
        return data;
    },
    
    
    getSubmitValue: function ()
    {
        return '' + this.getValue();
    },
    
    reset: function()
    {
        this.originalValue = this.originalValue || '';
        this.callParent(arguments);
    },
    
    getValue: function()
    {
        var value;
        if (this._monaco) 
        {
            value = this._monaco.getValue();
        }
        else if (this._futureValue != null)
        {
            value = this._futureValue;
        }
        else
        {
            value = this.initialConfig.value;
        }
        
        return value;
    },
    
    setValue: function(v)
    {
        v = v || '';
        
        this.callParent(arguments);
        
        if (this._monaco) 
        {
            this._monaco.setValue(v);
            this._previousValue = v;
        }
        else
        {
            this._futureValue = v;
        }        
    },
    
    /**
     * @private
     * {@link #event-initialize} listener
     */
    _init: function()
    {   
        if (!Ext.isEmpty(this._futureValue))
        {
            this.setValue(this._futureValue);
            this._futureValue = null;
        }
    },
    
    /**
     * Listens for change in editor
     * @param {Object} event The change event
     * @private
     */
    _onChange: function(event)
    {
        if (this._localChange)
        {
            return;
        }
        
        let previousValue = this._previousValue;
        let currentValue = this._monaco.getValue();
        
        // If single-line, join the different lines and filter out newline characters.
        if (this._singleLine)
        {
            var newtext = currentValue.join('').replace(/\n/g, '');
            this._monaco.setValue(newtext);
            return;
        }

        /**
         * @event beforechange
         * Fires before the content is changed.
         * @param {Ametys.form.field.CodeAdvanced} editor the current component
         * @param {String} newValue The new value
         * @param {String} oldValue The old value
         * @param {Object} event The change event
         */            
        if (this.fireEvent('beforechange', this, currentValue, previousValue, event) === false) 
        {
           // Annuler la modification en restaurant la valeur précédente
           this._localChange = true;
           // this._monaco.setValue(previousValue);
           //let me = this;
           if (event.isRedoing)
           {
              let me = this;
              setTimeout(function() {
                    me._monaco.getModel().undo();
              }, 0);
           }
           else
           {                
               this._monaco.getModel().undo();
           }
           this._localChange = false;
           return;
        }
         
        this.fireEvent('change', this, currentValue, previousValue, event);
        
        this._previousValue = currentValue;
    },
    
    getState: function()
    {
        return {
            value: Ext.JSON.encode(this.getValue())
        };
    },

    applyState: function(state)
    {
        if (state && state.value)
        {
            this.setValue(Ext.JSON.decode(state.value));
        }
        this.callParent(arguments);
    },
    
    onDestroy: function()
    {
        this.callParent(arguments);
        if (this._monaco)
        {
            this._monaco.dispose();
        }
    }
});