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

/**
 * This class is a singleton to handle tables in inline editor
 * @private
 */
Ext.define('Ametys.plugins.cms.editor.Tables', {
	singleton: true,
	
	/**
	 * Insert a table in the current editor of size determined
	 * @param {Number} cols The number of cells of the new table
	 * @param {Number} rows The number of rows of the new table
	 */
	createTable: function(cols, rows)
	{
		var html = this._createHTMLForTable(cols, rows, tinyMCE.activeEditor.schema.elements.table ? tinyMCE.activeEditor.schema.elements.table.attributes.class.defaultValue : "");
		
		// FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
		tinyMCE.activeEditor.execCommand('mceBeginUndoLevel');
		
		tinyMCE.insertHTMLAtRoot(html);

		tinymce.each(tinyMCE.activeEditor.dom.select('table[_mce_new]'), function(node) {
			var td = tinyMCE.activeEditor.dom.select('td', node);

			tinyMCE.activeEditor.selection.select(td[0], true);
			tinyMCE.activeEditor.selection.collapse();

			tinyMCE.activeEditor.dom.setAttrib(node, '_mce_new', '');
			tinyMCE.activeEditor.dom.setAttrib(node, '_mce_ribbon_select', '1');
		});

		tinyMCE.activeEditor.addVisual();
		tinyMCE.activeEditor.execCommand('mceEndUndoLevel');
		tinyMCE.activeEditor.focus();
	},
	
	/**
	 * Create the HTML markup for a table.
	 * @param {Number} cols The number of columns
	 * @param {Number} rows The number of rows
	 * @param {String} classname class attribute value
	 * @param {String} style The style attribute value. Can be null.
	 * @param {Boolean} isMarker a marker attribute is added if true.
	 * @return {String} The corresponding HTML markup
	 * @private
	 */
	_createHTMLForTable: function(cols, rows, classname, style, isMarker)
	{
		isMarker = isMarker == true;
		
		if (style == null)
		{
			style = "width:100%";
		}
		
		var html = '<table';
		html += ' class="' + classname + '"';
		if (style)
		{
			html += ' style="' + style + '"';
		}

		if (isMarker)
		{
			html += ' marker="marker"';
		}
		html += ' _mce_new="1"';
		html += '>';
		for (var i = 0; i < rows; i++) 
		{
			html += "<tr>";
			for (var j = 0; j < cols; j++) 
			{
				html += '<td>' + this._createInnerHTML() + '</td>';
			}
			html += "</tr>";
		}
		html += "</table>";
		
		return html;
	},
	
	/**
	 * Create the HTML markup to put inside a cell.
	 * @param {String} s the text (or html) to put in the cell.
	 * @return {String} The corresponding HTML markup
	 */
	_createInnerHTML: function(s)
	{
		s = s || '&#160;';

		if (!tinymce.isIE)
		{
			return '<p>' + s + '<br _mce_bogus="1"/></p>';
		}
		else
		{
			return '<p>' + s + '</p>';
		}
	},
	
	// ---------------------------------------- //
	//				CAPTION						//
	// ---------------------------------------- //

	/**
	 * Set 'caption' element to current &lt;table&gt; node when pressing ENTER or ESC key.
	 * @param {Ext.form.field.Text} input The input text
	 * @param {Ext.event.Event} e The event object
	 */
	setCaptionOnSpecialKey: function (input, e)
	{
		if (e.getKey() == e.ENTER) 
		{
			e.preventDefault();
			e.stopPropagation();
			this.setCaption(input.getValue());
			// FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
            tinyMCE.activeEditor.focus();
		}
		else if (e.getKey() == e.ESC) 
		{
			e.preventDefault();
			e.stopPropagation();
			input.setValue(this._getCaption());
			// FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
            tinyMCE.activeEditor.focus();
		}	
	},

	/**
	 * Set table caption when the input loses the focus
	 * @param {Ext.form.field.Text} input The input text
	 */
	setCaptionOnBlur: function (input)
	{
		if (input.getValue() != this._getCaption(this._currentTable))
		{
			this.setCaption(input.getValue());
		}
	},
	
	/**
	 * Set 'caption' element to current &lt;table&gt; node
	 * @param {String} value The value
     * @return {Boolean} Return the success of operation
	 */
	setCaption: function(value)
	{
		var elt = this._currentTable;
		if (elt != null)
		{
		    // FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
			var capEl = tinyMCE.activeEditor.dom.select('caption', elt)[0];
			if (value == "")
			{
				if (capEl)
				{
					capEl.parentNode.removeChild(capEl);
				}
			}
			else
			{
				var escapedValue = value.replace(/</g, "<").replace(/>/g, ">");
				if (!capEl) 
				{
					capEl = elt.ownerDocument.createElement('caption');
					capEl.className = "mceNonEditable";
					capEl.setAttribute("data-mce-contenteditable", "false");
					elt.insertBefore(capEl, elt.firstChild);
				}
				capEl.innerHTML = escapedValue;
			}
			tinyMCE.activeEditor.execCommand('mceAddUndoLevel');
			return true;
		}
		return false;
	},
	
	/**
	 * Get the caption of current table
	 * @param {HTMLElement} node The &lt;table&gt; node. Can be null to retrieve &lt;table&gt; node from cursor current position
	 * @return {String} The caption text
	 * @param {tinymce.Editor} [editor] The wrapper tinymce editor object
	 * @private
	 */
	_getCaption: function(node, editor)
	{
	    // FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
		editor = editor || tinyMCE.activeEditor;
		var capEl = editor.dom.select('caption', node)[0];
		if (capEl)
		{
			return capEl.innerHTML.replace(/</g, "<").replace(/>/g, ">");
		}
		else
		{
			return "";
		}
	},
	
	/**
	 * Enable/disable controller and set input value according the selected table
	 * @param {Ametys.ribbon.element.ui.FieldController} controller The controller
	 * @param {Ametys.cms.form.widget.RichText} field The current field. Can be null
	 * @param {HTMLElement} node The current selected node. Can be null.
	 */
	captionListener: function (controller, field, node)
	{
		this._currentTable = node;
		
		if (field == null || node == null || field.getEditor() == null || field.getEditor().dom == null || field.getEditor().selection == null || field.getEditor().selection.getStart() == null)
		{
			controller.setValue('');
		}
		else
		{
			var caption = this._getCaption(node, field.getEditor());
			controller.setValue(caption);
		}
	},
	
	// ---------------------------------------- //
	//				SUMMARY						//
	// ---------------------------------------- //
	
	/**
	 * Set 'summary' attribute to current &lt;table&gt; node when pressing ENTER or ESC key.
	 * @param {Ext.form.field.Text} input The input text
	 * @param {Ext.event.Event} e The event object
	 */
	setSummaryOnSpecialKey: function (input, e)
	{
		if (e.getKey() == e.ENTER) 
		{
			e.preventDefault();
			e.stopPropagation();
			this.setSummary(input.getValue());
			tinyMCE.activeEditor.focus();
		}
		else if (e.getKey() == e.ESC) 
		{
			e.preventDefault();
			e.stopPropagation();
			input.setValue(this._getSummary());
			tinyMCE.activeEditor.focus();
		}	
	},
	
	/**
	 * Set table caption when the input loses the focus
	 * @param {Ext.form.field.Text} input The input text
	 */
	setSummaryOnBlur: function (input)
	{
		if (input.getValue() != this._getSummary(this._currentTable))
		{
			this.setSummary(input.getValue());
		}
	},
	
	/**
	 * Set 'summary' attribute to current &lt;table&gt; node
	 * @param {String} value The value
	 */
	setSummary: function (value)
	{
		if (this._currentTable != null)
		{
		    // FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
			tinyMCE.activeEditor.dom.setAttrib(this._currentTable, 'summary', value);
			tinyMCE.activeEditor.execCommand('mceAddUndoLevel');
		}
	},
	
	/**
	 * Get the summary of current table
	 * @param {HTMLElement} node The &lt;table&gt; node. Can be null to retrieve &lt;table&gt; node from cursor current position
	 * @return {String} The summary text
	 * @private
	 */
	_getSummary: function(node)
	{
		return node.summary || "";
	},
	
	/**
	 * Enable/disable controller and set input value according the selected table
	 * @param {Ametys.ribbon.element.ui.FieldController} controller The controller
	 * @param {Ametys.cms.form.widget.RichText} field The current field. Can be null
	 * @param {HTMLElement} node The current selected node. Can be null.
	 */
	summaryListener: function (controller, field, node)
	{
		this._currentTable = node;

		if (node)
		{
			var summary = this._getSummary(node);
			controller.setValue(summary);
		}
		else
		{
			controller.setValue('');
		}
	},
	
	// ---------------------------------------- //
	//				ALIGN						//
	// ---------------------------------------- //
	
	/**
	 * Set the cells alignment
	 * @param {String} align the horizontal align
	 * @param {String} valign the vertical align
	 */
	alignTable: function(align, valign)
	{
	    // FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
		tinyMCE.activeEditor.focus();
		
		var cells = [];
		
		// First we look for every data-mce-selected table cell
		var table = tinyMCE.activeEditor.dom.getParent(tinyMCE.activeEditor.selection.getNode(), function (node) { return /^table$/i.test(node.nodeName); });
		var tdSelected = tinyMCE.activeEditor.dom.select("td[data-mce-selected=\"1\"]", table);
		for (var i = 0; i < tdSelected.length; i++)
		{
			cells.push(tdSelected[i]);
		}
		var thSelected = tinyMCE.activeEditor.dom.select("th[data-mce-selected=\"1\"]", table);
		for (var i = 0; i < thSelected.length; i++)
		{
			cells.push(thSelected[i]);
		}
		
		// If we do not find any, that's because the carret is only inside a cell
		if (cells.length == 0)
		{
			var c = tinyMCE.activeEditor.dom.getParent(tinyMCE.activeEditor.selection.getNode(), function (node) { return /^td|th$/i.test(node.nodeName); });
			if (c != null)
			{
				cells.push(c);
			}
		}
		
		if (cells.length > 0)
		{
			tinyMCE.activeEditor.execCommand('mceBeginUndoLevel');
			for (var i = 0; i < cells.length; i++)
			{
				var cell = cells[i];
				if (align != null)
				{
					cell.style.textAlign = align;
					cell.removeAttribute("data-mce-style");
					
					// moreover we want to remove text align on chil nodes
					for (var j = 0; j < cell.childNodes.length; j++)
					{
						var elt = cell.childNodes[j];
						if (elt.style)
						{
							elt.style.textAlign = "";
						}
					}
				}
				if (valign != null)
				{
					cell.style.verticalAlign = valign;
					cell.removeAttribute("data-mce-style");
				}
			}
			tinyMCE.activeEditor.execCommand('mceEndUndoLevel');
		}
	},
	
	/**
	 * Enable/disable controller and set input value according the selected table
	 * @param {Ametys.ribbon.element.ui.ButtonController} controller The controller
	 * @param {Ametys.cms.form.widget.RichText} field The current field. Can be null
	 * @param {HTMLElement} node The current selected node. Can be null.
	 */
	alignControllerListener: function(controller, field, node)
	{
		var align = controller.getInitialConfig ('align');
		var valign = controller.getInitialConfig ('valign');

        if (node)
        {
            var toggleFunction = Ext.bind(controller.toggle, controller);
            var editor = field.getEditor();
            this.alignListener(node, editor, toggleFunction, align, valign);
        }
	},
    /**
     * Enable/disable controller and set input value according the selected table
     * @param {HTMLElement} node The current selected node.
     * @param {Object} editor tinyMCE editor
     * @param {Function} toggleFunction function that will be called with true/false to select/unselect button
     * @param {String} align horizontal alignment
     * @param {String} valign vertical alignment
     */
    alignListener: function(node, editor, toggleFunction, align, valign)
    {
        this._currentCell = node;
        if (node)
        {
            var onVAlignValue = valign != null && editor.dom.getStyle(node, "vertical-align", true) == valign;
            var onAlignValue = align != null && editor.dom.getStyle(node, "text-align", true) == align;

            toggleFunction(onVAlignValue && onAlignValue);
        }
        else
        {
            toggleFunction(false);
        }
    },
	
	// ---------------------------------------- //
	//				FLOAT						//
	// ---------------------------------------- //
	
	/**
	 * Enable/disable and toggle/untoggle controller according the table alignment
	 * @param {Ametys.ribbon.element.ui.ButtonController} controller The controller
	 * @param {Ametys.cms.form.widget.RichText} field The current field. Can be null
	 * @param {HTMLElement} node The current selected node. Can be null.
	 */
	floatListener: function (controller, field, node)
	{
		this._currentNode = node;
		if (node)
		{
			if (node.style.width == '100%')
			{
				controller.setDisabled(true); 
			}
			controller.toggle(field.getEditor().dom.hasClass(node, controller.getInitialConfig('css-class'))); 
		}
		else
		{
			controller.toggle(false); 
		}
	},
	
	/**
	 * Align table on the text right
	 * @param {Boolean} always true in order to take the table marker into account, false otherwise
	 */ 
	applyFloatRight: function(always)
	{
	    // FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
		tinyMCE.activeEditor.focus();
		var node = this.getTable(this._currentNode, always);
		if (node != null)
		{
			tinyMCE.activeEditor.execCommand('mceBeginUndoLevel');
			this._applyFloatRight(node);
			tinyMCE.activeEditor.execCommand('mceEndUndoLevel');
		}
	},
	
	/**
	 * Align table on the text right
	 * @param {HTMLElement} node The table node
	 * @private
	 */
	_applyFloatRight: function(node)
	{
		// tinyMCE issue : mceItemTable has to be the last class
	    // FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
		var hasMceItemTableClass = tinyMCE.activeEditor.dom.hasClass(node, 'mceItemTable'); 
		if (hasMceItemTableClass)
		{
			tinyMCE.activeEditor.dom.removeClass(node, 'mceItemTable');
		}
		
		tinyMCE.activeEditor.dom.addClass(node, 'floatright');
		tinyMCE.activeEditor.dom.removeClass(node, 'floatleft');

		if (hasMceItemTableClass)
		{
			tinyMCE.activeEditor.dom.addClass(node, 'mceItemTable');
		}
	},
	
	/**
	 * Align table on the text left
	 * @param {Boolean} always true in order to take the table marker into account, false otherwise
	 */ 
	applyFloatLeft: function (always) 
	{
		tinyMCE.activeEditor.focus();
		var node = this.getTable(this._currentNode, always);
		if (node != null)
		{
			tinyMCE.activeEditor.execCommand('mceBeginUndoLevel');
			this._applyFloatLeft(node);
			tinyMCE.activeEditor.execCommand('mceEndUndoLevel');
		}
	},
	
	/**
	 * Align table on the text left
	 * @param {HTMLElement} node The table node
	 * @private
	 */
	_applyFloatLeft: function(node)
	{
		// tinyMCE issue : mceItemTable has to be the last class
	    // FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
		var hasMceItemTableClass = tinyMCE.activeEditor.dom.hasClass(node, 'mceItemTable'); 
		if (hasMceItemTableClass)
		{
			tinyMCE.activeEditor.dom.removeClass(node, 'mceItemTable');
		}
		
		tinyMCE.activeEditor.dom.addClass(node, 'floatleft');
		tinyMCE.activeEditor.dom.removeClass(node, 'floatright');

		if (hasMceItemTableClass)
		{
			tinyMCE.activeEditor.dom.addClass(node, 'mceItemTable');
		}
	},
	
	/**
	 * Align table with the text
	 * @param {Boolean} always true in order to take the table marker into account, false otherwise
	 */
	applyNoFloat: function (always) 
	{
	    // FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
		tinyMCE.activeEditor.focus();
		var node = this.getTable(this._currentNode, always);
		if (node != null)
		{
			tinyMCE.activeEditor.execCommand('mceBeginUndoLevel');
			this._applyNoFloat(node);
			tinyMCE.activeEditor.execCommand('mceEndUndoLevel');
		}
	},
	
	/**
	 * @private
	 * Align table with the text
	 * @param {HTMLElement} node The table node
	 */
	_applyNoFloat: function (node) 
	{
	    // FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
		tinyMCE.activeEditor.dom.removeClass(node, 'floatleft');
		tinyMCE.activeEditor.dom.removeClass(node, 'floatright');
	},
	
	/**
	 * Enable/disable and toggle/untoggle controller according the table alignment
	 * @param {Ametys.ribbon.element.ui.ButtonController} controller The controller
	 * @param {Ametys.cms.form.widget.RichText} field The current field. Can be null
	 * @param {HTMLElement} node The current selected node. Can be null.
	 */
	noFloatListener: function (controller, field, node)
	{
		if (node)
		{
			if (node.style.width == '100%')
			{
				controller.setDisabled(true); 
			}
			controller.toggle(!field.getEditor().dom.hasClass(node, 'floatleft') && !field.getEditor().dom.hasClass(node, 'floatright')); 
		}
		else
		{
			controller.toggle(false); 
		}
	},
	
	// ---------------------------------------- //
	//				ROW HEADER					//
	// ---------------------------------------- //
	
	/**
	 * This method toggle row header of the currently selected table. <br/>
	 * If there is row headers they will be removed, else 1 row header will be set (or 2 rows if 1 is not possible due to merge, or 3 rows...)
	 */
	toggleRowHeader: function ()
	{
		var me = this;
		
		function getMinimalHeaderSize(node)
		{
			// check if merge is authorized
			var i = 1;
			
			// FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
			var cellsSelected = tinyMCE.activeEditor.dom.select("[data-mce-selected=\"1\"]", node);
			if (cellsSelected.length > 0)
			{
				i = cellsSelected[cellsSelected.length - 1].parentNode.rowIndex + 1;
			}
			else
			{
				var tr = tinyMCE.activeEditor.dom.getParent(tinyMCE.activeEditor.selection.getNode(), 'tr');
				if (tr != null)
				{
					i = tr.rowIndex + 1;
				}
			}

			for (; i < node.rows.length; i++)
			{
				if (me._canHaveRowHeader(node, i))
				{
					return i;
				}
			}
			return node.rows.length;
		}
		
		tinyMCE.activeEditor.focus();

		// Find table
		var node = this.getTable(tinyMCE.activeEditor.selection.getNode());
		if (node != null)
		{
			var rowHeaders = this._countRowHeader(node); 
			this._setHeaderRows(rowHeaders > 0 ? 0 : getMinimalHeaderSize(node));
		}
	},
	
	/**
	 * This method will change the count of header rows of the currently selected table. 
	 * @param {Number} n The number of rows to convert
	 * @private
	 */
	_setHeaderRows: function(n)
	{
	    // FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
		tinyMCE.activeEditor.focus();

		// Find table
		var node = this.getTable(tinyMCE.activeEditor.selection.getNode());
		if (node != null)
		{
			var bm = tinyMCE.activeEditor.selection.getBookmark();
			tinyMCE.activeEditor.execCommand('mceBeginUndoLevel');

			this._removeHeaderRows(node);

			var grid = this._getTableGrid(node);
			for (var i = 0; i < n; i++)
			{
				for (var j = 0; j < grid[i].length; j++)
				{
					var cell = grid[i][j];
					if (/^td$/i.test(cell.nodeName) && cell.parentNode != null)
					{
						this._replaceCellWith(cell, 'th');
					}
				}
			}
			
			tinyMCE.activeEditor.execCommand('mceEndUndoLevel');
			tinyMCE.activeEditor.selection.moveToBookmark(bm);
		}
	},
	
	/**
	 * This method remove all header rows.
	 * If there are columns headers they will be preserved.
	 * @param {HTMLElement} node The table node
	 * @private
	 */
	_removeHeaderRows: function(node)
	{
		// Find columns of the first row
		var rowHeaderSize = this._countRowHeader(node);
		var colHeaderSize = this._countColHeader(node);
		
		this._removeHeader(node);
		
		if (colHeaderSize > 0 && node.rows.length > rowHeaderSize)
		{
			this._setHeaderCols(colHeaderSize);	
		}
	},
	
	/**
	 * This method will replace all existing header cells by normal cells
	 * @param {HTMLElement} elt The table node
	 * @private
	 */
	_removeHeader: function(elt)
	{
	    // FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
		var cells = tinyMCE.activeEditor.dom.select("tr th", elt);
		Ext.Array.each (cells,
			function(cell)
			{
				this._replaceCellWith(cell, 'td');
			},
			this
		);
	},
	
	/**
	 * This method determine if the given table can have a certain amount of header rows
	 * @param {HTMLElement} elt The table to check
	 * @param {Number} size The number of rows to test
	 * @return {boolean} true if the 'table' can have 'size' header rows
	 * @private
	 */
	_canHaveRowHeader: function (elt, size)
	{
		if (size == 0 || size == elt.rows.length)
		{
			return true;
		}
		else if (size > elt.rows.length)
		{
			return false;
		}
		else
		{
			var grid = this._getTableGrid(elt);
			for (var i = 0; i < grid[0].length; i++)
			{
				if (grid[size - 1][i] == grid[size][i])
				{
					return false;
				}
			}
			return true;
		}
	},
	
	/**
	 * This method can be used as a button listener to handle its state depending on the header row count
	 * @param {Ametys.ribbon.element.ui.ButtonController} controller The controller
	 * @param {Ametys.cms.form.widget.RichText} field The current field. Can be null
	 * @param {HTMLElement} node The current selected node. Can be null.
	 */
	rowHeaderListener : function (controller, field, node)
	{
		if (field != null && node != null && field.getEditor() != null)
		{
			this._rowHeaderListener(controller, 0, false, field.getEditor());
		}
	},
	
	/**
	 * This listener can be user as a gallery button lister to handle its state when there are 0 header rows
	 * @param {Ametys.ribbon.element.ui.ButtonController} controller The gallery button to handler
	 */
	row0HeaderListener: function (controller)
	{
		this._rowHeaderListener(controller, 0, true);
	},
	/**
	 * This listener can be user as a gallery button lister to handle its state when there are 1 header rows
	 * @param {Ametys.ribbon.element.ui.ButtonController} controller The gallery button to handler
	 */
	row1HeaderListener : function (controller)
	{
		this._rowHeaderListener(controller, 1, true);
	},
	/**
	 * This listener can be user as a gallery button lister to handle its state when there are 2 header rows
	 * @param {Ametys.ribbon.element.ui.ButtonController} controller The gallery button to handler
	 */
	row2HeaderListener : function (controller)
	{
		this._rowHeaderListener(controller, 2, true);
	},
	/**
	 * This listener can be user as a gallery button lister to handle its state when there are 3 header rows
	 * @param {Ametys.ribbon.element.ui.ButtonController} controller The gallery button to handler
	 */
	row3HeaderListener : function (controller)
	{
		this._rowHeaderListener(controller, 3, true);
	},
	/**
	 * This listener can be user as a gallery button lister to handle its state when there are 4 header rows or more
	 * @param {Ametys.ribbon.element.ui.ButtonController} controller The gallery button to handler
	 */
	rowXHeaderListener : function (controller)
	{
		this._rowHeaderListener(controller, 3, false);
	},
	
	/**
	 * This method will enable/disable/toggle the given button (or group of buttons) depending if the currenlty selected table count of header rows
	 * @param {Ext.button.Button/Ametys.ribbon.element.RibbonUIController} controller The button to change
	 * @param {Number} count The number of rows to check
	 * @param {Boolean} isCount If true, the count parameter will be considered as an exact amount. If false, the button will be toggled event if the number of rows is more than count.
	 * @param {tinymce.Editor} [editor] The wrapper tinymce editor object
	 * @private
	 */
	_rowHeaderListener: function (controller, count, isCount, editor)
	{
		var canhave = false;
		// FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
		editor = editor || tinyMCE.activeEditor;
		var tableNode = this.getTable(editor.selection.getNode());
		var toggleState = false;
		
		if (tableNode != null)
		{
			toggleState = isCount ? this._countRowHeader(tableNode) == count : this._countRowHeader(tableNode) > count;

			canhave = isCount ? this._canHaveRowHeader(tableNode, count) : tableNode.rows.length > count;
		}

		controller.setDisabled(!canhave);
		controller.toggle(toggleState);
	},
	
	// ---------------------------------------- //
	//				COL HEADER					//
	// ---------------------------------------- //
	
	/**
	 * This method toggle row header of the currently selected table. <br/>
	 * If there is row headers they will be removed, else 1 row header will be set (or 2 rows if 1 is not possible due to merge, or 3 rows...)
	 */
	toggleColHeader: function ()
	{
		var me = this;
		
		function getMinimalHeaderSize(node)
		{
			var i = 1;
			
			// FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
			var cellsSelected = tinyMCE.activeEditor.dom.select("[data-mce-selected=\"1\"]", node);
			if (cellsSelected.length > 0)
			{
				i = me._getCellIndex(cellsSelected[cellsSelected.length - 1]) + 1;
			}
			else
			{
				var cell = tinyMCE.activeEditor.dom.getParent(tinyMCE.activeEditor.selection.getNode(), function (s) { return /^td|th$/i.test(s.nodeName); } );
				if (cell != null)
				{
					i = me._getCellIndex(cell) + 1;
				}
			}

			var max = me._countCells(node);
			for (; i < max; i++)
			{
				if (me._canHaveColHeader(node, i))
				{
					return i;
				}
			}
			return max;
		}
		
		tinyMCE.activeEditor.focus();

		// Find table
		var node = this.getTable(tinyMCE.activeEditor.selection.getNode());
		if (node != null)
		{
			var colHeaders = this._countColHeader(node); 
			this._setHeaderCols(colHeaders > 0 ? 0 : getMinimalHeaderSize(node));
		}
	},
	
	/**
	 * This method will change the count of header columns of the currently selected table. 
	 * @param {Number} n The number of columns to convert
	 * @private
	 */
	_setHeaderCols: function(n)
	{
	    // FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
		tinyMCE.activeEditor.focus();
		
		// Find table
		var node = this.getTable(tinyMCE.activeEditor.selection.getNode());
		if (node != null)
		{
			var bm = tinyMCE.activeEditor.selection.getBookmark();
			tinyMCE.activeEditor.execCommand('mceBeginUndoLevel');

			this._removeHeaderCols(node);

			var grid = this._getTableGrid(node);
			for (var i = 0; i < grid.length; i++)
			{
				for (var j = 0; j < n; j++)
				{
					var cell = grid[i][j];
					if (/^td$/i.test(cell.nodeName) && cell.parentNode != null)
					{
						this._replaceCellWith(cell, 'th');
					}
				}
			}

			tinyMCE.activeEditor.execCommand('mceEndUndoLevel');
			tinyMCE.activeEditor.selection.moveToBookmark(bm);
		}
	},
	
	/**
	 * This method remove all header cols.
	 * If there are columns headers they will be preserved.
	 * @param {HTMLElement} elt The table node
	 * @private
	 */
	_removeHeaderCols: function (elt)
	{
		// Find columns of the first col
		var rowHeaderSize = this._countRowHeader(elt);
		var colHeaderSize = this._countColHeader(elt);
		var nbcells = this._countCells(elt);
		
		this._removeHeader(elt);
		
		if (rowHeaderSize > 0 && nbcells > colHeaderSize)
		{
			this._setHeaderRows(rowHeaderSize);	
		}
	},
	
	/**
	 * This method determine if the given table can have a certain amount of header cols
	 * @param {HTMLElement} elt The table to check
	 * @param {Number} size The number of cols to test
	 * @return {Boolean} true if the 'table' can have 'size' header cols
	 * @private
	 */
	_canHaveColHeader: function(elt, size)
	{
		var nbcells = this._countCells(elt);
		
		if (size == 0 || size == nbcells)
		{
			return true;
		}
		else if (size > nbcells)
		{
			return false;
		}
		else
		{
			var grid = this._getTableGrid(elt);
			for (var i = 0; i < grid.length; i++)
			{
				if (grid[i][size - 1] == grid[i][size])
				{
					return false;
				}
			}
			return true;
		}
	},
	
	/**
	 * This method can be used as a button listener to handle its state depending on the header col count
	 * @param {Ametys.ribbon.element.ui.ButtonController} controller The controller
	 * @param {Ametys.cms.form.widget.RichText} field The current field. Can be null
	 * @param {HTMLElement} node The current selected node. Can be null.
	 */
	colHeaderListener: function (controller, field, node)
	{
		if (node)
		{
			this._colHeaderListener(controller, 0, false);
		}
	},
	/**
	 * This listener can be user as a gallery button lister to handle its state when there are 0 header cols
	 * @param {Ametys.ribbon.element.ui.ButtonController} controller The gallery button to handler
	 */
	col0HeaderListener: function (controller)
	{
		this._colHeaderListener(controller, 0, true);
	},
	/**
	 * This listener can be user as a gallery button lister to handle its state when there are 1 header cols
	 * @param {Ametys.ribbon.element.ui.ButtonController} controller The gallery button to handler
	 */
	col1HeaderListener: function (controller)
	{
		this._colHeaderListener(controller, 1, true);
	},
	/**
	 * This listener can be user as a gallery button lister to handle its state when there are 2 header cols
	 * @param {Ametys.ribbon.element.ui.ButtonController} controller The gallery button to handler
	 */
	col2HeaderListener: function (controller)
	{
		this._colHeaderListener(controller, 2, true);
	},
	/**
	 * This listener can be user as a gallery button lister to handle its state when there are 3 header cols
	 * @param {Ametys.ribbon.element.ui.ButtonController} controller The gallery button to handler
	 */
	col3HeaderListener: function (controller)
	{
		this._colHeaderListener(controller, 3, true);
	},
	/**
	 * This listener can be user as a gallery button lister to handle its state when there are 4 header cols or more
	 * @param {Ametys.ribbon.element.ui.ButtonController} controller The gallery button to handler
	 */
	colXHeaderListener: function (controller)
	{
		this._colHeaderListener(controller, 3, false);
	},
	
	/**
	 * This method will enable/disable/toggle the given button (or group of buttons) depending if the currenlty selected table count of header cols
	 * @param {Ametys.ribbon.element.ui.ButtonController} controller The controller
	 * @param {Number} count The number of cols to check
	 * @param {Boolean} isCount If true, the count parameter will be considered as an exact amount. If false, the button will be toggled event if the number of cols is more than count.
	 * @private
	 */
	_colHeaderListener: function (controller, count, isCount)
	{
		var canhave = false;
		// FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
		var tableNode = this.getTable(tinyMCE.activeEditor.selection.getNode());
		var toggleState = false;
		
		if (tableNode != null)
		{
			var colHeader = this._countColHeader(tableNode);
			toggleState = isCount ? colHeader == count : this._countColHeader(tableNode) > count;

			canhave = isCount ? this._canHaveColHeader(tableNode, count) : this._countCells(tableNode) > count;
		}
		
		controller.setDisabled(!canhave); 
		controller.toggle(toggleState);
	},
	
	// ---------------------------------------- //
	//				INSERT ROW					//
	// ---------------------------------------- //
	/**
	 * Insert a new row before the current row
	 * @param {Boolean} always true in order to take the table marker into account, false otherwise
	 * @param {HTMLElement} table The table node element. Can be null. If null the table is get from current selected cell.
	 */
	insertRowUp: function (always, table)
	{
	    // FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
		tinyMCE.activeEditor.focus();

		// mceTableInsertRowBefore does not take care about th => emulation
		var tableNode = table || this.getTable(this._currentCell, always);
		if (tableNode != null)
		{
			tinyMCE.activeEditor.execCommand('mceBeginUndoLevel');

			var countColHeader = this._countColHeader(tableNode);
			
			tinyMCE.activeEditor.execCommand('mceTableInsertRowBefore', false, 1);
			
			if (countColHeader > 0)
			{
				this._setHeaderCols(countColHeader);
			}
			
			this._repairTable(tableNode); 
			
			tinyMCE.activeEditor.execCommand('mceEndUndoLevel');
		}
	},
	
	/**
	 * Enable/disable controller and set input value according the selected table
	 * @param {Ametys.ribbon.element.ui.ButtonController} controller The controller
	 * @param {Ametys.cms.form.widget.RichText} field The current field. Can be null
	 * @param {HTMLElement} node The current selected node. Can be null.
	 */
    currentCellListener: function(controller, field, node)
    {
        this._currentCell = node;
    },
	
	/**
	 * Insert a new row after the current row
	 * @param {Boolean} always true in order to take the table marker into account, false otherwise
	 * @param {HTMLElement} table The table node element. Can be null. If null the table is get from current selected cell.
	 */
	insertRowDown: function (always, table)
	{
		tinyMCE.activeEditor.focus();

		// mceTableInsertRowBefore does not take care about th => emulation
		var tableNode = table || this.getTable(this._currentCell, always);
		if (tableNode != null)
		{
			tinyMCE.activeEditor.execCommand('mceBeginUndoLevel');

			var countColHeader = this._countColHeader(tableNode);
			
			tinyMCE.activeEditor.execCommand('mceTableInsertRowAfter', false, 1);
			
			if (countColHeader > 0)
			{
				this._setHeaderCols(countColHeader);
			}
			
			this._repairTable(tableNode); 
			
			tinyMCE.activeEditor.execCommand('mceEndUndoLevel');
		}
	},

	// ---------------------------------------- //
	//				INSERT COLUMN				//
	// ---------------------------------------- //
	/**
	 * Insert a new column before the current column
	 * @param {Boolean} always true in order to take the table marker into account, false otherwise
	 */
	insertColumnRight: function (always)
	{
		tinyMCE.activeEditor.focus();
		var tableNode = this.getTable(this._currentCell, always);
		tinyMCE.activeEditor.execCommand('mceTableInsertColAfter');
		this._repairTable(tableNode); 
	},

	/**
	 * Insert a new column after the current column
	 * @param {Boolean} always true in order to take the table marker into account, false otherwise
	 */
	insertColumnLeft: function (always)
	{
	    // FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
		tinyMCE.activeEditor.focus();
		var tableNode = this.getTable(this._currentCell, always);
		tinyMCE.activeEditor.execCommand('mceTableInsertColBefore', false, 1);
		this._repairTable(tableNode); 
	},
	
	// ---------------------------------------- //
	//				DELETE 						//
	// ---------------------------------------- //
	/**
	 * Delete the table
	 */
	deleteTable: function ()
	{
	    // FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
		tinyMCE.activeEditor.execCommand('mceTableDelete', null, null, { skip_focus: true });
        tinyMCE.activeEditor.focus(); // We do not want to focus automatically, because it is too soon, and the selection would be the orphean TD/TH tag
	},
	
	/**
	 * Delete the selected row
	 */
	deleteRow: function ()
	{
	    // FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
		tinyMCE.activeEditor.execCommand('mceTableDeleteRow', null, null, { skip_focus: true });
        tinyMCE.activeEditor.focus(); // We do not want to focus automatically, because it is too soon, and the selection would be the orphean TD/TH tag
	},
	
	/**
	 * Delete the selected column
	 */
	deleteCol: function ()
	{
	    // FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
		tinyMCE.activeEditor.execCommand('mceTableDeleteCol', null, null, { skip_focus: true });
        tinyMCE.activeEditor.focus(); // We do not want to focus automatically, because it is too soon, and the selection would be the orphean TD/TH tag
	},
	
	// ---------------------------------------- //
	//				MERGE						//
	// ---------------------------------------- //
	
	/**
	 * Merge the selected cell with the right cell
	 * @param {Boolean} always true in order to take the table marker into account, false otherwise
	 */
	mergeCells: function (always)
	{
		var args = [];
		args["numcols"] = 1;
		args["numrows"] = 1;
		
		// FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
		tinyMCE.activeEditor.focus();
		
		var parentTable = this.getTable(tinyMCE.activeEditor.selection.getStart(), always);
		if (parentTable != null)
		{
			// check if merge is authorized
			var cellsSelected = tinyMCE.activeEditor.dom.select("[data-mce-selected=\"1\"]", parentTable);
			
			var x1 = this._getCellIndex(cellsSelected[0]);
			var y1 = cellsSelected[0].parentNode.rowIndex;

			var x2 = this._getCellIndex(cellsSelected[cellsSelected.length - 1]);
			var y2 = cellsSelected[cellsSelected.length - 1].parentNode.rowIndex;
			
			var rowHeaders = this._countRowHeader(parentTable);		
			var colHeaders = this._countColHeader(parentTable);
			
			if (x1 < colHeaders && x2 >= colHeaders || y1 < rowHeaders && y2 >= rowHeaders)
			{
				Ametys.Msg.alert("{{i18n CONTENT_EDITION_TABLE_MERGE_CELL_FORBIDEN_TITLE}}", "{{i18n CONTENT_EDITION_TABLE_MERGE_CELL_FORBIDEN_TEXT}}");
				return;
			}		
			
			if (x1 != x2)
			{
				cellsSelected[0].style.width = '';
				cellsSelected[0].removeAttribute("data-mce-style");
			}
			if (y1 != y2)
			{
				cellsSelected[0].style.height = '';
				cellsSelected[0].removeAttribute("data-mce-style");
			}
			
			// do the merge
			tinyMCE.activeEditor.execCommand('mceTableMergeCells', false, args);
		}
	},
	
	/**
	 * Enable/disable controller according the selected table
	 * @param {Ametys.ribbon.element.ui.ButtonController} controller The controller
	 * @param {Ametys.cms.form.widget.RichText} field The current field. Can be null
	 * @param {HTMLElement} node The current selected node. Can be null.
	 */
	mergeCellsListener: function (controller, field, node)
	{
		if (field == null || node == null || field.getEditor() == null || field.getEditor().dom == null || field.getEditor().selection == null || field.getEditor().selection.getStart() == null)
		{
			controller.disable();
			return;
		}

		var on = false;
		var parentTable = this.getTable(field.getEditor().selection.getStart(), null, field.getEditor());
		if (parentTable != null)
		{
			var tdSelected = field.getEditor().dom.select("td[data-mce-selected=\"1\"]", parentTable);
			var thSelected = field.getEditor().dom.select("th[data-mce-selected=\"1\"]", parentTable);

			on = (tdSelected.length > 1 && thSelected.length == 0) 
				|| (tdSelected.length == 0 && thSelected.length > 1);
		}
		
		if (!on)
		{
			controller.setDisabled(true);
		}
	},
	
	// ---------------------------------------- //
	//				SPLIT						//
	// ---------------------------------------- //
	
	/**
	 * Slip the selected cell in two cells
	 * @param {Boolean} always true in order to take the table marker into account, false otherwise 
	 */
	splitCells: function (always)
	{
	    // FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
		tinyMCE.activeEditor.focus();
		
		var parentTable = this.getTable(tinyMCE.activeEditor.selection.getStart(), always);
		
		var countColHeader = parentTable != null && this._countColHeader(parentTable);
		var countRowHeader = parentTable != null && this._countRowHeader(parentTable);
		
		tinyMCE.activeEditor.execCommand('mceBeginUndoLevel');
		tinyMCE.activeEditor.execCommand('mceTableSplitCells');
		
		this._setHeaderCols(countColHeader);
		this._setHeaderRows(countRowHeader);
		
		tinyMCE.activeEditor.execCommand('mceEndUndoLevel');
	},
	
	/**
	 * Enable/disable controller according the selected table
	 * @param {Ametys.ribbon.element.ui.ButtonController} controller The controller
	 * @param {Ametys.cms.form.widget.RichText} field The current field. Can be null
	 * @param {HTMLElement} node The current selected node. Can be null.
	 */
	splitCellsListener: function (controller, field, node)
	{
		if (field == null || node == null || node.getAttribute == null || field.getEditor() == null || field.getEditor().dom == null)
		{
			controller.disable();
			return;
		}

		var on = false;
		var cell = field.getEditor().dom.getParent(field.getEditor().selection.getNode(), function (node) { return /^td|th$/i.test(node.nodeName); });

		if (cell != null)
		{
			var rowcolspan = this._getColRowSpan(cell);
			var colspan = rowcolspan.colspan;
			var rowspan = rowcolspan.rowspan;
			on = ((!isNaN(colspan) && colspan != 1) || (!isNaN(rowspan) && rowspan != 1));
		}

		if (!on)
		{
			controller.setDisabled(true);
		}
	},
	
	// ---------------------------------------- //
	//				COL SIZE					//
	// ---------------------------------------- //

	/**
	 * Set the width of current column when pressing ENTER or ESC key.
	 * @param {Ext.form.field.Text} input The input text
	 * @param {Ext.event.Event} e The event object
	 */
	setColSizeOnSpecialKey: function (input, e)
	{
		if (e.getKey() == e.ENTER) 
		{
			e.preventDefault();
			e.stopPropagation();
			this.setColSize(input.getValue());
			tinyMCE.activeEditor.focus();
		}
		else if (e.getKey() == e.ESC) 
		{
			e.preventDefault();
			e.stopPropagation();
			input.setValue(this._getCellWidth(this._currentCell));
			tinyMCE.activeEditor.focus();
		}
	},

	/**
	 * Set the width of current column when the input looses the focus
	 * @param {Ext.form.field.Text} input The input text
	 */
	setColSizeOnBlur: function(input)
	{
		var inputValue = input.getValue();
		if (this._currentCell && inputValue != null && this._currentCell.style.width != inputValue + 'px')
		{
			this.setColSize(inputValue);
		}
	},
	
	/**
	 * Set the width of current column when the spinner is made to spin up or down
	 * @param {Ext.form.field.Text} input The input text
	 * @param {String} direction The direction: 'up' if spinning up, or 'down' if spinning down.
	 */
	setColSizeOnSpin: function(input, direction)
	{
		this.setColSize(input.getValue() + (direction == 'up' ? 1 : -1));
	},
	
	/**
	 * Set the width of the current column
	 * @param {String} value The value
	 */
	setColSize: function (value)
	{
		var cell = this._currentCell;
		if (cell != null)
		{
			this._setColWidth(cell, value != '' ? value + "px" : '');
			tinyMCE.activeEditor.execCommand('mceAddUndoLevel');
		}
	},

	/**
	 * Enable/disable controller and set input value according the selected column
	 * @param {Ametys.ribbon.element.ui.FieldController} controller The controller
	 * @param {Ametys.cms.form.widget.RichText} field The current field. Can be null
	 * @param {HTMLElement} node The current selected node. Can be null.
	 */
	colSizeListener: function (controller, field, node)
	{
		var cell = (field != null && node != null) ? field.getEditor().dom.getParent(field.getEditor().selection.getNode(), function (node) { return /^td|th$/i.test(node.nodeName); }) : null;
		if (cell != null)
		{
			var cellWidth = this._getCellWantedWidth(cell);
			var realWidth = this._getCellWidth(cell);
			controller.enable(); 
			controller.setValue(cellWidth);
			controller.getUIControls().each(function (input) {
				if (typeof input.setReferenceValue == 'function')
				{
					input.setReferenceValue(realWidth + " px");
				}
			});	
		}
		else
		{
			controller.disable();
			controller.setValue("");
			controller.getUIControls().each(function (input) {
				if (typeof input.setReferenceValue == 'function')
				{
					input.setReferenceValue('');
				}
			});	
		}
		this._currentCell = cell;
	},
	
	// ---------------------------------------- //
	//				ROW SIZE					//
	// ---------------------------------------- //
	/**
	 * Set the height of current row when pressing ENTER or ESC key.
	 * @param {Ext.form.field.Text} input The input text
	 * @param {Ext.event.Event} e The event object
	 */
	setRowSizeOnSpecialKey: function (input, e)
	{
		if (e.getKey() == e.ENTER) 
		{
			e.preventDefault();
			e.stopPropagation();
			this.setRowSize(input.getValue());
			// FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
			tinyMCE.activeEditor.focus();
		}
		else if (e.getKey() == e.ESC) 
		{
			e.preventDefault();
			e.stopPropagation();
			input.setValue(this._getCellHeight(this._currentCell));
			// FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
			tinyMCE.activeEditor.focus();
		}	
	},

	/**
	 * Set the height of current row when the input looses the focus
	 * @param {Ext.form.field.Text} input The input text
	 */
	setRowSizeOnBlur: function(input)
	{
		var inputValue = input.getValue();
		if (this._currentCell && inputValue != null && this._currentCell.style.height != inputValue + 'px')
		{
			this.setRowSize(inputValue);
		}
	},
	
	/**
	 * Set the height of current row when the spinner is made to spin up or down
	 * @param {Ext.form.field.Text} input The input text
	 * @param {String} direction The direction: 'up' if spinning up, or 'down' if spinning down.
	 */
	setRowSizeOnSpin: function(input, direction)
	{
		this.setRowSize(input.getValue() + (direction == 'up' ? 1 : -1));
	},
	
	/**
	 * Set the height of the current row
	 * @param {String} value The value
	 */
	setRowSize: function(value)
	{
		var cell = this._currentCell;
		if (cell != null)
		{
			this._setRowHeight(cell, value != '' ? value + "px" : '');
			// FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
			tinyMCE.activeEditor.execCommand('mceAddUndoLevel');
		}
	},

	/**
	 * Enable/disable controller and set input value according the selected row
	 * @param {Ametys.ribbon.element.ui.FieldController} controller The controller
	 * @param {Ametys.cms.form.widget.RichText} field The current field. Can be null
	 * @param {HTMLElement} node The current selected node. Can be null.
	 */
	rowSizeListener: function (controller, field, node)
	{
		var cell = (field != null && node != null) ? field.getEditor().dom.getParent(field.getEditor().selection.getNode(), function (node) { return /^td|th$/i.test(node.nodeName); }) : null;
		if (cell != null)
		{
			var cellHeight = this._getCellWantedHeight(cell);
			var realHeight = this._getCellHeight(cell);
			controller.setDisabled(false); 
			controller.setValue(cellHeight);
			controller.getUIControls().each(function (input) {
				if (typeof input.setReferenceValue == 'function')
				{
					input.setReferenceValue(realHeight + " px");
				}
			});	
		}
		else
		{
			controller.setDisabled(true); 
			controller.setValue("");
			controller.getUIControls().each(function (input) {
				if (typeof input.setReferenceValue == 'function')
				{
					input.setReferenceValue('');
				}
			});			
		}
		this._currentCell = cell;
	},
	
	/**
	 * Ajust table width to the content
	 * @param {Boolean} always true in order to take the table marker into account, false otherwise
	 */
	colSizeAjustToContent: function (always)
	{
		// Find table
	    // FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
		tinyMCE.activeEditor.focus();
		var node = this.getTable(tinyMCE.activeEditor.selection.getNode(), always);
		if (node != null)
		{
			tinyMCE.activeEditor.execCommand('mceBeginUndoLevel');
			
			// remove table global size
			node.style.width = '';
			node.removeAttribute("data-mce-style");
			
			// remove each cell size
			var cells = tinyMCE.activeEditor.dom.select("tr *", node);
			Ext.Array.each (cells, 
				function (cell)
				{
					cell.style.width = "";
					cell.removeAttribute("data-mce-style");
				}
			);
			
			tinyMCE.activeEditor.execCommand('mceEndUndoLevel');
			tinyMCE.activeEditor.execCommand('mceRepaint');
		}
	},
	
	/**
	 * Ajust table width to the page
	 * @param {Boolean} always true in order to take the table marker into account, false otherwise
	 */
	colSizeAjustToWindow: function (always)
	{
		// Find table
		tinyMCE.activeEditor.focus();
		var node = this.getTable(tinyMCE.activeEditor.selection.getNode(), always);
		if (node != null)
		{
			node.style.width = '100%';
			node.removeAttribute("data-mce-style");
			tinyMCE.activeEditor.execCommand('mceAddUndoLevel');
		}
	},

	/**
	 * Fix table width
	 * @param {Boolean} always true in order to take the table marker into account, false otherwise
	 */
	colSizeFixed: function (always)
	{
	    // FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
		tinyMCE.activeEditor.focus();
		var node = this.getTable(tinyMCE.activeEditor.selection.getNode(), always);
		if (node != null)
		{
			tinyMCE.activeEditor.execCommand('mceBeginUndoLevel');
			
			// remove each cell size
			var me = this;
			var cells = tinyMCE.activeEditor.dom.select("tr > *", node);
			Ext.Array.each (cells, 
				function (cell)
				{
					// need to be done twice since a read after the first modication can render a different result
					cell.style.width = me._getCellWidth(cell) + "px";
					cell.style.width = me._getCellWidth(cell) + "px";
					cell.removeAttribute("data-mce-style");
				}
			);
			
			// remove table global size
			node.style.width = '';
			node.removeAttribute("data-mce-style");
			
			tinyMCE.activeEditor.execCommand('mceEndUndoLevel');
		}
	},
	
	// ---------------------------------------- //
	//				GUIDELINES					//
	// ---------------------------------------- //
	/**
	 * Show or hide guidelines
	 */
	toggleVisualAid: function ()
	{
	    // FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
		tinyMCE.activeEditor.execCommand('mceToggleVisualAid');
	},
	
	/**
	 * Enable/disable and toggle/untoggle controller according the guidelines status
	 * @param {Ametys.ribbon.element.ui.ButtonController} controller The controller
	 * @param {Ametys.cms.form.widget.RichText} field The current field. Can be null
	 * @param {HTMLElement} node The current selected node. Can be null.
	 */
	visualAidListener: function (controller, field, node)
	{
		var toggleState = field != null && node != null && field.getEditor() != null && field.getEditor().dom != null && field.getEditor().hasVisual;
		controller.toggle(toggleState);
	},
	
	/**
	 * Enable/disable and toggle/untoggle controller according to the selected node
	 * @param {Ametys.ribbon.element.ui.ButtonController} controller The controller
	 * @param {Ametys.cms.form.widget.RichText} field The current field. Can be null
	 * @param {HTMLElement} node The current selected node. Can be null.
	 */
	tableListener: function (controller, field, node)
	{
		this._currentNode = node;
	},
	
	// ---------------------------------------- //
	//				UTILITARY					//
	// ---------------------------------------- //
	/**
	 * Get the surrounding table
	 * @param {HTMLElement} node The node element inside a table
	 * @param {Boolean} always true in order to take the table marker into account, false otherwise
	 * @param {tinymce.Editor} [editor] The wrapper tinymce editor object
	 * @return {HTMLElement} The table node or null if not found
	 */
	getTable: function(node, always, editor)
	{
	    // FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
		editor = editor || tinyMCE.activeEditor;
		node = editor.dom.getParent(node, 'table');
		if (always != true && node != null && node.getAttribute("marker") != null)
		{
			node = null;
		}
		
		return node;
	},	
	/**
	 * Repair the table
	 * @param {HTMLElement} table The table
	 * @private
	 */
	_repairTable: function(table)
	{
		if (table == null)
		{
			return;
		}
		
		for (var i = 0; i < table.rows.length; i++)
		{
			for (var j = 0; j < table.rows[i].cells.length; j++)
			{
				var cell = table.rows[i].cells[j];
				if (cell.innerHTML == '' || cell.innerHTML == '&#160;' || cell.childNodes.length == 1 && /^br$/i.test(cell.childNodes[0].tagName))
				{
					cell.innerHTML = this._createInnerHTML();
				}
			}
		}
	},
	
	/**
	 * @private
	 * Determine the number of row headers in the table
	 * @param {HTMLElement} tableElt A table html element
	 * @return {Number} 0 if the table has no row header or the number of header rows in other cases.
	 */
    _countRowHeader: function (tableElt)
    {
    	var tdEls = tinyMCE.activeEditor.dom.select("td:first-of-type", tableElt);
        if (tdEls.length > 0)
        {
            return tdEls[0].parentNode.rowIndex;
        }
        else
        {
            return tableElt.rows.length;
        }
    },
    
    /**
     * @private
     * Determine the number of column headers
     * @param {HTMLElement} tableElt A table html element
     * @return {Number} 0 if the table has no col header or the number of header cols in other cases.
     */
    _countColHeader: function (tableElt)
    {
        var tdEls = tinyMCE.activeEditor.dom.select("td:first-of-type", tableElt);
        if (tdEls.length > 0)
        {
            return this._getCellIndex(tdEls[0]);
        }
        else
        {
            return this._countCells(tableElt);
        }
    },

	/**
	 * @private
	 * Determine the cell real size
	 * @param {HTMLElement} cell The cell node
	 * @return {Number} The cell width
	 */
	_getCellWidth: function(cell)
	{
	    // FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
		return tinyMCE.activeEditor.dom.getSize(cell).w;
	},
	
	/**
	 * @private
	 * Get the desired width. Can be empty.
	 */
	_getCellWantedWidth: function(cell)
	{
		if (cell)
		{
			var w = cell.style.width;
			var wi = parseInt(w);
			return isNaN(wi) ? '' : '' + wi;
		}
		else
		{
			return '';
		}
	},

	/**
	 * @private
	 * Determine the size of a column
	 * @param {HTMLElement} cell The html table cell which size have to be determined
	 */
	_getColWidth: function (cell)
	{
		var table = this.getTable(cell);
		var grid = this._getTableGrid(table);
		
		var i = this._getCellIndex(cell, grid);

		for (var x = 0; x < grid.length; x++)
		{
			if (this._getColRowSpan(grid[x][i]).colspan == 1)
			{
				return this._getCellWidth(grid[x][i]);
			}
		}
		
		return this._getCellWidth(cell);
	},
	
	/**
	 * Get the desired width of column
	 * @param {Number} index The index of the column
	 * @param {HTMLElement} grid The table
	 * @private
	 */
	_getColWantedWidthByIndex: function(index, grid)
	{
		var mincolspan = 1000000;
		var mincolspanAssociatedX = -1;
		
		for (var x = 0; x < grid.length; x++)
		{
			var colrowspan = this._getColRowSpan(grid[x][index]);
			if (colrowspan.colspan == 1)
			{
				return this._getCellWantedWidth(grid[x][index]);
			}
			else if (colrowspan.colspan < mincolspan)
			{
				mincolspan = colrowspan.colspan;
				mincolspanAssociatedX = x;
			}
		}

		return this._getCellWantedWidth(grid[mincolspanAssociatedX][index]) / mincolspan;
	},
	
	/**
	 * @private
	 * Set the size of a column
	 */
	_setColWidth: function(cell, size)
	{
		var table = this.getTable(cell);
		var grid = this._getTableGrid(table);
		var cellIndex = this._getCellIndex(cell, grid);
		
		this._setColWidthByIndex (cellIndex, size, grid);
	},
	
	/**
	 * Set the column width
	 * @param {Number} cellIndex The column index
	 * @param {String} size The desired width
	 * @param {HTMLElement} grid The table
	 * @private
	 */
	_setColWidthByIndex: function(cellIndex, size, grid)
	{
		for (var x = 0; x < grid.length; x++)
		{
			var gridColRowSpan = this._getColRowSpan(grid[x][cellIndex]);
			if (gridColRowSpan.colspan == 1 && grid[x][cellIndex])
			{
				grid[x][cellIndex].style.width = size;
				grid[x][cellIndex].removeAttribute("data-mce-style");
			}
		}
	},

	/**
	 * @private
	 * Determine the cell real size
	 * @param {HTMLElement} cell The cell
	 */
	_getCellHeight: function(cell)
	{
	    // FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
		return tinyMCE.activeEditor.dom.getSize(cell).h;
	},
	
	/**
	 * @private
	 * Get the desired height. Can be empty.
	 * @param {HTMLElement} cell The cell
	 */
	_getCellWantedHeight: function(cell)
	{
		var h = cell.style.height;
		var hi = parseInt(h);
		return isNaN(hi) ? '' : '' + hi;
	},
	
	/**
	 * Get the desired height of a row
	 * @param {Number} index The row index
	 * @param {HTMLElement} grid The table
	 */
	_getRowWantedHeightByIndex: function(index, grid)
	{
		var minrowspan = 1000000;
		var minrowspanAssociatedX = -1;
		
		for (var x = 0; x < grid[index].length; x++)
		{
			var colrowspan = this._getColRowSpan(grid[index][x]);
			if (colrowspan.rowspan == 1)
			{
				return this._getCellWantedHeight(grid[index][x]);
			}
			else if (colrowspan.rowspan < minrowspan)
			{
				minrowspan = colrowspan.rowspan;
				minrowspanAssociatedX = x;
			}
		}

		return this._getCellWantedHeight(grid[index][minrowspanAssociatedX]) / minrowspan;
	},
	
	/**
	 * @private
	 * Set the size of a row
	 */
	_setRowHeight: function(cell, size)
	{
		var table = this.getTable(cell);
		var grid = this._getTableGrid(table);
		var rowIndex = cell.parentNode.rowIndex;
		
		this._setRowHeightByIndex(rowIndex, size, grid);
	},
	
	/**
	 * Set the row height
	 * @param {Number} rowIndex The row index
	 * @param {String} size The desired height
	 * @param {HTMLElement} grid The table grid
	 * @private
	 */
	_setRowHeightByIndex: function(rowIndex, size, grid)
	{
		for (var x = 0; x < grid[rowIndex].length; x++)
		{
			var gridColRowSpan = this._getColRowSpan(grid[rowIndex][x]);
			if (gridColRowSpan.rowspan == 1)
			{
				grid[rowIndex][x].style.height = size;
				grid[rowIndex][x].removeAttribute("data-mce-style");
			}
		}
	},

	/**
	 * @private
	 * Compute the colspan and the rowspan of a cell
	 * @param {HTMLElement} td The cell to compute
	 * @return {Object} A map with colspan and rowspan
	 */
	_getColRowSpan: function(td) 
	{
		// UPGRADE (tinyMCE 3.2.5) plugins/table/editor_plugin_src.js _doExecCommand getColRowSpan
		
	    // FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
		var colspan = tinyMCE.activeEditor.dom.getAttrib(td, "colspan");
		var rowspan = tinyMCE.activeEditor.dom.getAttrib(td, "rowspan");

		colspan = colspan == "" ? 1 : parseInt(colspan);
		rowspan = rowspan == "" ? 1 : parseInt(rowspan);

		return {colspan : colspan, rowspan : rowspan};
	},
	
	/**
	 * @private
	 * Will return the col index of the cell by taking in account rowspan of preceding lines
	 * @param {HTMLElement} cell The cell
	 * @param {HTMLElement[]} grid Optionnaly the grid of the table if already computed
	 * @return {Number} The real cell index
	 */
	_getCellIndex: function(cell, grid)
	{
		var rowIndex = cell.parentNode.rowIndex;
		
		if (rowIndex == 1)
		{
			return cell.cellIndex;
		}
		else
		{
			grid = grid || this._getTableGrid(this.getTable(cell, true), rowIndex + 1);
			for (var i = 0; i < grid[rowIndex].length; i++)
			{
				if (grid[rowIndex][i] == cell)
				{
					return i;
				}
			}
		}
	},
	
	/**
	 * Count cells on each rows for the given table.
	 * @param {HTMLElement} elt The table.
	 * @private
	 */
	_countCells: function(elt)
	{
		var nbcell = 0;
		
		var k = 0;
		while (elt.rows[0].cells[k] != null)
		{
			var colspan = this._getColRowSpan(elt.rows[0].cells[k]).colspan;
			nbcell += colspan;
			k++;
		}
		return nbcell;
	},
	
	/**
	 * @private
	 * Compute a grid for the table (integrating colspan and rowspan)
	 * @param {HTMLElement} table The table to compute
	 * @param {Number} maxrow Optionnaly, you can specify a maxrow where computing will stop for performance issue.
	 * @return {HTMLElement[][]} An array of cell. Cells with rowspan or colspan are set several times
	 */
	_getTableGrid: function(table, maxrow) 
	{
		// UPGRADE (tinyMCE 3.2.5) plugins/table/editor_plugin_src.js _doExecCommand getTableGrid
		
		var grid = [], rows = table.rows, x, y, td, sd, xstart, x2, y2;
		
		maxrow = maxrow || rows.length;

		for (y=0; y<maxrow; y++) {
			for (x=0; x<rows[y].cells.length; x++) {
				td = rows[y].cells[x];
				sd = this._getColRowSpan(td);

				// All ready filled
				for (xstart = x; grid[y] && grid[y][xstart]; xstart++) ;

				// Fill box
				for (y2=y; y2<y+sd['rowspan']; y2++) {
					if (!grid[y2])
						grid[y2] = [];

					for (x2=xstart; x2<xstart+sd['colspan']; x2++)
						grid[y2][x2] = td;
				}
			}
		}

		return grid;
	},

	/**
	 * @private
	 * Determine if we are in a cell
	 * @param {Ametys.cms.form.widget.RichText} field The current field
	 * @param {HTMLElement} node The current node selected 
	 * @return {boolean} true if the node is in a cell
	 */
	_isInCellOrHeader: function (field, node)
	{
		var off = field == null || node == null;
		if (off)
		{
			return false;
		}
		return field.getEditor() != null && field.getEditor().dom != null && field.getEditor().dom.getParent(node, function (s) { return /^td|th$/i.test(s.nodeName); } ) != null;
	},

	/**
	 * @private
	 * Change a cell tag to another tag
	 * @param {Ext.Element} cell The cell to change
	 * @param {String} tag The tag name of the element to replace with
	 * @return {boolean} true if the table has a least a th in its firt row
	 */
	_replaceCellWith: function (cell, tag)
	{
		var colspan = cell.getAttribute("colspan");
		var rowspan = cell.getAttribute("rowspan");
		var style = cell.hasAttribute("style") ? cell.getAttribute("style") : null;
		
		// FIXME "tinyMCE.activeEditor" a better method is to use the field.getEditor()
		var elt = tinyMCE.activeEditor.dom.doc.createElement(tag);
	    if (colspan)
	    {
	    	elt.setAttribute("colspan", colspan);
	    }
	    if (rowspan)
	    {
	    	elt.setAttribute("rowspan", rowspan);
	    }
	    if (style)
	    {
	    	elt.setAttribute("style", style);
	    }
		
		while (cell.childNodes.length > 0)
		{
			elt.appendChild(cell.childNodes[0]);
		}
		
		cell.parentNode.insertBefore(elt, cell);
		cell.parentNode.removeChild(cell);
		return elt;
	},
    /*
     * Controller helpers
     */

    /**
     * Creates an array of squares to choose table dimension
     */
    createTabCreator: function ()
    {
        var col = 10;
        var row = 8;

        var html = ""

        html += "<table class='insert-table'>";
        for (var r = 0; r < row; r++)
        {
            html += "<tr>";
            for (var c = 0; c < col; c++)
            {
                html += "<td>&#160;</td>";
            }
            html += "</tr>";
        }
        html += "</table>";

        html += "<div class='insert-table-hint'>" + "{{i18n CONTENT_EDITION_TABLE_ARRAY_DECRIPTION}}" + "</div>";

        return html;
    },
    /**
     * Highlight the selected rows and cols
     * @param {HTMLElement} table the table to modify
     * @param {Number} rows The number of rows to highlight
     * @param {Number} cols The number of columns to highlight
     */
    drawMenuTable: function(table, rows, cols)
    {
        for (var j = 0; j < table.rows.length; j++)
        {
            for (var i = 0; i < table.rows[j].cells.length; i++)
            {
                var cell = table.rows[j].cells[i];

                if (j <= rows && i <= cols)
                {
                    cell.className = "over";
                }
                else
                {
                    cell.className = "";
                }
            }
        }
    },
    /**
     * Clear the menu table
     * @param {HTMLElement} table table displayed
     * @param {Function} setTitleFunction method that will be called to set the title
     */
    clearMenuTable: function(table, setTitleFunction)
    {
        for (var j = 0; j < table.rows.length; j++)
        {
            for (var i = 0; i < table.rows[j].cells.length; i++)
            {
                var cell = table.rows[j].cells[i];
                cell.className = "";
            }
        }
        setTitleFunction();
    },
    /**
     * When a mouse hover over a cell of the table
     * @param {HTMLElement} target object under the mouse
     * @param {HTMLElement} table table displayed
     * @param {Function} saveSizeFunction method that will be called to save the current selection
     * @param {Function} setTitleFunction method that will be called to set the title
     */
    onMouseOver: function(target, table, saveSizeFunction, setTitleFunction)
    {
        if (/^td$/i.test(target.tagName))
        {
            rows = target.parentNode.rowIndex;
            cols = target.cellIndex;

            Ametys.plugins.cms.editor.Tables.drawMenuTable(table, rows, cols);
            saveSizeFunction(rows, cols);
            setTitleFunction(rows, cols);
        }
    }
});