/*******************************************************************************
 * Copyright (c) 2009, 2015 IBM Corporation and others.
 *
 * 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
 *     Lars Vogel <Lars.Vogel@vogella.com> - Bug 472654
 *******************************************************************************/
package org.eclipse.ui.internal.dialogs.cpd;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.resource.LocalResourceManager;
import org.eclipse.jface.viewers.CheckStateChangedEvent;
import org.eclipse.jface.viewers.CheckboxTableViewer;
import org.eclipse.jface.viewers.CheckboxTreeViewer;
import org.eclipse.jface.viewers.IBaseLabelProvider;
import org.eclipse.jface.viewers.ICheckStateListener;
import org.eclipse.jface.viewers.ICheckStateProvider;
import org.eclipse.jface.viewers.ICheckable;
import org.eclipse.jface.viewers.IContentProvider;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.graphics.Image;

/**
 * Manages a tree which provides "standard checkbox tree behavior". I.e. it
 * follows these rules:
 * <ol>
 * <li>If a check box is checked, all its children must be checked.</li>
 * <li>If a check box is unchecked, all its children must be unchecked.</li>
 * <li>If a check box is grayed, each of its children may be either checked or
 * unchecked, however, there must be one of each.</li>
 * <li>If a user checks a check box, its children or parents must change state
 * accordingly.</li>
 * </ol>
 * <p>
 * <b>Note:</b> be sure to call dispose()
 * </p>
 *
 * @since 3.5
 *
 */
public class TreeManager {
	public static final int CHECKSTATE_UNCHECKED = 0;
	public static final int CHECKSTATE_GRAY = 1;
	public static final int CHECKSTATE_CHECKED = 2;

	private static ICheckStateProvider checkStateProvider = null;
	private static IBaseLabelProvider labelProvider = null;
	private static ICheckStateListener viewerCheckListener = null;
	private static ITreeContentProvider treeContentProvider = null;

	private List<CheckListener> listeners = new ArrayList<>();
	private LocalResourceManager resourceManager = new LocalResourceManager(JFaceResources.getResources());

	/**
	 * Instances of this interface will handle changes in the model representation
	 * of checkstates.
	 */
	public interface CheckListener {
		/**
		 * Invoked when a {@link TreeManager.TreeItem}'s check state has changed.
		 *
		 * @param changedItem The item whose check state has changed
		 */
		void checkChanged(TreeItem changedItem);
	}

	/**
	 * Implementation of {@link TreeManager.CheckListener} for use in a
	 * {@link CheckboxTreeViewer}.
	 */
	public static class ModelListenerForCheckboxTree implements CheckListener {
		private CheckboxTreeViewer treeViewer;

		public ModelListenerForCheckboxTree(TreeManager manager, CheckboxTreeViewer treeViewer) {
			this.treeViewer = treeViewer;
			manager.addListener(this);
		}

		@Override
		public void checkChanged(TreeItem changedItem) {
			treeViewer.update(changedItem, null);
		}
	}

	/**
	 * Implementation of {@link TreeManager}CheckListener for use in a
	 * {@link CheckboxTableViewer}.
	 */
	public static class ModelListenerForCheckboxTable implements CheckListener {
		private CheckboxTableViewer tableViewer;

		public ModelListenerForCheckboxTable(TreeManager manager, CheckboxTableViewer tableViewer) {
			this.tableViewer = tableViewer;
			manager.addListener(this);
		}

		@Override
		public void checkChanged(TreeItem changedItem) {
			tableViewer.update(changedItem, null);
		}
	}

	public static class ViewerCheckStateListener implements ICheckStateListener {
		@Override
		public void checkStateChanged(CheckStateChangedEvent event) {
			Object checked = event.getElement();
			if (checked instanceof TreeItem) {
				((TreeItem) checked).setChangedByUser(true);
				((TreeItem) checked).setCheckState(event.getChecked());
			}
		}
	}

	/**
	 * An {@link ICheckStateProvider} which properly provides checkbox state on
	 * {@link TreeManager.TreeItem}s.
	 */
	public static class CheckStateProvider implements ICheckStateProvider {
		@Override
		public boolean isChecked(Object element) {
			return ((TreeItem) element).checkState != CHECKSTATE_UNCHECKED;
		}

		@Override
		public boolean isGrayed(Object element) {
			return ((TreeItem) element).checkState == CHECKSTATE_GRAY;
		}
	}

	/**
	 * A {@link IBaseLabelProvider} for {@link TreeManager.TreeItem}s.
	 */
	public static class TreeItemLabelProvider extends LabelProvider {
		@Override
		public String getText(Object element) {
			if (element instanceof TreeItem) {
				return ((TreeItem) element).getLabel();
			}
			return super.getText(element);
		}

		@Override
		public Image getImage(Object element) {
			if (element instanceof TreeItem) {
				return ((TreeItem) element).getImage();
			}
			return super.getImage(element);
		}
	}

	/**
	 * An {@link ITreeContentProvider} for {@link TreeManager.TreeItem}s - will
	 * completely build the tree structure represented by
	 * {@link TreeManager.TreeItem}s.
	 */
	public static class TreeItemContentProvider implements ITreeContentProvider {
		@Override
		public Object[] getChildren(Object parentElement) {
			return ((TreeItem) parentElement).getChildren().toArray();
		}

		@Override
		public Object getParent(Object element) {
			return ((TreeItem) element).getParent();
		}

		@Override
		public boolean hasChildren(Object element) {
			return getChildren(element).length > 0;
		}

		@Override
		public Object[] getElements(Object inputElement) {
			return getChildren(inputElement);
		}

		@Override
		public void dispose() {
		}

		@Override
		public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
		}
	}

	/**
	 * @return an {@link ICheckStateProvider} which will operate on
	 *         {@link TreeItem}s
	 */
	public static ICheckStateProvider getCheckStateProvider() {
		if (checkStateProvider == null) {
			checkStateProvider = new CheckStateProvider();
		}
		return checkStateProvider;
	}

	/**
	 * @return an {@link IBaseLabelProvider} which will provide the labels and
	 *         images of {@link TreeItem}s
	 */
	public static IBaseLabelProvider getLabelProvider() {
		if (labelProvider == null) {
			labelProvider = new TreeItemLabelProvider();
		}
		return labelProvider;
	}

	/**
	 * @return an {@link ITreeContentProvider} which will provide {@link TreeItem}
	 *         content in tree format.
	 */
	public static ITreeContentProvider getTreeContentProvider() {
		if (treeContentProvider == null)
			treeContentProvider = new TreeItemContentProvider();
		return treeContentProvider;
	}

	/**
	 * @return an {@link ICheckStateListener} which will respond to
	 *         {@link CheckStateChangedEvent}s by updating the model to reflect them
	 */
	public ICheckStateListener getViewerCheckStateListener() {
		if (viewerCheckListener == null)
			viewerCheckListener = new ViewerCheckStateListener();
		return viewerCheckListener;
	}

	/**
	 * A single item in a tree of managed checkbox states.
	 */
	public class TreeItem {
		private String label;
		private ImageDescriptor imageDescriptor;
		private Image image;
		private TreeItem parent;
		private List<TreeItem> children;
		private int checkState;
		private boolean changedByUser;

		public TreeItem(String label) {
			this.label = label;
			this.children = new ArrayList<>();
		}

		public String getLabel() {
			return label;
		}

		public void setLabel(String label) {
			this.label = label;
		}

		public Image getImage() {
			if (image == null) {
				if (imageDescriptor == null) {
					return null;
				}
				image = resourceManager.create(imageDescriptor);
			}
			return image;
		}

		public void setImageDescriptor(ImageDescriptor imageDescriptor) {
			this.imageDescriptor = imageDescriptor;
		}

		public void addChild(TreeItem newChild) {
			newChild.parent = this;
			children.add(newChild);
			synchParents(newChild);
		}

		public List<TreeItem> getChildren() {
			return children;
		}

		public int getChildrenCount() {
			return children.size();
		}

		public TreeItem getParent() {
			return parent;
		}

		/**
		 * An internal call that forwards the change events but does <b>not</b> cause
		 * any iterative synchronization to take place.
		 *
		 * @param newState
		 */
		private void internalSetCheckState(int newState) {
			if (newState == checkState)
				return;

			checkState = newState;
			fireListeners(this);
		}

		/**
		 * External call to explicitly set the particular state of a
		 * {@link TreeManager.TreeItem}. This is usually a response to an SWT check
		 * changed event generated by a Tree/Table.
		 *
		 * @param checked
		 */
		public void setCheckState(boolean checked) {
			int newState = checked ? CHECKSTATE_CHECKED : CHECKSTATE_UNCHECKED;
			if (checkState == newState)
				return;
			// Actually set the state and fire the CheckChangeEvent
			internalSetCheckState(newState);

			// Enforce the SWT rules for checked/gray behavior
			synchChildren(this);
			synchParents(this);
		}

		/**
		 * From the client's perspective the state is a boolean.
		 *
		 * @return <code>true</code> if the state is not UNCHECKED
		 */
		public boolean getState() {
			return !(checkState == CHECKSTATE_UNCHECKED);
		}

		int getCheckState() {
			return checkState;
		}

		/**
		 * If the new state is not "GRAY" then force all children to match that state
		 * (recursively).
		 *
		 * @param changedItem
		 */
		private void synchChildren(TreeItem changedItem) {
			int newState = changedItem.checkState;

			// if the new state is 'GRAY'
			if (newState != CHECKSTATE_GRAY) {
				for (TreeItem treeItem : changedItem.children) {
					TreeItem curItem = treeItem;
					curItem.internalSetCheckState(newState);
					curItem.setChangedByUser(changedItem.isChangedByUser());

					synchChildren(curItem);
				}
			}
		}

		/**
		 * Set the parent's state based on the aggregate state of its children using the
		 * following rules:
		 * <ul>
		 * <li>All children checked...parent checked</li>
		 * <li>All children unchecked...parent unchecked</li>
		 * <li>else...parent GRAY</li>
		 * </ul>
		 *
		 * @param changedItem
		 */
		private void synchParents(TreeItem changedItem) {
			if (changedItem.parent == null)
				return;

			int newState = changedItem.checkState;

			if (newState == CHECKSTATE_GRAY) {
				// if the new state is 'GRAY' then -ALL- the parents are gray
				while (changedItem.parent != null && changedItem.parent.checkState != CHECKSTATE_GRAY) {
					changedItem.parent.internalSetCheckState(CHECKSTATE_GRAY);
					if (changedItem.isChangedByUser()) {
						changedItem.parent.setChangedByUser(true);
					}
					changedItem = changedItem.parent;
				}
			} else {
				// compute the parent's state - checked if all children are
				// checked, unchecked if all children are unchecked, gray if
				// some of each
				boolean checkedFound = newState == CHECKSTATE_CHECKED;
				boolean uncheckedFound = newState == CHECKSTATE_UNCHECKED;
				for (Iterator<TreeItem> i = changedItem.parent.children.iterator(); i.hasNext()
						&& (!checkedFound || !uncheckedFound);) {
					TreeItem item = i.next();
					switch (item.checkState) {
					case CHECKSTATE_CHECKED: {
						checkedFound = true;
						break;
					}
					case CHECKSTATE_GRAY: {
						checkedFound = uncheckedFound = true;
						break;
					}
					case CHECKSTATE_UNCHECKED: {
						uncheckedFound = true;
						break;
					}
					}
				}

				int oldState = changedItem.parent.checkState;
				if (checkedFound && uncheckedFound) {
					changedItem.parent.internalSetCheckState(CHECKSTATE_GRAY);
				} else if (checkedFound) {
					changedItem.parent.internalSetCheckState(CHECKSTATE_CHECKED);
				} else {
					changedItem.parent.internalSetCheckState(CHECKSTATE_UNCHECKED);
				}
				if (oldState != changedItem.parent.checkState) {
					if (changedItem.isChangedByUser()) {
						changedItem.parent.setChangedByUser(true);
					}
					synchParents(changedItem.parent);
				}
			}
		}

		/**
		 * @param changedByUser The changedByUser to set.
		 */
		public void setChangedByUser(boolean changedByUser) {
			this.changedByUser = changedByUser;
		}

		/**
		 * @return Returns the changedByUser.
		 */
		public boolean isChangedByUser() {
			return changedByUser;
		}

		@Override
		public String toString() {
			return label + ", check=" + getState() + ", changed=" + changedByUser; //$NON-NLS-1$ //$NON-NLS-2$
		}

	}

	/**
	 * Creates a new {@link TreeManager}.
	 */
	public TreeManager() {
		listeners = new ArrayList<>();
	}

	/**
	 * Add a {@link CheckListener} whose
	 * {@link CheckListener#checkChanged(TreeManager.TreeItem)} method will be
	 * invoked when a {@link TreeItem} created in this {@link TreeManager} has a
	 * check state change.
	 *
	 * @param listener
	 */
	public void addListener(CheckListener listener) {
		listeners.add(listener);
	}

	/**
	 * Provides a {@link CheckListener} which updates a viewer whenever the
	 * {@link TreeManager} model changes.
	 *
	 * @param viewer The viewer whose check states will be appropriately updated on
	 *               a change to the model.
	 * @return The created {@link CheckListener}.
	 */
	public CheckListener getCheckListener(ICheckable viewer) {
		if (viewer instanceof CheckboxTreeViewer)
			return new ModelListenerForCheckboxTree(this, (CheckboxTreeViewer) viewer);
		if (viewer instanceof CheckboxTableViewer)
			return new ModelListenerForCheckboxTable(this, (CheckboxTableViewer) viewer);
		return null;
	}

	/**
	 * Sets up this {@link TreeManager} for standard interaction with the provided
	 * {@link CheckboxTreeViewer}. In particular:
	 * <ul>
	 * <li>Adds a Label Provider for {@link TreeItem}s which provides both labels
	 * and images.</li>
	 * <li>Adds a {@link CheckStateProvider} for {@link TreeItem}s.</li>
	 * <li>Adds an {@link IContentProvider} to build a tree from input
	 * {@link TreeItem}s.</li>
	 * <li>Adds an {@link ICheckStateListener} to the viewer to update the
	 * appropriate {@link TreeItem}s upon a check box state change in the
	 * viewer.</li>
	 * <li>Adds a {@link CheckListener} to the {@link TreeManager} which will
	 * automatically update the viewer on a {@link TreeItem} check state
	 * change.</li>
	 * </ul>
	 *
	 * @param viewer the viewer to configure with this TreeManager.
	 */
	public void attachAll(CheckboxTreeViewer viewer) {
		viewer.setLabelProvider(getLabelProvider());
		viewer.setCheckStateProvider(getCheckStateProvider());
		viewer.setContentProvider(getTreeContentProvider());
		viewer.addCheckStateListener(getViewerCheckStateListener());
		getCheckListener(viewer);
	}

	/**
	 * Sets up this {@link TreeManager} for standard interaction with the provided
	 * {@link CheckboxTableViewer}. In particular:
	 * <ul>
	 * <li>Adds a Label Provider for {@link TreeItem}s which provides both labels
	 * and images.</li>
	 * <li>Adds a {@link CheckStateProvider} for {@link TreeItem}s.</li>
	 * <li>Adds an {@link IContentProvider} to build a tree from input
	 * {@link TreeItem}s.</li>
	 * <li>Adds an {@link ICheckStateListener} to the viewer to update the
	 * appropriate {@link TreeItem}s upon a check box state change in the
	 * viewer.</li>
	 * <li>Adds a {@link CheckListener} to the {@link TreeManager} which will
	 * automatically update the viewer on a {@link TreeItem} check state
	 * change.</li>
	 * </ul>
	 *
	 * @param viewer the viewer to configure with this TreeManager.
	 */
	public void attachAll(CheckboxTableViewer viewer) {
		viewer.setLabelProvider(getLabelProvider());
		viewer.setCheckStateProvider(getCheckStateProvider());
		viewer.setContentProvider(getTreeContentProvider());
		viewer.addCheckStateListener(getViewerCheckStateListener());
		getCheckListener(viewer);
	}

	/**
	 * Dissociates a listener.
	 *
	 * @param listener The listener to remove.
	 */
	public void removeListener(CheckListener listener) {
		listeners.remove(listener);
	}

	/**
	 * Fires all listeners.
	 *
	 * @param item The {@link TreeItem} that changed.
	 */
	private void fireListeners(TreeItem item) {
		for (CheckListener checkListener : listeners) {
			CheckListener listener = checkListener;
			listener.checkChanged(item);
		}
	}

	public void dispose() {
		resourceManager.dispose();
		resourceManager = null;
		listeners.clear();
		listeners = null;
	}
}
