/*******************************************************************************
 * Copyright (c) 2004, 2005 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
 *******************************************************************************/
package org.eclipse.wst.common.snippets.internal.util;



import java.util.ArrayList;
import java.util.List;

import org.eclipse.jface.util.Assert;
import org.eclipse.jface.viewers.CellEditor;
import org.eclipse.jface.viewers.IBaseLabelProvider;
import org.eclipse.jface.viewers.ICellModifier;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.StructuredViewer;
import org.eclipse.jface.viewers.ViewerSorter;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.TableEditor;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Item;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.Widget;

/**
 * AccessibleTableViewer 
 * 
 * A concrete viewer based on a SWT <code>Table</code> control.
 * <p>
 * This class is not intended to be subclassed outside the viewer framework.
 * It is designed to be instantiated with a pre-existing SWT table control and
 * configured with a domain-specific content provider, table label provider,
 * element filter (optional), and element sorter (optional).
 * </p>
 * <p>
 * Label providers for table viewers must implement either the
 * <code>ITableLabelProvider</code> or the <code>ILabelProvider</code>
 * interface (see <code>TableViewer.setLabelProvider</code> for more
 * details).
 * </p>
 */
public class AccessibleTableViewer extends StructuredViewer {

	/**
	 * This viewer's table control.
	 */
	protected Table fTable;

	/**
	 * This viewer's table editor.
	 */
	protected TableEditor fTableEditor;


	/**
	 * Internal table viewer implementation.
	 */
	protected TableViewerImpl fTableViewerImpl;

	/**
	 * Creates a table viewer on a newly-created table control under the given
	 * parent. The table control is created using the SWT style bits
	 * <code>MULTI, H_SCROLL, V_SCROLL,</code> and <code>BORDER</code>.
	 * The viewer has no input, no content provider, a default label provider,
	 * no sorter, and no filters. The table has no columns.
	 * 
	 * @param parent
	 *            the parent control
	 */
	public AccessibleTableViewer(Composite parent) {
		this(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL | SWT.BORDER);
	}

	/**
	 * Creates a table viewer on a newly-created table control under the given
	 * parent. The table control is created using the given style bits. The
	 * viewer has no input, no content provider, a default label provider, no
	 * sorter, and no filters. The table has no columns.
	 * 
	 * @param parent
	 *            the parent control
	 * @param style
	 *            SWT style bits
	 */
	public AccessibleTableViewer(Composite parent, int style) {
		this(new Table(parent, style));
	}

	/**
	 * Creates a table viewer on the given table control. The viewer has no
	 * input, no content provider, a default label provider, no sorter, and no
	 * filters.
	 * 
	 * @param table
	 *            the table control
	 */
	public AccessibleTableViewer(Table table) {
		fTable = table;
		addKeyListener();
		hookControl(table);
		fTableEditor = new TableEditor(table);
		initTableViewerImpl();
	}

	/**
	 * Adds the given element to this table viewer. If this viewer does not
	 * have a sorter, the element is added at the end; otherwise the element
	 * is inserted at the appropriate position.
	 * <p>
	 * This method should be called (by the content provider) when a single
	 * element has been added to the model, in order to cause the viewer to
	 * accurately reflect the model. This method only affects the viewer, not
	 * the model. Note that there is another method for efficiently processing
	 * the simultaneous addition of multiple elements.
	 * </p>
	 * 
	 * @param element
	 *            the element to add
	 */
	public void add(Object element) {
		add(new Object[]{element});
	}

	/**
	 * Adds the given elements to this table viewer. If this viewer does not
	 * have a sorter, the elements are added at the end in the order given;
	 * otherwise the elements are inserted at appropriate positions.
	 * <p>
	 * This method should be called (by the content provider) when elements
	 * have been added to the model, in order to cause the viewer to
	 * accurately reflect the model. This method only affects the viewer, not
	 * the model.
	 * </p>
	 * 
	 * @param elements
	 *            the elements to add
	 */
	public void add(Object[] elements) {
		Object[] filtered = filter(elements);
		for (int i = 0; i < filtered.length; i++) {
			Object element = filtered[i];
			int index = indexForElement(element);
			updateItem(new TableItem(getTable(), SWT.NONE, index), element);
		}
	}

	private void addKeyListener() {
		if (fTable != null && !fTable.isDisposed()) {
			fTable.addKeyListener(new KeyAdapter() {
				public void keyPressed(KeyEvent e) {
					if (e.character == '\r' || e.character == ' ') {
						TableItem[] selectedItems = fTable.getSelection();
						if (selectedItems != null && selectedItems.length == 1) {
							fTableViewerImpl.setTableItem(selectedItems[0]);
							fTableViewerImpl.activateFirstCellEditor();
						}
					}
				}
			});
		}
	}

	/**
	 * Cancels a currently active cell editor. All changes already done in the
	 * cell editor are lost.
	 */
	public void cancelEditing() {
		fTableViewerImpl.cancelEditing();
	}

	/*
	 * (non-Javadoc) Method declared on StructuredViewer.
	 */
	protected Widget doFindInputItem(Object element) {
		if (element.equals(getRoot()))
			return getTable();
		return null;
	}

	/*
	 * (non-Javadoc) Method declared on StructuredViewer.
	 */
	protected Widget doFindItem(Object element) {
		TableItem[] children = fTable.getItems();
		for (int i = 0; i < children.length; i++) {
			TableItem item = children[i];
			Object data = item.getData();
			if (data != null && data.equals(element))
				return item;
		}

		return null;
	}

	/*
	 * (non-Javadoc) Method declared on StructuredViewer.
	 */
	protected void doUpdateItem(Widget widget, Object element, boolean fullMap) {
		if (widget instanceof TableItem) {
			TableItem item = (TableItem) widget;

			// remember element we are showing
			if (fullMap) {
				associate(element, item);
			}
			else {
				item.setData(element);
				mapElement(element, item);
			}

			IBaseLabelProvider prov = getLabelProvider();
			ITableLabelProvider tprov = null;
			ILabelProvider lprov = null;
			if (prov instanceof ITableLabelProvider) {
				tprov = (ITableLabelProvider) prov;
			}
			else {
				lprov = (ILabelProvider) prov;
			}
			int columnCount = fTable.getColumnCount();
			// Also enter loop if no columns added. See 1G9WWGZ: JFUIF:WINNT -
			// TableViewer with 0 columns does not work
			for (int column = 0; column < columnCount || column == 0; column++) {
				// Similar code in TableTreeViewer.doUpdateItem()
				TableItem ti = item;
				String text = ""; //$NON-NLS-1$
				Image image = null;
				if (tprov != null) {
					text = tprov.getColumnText(element, column);
					image = tprov.getColumnImage(element, column);
				}
				else {
					if (column == 0) {
						text = lprov.getText(element);
						image = lprov.getImage(element);
					}
				}
				ti.setText(column, text);
				if (ti.getImage(column) != image) {
					ti.setImage(column, image);
				}
			}
		}
	}

	/**
	 * Starts editing the given element.
	 * 
	 * @param element
	 *            the element
	 * @param column
	 *            the column number
	 */
	public void editElement(Object element, int column) {
		fTableViewerImpl.editElement(element, column);
	}

	/**
	 * Deactivates the currently active cell editor. All changes already done
	 * in the cell editor are saved.
	 */
	public void finishEditing() {
		fTableViewerImpl.applyEditorValue();
	}

	/**
	 * Returns the cell editors of this table viewer.
	 * 
	 * @return the list of cell editors
	 */
	public CellEditor[] getCellEditors() {
		return fTableViewerImpl.getCellEditors();
	}

	/**
	 * Returns the cell modifier of this table viewer.
	 * 
	 * @return the cell modifier
	 */
	public ICellModifier getCellModifier() {
		return fTableViewerImpl.getCellModifier();
	}

	/**
	 * Returns the column properties of this table viewer. The properties must
	 * correspond with the columns of the table control. They are used to
	 * identify the column in a cell modifier.
	 * 
	 * @return the list of column properties
	 */
	public Object[] getColumnProperties() {
		return fTableViewerImpl.getColumnProperties();
	}

	/*
	 * (non-Javadoc) Method declared on Viewer.
	 */
	public Control getControl() {
		return fTable;
	}

	/**
	 * Returns the element with the given index from this table viewer.
	 * Returns <code>null</code> if the index is out of range.
	 * <p>
	 * This method is internal to the framework.
	 * </p>
	 * 
	 * @param index
	 *            the zero-based index
	 * @return the element at the given index, or <code>null</code> if the
	 *         index is out of range
	 */
	public Object getElementAt(int index) {
		if (index >= 0 && index < fTable.getItemCount()) {
			TableItem i = fTable.getItem(index);
			if (i != null)
				return i.getData();
		}
		return null;
	}

	/**
	 * The table viewer implementation of this <code>Viewer</code> framework
	 * method returns the label provider, which in the case of table viewers
	 * will be an instance of either <code>ITableLabelProvider</code> or
	 * <code>ILabelProvider</code>. If it is an
	 * <code>ITableLabelProvider</code>, then it provides a separate label
	 * text and image for each column. If it is an <code>ILabelProvider</code>,
	 * then it provides only the label text and image for the first column,
	 * and any remaining columns are blank.
	 */
	public IBaseLabelProvider getLabelProvider() {
		return super.getLabelProvider();
	}

	/*
	 * (non-Javadoc) Method declared on StructuredViewer.
	 */
	protected List getSelectionFromWidget() {
		Widget[] items = fTable.getSelection();
		ArrayList list = new ArrayList(items.length);
		for (int i = 0; i < items.length; i++) {
			Widget item = items[i];
			Object e = item.getData();
			if (e != null)
				list.add(e);
		}
		return list;
	}

	/**
	 * Returns this table viewer's table control.
	 * 
	 * @return the table control
	 */
	public Table getTable() {
		return fTable;
	}

	/*
	 * (non-Javadoc) Method declared on StructuredViewer.
	 */
	protected void hookControl(Control control) {
		super.hookControl(control);
		Table tableControl = (Table) control;
		tableControl.addSelectionListener(new SelectionListener() {

			public void widgetDefaultSelected(SelectionEvent e) {
				handleDoubleSelect(e);
			}

			public void widgetSelected(SelectionEvent e) {
				handleSelect(e);
			}
		});
		tableControl.addMouseListener(new MouseAdapter() {

			public void mouseDoubleClick(MouseEvent e) {
				fTableViewerImpl.handleMouseDoubleClick(e);
			}

			public void mouseDown(MouseEvent e) {
				fTableViewerImpl.handleMouseDown(e);
			}
		});
	}

	/*
	 * Returns the index where the item should be inserted.
	 */
	protected int indexForElement(Object element) {
		ViewerSorter sorter = getSorter();
		if (sorter == null)
			return fTable.getItemCount();
		int count = fTable.getItemCount();
		int min = 0, max = count - 1;
		while (min <= max) {
			int mid = (min + max) / 2;
			Object data = fTable.getItem(mid).getData();
			int compare = sorter.compare(this, data, element);
			if (compare == 0) {
				// find first item > element
				while (compare == 0) {
					++mid;
					if (mid >= count) {
						break;
					}
					data = fTable.getItem(mid).getData();
					compare = sorter.compare(this, data, element);
				}
				return mid;
			}
			if (compare < 0)
				min = mid + 1;
			else
				max = mid - 1;
		}
		return min;
	}

	/**
	 * Initializes the table viewer implementation.
	 */
	protected void initTableViewerImpl() {
		fTableViewerImpl = new TableViewerImpl(fTable) {
			Rectangle getBounds(Item item, int columnNumber) {
				return ((TableItem) item).getBounds(columnNumber);
			}

			int getColumnCount() {
				return getTable().getColumnCount();
			}

			Item[] getSelection() {
				return getTable().getSelection();
			}

			void setEditor(Control w, Item item, int columnNumber) {
				fTableEditor.setEditor(w, (TableItem) item, columnNumber);
			}

			void setLayoutData(CellEditor.LayoutData layoutData) {
				fTableEditor.grabHorizontal = layoutData.grabHorizontal;
				fTableEditor.horizontalAlignment = layoutData.horizontalAlignment;
				fTableEditor.minimumWidth = layoutData.minimumWidth;
			}

			void setSelection(StructuredSelection selection, boolean b) {
				AccessibleTableViewer.this.setSelection(selection, b);
			}

			void showSelection() {
				getTable().showSelection();
			}
		};
	}

	/*
	 * (non-Javadoc) Method declared on Viewer.
	 */
	protected void inputChanged(Object input, Object oldInput) {
		getControl().setRedraw(false);
		try {
			// refresh() attempts to preserve selection, which we want here
			refresh();
		}
		finally {
			getControl().setRedraw(true);
		}
	}

	/**
	 * Inserts the given element into this table viewer at the given position.
	 * If this viewer has a sorter, the position is ignored and the element is
	 * inserted at the correct position in the sort order.
	 * <p>
	 * This method should be called (by the content provider) when elements
	 * have been added to the model, in order to cause the viewer to
	 * accurately reflect the model. This method only affects the viewer, not
	 * the model.
	 * </p>
	 * 
	 * @param element
	 *            the element
	 * @param position
	 *            a 0-based position relative to the model, or -1 to indicate
	 *            the last position
	 */
	public void insert(Object element, int position) {
		fTableViewerImpl.applyEditorValue();
		if (getSorter() != null || hasFilters()) {
			add(element);
			return;
		}
		if (position == -1)
			position = fTable.getItemCount();
		updateItem(new TableItem(fTable, SWT.NONE, position), element);
	}

	/*
	 * (non-Javadoc) Method declared on StructuredViewer.
	 */
	protected void internalRefresh(Object element) {
		internalRefresh(element, true);
	}

	/*
	 * (non-Javadoc) Method declared on StructuredViewer.
	 */
	protected void internalRefresh(Object element, boolean updateLabels) {
		fTableViewerImpl.applyEditorValue();
		if (element == null || element.equals(getRoot())) {
			// the parent

			// in the code below, it is important to do all disassociates
			// before any associates, since a later disassociate can undo an
			// earlier associate
			// e.g. if (a, b) is replaced by (b, a), the disassociate of b to
			// item 1 could undo
			// the associate of b to item 0.

			Object[] children = getSortedChildren(getRoot());
			TableItem[] items = fTable.getItems();
			int min = Math.min(children.length, items.length);
			for (int i = 0; i < min; ++i) {
				if (children[i] != null && items[i] != null) {
					// if the element is unchanged, update its label if
					// appropriate
					if (children[i].equals(items[i].getData())) {
						if (updateLabels) {
							updateItem(items[i], children[i]);
						}
					}
					else {
						// updateItem does an associate(...), which can mess
						// up
						// the associations if the order of elements has
						// changed.
						// E.g. (a, b) -> (b, a) first replaces a->0 with
						// b->0, then replaces b->1 with a->1, but this
						// actually removes b->0.
						// So, if the object associated with this item has
						// changed,
						// just disassociate it for now, and update it below.
						disassociate(items[i]);
					}
				}
			}

			// dispose of all items beyond the end of the current elements
			if (min < items.length) {
				for (int i = items.length; --i >= min;) {
					disassociate(items[i]);
				}
				fTable.remove(min, items.length - 1);
			}

			// Workaround for 1GDGN4Q: ITPUI:WIN2000 - TableViewer icons get
			// scrunched
			if (fTable.getItemCount() == 0) {
				fTable.removeAll();
			}

			// Update items which were removed above
			for (int i = 0; i < min; ++i) {
				if (items[i].getData() == null) {
					updateItem(items[i], children[i]);
				}
			}

			// add any remaining elements
			for (int i = min; i < children.length; ++i) {
				updateItem(new TableItem(fTable, SWT.NONE, i), children[i]);
			}
		}
		else {
			Widget w = findItem(element);
			if (w != null) {
				updateItem(w, element);
			}
		}
	}

	/**
	 * Removes the given elements from this table viewer.
	 * 
	 * @param elements
	 *            the elements to remove
	 */
	private void internalRemove(final Object[] elements) {
		Object input = getInput();
		for (int i = 0; i < elements.length; ++i) {
			if (elements[i].equals(input)) {
				setInput(null);
				return;
			}
		}
		// use remove(int[]) rather than repeated TableItem.dispose() calls
		// to allow SWT to optimize multiple removals
		int[] indices = new int[elements.length];
		int count = 0;
		for (int i = 0; i < elements.length; ++i) {
			Widget w = findItem(elements[i]);
			if (w instanceof TableItem) {
				TableItem item = (TableItem) w;
				disassociate(item);
				indices[count++] = fTable.indexOf(item);
			}
		}
		if (count < indices.length) {
			System.arraycopy(indices, 0, indices = new int[count], 0, count);
		}
		fTable.remove(indices);

		// Workaround for 1GDGN4Q: ITPUI:WIN2000 - TableViewer icons get
		// scrunched
		if (fTable.getItemCount() == 0) {
			fTable.removeAll();
		}
	}

	/**
	 * Returns whether there is an active cell editor.
	 * 
	 * @return <code>true</code> if there is an active cell editor, and
	 *         <code>false</code> otherwise
	 */
	public boolean isCellEditorActive() {
		return fTableViewerImpl.isCellEditorActive();
	}

	/**
	 * Removes the given element from this table viewer. The selection is
	 * updated if necessary.
	 * <p>
	 * This method should be called (by the content provider) when a single
	 * element has been removed from the model, in order to cause the viewer
	 * to accurately reflect the model. This method only affects the viewer,
	 * not the model. Note that there is another method for efficiently
	 * processing the simultaneous removal of multiple elements.
	 * </p>
	 * 
	 * @param element
	 *            the element
	 */
	public void remove(Object element) {
		remove(new Object[]{element});
	}

	/**
	 * Removes the given elements from this table viewer. The selection is
	 * updated if required.
	 * <p>
	 * This method should be called (by the content provider) when elements
	 * have been removed from the model, in order to cause the viewer to
	 * accurately reflect the model. This method only affects the viewer, not
	 * the model.
	 * </p>
	 * 
	 * @param elements
	 *            the elements to remove
	 */
	public void remove(final Object[] elements) {
		preservingSelection(new Runnable() {
			public void run() {
				internalRemove(elements);
			}
		});
	}

	/*
	 * Non-Javadoc. Method defined on StructuredViewer.
	 */
	public void reveal(Object element) {
		Widget w = findItem(element);
		if (w instanceof TableItem)
			getTable().showItem((TableItem) w);
	}

	/**
	 * Sets the cell editors of this table viewer.
	 * 
	 * @param editors
	 *            the list of cell editors
	 */
	public void setCellEditors(CellEditor[] editors) {
		fTableViewerImpl.setCellEditors(editors);
	}

	/**
	 * Sets the cell modifier of this table viewer.
	 * 
	 * @param modifier
	 *            the cell modifier
	 */
	public void setCellModifier(ICellModifier modifier) {
		fTableViewerImpl.setCellModifier(modifier);
	}

	/**
	 * Sets the column properties of this table viewer. The properties must
	 * correspond with the columns of the table control. They are used to
	 * identify the column in a cell modifier.
	 * 
	 * @param columnProperties
	 *            the list of column properties
	 */
	public void setColumnProperties(String[] columnProperties) {
		fTableViewerImpl.setColumnProperties(columnProperties);
	}

	/**
	 * The table viewer implementation of this <code>Viewer</code> framework
	 * method ensures that the given label provider is an instance of either
	 * <code>ITableLabelProvider</code> or <code>ILabelProvider</code>.
	 * If it is an <code>ITableLabelProvider</code>, then it provides a
	 * separate label text and image for each column. If it is an
	 * <code>ILabelProvider</code>, then it provides only the label text
	 * and image for the first column, and any remaining columns are blank.
	 */
	public void setLabelProvider(IBaseLabelProvider labelProvider) {
		Assert.isTrue(labelProvider instanceof ITableLabelProvider || labelProvider instanceof ILabelProvider);
		super.setLabelProvider(labelProvider);
	}

	/*
	 * (non-Javadoc) Method declared on StructuredViewer.
	 */
	protected void setSelectionToWidget(List list, boolean reveal) {
		if (list == null) {
			fTable.deselectAll();
			return;
		}
		int size = list.size();
		TableItem[] items = new TableItem[size];
		TableItem firstItem = null;
		int count = 0;
		for (int i = 0; i < size; ++i) {
			Object o = list.get(i);
			Widget w = findItem(o);
			if (w instanceof TableItem) {
				TableItem item = (TableItem) w;
				items[count++] = item;
				if (firstItem == null)
					firstItem = item;
			}
		}
		if (count < size) {
			System.arraycopy(items, 0, items = new TableItem[count], 0, count);
		}
		fTable.setSelection(items);

		if (reveal && firstItem != null) {
			fTable.showItem(firstItem);
		}
	}

	public void setSingleClickCellSelect(boolean b) {
		fTableViewerImpl.setSingleClickCellSelect(b);
	}
}
