/*
 *  Copyright 2013 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.Code', {
    extend: 'Ext.form.field.TextArea',
    alias: ['widget.code'],
    
    /** 
     * @cfg {String} [mode="htmlmixed"] The CodeMirror mode
     */
    /** 
     * @cfg {Object} cmParams The CodeMirror parameters.
     * The `mode` and `value` standard parameters will be ignored.
     */
    /** 
     * @cfg {Boolean} [singleLine=false] Set to `true` to get a single-line editor.
     */
    
    /**
     * @property {String} The CodeMirror mode. See #cfg-mode
     */
    _mode: null,
    /**
     * @property {Object} _codeMirror The CodeMirror instance.
     * @private
     */
    _codeMirror: null,
    /**
     * @property {Boolean} _singleLine `true` when the editor is single-line.
     * @private
     */
    _singleLine: false,
    
    /**
     * @property {Boolean} _initialized True when the CodeMirror area is initialized. 
     * @private
     */
    _initialized: false,
    
    /**
     * @property {String} _futureValue The value to set when after code mirror initialization.
     * @private 
     */
    _futureValue: '',
    
    /**
     * @property {Boolean} [_readOnly=false] Set to 'true' to open tool in read-only mode
     */
    _readOnly: false,
    
    focusable: true,
    liquidLayout: false,
    
    /**
     * @inheritdoc
     */
    initComponent: function()
    {
            /**
             * @event initialize
             * Fires when the CodeMirror area is initialized.
             */
            
            /**
             * @event change
             * Fires when the content was changed.
             */
    
        this.callParent(arguments);
        
        // listeners
        this.on('render', this._onRender, this),
        this.on('resize', this._onResize, this);
        this.on('move', this._onMove, this);
        this.on('initialize', this._init, this);
    },
    
    /**
     * Creates a code
     * @param {Object} config The configuration
     */
    constructor: function (config)
    {
        this.callParent(arguments);
        this._mode = config.mode || 'htmlmixed';
        this._singleLine = config.singleLine || false;
        this._readOnly = config.readOnly === true;
    },
    
    /**
     * Get the code mirror instance
     */
    getCM: function()
    {
        return this._codeMirror;
    },
    
    setReadOnly: function (readOnly)
    {
        this._readOnly = readOnly;
        if (this._codeMirror) 
        {
            this._codeMirror.setOption ('readOnly', readOnly);
        }
    },
    
    /**
     * 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._codeMirror) 
        {
            value = this._codeMirror.getValue();
        }
        else if (this._futureValue != null)
        {
            value = this._futureValue;
        }
        else
        {
            value = this.initialConfig.value;
        }
        
        if (this.rendered)
        {
            this.inputEl.dom.value = value;
            return this.callParent(arguments);
        }
        
        return value;
    },

    setValue: function (v)
    {
        v = v || '';
        
        this.callParent([v]);
        
        if (this._codeMirror) 
        {
            this._codeMirror.setValue(v);
        }
        else
        {
            this._futureValue = v;
        }
    },
    
    /**
     * @private
     * {@link #event-initialize} listener
     */
    _init: function()
    {   
    	if (!Ext.isEmpty(this._futureValue))
        {
            this.setValue(this._futureValue);
            this._futureValue = null;
        }
    },
    
    /**
     * Fires when the base textarea is rendered: initializes the CodeMirror area from the textarea.
     * @private
     */
    _onRender: function()
    {
        this._initializeCodeMirror();
    },

    /**
     * @private
     * {@link #event-resize} listener
     */
    _onResize: function(editor, width, height)
    {
        if (this._codeMirror)
        {
            var s = this.triggerWrap.getSize(true);
            this._codeMirror.setSize(s.width - 2, height - 2); // This - 2 is a hack due to border, but will soon or later fail
            // Here we don't use s.height to fix RUNTIME-1643 issue but ...
            // FIXME If label is displayed on top, this will not work
        }
    },
    
    /**
     * @private
     * {@link #event-move} listener
     */
    _onMove: function(editor, x, y)
    {
        if (this._codeMirror)
        {
            this._codeMirror.scrollTo(x, y);
        }
    },
    
    focus: function()
    {
        this.callParent(arguments);
        
        if (this._codeMirror) 
        {
            this._codeMirror.focus();
        }
    },
    
    /**
     * Initialize the CodeMirror
     * @private
     */
    _initializeCodeMirror: function()
    {
        var me = this;
        
        if (this._codeMirror == null)
        {
            // Get the textarea HTMLElement.
            var textarea = Ext.dom.Query.selectNode('textarea', this.getEl().dom);
            
            // Default parameters.
            var defaultParams = {
                matchBrackets: true, // add-on
                lineNumbers: true,
                styleActiveLine: true,
                
                extraKeys: {
                    "Ctrl-U": "undo",
                    "Ctrl-Y": "redo"
                }
            };
            
            // Non-overridable parameters.
            var forcedParams = {
                mode: this._mode,
                value: me.initialConfig.value || '',
                readOnly: this._readOnly
            };
            
            // Merge default params, then user params, then forced params.
            var params = Ext.Object.merge(defaultParams, me.initialConfig.cmParams || {}, forcedParams);
            
            // Create the CodeMirror instance.
            this._codeMirror = CodeMirror.fromTextArea(textarea, params);
            
            // Used to be able to modify the change (for instance, filter newline chars).
            this._codeMirror.on('beforeChange', Ext.bind(this._onBeforeChange, this));
            
            /**
             * @event beforechange
             * Fires before the content is changed.
             * @param {Object} codeMirror the current codeMirror instance
             * @param {Object} changes The object with the current changes, with properties from, to and text, a cancel() and a update() method.
             */            
            this._codeMirror.on("beforeChange", function(cm, change) { 
                me.fireEvent('beforechange', cm, change); 
            });
            
            // Relay on change event
            this._codeMirror.on("change", Ext.bind(this._onChange, this));
            
            me.fireEvent('initialize', true);
            me._initialized = true;
        }
    },
    
    /**
     * Fired before a change is applied in the CodeMirror editor.
     * @param {Object} cm The CodeMirror editor
     * @param {Object} change The change
     * @private
     */
    _onBeforeChange: function(cm, change)
    {
        // If single-line, join the different lines and filter out newline characters.
        if (this._singleLine && change.update)
        {
            var newtext = change.text.join('').replace(/\n/g, '');
            change.update(change.from, change.to, [newtext]);
        }
    },
    
    /**
     * Listens for change in CodeMirror editor
     * @param {Object} cm The CodeMirror editor
     * @param {Object} change The change
     * @private
     */
    _onChange: function(cm, change)
    {
        var oldValue = this.value, // value is updated when calling this#getValue
            newValue = this.getValue();
        this.fireEvent('change', this, newValue, oldValue);
    },
    
    onFocusEnter: function(e)
    {
        this.callParent(arguments);
        this.onFocus(e);
    },

    onFocusLeave: function(e)
    {
        this.callParent(arguments);
        this.onBlur(e);
    },
    
    getState: function()
    {
        var state = this.callParent(arguments);
        if (state && state.value)
        {
            state.value = Ext.JSON.encode(state.value);
        }
        return state;
    },
    
    applyState: function(state)
    {
        if (state && state.value)
        {
            state.value = Ext.JSON.decode(state.value);
        }
        this.callParent(arguments);
    }
});