/*******************************************************************************
 * Copyright (c) 2006, 2007 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Tom Schindl <tom.schindl@bestsolution.at> - refactoring (bug 153993)
 *     											   fix in bug 151295,166500,200337
 *******************************************************************************/

package org.eclipse.jface.viewers;

import org.eclipse.core.runtime.ListenerList;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.FocusAdapter;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.TraverseEvent;
import org.eclipse.swt.events.TraverseListener;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Item;

/**
 * This is the base for all editor implementations of Viewers. ColumnViewer
 * implementators have to subclass this class and implement the missing methods
 *
 * @since 3.3
 * @see TableViewerEditor
 * @see TreeViewerEditor
 */
public abstract class ColumnViewerEditor {
	private CellEditor cellEditor;

	private ICellEditorListener cellEditorListener;

	private FocusListener focusListener;

	private MouseListener mouseListener;

	private ColumnViewer viewer;

	private TraverseListener tabeditingListener;

	private int activationTime;

	private ViewerCell cell;

	private ColumnViewerEditorActivationEvent activationEvent;

	private ListenerList editorActivationListener;

	private ColumnViewerEditorActivationStrategy editorActivationStrategy;

	/**
	 * Tabbing from cell to cell is turned off
	 */
	public static final int DEFAULT = 1;

	/**
	 * Should if the end of the row is reach started from the start/end of the
	 * row below/above
	 */
	public static final int TABBING_MOVE_TO_ROW_NEIGHBOR = 1 << 1;

	/**
	 * Should if the end of the row is reach started from the beginning in the
	 * same row
	 */
	public static final int TABBING_CYCLE_IN_ROW = 1 << 2;

	/**
	 * Support tabbing to Cell above/below the current cell
	 */
	public static final int TABBING_VERTICAL = 1 << 3;

	/**
	 * Should tabbing from column to column with in one row be supported
	 */
	public static final int TABBING_HORIZONTAL = 1 << 4;

	/**
	 * Style mask used to enable keyboard activation
	 */
	public static final int KEYBOARD_ACTIVATION = 1 << 5;

	private int feature;

	/**
	 * @param viewer
	 *            the viewer this editor is attached to
	 * @param editorActivationStrategy
	 *            the strategy used to decide about editor activation
	 * @param feature
	 *            bit mask controlling the editor
	 *            <ul>
	 *            <li>{@link ColumnViewerEditor#DEFAULT}</li>
	 *            <li>{@link ColumnViewerEditor#TABBING_CYCLE_IN_ROW}</li>
	 *            <li>{@link ColumnViewerEditor#TABBING_HORIZONTAL}</li>
	 *            <li>{@link ColumnViewerEditor#TABBING_MOVE_TO_ROW_NEIGHBOR}</li>
	 *            <li>{@link ColumnViewerEditor#TABBING_VERTICAL}</li>
	 *            </ul>
	 */
	protected ColumnViewerEditor(ColumnViewer viewer,
			ColumnViewerEditorActivationStrategy editorActivationStrategy,
			int feature) {
		this.viewer = viewer;
		this.editorActivationStrategy = editorActivationStrategy;
		if ((feature & KEYBOARD_ACTIVATION) == KEYBOARD_ACTIVATION) {
			this.editorActivationStrategy
					.setEnableEditorActivationWithKeyboard(true);
		}
		this.feature = feature;
		initCellEditorListener();
	}

	private void initCellEditorListener() {
		cellEditorListener = new ICellEditorListener() {
			public void editorValueChanged(boolean oldValidState,
					boolean newValidState) {
				// Ignore.
			}

			public void cancelEditor() {
				ColumnViewerEditor.this.cancelEditing();
			}

			public void applyEditorValue() {
				ColumnViewerEditor.this.applyEditorValue();
			}
		};
	}

	void activateCellEditor() {

		ViewerColumn part = viewer.getViewerColumn(cell.getColumnIndex());
		Object element = cell.getElement();

		if (part != null && part.getEditingSupport() != null
				&& part.getEditingSupport().canEdit(element)) {

			cellEditor = part.getEditingSupport().getCellEditor(element);
			if (cellEditor != null) {
				if (editorActivationListener != null
						&& !editorActivationListener.isEmpty()) {
					Object[] ls = editorActivationListener.getListeners();
					for (int i = 0; i < ls.length; i++) {

						if (activationEvent.cancel) {
							return;
						}

						((ColumnViewerEditorActivationListener) ls[i])
								.beforeEditorActivated(activationEvent);
					}
				}

				updateFocusCell(cell, activationEvent);

				cellEditor.addListener(cellEditorListener);
				part.getEditingSupport().initializeCellEditorValue(cellEditor,
						cell);

				// Tricky flow of control here:
				// activate() can trigger callback to cellEditorListener which
				// will clear cellEditor
				// so must get control first, but must still call activate()
				// even if there is no control.
				final Control control = cellEditor.getControl();
				cellEditor.activate(activationEvent);
				if (control == null) {
					return;
				}
				setLayoutData(cellEditor.getLayoutData());
				setEditor(control, (Item) cell.getItem(), cell.getColumnIndex());
				cellEditor.setFocus();

				if( cellEditor.dependsOnExternalFocusListener() ) {
					if (focusListener == null) {
						focusListener = new FocusAdapter() {
							public void focusLost(FocusEvent e) {
								applyEditorValue();
							}
						};
					}
					control.addFocusListener(focusListener);
				}

				mouseListener = new MouseAdapter() {
					public void mouseDown(MouseEvent e) {
						// time wrap?
						// check for expiration of doubleClickTime
						if (e.time <= activationTime) {
							control.removeMouseListener(mouseListener);
							cancelEditing();
							handleDoubleClickEvent();
						} else if (mouseListener != null) {
							control.removeMouseListener(mouseListener);
						}
					}
				};
				control.addMouseListener(mouseListener);

				if (tabeditingListener == null) {
					tabeditingListener = new TraverseListener() {

						public void keyTraversed(TraverseEvent e) {
							if ((feature & DEFAULT) != DEFAULT) {
								processTraverseEvent(cell.getColumnIndex(),
										viewer.getViewerRowFromItem(cell
												.getItem()), e);
							}
						}
					};
				}

				control.addTraverseListener(tabeditingListener);

				if (editorActivationListener != null
						&& !editorActivationListener.isEmpty()) {
					Object[] ls = editorActivationListener.getListeners();
					for (int i = 0; i < ls.length; i++) {
						((ColumnViewerEditorActivationListener) ls[i])
								.afterEditorActivated(activationEvent);
					}
				}
			}
		}
	}

	/**
	 * Applies the current value and deactivates the currently active cell
	 * editor.
	 */
	void applyEditorValue() {
		CellEditor c = this.cellEditor;
		if (c != null && this.cell != null) {
			// null out cell editor before calling save
			// in case save results in applyEditorValue being re-entered
			// see 1GAHI8Z: ITPUI:ALL - How to code event notification when
			// using cell editor ?
			ColumnViewerEditorDeactivationEvent tmp = new ColumnViewerEditorDeactivationEvent(
					cell);
			if (editorActivationListener != null
					&& !editorActivationListener.isEmpty()) {
				Object[] ls = editorActivationListener.getListeners();
				for (int i = 0; i < ls.length; i++) {

					((ColumnViewerEditorActivationListener) ls[i])
							.beforeEditorDeactivated(tmp);
				}
			}

			Item t = (Item) this.cell.getItem();

			// don't null out table item -- same item is still selected
			if (t != null && !t.isDisposed()) {
				saveEditorValue(c);
			}
						
			setEditor(null, null, 0);
			c.removeListener(cellEditorListener);
			Control control = c.getControl();
			if (control != null) {
				if (mouseListener != null) {
					control.removeMouseListener(mouseListener);
					// Clear the instance not needed any more
					mouseListener = null;
				}
				if (focusListener != null) {
					control.removeFocusListener(focusListener);
				}

				if (tabeditingListener != null) {
					control.removeTraverseListener(tabeditingListener);
				}
			}
			c.deactivate();

			if (editorActivationListener != null
					&& !editorActivationListener.isEmpty()) {
				Object[] ls = editorActivationListener.getListeners();
				for (int i = 0; i < ls.length; i++) {
					((ColumnViewerEditorActivationListener) ls[i])
							.afterEditorDeactivated(tmp);
				}
			}
			
			this.cellEditor = null;
			this.activationEvent = null;
			this.cell = null;
		}
	}

	/**
	 * Cancel editing
	 */
	void cancelEditing() {
		if (cellEditor != null) {
			ColumnViewerEditorDeactivationEvent tmp = new ColumnViewerEditorDeactivationEvent(
					cell);
			if (editorActivationListener != null
					&& !editorActivationListener.isEmpty()) {
				Object[] ls = editorActivationListener.getListeners();
				for (int i = 0; i < ls.length; i++) {

					((ColumnViewerEditorActivationListener) ls[i])
							.beforeEditorDeactivated(tmp);
				}
			}

			setEditor(null, null, 0);
			cellEditor.removeListener(cellEditorListener);

			Control control = cellEditor.getControl();
			if (control != null) {
				if (mouseListener != null) {
					control.removeMouseListener(mouseListener);
					// Clear the instance not needed any more
					mouseListener = null;
				}
				if (focusListener != null) {
					control.removeFocusListener(focusListener);
				}

				if (tabeditingListener != null) {
					control.removeTraverseListener(tabeditingListener);
				}
			}

			CellEditor oldEditor = cellEditor;
			oldEditor.deactivate();

			if (editorActivationListener != null
					&& !editorActivationListener.isEmpty()) {
				Object[] ls = editorActivationListener.getListeners();
				for (int i = 0; i < ls.length; i++) {
					((ColumnViewerEditorActivationListener) ls[i])
							.afterEditorDeactivated(tmp);
				}
			}
			
			this.cellEditor = null;
			this.activationEvent = null;
			this.cell = null;
		}
	}

	/**
	 * Enable the editor by mouse down
	 *
	 * @param event
	 */
	void handleEditorActivationEvent(ColumnViewerEditorActivationEvent event) {
		if (editorActivationStrategy.isEditorActivationEvent(event)) {
			if (cellEditor != null) {
				applyEditorValue();
			}

			this.cell = (ViewerCell) event.getSource();

			activationEvent = event;
			activationTime = event.time
					+ Display.getCurrent().getDoubleClickTime();

			activateCellEditor();
		}
	}

	private void saveEditorValue(CellEditor cellEditor) {
		ViewerColumn part = viewer.getViewerColumn(cell.getColumnIndex());

		if (part != null && part.getEditingSupport() != null) {
			part.getEditingSupport().saveCellEditorValue(cellEditor, cell);
		}
	}

	/**
	 * Return whether there is an active cell editor.
	 *
	 * @return <code>true</code> if there is an active cell editor; otherwise
	 *         <code>false</code> is returned.
	 */
	boolean isCellEditorActive() {
		return cellEditor != null;
	}

	void handleDoubleClickEvent() {
		viewer.fireDoubleClick(new DoubleClickEvent(viewer, viewer
				.getSelection()));
		viewer.fireOpen(new OpenEvent(viewer, viewer.getSelection()));
	}

	/**
	 * Adds the given listener, it is to be notified when the cell editor is
	 * activated or deactivated.
	 *
	 * @param listener
	 *            the listener to add
	 */
	public void addEditorActivationListener(
			ColumnViewerEditorActivationListener listener) {
		if (editorActivationListener == null) {
			editorActivationListener = new ListenerList();
		}
		editorActivationListener.add(listener);
	}

	/**
	 * Removes the given listener.
	 *
	 * @param listener
	 *            the listener to remove
	 */
	public void removeEditorActivationListener(
			ColumnViewerEditorActivationListener listener) {
		if (editorActivationListener != null) {
			editorActivationListener.remove(listener);
		}
	}

	/**
	 * Process the traverse event and opens the next available editor depending
	 * of the implemented strategy. The default implementation uses the style
	 * constants
	 * <ul>
	 * <li>{@link ColumnViewerEditor#TABBING_MOVE_TO_ROW_NEIGHBOR}</li>
	 * <li>{@link ColumnViewerEditor#TABBING_CYCLE_IN_ROW}</li>
	 * <li>{@link ColumnViewerEditor#TABBING_VERTICAL}</li>
	 * <li>{@link ColumnViewerEditor#TABBING_HORIZONTAL}</li>
	 * </ul>
	 *
	 * <p>
	 * Subclasses may overwrite to implement their custom logic to edit the next
	 * cell
	 * </p>
	 *
	 * @param columnIndex
	 *            the index of the current column
	 * @param row
	 *            the current row - may only be used for the duration of this
	 *            method call
	 * @param event
	 *            the traverse event
	 */
	protected void processTraverseEvent(int columnIndex, ViewerRow row,
			TraverseEvent event) {

		ViewerCell cell2edit = null;

		if (event.detail == SWT.TRAVERSE_TAB_PREVIOUS) {
			event.doit = false;

			if ((event.stateMask & SWT.CTRL) == SWT.CTRL
					&& (feature & TABBING_VERTICAL) == TABBING_VERTICAL) {
				cell2edit = searchCellAboveBelow(row, viewer, columnIndex, true);
			} else if ((feature & TABBING_HORIZONTAL) == TABBING_HORIZONTAL) {
				cell2edit = searchPreviousCell(row, viewer, columnIndex,
						columnIndex);
			}
		} else if (event.detail == SWT.TRAVERSE_TAB_NEXT) {
			event.doit = false;

			if ((event.stateMask & SWT.CTRL) == SWT.CTRL
					&& (feature & TABBING_VERTICAL) == TABBING_VERTICAL) {
				cell2edit = searchCellAboveBelow(row, viewer, columnIndex,
						false);
			} else if ((feature & TABBING_HORIZONTAL) == TABBING_HORIZONTAL) {
				cell2edit = searchNextCell(row, viewer, columnIndex,
						columnIndex);
			}
		}

		if (cell2edit != null) {

			viewer.getControl().setRedraw(false);
			ColumnViewerEditorActivationEvent acEvent = new ColumnViewerEditorActivationEvent(
					cell2edit, event);
			viewer.triggerEditorActivationEvent(acEvent);
			viewer.getControl().setRedraw(true);
		}
	}

	private ViewerCell searchCellAboveBelow(ViewerRow row, ColumnViewer viewer,
			int columnIndex, boolean above) {
		ViewerCell rv = null;

		ViewerRow newRow = null;

		if (above) {
			newRow = row.getNeighbor(ViewerRow.ABOVE, false);
		} else {
			newRow = row.getNeighbor(ViewerRow.BELOW, false);
		}

		if (newRow != null) {
			ViewerColumn column = viewer.getViewerColumn(columnIndex);
			if (column != null
					&& column.getEditingSupport() != null
					&& column.getEditingSupport().canEdit(
							newRow.getItem().getData())) {
				rv = newRow.getCell(columnIndex);
			} else {
				rv = searchCellAboveBelow(newRow, viewer, columnIndex, above);
			}
		}

		return rv;
	}

	private ViewerCell searchPreviousCell(ViewerRow row, ColumnViewer viewer,
			int columnIndex, int startIndex) {
		ViewerCell rv = null;

		if (columnIndex - 1 >= 0) {
			ViewerColumn column = viewer.getViewerColumn(columnIndex - 1);
			if (column != null
					&& column.getEditingSupport() != null
					&& column.getEditingSupport().canEdit(
							row.getItem().getData())) {
				rv = row.getCell(columnIndex - 1);
			} else {
				rv = searchPreviousCell(row, viewer, columnIndex - 1,
						startIndex);
			}
		} else {
			if ((feature & TABBING_CYCLE_IN_ROW) == TABBING_CYCLE_IN_ROW) {
				// Check that we don't get into endless loop
				if (columnIndex - 1 != startIndex) {
					// Don't subtract -1 from getColumnCount() we need to
					// start in the virtual column
					// next to it
					rv = searchPreviousCell(row, viewer, row.getColumnCount(),
							startIndex);
				}
			} else if ((feature & TABBING_MOVE_TO_ROW_NEIGHBOR) == TABBING_MOVE_TO_ROW_NEIGHBOR) {
				ViewerRow rowAbove = row.getNeighbor(ViewerRow.ABOVE, false);
				if (rowAbove != null) {
					rv = searchPreviousCell(rowAbove, viewer, rowAbove
							.getColumnCount(), startIndex);
				}
			}
		}

		return rv;
	}

	private ViewerCell searchNextCell(ViewerRow row, ColumnViewer viewer,
			int columnIndex, int startIndex) {
		ViewerCell rv = null;

		if (columnIndex + 1 < row.getColumnCount()) {
			ViewerColumn column = viewer.getViewerColumn(columnIndex + 1);
			if (column != null
					&& column.getEditingSupport() != null
					&& column.getEditingSupport().canEdit(
							row.getItem().getData())) {
				rv = row.getCell(columnIndex + 1);
			} else {
				rv = searchNextCell(row, viewer, columnIndex + 1, startIndex);
			}
		} else {
			if ((feature & TABBING_CYCLE_IN_ROW) == TABBING_CYCLE_IN_ROW) {
				// Check that we don't get into endless loop
				if (columnIndex + 1 != startIndex) {
					// Start from -1 from the virtual column before the
					// first one
					rv = searchNextCell(row, viewer, -1, startIndex);
				}
			} else if ((feature & TABBING_MOVE_TO_ROW_NEIGHBOR) == TABBING_MOVE_TO_ROW_NEIGHBOR) {
				ViewerRow rowBelow = row.getNeighbor(ViewerRow.BELOW, false);
				if (rowBelow != null) {
					rv = searchNextCell(rowBelow, viewer, -1, startIndex);
				}
			}
		}

		return rv;
	}

	/**
	 * Position the editor inside the control
	 *
	 * @param w
	 *            the editor control
	 * @param item
	 *            the item (row) in which the editor is drawn in
	 * @param fColumnNumber
	 *            the column number in which the editor is shown
	 */
	protected abstract void setEditor(Control w, Item item, int fColumnNumber);

	/**
	 * set the layout data for the editor
	 *
	 * @param layoutData
	 *            the layout data used when editor is displayed
	 */
	protected abstract void setLayoutData(CellEditor.LayoutData layoutData);

	/**
	 * @param focusCell
	 *            updates the cell with the current input focus
	 * @param event
	 *            the event requesting to update the focusCell
	 */
	protected abstract void updateFocusCell(ViewerCell focusCell,
			ColumnViewerEditorActivationEvent event);

	/**
	 * @return the cell currently holding the focus if no cell has the focus or
	 *         the viewer implementation doesn't support <code>null</code> is
	 *         returned
	 *
	 */
	public ViewerCell getFocusCell() {
		return null;
	}

	/**
	 * @return the viewer working for
	 */
	protected ColumnViewer getViewer() {
		return viewer;
	}
}