/*******************************************************************************
 * Copyright (c) 2014, 2017 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
 *     Jeanderson Candido <http://jeandersonbc.github.io> - Bug 433603
 *******************************************************************************/
package org.eclipse.ui.tests.manual;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import org.eclipse.core.databinding.observable.Diffs;
import org.eclipse.core.databinding.observable.list.WritableList;
import org.eclipse.core.databinding.observable.map.ComputedObservableMap;
import org.eclipse.core.databinding.observable.set.IObservableSet;
import org.eclipse.core.databinding.observable.value.ComputedValue;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.databinding.observable.value.IValueChangeListener;
import org.eclipse.core.databinding.observable.value.WritableValue;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.databinding.viewers.ObservableListContentProvider;
import org.eclipse.jface.databinding.viewers.ObservableMapLabelProvider;
import org.eclipse.jface.databinding.viewers.typed.ViewerProperties;
import org.eclipse.jface.internal.databinding.provisional.swt.ControlUpdater;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.ui.ISaveablePart;
import org.eclipse.ui.ISaveablesLifecycleListener;
import org.eclipse.ui.ISaveablesSource;
import org.eclipse.ui.Saveable;
import org.eclipse.ui.SaveablesLifecycleEvent;
import org.eclipse.ui.part.ViewPart;

public class ViewWithSaveables extends ViewPart implements ISaveablesSource,
		ISaveablePart {

	WritableList<Saveable> saveables = new WritableList<>();
	IObservableValue<?> dirty = new ComputedValue<>() {
		@Override
		protected Object calculate() {
			for (Object obj : saveables) {
				MySaveable saveable = (MySaveable) obj;
				if (saveable.isDirty()) {
					return Boolean.TRUE;
				}
			}
			return Boolean.FALSE;
		}
	};
	private TableViewer viewer;
	private IObservableValue<?> selection;

	public ViewWithSaveables() {
	}

	@Override
	public void createPartControl(Composite parent) {
		viewer = new TableViewer(parent, SWT.BORDER);

		GridDataFactory.fillDefaults().grab(true, true).span(4, 1)
				.applyTo(viewer.getControl());
		ObservableListContentProvider observableListContentProvider = new ObservableListContentProvider();

		viewer.setContentProvider(observableListContentProvider);
		viewer.setLabelProvider(new ObservableMapLabelProvider(
				new DirtyObservableMap(observableListContentProvider
						.getKnownElements())) {
			@Override
			public String getColumnText(Object element, int columnIndex) {
				return getText(element);
			}

			@Override
			public String getText(Object element) {
				MySaveable saveable = (MySaveable) element;
				return (saveable.isDirty() ? "*" : "") + saveable;
			}
		});
		viewer.setInput(saveables);
		{
			Button button = new Button(parent, SWT.PUSH);
			button.setText("Add");
			button.addSelectionListener(new SelectionAdapter() {
				@Override
				public void widgetSelected(SelectionEvent e) {
					addSaveable();
				}
			});
		}


		selection = ViewerProperties.singleSelection().observe(viewer);
		{
			final Button button = new Button(parent, SWT.PUSH);
			button.setText("Remove");
			button.addSelectionListener(new SelectionAdapter() {
				@Override
				public void widgetSelected(SelectionEvent e) {
					removeSaveable();
				}
			});
			new ControlUpdater(button) {
				@Override
				protected void updateControl() {
					button.setEnabled(selection.getValue() != null);
				}
			};
		}
		final Button button = new Button(parent, SWT.CHECK);
		button.setText("dirty");
		button.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				MySaveable saveable = (MySaveable) selection.getValue();
				saveable.setDirty(button.getSelection());
			}
		});
		new ControlUpdater(button) {
			@Override
			protected void updateControl() {
				MySaveable saveable = (MySaveable) selection.getValue();
				if (saveable == null) {
					button.setEnabled(false);
					button.setSelection(false);
				} else {
					button.setEnabled(true);
					// we know that isDirty is implemented using a
					// WritableValue,
					// and thus a dependency on that writable value will
					// result from
					// calling isDirty().
					button.setSelection(saveable.isDirty());
				}
			}
		};
		getSite().setSelectionProvider(viewer);
		dirty.addValueChangeListener(event -> firePropertyChange(ISaveablePart.PROP_DIRTY));
		GridLayoutFactory.fillDefaults().numColumns(4).equalWidth(false)
				.generateLayout(parent);
	}

	void removeSaveable() {
		Saveable[] toRemove = getActiveSaveables();
		ISaveablesLifecycleListener lifecycleListener = getSite()
				.getService(ISaveablesLifecycleListener.class);
		SaveablesLifecycleEvent event = new SaveablesLifecycleEvent(this,
				SaveablesLifecycleEvent.PRE_CLOSE, toRemove, false);
		lifecycleListener.handleLifecycleEvent(event);
		if (!event.isVeto()) {
			saveables.removeAll(Arrays.asList(toRemove));
			lifecycleListener.handleLifecycleEvent(new SaveablesLifecycleEvent(
					this, SaveablesLifecycleEvent.POST_CLOSE, toRemove, false));
		}
	}

	void addSaveable() {
		MySaveable saveable = new MySaveable();
		saveables.add(saveable);
		ISaveablesLifecycleListener lifecycleListener = getSite()
				.getService(ISaveablesLifecycleListener.class);
		lifecycleListener.handleLifecycleEvent(new SaveablesLifecycleEvent(
				this, SaveablesLifecycleEvent.POST_OPEN,
				new Saveable[] { saveable }, false));
	}

	@Override
	public void setFocus() {
	}

	@Override
	public Saveable[] getActiveSaveables() {
		Saveable selectedSaveable = (Saveable) selection.getValue();
		return selectedSaveable == null ? new Saveable[0]
				: new Saveable[] { selectedSaveable };
	}

	@Override
	public Saveable[] getSaveables() {
		return saveables.toArray(new Saveable[saveables.size()]);
	}

	@Override
	public void doSave(IProgressMonitor monitor) {
		Assert.isTrue(false,
				"Save operations should happen through the saveables.");
	}

	@Override
	public void doSaveAs() {
	}

	@Override
	public boolean isDirty() {
		return ((Boolean) dirty.getValue()).booleanValue();
	}

	@Override
	public boolean isSaveAsAllowed() {
		return false;
	}

	@Override
	public boolean isSaveOnCloseNeeded() {
		return true;
	}

	class MySaveable extends Saveable {

		private IObservableValue<Boolean> myDirty = new WritableValue<>(Boolean.FALSE,
				Boolean.TYPE);

		@Override
		public void doSave(IProgressMonitor monitor) {
			setDirty(false);
		}

		@Override
		public boolean equals(Object object) {
			return this == object;
		}

		@Override
		public ImageDescriptor getImageDescriptor() {
			return null;
		}

		@Override
		public String getName() {
			return toString();
		}

		@Override
		public String getToolTipText() {
			return toString();
		}

		@Override
		public int hashCode() {
			return System.identityHashCode(this);
		}

		@Override
		public boolean isDirty() {
			return myDirty.getValue().booleanValue();
		}

		IObservableValue<Boolean> getDirty() {
			return myDirty;
		}

		void setDirty(boolean dirty) {
			myDirty.setValue(Boolean.valueOf(dirty));
		}

	}

	class DirtyObservableMap extends ComputedObservableMap<Object, Object> {

		Map<IObservableValue<Boolean>, MySaveable> writableValueToElement = new HashMap<>();

		private IValueChangeListener<Boolean> valueChangeListener = event -> fireMapChange(
				Diffs.createMapDiffSingleChange(
				writableValueToElement.get(event.getSource()), event.diff.getOldValue(), event.diff.getNewValue()));

		public DirtyObservableMap(IObservableSet knownElements) {
			super(knownElements);
			init();
		}

		@Override
		protected Object doGet(Object key) {
			MySaveable saveable = (MySaveable) key;
			return Boolean.valueOf(saveable.isDirty());
		}

		@Override
		protected Object doPut(Object key, Object value) {
			MySaveable saveable = (MySaveable) key;
			Boolean oldValue = Boolean.valueOf(saveable.isDirty());
			saveable.setDirty(((Boolean) value).booleanValue());
			keySet().add(key);
			return oldValue;
		}

		@Override
		protected void hookListener(Object key) {
			MySaveable saveable = (MySaveable) key;
			IObservableValue<Boolean> oValue = saveable.getDirty();
			writableValueToElement.put(oValue, saveable);
			oValue.addValueChangeListener(valueChangeListener);
		}

		@Override
		protected void unhookListener(Object key) {
			MySaveable saveable = (MySaveable) key;
			saveable.getDirty().removeValueChangeListener(valueChangeListener);
			writableValueToElement.remove(saveable.getDirty());
		}

	}

}
