/**
 * @class Spread.grid.plugin.Editable
 * @extends Spread.grid.plugin.AbstractPlugin
 * Allows the spreadsheet to get edited by a text field as known from standard spreadsheet applications.
 *
 * TODO: Support string fields without allowedKeys config to enter special chars!
 */
Ext.define('Spread.grid.plugin.Editable', {

    'extend': 'Spread.grid.plugin.AbstractPlugin',

    'requires': ['Spread.grid.plugin.AbstractPlugin'],

    'alias': 'editable',

    editableColumns: [],
    editableColumnIndexes: [],
    editable: false,
    isEditing: false,
    cellClear: false,

    /**
     * @cfg {Boolean} autoCommit
     * Automatically commit changed records or wait for manually store.sync() / record.commit()?
     * (Generally, can be specially configured per column config too)
     */
    autoCommit: true,

    /**
     * @cfg {Number} stopEditingFocusDelay
     * Delay of timeout until view gets focused again after editing in ms
     */
    stopEditingFocusDelay: 50,

    /**
     * @cfg {Number} retryFieldElFocusDelay
     * Delay of timeout until the edit field gets tried to focused again (special case)
     */
    retryFieldElFocusDelay: 20,

    /**
     * @cfg {Number} chunkRenderDelay
     * Delay of rendering chunks in ms (too low values may let the browser freeze if grid is very big).
     * This performance optimization technique is used only when editModeStyling is activated and cells
     * need to be re-inked on this.setDisabled() / grid's setEditable() calls.
     */
    chunkRenderDelay: 0.1,

    /**
     * @cfg {Number} cellChunkSize
     * Size of the chunks (cells) to render at once (see chunkRenderDelay for further information)
     */
    cellChunkSize: 300,

    /**
     * @property {Spread.selection.Position}
     * Currently active editing position
     */
    activePosition: null,

    /**
     * @property {Ext.dom.Element}
     * Currently active cover element
     */
    activeCoverEl: null,

    /**
     * @property {Ext.dom.Element}
     * Currently active cell td element
     */
    activeCellTdEl: null,

    /**
     * @property {Object}
     * Currently active cover element size (containing width, height properties measured in pixels)
     */
    activeCoverElSize: null,

    /**
     * @property {Array}
     * Currently active cover element position (top, left pixel position relative to view)
     */
    activeCoverElPosition: null,

    /**
     * @property {Ext.dom.Element}
     * Reference to the origin edit field input-HTML-element
     */
    cellCoverEditFieldEl: null,

    /**
     * @cfg {Boolean} editModeStyling
     * Allows to style the cells when in edit mode
     */
    editModeStyling: true,

    /**
     * @cfg {String} editableCellCls
     * Name of the css class for editable spreadsheet cells
     */
    editableCellCls: 'spreadsheet-cell-editable',

    /**
     * @cfg {String} editableDirtyCellCls
     * Name of the css class for editable spreadsheet cells which are dirty too
     */
    editableDirtyCellCls: 'spreadsheet-cell-editable-dirty',

    /**
     * @property {Mixed} lastEditFieldValue
     * Stores the last edit field value
     */
    lastEditFieldValue: null,

    /**
     * @protected
     * Registers the hook for cover-double-click editing
     * @param {Spread.grid.View} view View instance
     * @return void
     */
    init: function(view) {

        var me = this;

        me.callParent(arguments);

        // Add events
        me.addEvents(

            /**
             * @event beforeeditfieldblur
             * Fires before a edit field gets blur()'ed. Return false in listener to stop the event processing.
             * @param {Spread.grid.plugin.Editable} editable Editable plugin instance
             */
            'beforeeditfieldblur',

            /**
             * @event editfieldblur
             * Fires when a edit field gets blur()'ed.
             * @param {Spread.grid.plugin.Editable} editable Editable plugin instance
             */
            'editfieldblur',

            /**
             * @event beforecoverdblclick
             * Fires before a covers dbl-click action happened (starting editing). Return false in listener to stop the event processing.
             * @param {Spread.grid.plugin.Editable} editable Editable plugin instance
             */
            'beforecoverdblclick',

            /**
             * @event coverdblclick
             * Fires when a covers dbl-click action happened (starting editing).
             * @param {Spread.grid.plugin.Editable} editable Editable plugin instance
             */
            'coverdblclick',

            /**
             * @event beforecoverkeypressed
             * Fires before a cover's keypress-click action happened (starting editing). Return false in listener to stop the event processing.
             * @param {Spread.grid.plugin.Editable} editable Editable plugin instance
             */
            'beforecoverkeypressed',

            /**
             * @event coverkeypressed
             * Fires when a cover's keypress-click action happened (starting editing).
             * @param {Spread.grid.plugin.Editable} editable Editable plugin instance
             */
            'coverkeypressed',

            /**
             * @event beforeeditingenabled
             * Fires before editing gets generally activated. Return false in listener to stop the event processing.
             * @param {Spread.grid.plugin.Editable} editable Editable plugin instance
             */
            'beforeeditingenabled',

            /**
             * @event editingenabled
             * Fires when editing gets generally activated.
             * @param {Spread.grid.plugin.Editable} editable Editable plugin instance
             */
            'editingenabled',

            /**
             * @event beforeeditingdisabled
             * Fires before editing gets generally deactivated. Return false in listener to stop the event processing.
             * @param {Spread.grid.plugin.Editable} editable Editable plugin instance
             */
            'beforeeditingdisabled',

            /**
             * @event editingdisabled
             * Fires when editing gets generally deactivated.
             * @param {Spread.grid.plugin.Editable} editable Editable plugin instance
             */
            'editingdisabled',

            /**
             * @event covercelleditable
             * Fires after a cell got covered for editing.
             * @param {Spread.grid.plugin.Editable} editable Editable plugin instance
             * @param {Spread.grid.View} view Spread view instance
             * @param {Spread.selection.Position} position Position to be covered
             * @param {Ext.dom.Element} coverEl Cover element
             */
            'covercelleditable',

            /**
             * @event editablechange
             * Fires after the editable flag has changed and all re-rendering has been done.
             * Use this event if you e.g. want to reload the store "directly" after calling setEditable() etc.
             * @param {Spread.grid.plugin.Editable} editable Editable plugin instance
             * @param {Boolean} isEditable Indicator if the spread is now editable or not
             */
            'editablechange'
        );

        // Register eventing hook
        me.initCoverEventing();
    },

    /**
     * @protected
     * Registers key and mouse eventing on the cover element of the view
     * @return void
     */
    initCoverEventing: function() {

        var me = this;

        // Call the following methods after rendering...
        me.getView().on('afterrender', function() {

            // Collect editable flags from the columns
            me.initEditingColumns();

            // Initialize editable eventing
            me.initEventing();
        });
    },

    /**
     * @protected
     * Implements listeners and hooks for eventing which belongs
     * to the edit field, cover element, view and selection model.
     * @return void
     */
    initEventing: function() {

        // Handle eventing of cover element
        var me = this,
            view = me.getView(),
            coverEl = view.getCellCoverEl();

        //console.log('initEventing!', coverEl);
        if (coverEl) {

            //console.log('found a view to hook on', coverEl, this.cellCoverEditFieldEl);

            // Render the text field
            me.initTextField(coverEl);

            // Listen to cover double click
            //coverEl.on('dblclick', me.onCoverDblClick, me);

            // Double-click based edit mode handler
            view.getEl().on('dblclick', me.onCoverDblClick, me);

            // Listen to cover key pressed (up)
            view.getEl().on('keydown', me.onCoverKeyPressed, me);

            // Listen to view's cover
            view.on('covercell', me.onCellCovered, me);

            // Handle TAB and ENTER select while editing (save and focus next cell)
            //me.getSelectionModel().on('tabselect', me.blurEditFieldIfEditing, me);
            //me.getSelectionModel().on('enterselect', me.blurEditFieldIfEditing, me);
            me.getSelectionModel().on('beforecellfocus', me.blurEditFieldIfEditing, me);
            me.getSelectionModel().on('keynavigate', me.blurEditFieldIfEditing, me);
            //me.getSelectionModel().on('cellblur', me.blurEditFieldIfEditing, me);

        } else {
            throw "Cover element not found, initializing editing failed! Please check proper view rendering.";
        }
    },

    /**
     * @protected
     * Collects the 'editable' flags from the columns and stores them in
     * this.editableColumns array initially.
     * @return void
     */
    initEditingColumns: function() {

        var me = this, view = me.getView(),
            columns = view.getHeaderCt().getGridColumns();

        // Initialize arrays
        me.editableColumns = [];
        me.editableColumnIndexes = [];

        for (var i=0; i<columns.length; i++) {

            if (columns[i].editable) {

                // Push to list of editable columns
                me.editableColumns.push(columns[i]);

                // Set reference on column
                columns[i].columnIndex = i;

                // Push to list of editable columns indexes
                me.editableColumnIndexes.push(i);
            }
        }
    },

    /**
     * @protected
     * For initializing, the text field DOM elements need to be generated.
     * @param {Ext.dom.Element} coverEl Cover element reference
     * @return void
     */
    initTextField: function(coverEl) {

        var me = this;

        // Check for field existence (already created?)
        if (!me.cellCoverEditFieldEl) {

            //console.log('initTextField', arguments);

            // Build editor field
            me.cellCoverEditFieldEl = Ext.get(

                Ext.DomHelper.append(coverEl, {
                    id: Ext.id() + '-cover-input',
                    tag: 'input',
                    type: 'text',
                    cls: 'spreadsheet-cell-cover-edit-field',
                    value: ''
                })
            );

            // Register key up handler
            me.cellCoverEditFieldEl.on('keypress', me.onEditFieldKeyPressed, me);
        }
    },

    /**
     * @protected
     * Stops the edit mode
     * @return void
     */
    onEditFieldBlur: function() {

        //console.log('onEditFieldBlur');
        var me = this;

        // Fire interceptable event
        if (me.fireEvent('beforeeditfieldblur', me) !== false) {

            // Internal flag to prevent two-time rendering
            me.view.dataChangedRecently = true;

            // Stop editing (mode)
            me.setEditing(false);

            // Write changed value back to record/field
            me.activePosition.setValue(
                me.getEditingValue(),
                me.autoCommit
            );

            // Recolorize for dirty flag!
            me.handleDirtyMarkOnEditModeStyling();

            // Fire event
            me.fireEvent('editfieldblur', me);
        }
    },

    /**
     * @protected
     * Full redraw on edit mode styling after each edit field change
     * @return void
     */
    handleDirtyMarkOnEditModeStyling: function() {

        var me = this;

        // Full redraw
        if (me.getView().ownerCt.editModeStyling) {
            me.displayCellsEditing(true);
        } else {
            me.displayCellsEditing(false);
        }
    },

    /**
     * @protected
     * Blurs the editor field if editing is happening and
     * the user pressed TAB or ENTER to focus next cell.
     * (blur causes the editor to save its changed data)
     * @return void
     */
    blurEditFieldIfEditing: function() {

        var me = this;
        //console.log('blurEditFieldIfEditing', this.isEditing)

        if (me.isEditing) {
            me.onEditFieldBlur();
        }
    },

    /**
     * @protected
     * Handles special keys (ENTER, TAB) and
     * allowed input character limiting.
     * @param {Ext.EventObject} evt Key event
     * @return {Boolean}
     */
    onEditFieldKeyPressed: function(evt) {

        var me = this,
            view = me.getView();

        if (me.isEditing) {

            if (Spread.util.Key.isNavigationKey(evt) || Spread.util.Key.isDelKey(evt)) {
                return true;
            }

            if (Spread.util.Key.isCancelEditKey(evt)) {

                //console.log('is cancel edit key')
                me.blurEditFieldIfEditing();
                return true;
            }

            // If there is a list of allowed keys, check for them
            if (me.activePosition.columnHeader.allowedEditKeys.length) {

                // Stop key input if not in allowed keys list
                if (
                    Ext.Array.indexOf(me.activePosition.columnHeader.allowedEditKeys,
                        String.fromCharCode(evt.getCharCode())
                    ) === -1
                    && evt.getKey() !== evt.BACKSPACE
                )
                {
                    evt.stopEvent();
                }
            }

        } else {

            // Save and jump next cell
            if (evt.getKey() === evt.ENTER) {
                me.getSelectionModel().onKeyEnter(evt);
            }

            // Save and jump next cell
            if (evt.getKey() === evt.TAB) {
                me.getSelectionModel().onKeyTab(evt);
            }

            // Key navigation support (jumping out of field)
            if (evt.getKey() === evt.LEFT) {
                me.getSelectionModel().onKeyLeft(evt);
            }

            if (evt.getKey() === evt.RIGHT) {
                me.getSelectionModel().onKeyRight(evt);
            }

            if (evt.getKey() === evt.UP) {
                me.getSelectionModel().onKeyUp(evt);
            }

            if (evt.getKey() === evt.DOWN) {
                me.getSelectionModel().onKeyDown(evt);
            }
        }
    },

    // Internal method for checking if a user clicked on a cell cover
    // which is covering the currently focused cell.
    isOriginCellClick: function(evt) {

        var me = this, clickedOnCell = false,
            clickTargetElIdTextParent = evt.getTarget().parentNode.parentNode.id,
            clickTargetElIdText = evt.getTarget().parentNode.id,
            clickTargetElId = evt.getTarget().id,
            currentPosCellElId = me.getSelectionModel().getCurrentFocusPosition().cellEl.id;

        if (Ext.isIE) {

            if (clickTargetElId.indexOf(currentPosCellElId) > -1 ||
                clickTargetElIdText.indexOf(currentPosCellElId) > -1 ||
                clickTargetElIdTextParent.indexOf(currentPosCellElId) > -1) {
                clickedOnCell = true;
            }

        } else {

            if (clickTargetElId.indexOf(currentPosCellElId) > -1) {
                clickedOnCell = true;
            }
        }
        return clickedOnCell;
    },


    /**
     * @protected
     * When a user double-clicks on a cell cover, this method
     * gets called and chooses if the text field should be shown
     * based on the pre-annotation already made by this.onCellCovered.
     * @param {Ext.EventObject} evt Key event
     * @return void
     */
    onCoverDblClick: function(evt) {

        var me = this;

        if (me.fireEvent('beforecoverdblclick', me) !== false) {

            // Clicked on grid view
            // ...and not already editing
            // ...and clicked on cell cover of the current selected cell position
            // ...and if position is generally editable
            if (!Ext.get(evt.getTarget()).hasCls('x-grid-view') &&
                !me.isEditing &&
                me.isOriginCellClick(evt) &&
                me.isPositionEditable()) {

                //console.log('onCoverDblClick, setEditable!');

                // Activates the editor
                me.setEditing(true);

                // Set current value of field in record
                me.setEditingValue(
                    me.activePosition.getValue()
                );
            }
            me.fireEvent('coverdblclick', me);
        }
    },

    /**
     * @protected
     * Handles key-up-events when a key is pressed when a cell is covered and focused.
     * @param {Ext.EventObject} evt Key event
     * @param {Ext.dom.Element} viewEl View's element
     * @return void
     */
    onCoverKeyPressed: function(evt, viewEl) {

        var me = this;

        // no key is pressed on a cover,
        // if we're editing...
        if (me.isEditing) {
            return;
        }

        if (me.fireEvent('beforecoverkeypressed', me) !== false) {

            if (Spread.util.Key.isDelKey(evt) && !me.isEditing) {

                if (me.isPositionEditable()) {

                    var clearRangePlugin = me.getSpreadPanel().getPlugin('Spread.grid.plugin.ClearRange');

                    if (clearRangePlugin) {
                        clearRangePlugin.clearCurrentFocusPosition();
                    }
                }
            }

            if (Spread.util.Key.isStartEditKey(evt) && !me.isEditing) {

                if (me.isPositionEditable()) {

                    // Activates the editor
                    me.setEditing(true);

                    // Reset the editor value
                    me.setEditingValue('');
                }
            }
            me.fireEvent('coverkeypressed', me);
        }
    },

    /**
     * @protected
     * When a cell gets covered, this method chooses if the text field,
     * generated by this.initTextField() gets active or not based on the
     * cell columns editable flag. It also updates the text fields meta
     * properties to react fast and responsive on UI eventing
     * (pre-annotation of field value etc. pp.)
     * @return void
     */
    onCellCovered: function(view, position, coverEl, tdEl, coverElSize, coverElPosition) {

        //console.log('onCellCovered', position);
        var me = this;

        // Set internal references
        me.activePosition = position;
        me.activeCellTdEl = tdEl;
        me.activeCoverEl = coverEl;
        me.activeCoverElSize = coverElSize;
        me.activeCoverElPosition = coverElPosition;

        // But hide, until this.setEditing() is called through UI event
        me.cellCoverEditFieldEl.dom.style.display = 'none';

        me.fireEvent('covercelleditable', me, view, position, coverEl);
    },

    /**
     * Checks if the current position is editable
     * @return {Boolean}
     */
    isPositionEditable: function() {

        var me = this;
        // Check for row to be editable or not
        // TODO!

        // Check for column to be editable or not
        if ((me.activePosition && !me.activePosition.columnHeader.editable) ||
            !me.editable || !me.activePosition.isEditable()) {

            //console.log('!this.activePosition.columnHeader.editable || !this.editable', !this.activePosition.columnHeader.editable, !this.editable)
            return false;
        }
        return true;
    },

    /**
     * Sets the editor active or inactive
     * @param {Boolean} doEdit=true Should edit mode be started?
     * @return void
     */
    setEditing: function(doEdit) {

        var me = this;

        // Default value = true
        if (!Ext.isDefined(doEdit)) {
            doEdit = true;
        }

        // Check global and column edit-ability
        if (!me.isPositionEditable()) {
            return false;
        }

        //console.log('setEditing ', doEdit);

        // Set editing
        if (doEdit) {

            if (me.fireEvent('beforeeditingenabled', me) !== false) {

                // Enable edit mode
                me.isEditing = true;

                // Show the editor
                me.cellCoverEditFieldEl.dom.style.display = 'inline';

                // Focus the edit field
                try {
                    me.cellCoverEditFieldEl.dom.focus();
                } catch(e) {}

                // Re-try after a small delay to ensure focus
                // (e.g. when rendering delay takes place while cell-to-cell edit mode jumps)
                setTimeout(function() {

                    try {
                        me.cellCoverEditFieldEl.dom.focus();
                    } catch(e) {}

                }, me.retryFieldElFocusDelay);

                me.fireEvent('editingenabled', me);
            }

        } else {

            if (me.fireEvent('beforeeditingdisabled', me) !== false) {

                // Hide the editor
                me.cellCoverEditFieldEl.dom.style.display = 'none';

                // Blur the edit field (and focus view element again to re-enable key-stroke navigation)
                setTimeout(function() {

                    try {
                        me.getView().focus();
                    } catch(e) {}

                }, me.stopEditingFocusDelay);

                // Disable edit mode
                me.isEditing = false;

                me.fireEvent('editingdisabled', me);
            }
        }
    },

    /**
     * Sets the edit field value
     * @param {String} value Editing value
     * @return void
     */
    setEditingValue: function(value) {

        // Set value in editor field
        this.cellCoverEditFieldEl.dom.value = value;
    },

    /**
     * Returns the current edit field value
     * @return {String}
     */
    getEditingValue: function() {
        return this.cellCoverEditFieldEl.dom.value;
    },

    /**
     * En/Disables editing grid-wide
     * @param {Boolean} allowEditing Is editing allowed?
     * @return void
     */
    setDisabled: function(allowEditing) {

        //console.log('en/disable editing globally:', allowEditing, this);

        // Closure, column editable processor
        var me = this,
            toggleColumnsEditable = function(isEditable) {

            for (var i=0; i<me.editableColumns.length; i++) {
                me.editableColumns[i].editable = isEditable;
            }
        };

        if (!allowEditing) {

            // Disable editing if currently
            me.setEditing(false);

            // Set flag
            me.editable = false;

            // Loop and disable editing on columns
            toggleColumnsEditable(false);

            // Display cells in read mode
            if (me.editModeStyling) {

                me.displayCellsEditing(false, function() {

                    // Fire event
                    me.fireEvent('editablechange', me, allowEditing);
                });
            } else {

                // Fire event
                me.fireEvent('editablechange', me, allowEditing);
            }

        } else {

            // Set flag
            me.editable = true;

            // Loop and disable editing on columns
            toggleColumnsEditable(true);

            // Display cells in edit mode

            if (me.editModeStyling) {

                me.displayCellsEditing(true, function() {

                    // Fire event
                    me.fireEvent('editablechange', me, allowEditing);
                });
            } else {

                // Fire event
                me.fireEvent('editablechange', me, allowEditing);
            }
        }
    },

    /**
     * Displays the grid cells in edit or read mode
     * @param {Boolean} displayEditing Display cells as editing?
     * @param {Function} [onRenderReady] Function to be called when ready
     * @return void
     */
    displayCellsEditing: function(displayEditing, onRenderReady) {

        var me = this, view = me.getView(), viewCells = view.getEl().query(
            view.cellSelector
        ), viewColumns = view.getHeaderCt().getGridColumns(),
        columnCount = viewColumns.length,
        displayCellEditing = true, row, displayCellEditingState;

        if (Ext.isIE6 || Ext.isIE7 || Ext.isIE8) {
            me.chunkRenderDelay = 0.3;
            me.cellChunkSize = 200;
        }

        // Chunk-style cells
        var chunkCellProcessor = function(startIdx, stopIdx) {

            for (var i=startIdx; i<stopIdx; i++) {

                // Evaluate cell edit mode displaying
                displayCellEditing = true;

                if (viewCells[i]) {

                    // Calculate row index from cell index/column count
                    row = Math.floor(i/columnCount);

                    displayCellEditingState = Spread.util.State.getPositionState({
                        row: row,
                        spreadPanel: me.getSpreadPanel(),
                        column: viewCells[i].cellIndex
                    }, 'editmodestyling');

                    if (Ext.isDefined(displayCellEditingState)) {
                        displayCellEditing = displayCellEditingState;
                    }
                }

                // Jump-over non-exiting AND non-editable cells (of non-editable columns) AND
                // when a column should be inked which has an implicit editModeStyling=false flag!
                if (!viewCells[i] ||
                    Ext.Array.indexOf(me.editableColumnIndexes, viewCells[i].cellIndex) < 0 ||
                    (viewColumns[viewCells[i].cellIndex] &&
                     viewColumns[viewCells[i].cellIndex].editModeStyling === false)) {
                    continue;
                }

                if (displayEditing && displayCellEditing) {

                    // Add css class
                    if (!Ext.fly(viewCells[i]).hasCls(me.editableCellCls)) {

                        if (Ext.fly(viewCells[i]).hasCls('x-grid-dirty-cell')) {
                            Ext.fly(viewCells[i]).addCls(me.editableDirtyCellCls);
                        } else {
                            Ext.fly(viewCells[i]).addCls(me.editableCellCls);
                        }
                    }

                } else {

                    Ext.fly(viewCells[i]).removeCls(me.editableCellCls);
                    Ext.fly(viewCells[i]).removeCls(me.editableDirtyCellCls);
                }
            }

            if (stopIdx < viewCells.length) {

                startIdx += me.cellChunkSize;
                stopIdx += me.cellChunkSize;

                // Render delayed
                setTimeout(function() {

                    // Recursive call
                    chunkCellProcessor(startIdx, stopIdx);

                }, me.chunkRenderDelay);
            } else {

                if (onRenderReady && Ext.isFunction(onRenderReady)) {
                    onRenderReady();
                }
            }
        };

        // Chunk the for processing
        chunkCellProcessor(0, me.cellChunkSize);
    }
});