/*******************************************************************************
 * Copyright (c) 2007 Richard Michalsky.
 *
 * 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 :
 * [email protected] (Richard Michalsky) - initial API and implementation
 * [email protected] (Laurent CARON) - code cleaning and finish implementation
 *******************************************************************************/
package org.eclipse.nebula.jface.pshelfviewer;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.Assert;
import org.eclipse.jface.viewers.ContentViewer;
import org.eclipse.jface.viewers.IContentProvider;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.StructuredViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.nebula.widgets.opal.commons.ReflectionUtils;
import org.eclipse.nebula.widgets.pshelf.PShelf;
import org.eclipse.nebula.widgets.pshelf.PShelfItem;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Widget;

/**
 * Viewer for PShelf widget.
 *
 * Uses label provider and content provider (which must implement {@link ITreeContentProvider})
 * to populate the shelves and {@link IShelfViewerFactory} instance to lazily create nested viewers.
 * Content provider uses top level of the input elements hierarchy to get shelf labels and icons
 * and passes <b>children</b> of each shelf item as input to the nested viewer.
 *
 * Also allows transfering selection between individual viewers - useful when viewers show the same
 * input in different arrangements.
 */
/**
 * 
 */
public class PShelfViewer extends StructuredViewer {
	private PShelf pshelf;
	private final IShelfViewerFactory viewerFactory;

	/** Viewer --> PShelfItem map */
	private Map<PShelfItem, Viewer> viewersMap = new HashMap<>();

	private final ArrayList<?> EMPTY_SELECTION_LIST = new ArrayList<>(0);
	protected List<?> lastFiredSelection = EMPTY_SELECTION_LIST;
	private boolean transferSelection;
	private ISelection transferredSelection = StructuredSelection.EMPTY;

	/**
	 * Create an instance of this viewer
	 * 
	 * @param container composite that holds the PShelf widget
	 * @param style style of the PShelf
	 * @param viewerFactory associated view factory
	 */
	public PShelfViewer(Composite container, int style, IShelfViewerFactory viewerFactory) {
		this.viewerFactory = viewerFactory;
		pshelf = new PShelf(container, style);
		hookControl(pshelf);
	}

	/**
	 * @see org.eclipse.jface.viewers.Viewer#getControl()
	 */
	public Control getControl() {
		return getPShelf();
	}

	/**
	 * Returns the underlying PShelf Control.
	 *
	 * @return PShelf control.
	 */
	public PShelf getPShelf() {
		return pshelf;
	}

	/**
	 * Transfer selection behavior. See {@link #setTransferSelection(boolean)}.
	 *
	 * @return Transfer selection behavior
	 */
	public boolean isTransferSelection() {
		return transferSelection;
	}

	/**
	 * Sets transfer selection behavior when another PShelf item is revealed.
	 *
	 * When set to <code>true</code>, PShelfViewer tries to set current
	 * selection to newly revealed viewer. This is helpful when
	 * individual viewers show the same model in different arrangement.
	 *
	 * <code>False</code> (the default) causes each viewer to retain its own
	 * selection.
	 *
	 * @param transferSelection
	 */
	public void setTransferSelection(boolean transferSelection) {
		this.transferSelection = transferSelection;
	}

	/**
	 * @see org.eclipse.jface.viewers.ContentViewer#labelProviderChanged()
	 */
	protected void labelProviderChanged() {
		Assert.isNotNull(getLabelProvider());
		if (!(getLabelProvider() instanceof ILabelProvider))
			throw new IllegalArgumentException("Label provider must implement ILabelProvider" + ", got " + getLabelProvider() == null ? "null" : getLabelProvider().getClass().toString());

		if (pshelf != null) {
			PShelfItem[] shelfItems = pshelf.getItems();

			for (int i = 0; i < shelfItems.length; i++) {
				PShelfItem item = shelfItems[i];

				// re-query texts and images for pshelf items
				ILabelProvider lp = (ILabelProvider) getLabelProvider();
				item.setText(lp.getText(item.getData()));
				item.setImage(lp.getImage(item.getData()));

				// change provider for sub-viewers
				Viewer viewer = getViewerForItem(item);
				ContentViewer contentViewer = (ContentViewer) viewer;
				if (contentViewer != null)
					contentViewer.setLabelProvider(lp);
			}
		}

		// refresh in super impl
		super.labelProviderChanged();
	}

	/**
	 * Returns a viewer, whose widget is embedded in <code>item</code>.
	 *
	 * Viewer is the one previously created by {@link IShelfViewerFactory}
	 * passed to constructor of PShelfViewer.
	 *
	 * @param item
	 * @return Viewer or <code>null</code> if <code>IShelfViewerFactory</code>
	 *         didn't create any viewer for the widget.
	 */
	public Viewer getViewerForItem(PShelfItem item) {
		return (Viewer) viewersMap.get(item);
	}

	/**
	 * @see org.eclipse.jface.viewers.Viewer#inputChanged(java.lang.Object, java.lang.Object)
	 */
	protected void inputChanged(Object input, Object oldInput) {
		preservingSelection(() -> {
			pshelf.setRedraw(false);
			removeAll();
			pshelf.setData(getRoot());
			internalInitializeWidget();
			pshelf.setRedraw(true);
		});
	}

	protected void internalInitializeWidget() {
		// create items, assuming there are no items yet
		if (pshelf.getItems().length > 0)
			throw new IllegalStateException("Cannot initialize nonempty pshelf widget.");

		ITreeContentProvider cp = (ITreeContentProvider) getContentProvider();
		Object[] elements = cp.getElements(getInput());
		for (int i = 0; i < elements.length; i++) {
			Object modelNode = elements[i];
			PShelfItem item;

			item = new PShelfItem(pshelf, SWT.NONE);
			item.setData(modelNode);
			item.getBody().setLayout(new FillLayout());

			// re-query texts and images for pshelf items
			ILabelProvider lp = (ILabelProvider) getLabelProvider();
			item.setText(lp.getText(modelNode));

			// create viewer for the item and initialize it
			Viewer contentViewer = viewerFactory.createViewerForContent(item.getBody(), modelNode);

			item.setImage(lp.getImage(modelNode));

			if (contentViewer != null) {
				viewersMap.put(item, contentViewer);
				contentViewer.setInput(cp.getChildren(modelNode));
				contentViewer.addSelectionChangedListener(sharedViewersListener);
			}

		}

		pshelf.addSelectionListener(pshelfSelectionListener);
		pshelfSelectionListener.widgetSelected(null); // initial selection notification
	}

	private SelectionListener pshelfSelectionListener = new SelectionAdapter() {
		public void widgetSelected(SelectionEvent e) {
			if (transferSelection) {
				preservingSelection(() -> {
					setSelection(transferredSelection);
				});
			}

			SelectionChangedEvent event = new SelectionChangedEvent(PShelfViewer.this, new StructuredSelection(getSelectionFromWidget()));
			fireSelectionChanged(event);
		}
	};

	// all content viewers share this single listener
	private ISelectionChangedListener sharedViewersListener = event -> {
		// event cannot be re-fired, this viewer must claim itself selection
		// provider
		SelectionChangedEvent newEvent = new SelectionChangedEvent(PShelfViewer.this, event.getSelection());
		fireSelectionChanged(newEvent);
		transferredSelection = event.getSelection();
	};

	protected void fireSelectionChanged(SelectionChangedEvent event) {
		List<?> selectionList = ((IStructuredSelection) event.getSelection()).toList();
		if (selectionList.equals(lastFiredSelection))
			return; // don't fire the same selection again

		super.fireSelectionChanged(event);
		lastFiredSelection = selectionList;
	}

	/**
	 * Removes all shelves.
	 */
	protected void removeAll() {
		pshelf.removeAll();
		viewersMap.clear();
	}

	/**
	 * @see org.eclipse.jface.viewers.StructuredViewer#doFindInputItem(java.lang.Object)
	 */
	protected Widget doFindInputItem(Object element) {
		Viewer viewer = getViewerForItem(pshelf.getSelection());
		if (viewer instanceof StructuredViewer) {
			return (Widget) ReflectionUtils.callMethod(viewer, "doFindInputItem", element);
		}
		return null;
	}

	protected Widget doFindItem(Object element) {
		Viewer viewer = getViewerForItem(pshelf.getSelection());
		if (viewer instanceof StructuredViewer) {
			return (Widget) ReflectionUtils.callMethod(viewer, "doFindItem", element);
		}
		return null;
	}

	/** 
	 * @see org.eclipse.jface.viewers.StructuredViewer#doUpdateItem(org.eclipse.swt.widgets.Widget, java.lang.Object, boolean)
	 */
	protected void doUpdateItem(Widget item, Object element, boolean fullMap) {
		Viewer viewer = getViewerForItem(pshelf.getSelection());
		if (viewer instanceof StructuredViewer) {
			ReflectionUtils.callMethod(viewer, "doUpdateItem", item, element, fullMap);
		}
	}

	/**
	 * @see org.eclipse.jface.viewers.StructuredViewer#getSelectionFromWidget()
	 */
	@SuppressWarnings("rawtypes")
	protected List getSelectionFromWidget() {
		PShelfItem item = pshelf.getSelection();
		List retList = EMPTY_SELECTION_LIST;

		Viewer viewer = getViewerForItem(item);
		if (viewer == null)
			return retList;

		// cannot get a list of items when viewer doesn't return structured selection
		IStructuredSelection selection = null;
		if (viewer.getSelection() instanceof IStructuredSelection)
			selection = (IStructuredSelection) viewer.getSelection();
		if (selection != null)
			retList = selection.toList();
		return retList;
	}

	/**
	 * @see org.eclipse.jface.viewers.StructuredViewer#internalRefresh(java.lang.Object)
	 */
	protected void internalRefresh(Object element) {
		pshelf.redraw();
	}

	/**
	 * @see org.eclipse.jface.viewers.StructuredViewer#reveal(java.lang.Object)
	 */
	public void reveal(Object element) {
		Viewer viewer = getViewerForItem(pshelf.getSelection());
		if (viewer instanceof StructuredViewer) {
			((StructuredViewer) viewer).reveal(element);
		}
	}

	/**
	 * @see org.eclipse.jface.viewers.StructuredViewer#setSelectionToWidget(java.util.List, boolean)
	 */
	@SuppressWarnings("rawtypes")
	protected void setSelectionToWidget(List l, boolean reveal) {
		if (l == null) // fail-fast
			throw new NullPointerException();
		Viewer viewer = getViewerForItem(pshelf.getSelection());
		if (viewer != null)
			viewer.setSelection(new StructuredSelection(l), reveal);
	}

	/**
	 * @see org.eclipse.jface.viewers.StructuredViewer#assertContentProviderType(org.eclipse.jface.viewers.IContentProvider)
	 */
	protected void assertContentProviderType(IContentProvider provider) {
		if (!(provider instanceof ITreeContentProvider))
			throw new IllegalArgumentException("Content provider for PShelf must implement ITreeContentProvider!");
		super.assertContentProviderType(provider);
	}
}