/*******************************************************************************
 * Copyright (c) 2004, 2006 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/


package org.eclipse.wst.xml.ui.internal.tabletree;

import java.util.Iterator;
import java.util.List;
import java.util.Vector;

import org.eclipse.jface.viewers.CellEditor;
import org.eclipse.jface.viewers.ICellEditorListener;
import org.eclipse.jface.viewers.ICellModifier;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.ScrollBar;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.ui.PlatformUI;


public class TreeExtension implements PaintListener {

	protected Tree fTree;
	protected EditManager editManager;
	protected String[] fColumnProperties;
	protected ICellModifier cellModifier;
	protected int columnPosition = 300;
	protected int columnHitWidth = 5;
	protected Color tableLineColor;
	protected int controlWidth;
	protected DelayedDrawTimer delayedDrawTimer;
	private boolean fisUnsupportedInput = false;

	public TreeExtension(Tree tree) {
		this.fTree = tree;
		InternalMouseListener listener = new InternalMouseListener();
		tree.addMouseMoveListener(listener);
		tree.addMouseListener(listener);
		tree.addPaintListener(this);
		editManager = new EditManager(tree);
		delayedDrawTimer = new DelayedDrawTimer(tree);

		tableLineColor = tree.getDisplay().getSystemColor(SWT.COLOR_WIDGET_LIGHT_SHADOW);
	}

	public void dispose() {
		tableLineColor.dispose();
	}

	public void setCellModifier(ICellModifier modifier) {
		cellModifier = modifier;
	}

	public void resetCachedData() {
		// todo: sure seems we should reset something?
	}

	public ICellModifier getCellModifier() {
		return cellModifier;
	}

	public List getItemList() {
		List list = new Vector();
		getItemListHelper(fTree.getItems(), list);
		return list;
	}

	protected void getItemListHelper(TreeItem[] items, List list) {
		for (int i = 0; i < items.length; i++) {
			TreeItem item = items[i];
			list.add(item);
			getItemListHelper(item.getItems(), list);
		}
	}

	protected TreeItem getTreeItemOnRow(int px, int py) {
		TreeItem result = null;
		List list = getItemList();
		for (Iterator i = list.iterator(); i.hasNext();) {
			TreeItem item = (TreeItem) i.next();
			Rectangle r = item.getBounds();
			if ((r != null) && (px >= r.x) && (py >= r.y) && (py <= r.y + r.height)) {
				result = item;
			}
		}
		return result;
	}

	protected class InternalMouseListener extends MouseAdapter implements MouseMoveListener {
		protected int columnDragged = -1;
		protected boolean isDown = false;
		protected int prevX = -1;
		protected Cursor cursor = null;

		public void mouseMove(MouseEvent e) {
			if ((e.x > columnPosition - columnHitWidth) && (e.x < columnPosition + columnHitWidth)) {
				if (cursor == null) {
					cursor = new Cursor(fTree.getDisplay(), SWT.CURSOR_SIZEWE);
					fTree.setCursor(cursor);
				}
			}
			else {
				if (cursor != null) {
					fTree.setCursor(null);
					cursor.dispose();
					cursor = null;
				}
			}

			if (columnDragged != -1) {
				// using the delay timer will make redraws less flickery
				if (e.x > 20) {
					columnPosition = e.x;
					delayedDrawTimer.reset(20);
				}
			}
		}

		public void mouseDown(MouseEvent e) {
			// here we handle the column resizing by detect if the user has
			// click on a column separator
			//
			columnDragged = -1;
			editManager.deactivateCellEditor();

			if ((e.x > columnPosition - columnHitWidth) && (e.x < columnPosition + columnHitWidth)) {
				columnDragged = 0;
			}

			// here we handle selecting tree items when any thing on the 'row'
			// is clicked
			//
			TreeItem item = fTree.getItem(new Point(e.x, e.y));
			if (item == null) {
				item = getTreeItemOnRow(e.x, e.y);
				if (item != null) {
					TreeItem[] items = new TreeItem[1];
					items[0] = item;
					fTree.setSelection(items);
				}
			}
		}

		public void mouseUp(MouseEvent e) {
			columnDragged = -1;
		}
	}

	public String[] getColumnProperties() {
		return fColumnProperties;
	}

	public void setColumnProperties(String[] columnProperties) {
		this.fColumnProperties = columnProperties;
	}

	public void paintControl(PaintEvent event) {
		GC gc = event.gc;
		Rectangle treeBounds = fTree.getBounds();

		controlWidth = treeBounds.width;
		Color bg = fTree.getDisplay().getSystemColor(SWT.COLOR_LIST_BACKGROUND);
		Color bg2 = fTree.getDisplay().getSystemColor(SWT.COLOR_LIST_SELECTION);

		gc.setBackground(bg2);

		// // This next part causes problems on LINUX, so let's not do it
		// there
		// if (B2BHacks.IS_UNIX == false) {
		// TreeItem[] selectedItems = tree.getSelection();
		// if (selectedItems.length > 0) {
		// for (int i = 0; i < selectedItems.length; i++) {
		// TreeItem item = selectedItems[i];
		// Rectangle bounds = item.getBounds();
		// if (bounds != null) {
		// gc.fillRectangle(bounds.x + bounds.width, bounds.y, controlWidth,
		// bounds.height);
		// }
		// }
		// }
		// }
		//
		if (!fisUnsupportedInput) {
			TreeItem[] items = fTree.getItems();
			if (items.length > 0) {
				gc.setForeground(tableLineColor);
				gc.setBackground(bg);

				gc.fillRectangle(columnPosition, treeBounds.x, treeBounds.width, treeBounds.height);

				Rectangle itemBounds = items[0].getBounds();
				int height = computeTreeItemHeight();

				if (itemBounds != null) {
					/*
					 * Bounds will be for the first item, which will either be
					 * visible at the top of the Tree, or scrolled off with
					 * negative values
					 */
					int startY = itemBounds.y;

					/* Only draw lines within the Tree boundaries */
					for (int i = startY; i < treeBounds.height; i += height) {
						if (i >= treeBounds.y) {
							gc.drawLine(0, i, treeBounds.width, i);
						}
					}
				}
				gc.drawLine(columnPosition, 0, columnPosition, treeBounds.height);
				paintItems(gc, items, treeBounds);

			}
			else {
				addEmptyTreeMessage(gc);
			}
		}
		else {
			addUnableToPopulateTreeMessage(gc);
		}
	}

	protected int computeTreeItemHeight() {
		int result = -1;

		/*
		 * On GTK tree.getItemHeight() seems to lie to us. It reports that the
		 * tree item occupies a few pixles less vertical space than it should
		 * (possibly because of the image height vs. the text height?). This
		 * foils our code that draws the 'row' lines since we assume that
		 * lines should be drawn at 'itemHeight' increments. Don't trust
		 * getItemHeight() to compute the increment... instead compute the
		 * value based on distance between two TreeItems, and then use the
		 * larger value.
		 * 
		 * This strategy only works on trees where the items are of even
		 * height, however bug
		 * https://bugs.eclipse.org/bugs/show_bug.cgi?id=117201 indicates that
		 * this is no longer promised, at least on win32 and likely on other
		 * platforms soon.
		 */
		if (fTree.getItemCount() > 0) {
			TreeItem[] items = fTree.getItems();
			Rectangle itemBounds = items[0].getBounds();

			if (items[0].getExpanded()) {
				TreeItem[] children = items[0].getItems();
				if (children.length > 0) {
					result = children[0].getBounds().y - itemBounds.y;
				}
			}
			else if (items.length > 1) {
				result = items[1].getBounds().y - itemBounds.y;
			}
		}

		result = Math.max(fTree.getItemHeight(), result);
		return result;
	}

	protected void addEmptyTreeMessage(GC gc) {
		// nothing to add here
	}

	private void addUnableToPopulateTreeMessage(GC gc) {
		// here we print a message when the document cannot be displayed just
		// to give the
		// user a visual cue
		// so that they know how to proceed to edit the blank view
		gc.setForeground(fTree.getDisplay().getSystemColor(SWT.COLOR_BLACK));
		gc.setBackground(fTree.getDisplay().getSystemColor(SWT.COLOR_LIST_BACKGROUND));
		gc.drawString(XMLEditorMessages.TreeExtension_0, 10, 10);
	}

	void setIsUnsupportedInput(boolean isUnsupported) {
		fisUnsupportedInput = isUnsupported;
	}

	public void paintItems(GC gc, TreeItem[] items, Rectangle treeBounds) {
		if (items != null) {
			for (int i = 0; i < items.length; i++) {
				TreeItem item = items[i];
				if (item != null) {
					Rectangle bounds = item.getBounds();
					if (bounds != null) {
						if (treeBounds.intersects(bounds)) {
							paintItem(gc, item, bounds);
						}
					}

					// defect 241039
					//
					if (item.getExpanded()) {
						paintItems(gc, item.getItems(), treeBounds);
					}
				}
			}
		}
	}

	protected void paintItem(GC gc, TreeItem item, Rectangle bounds) {
		// nothing to paint
	}

	public interface ICellEditorProvider {
		CellEditor getCellEditor(Object o, int col);
	}

	/**
	 * This class is used to improve drawing during a column resize.
	 */
	public class DelayedDrawTimer implements Runnable {
		protected Control control;

		public DelayedDrawTimer(Control control1) {
			this.control = control1;
		}

		public void reset(int milliseconds) {
			getDisplay().timerExec(milliseconds, this);
		}

		public void run() {
			control.redraw();
		}
	}

	Display getDisplay() {

		return PlatformUI.getWorkbench().getDisplay();
	}

	/**
	 * EditManager
	 */
	public class EditManager {
		protected Tree fTree1;
		protected Control cellEditorHolder;
		protected CellEditorState cellEditorState;

		public EditManager(Tree tree) {
			this.fTree1 = tree;
			this.cellEditorHolder = new Composite(tree, SWT.NONE);

			final Tree theTree = tree;

			MouseAdapter theMouseAdapter = new MouseAdapter() {
				public void mouseDown(MouseEvent e) {
					deactivateCellEditor();

					if (e.x > columnPosition + columnHitWidth) {
						TreeItem[] items = theTree.getSelection();
						// No edit if more than one row is selected.
						if (items.length == 1) {
							Rectangle bounds = items[0].getBounds();
							if ((bounds != null) && (e.y >= bounds.y) && (e.y <= bounds.y + bounds.height)) {
								int columnToEdit = 1;
								activateCellEditor(items[0], columnToEdit);
							}
						}
					}
				}
			};

			SelectionListener selectionListener = new SelectionListener() {
				public void widgetDefaultSelected(SelectionEvent e) {
					applyCellEditorValue();
				}

				public void widgetSelected(SelectionEvent e) {
					applyCellEditorValue();
				}
			};

			KeyListener keyListener = new KeyAdapter() {
				public void keyPressed(KeyEvent e) {
					if (e.character == SWT.CR) {
						deactivateCellEditor();
						TreeItem[] items = theTree.getSelection();
						if (items.length == 1) {
							activateCellEditor(items[0], 1);
						}
					}
				}
			};

			tree.addMouseListener(theMouseAdapter);
			tree.addKeyListener(keyListener);
			ScrollBar hBar = tree.getHorizontalBar();
			if (hBar != null) {
				hBar.addSelectionListener(selectionListener);
			}
			ScrollBar vBar = tree.getVerticalBar();
			if (vBar != null) {
				vBar.addSelectionListener(selectionListener);
			}
		}

		public boolean isCellEditorActive() {
			return cellEditorState != null;
		}

		public void applyCellEditorValue() {
			if ((cellEditorState != null) && (cellModifier != null)) {
				TreeItem treeItem = cellEditorState.fTreeItem;

				// The area below the cell editor needs to be explicity
				// repainted on Linux
				//
				// Rectangle r = B2BHacks.IS_UNIX ? treeItem.getBounds() :
				// null;

				Object value = cellEditorState.fCellEditor.getValue();
				String property = cellEditorState.fProperty;

				deactivateCellEditor();

				cellModifier.modify(treeItem, property, value);

				// if (r != null) {
				// tree.redraw(r.x, r.y, tree.getBounds().width, r.height,
				// false);
				// }
			}
		}

		public void deactivateCellEditor() {
			// Clean up any previous editor control
			if (cellEditorState != null) {
				cellEditorState.deactivate();
				cellEditorState = null;
			}
		}

		public void activateCellEditor(TreeItem treeItem, int column) {
			if (cellModifier instanceof ICellEditorProvider) {
				ICellEditorProvider cellEditorProvider = (ICellEditorProvider) cellModifier;
				Object data = treeItem.getData();
				if (fColumnProperties.length > column) {
					String property = fColumnProperties[column];
					if (cellModifier.canModify(data, property)) {
						CellEditor newCellEditor = cellEditorProvider.getCellEditor(data, column);
						if (newCellEditor != null) {
							// The control that will be the editor must be a
							// child of the columnPosition
							Control control = newCellEditor.getControl();
							if (control != null) {
								cellEditorState = new CellEditorState(newCellEditor, control, treeItem, column, property);
								cellEditorState.activate();
							}
						}
					}
				}
			}
		}

		/**
		 * this class holds the state that is need on a per cell editor
		 * invocation basis
		 */
		public class CellEditorState implements ICellEditorListener, FocusListener {
			public CellEditor fCellEditor;
			public Control fControl;
			public TreeItem fTreeItem;
			public int fColumnNumber;
			public String fProperty;

			public CellEditorState(CellEditor cellEditor, Control control, TreeItem treeItem, int columnNumber, String property) {
				this.fCellEditor = cellEditor;
				this.fControl = control;
				this.fTreeItem = treeItem;
				this.fColumnNumber = columnNumber;
				this.fProperty = property;
			}

			public void activate() {
				Object element = fTreeItem.getData();
				String value = cellModifier.getValue(element, fProperty).toString();
				if (fControl instanceof Text) {
					Text text = (Text) fControl;
					int requiredSize = value.length() + 100;
					if (text.getTextLimit() < requiredSize) {
						text.setTextLimit(requiredSize);
					}
				}
				Rectangle r = fTreeItem.getBounds();
				if (r != null) {
					fControl.setBounds(columnPosition + 5, r.y + 1, fTree1.getClientArea().width - (columnPosition + 5), r.height - 1);
					fControl.setVisible(true);
					fCellEditor.setValue(value);
					fCellEditor.addListener(this);
					fCellEditor.setFocus();
					fControl.addFocusListener(this);
				}
			}

			public void deactivate() {
				fCellEditor.removeListener(this);
				fControl.removeFocusListener(this);
				fCellEditor.deactivate();
				fTree1.forceFocus();
			}

			// ICellEditorListener methods
			//
			public void applyEditorValue() {
				applyCellEditorValue();
			}

			public void cancelEditor() {
				deactivateCellEditor();
			}

			public void editorValueChanged(boolean oldValidState, boolean newValidState) {
				// nothing, for now
			}

			// FocusListener methods
			//
			public void focusGained(FocusEvent e) {
				// do nothing
			}

			public void focusLost(FocusEvent e) {
				applyCellEditorValue();
			}
		}
	}
}
