/*******************************************************************************
 * Copyright (c) 2007, 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
 *     Matthew Hall - bug 234496
 *******************************************************************************/

package org.eclipse.jface.databinding.viewers;

import org.eclipse.core.databinding.Binding;
import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.UpdateValueStrategy;
import org.eclipse.core.databinding.observable.ChangeEvent;
import org.eclipse.core.databinding.observable.IChangeListener;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.databinding.property.value.IValueProperty;
import org.eclipse.core.runtime.Assert;
import org.eclipse.jface.viewers.CellEditor;
import org.eclipse.jface.viewers.ColumnViewer;
import org.eclipse.jface.viewers.ColumnViewerEditorActivationEvent;
import org.eclipse.jface.viewers.ColumnViewerEditorActivationListener;
import org.eclipse.jface.viewers.ColumnViewerEditorDeactivationEvent;
import org.eclipse.jface.viewers.EditingSupport;
import org.eclipse.jface.viewers.ViewerCell;

/**
 * {@link EditingSupport} using the JFace Data Binding concepts to handle the
 * updating of an element from a {@link CellEditor}.
 *
 * @since 1.2
 */
public abstract class ObservableValueEditingSupport extends EditingSupport {
	/**
	 * Returns an ObservableValueEditingSupport instance which binds the given
	 * cell editor property to the given element property.
	 *
	 * @param viewer
	 *            the column viewer
	 * @param dbc
	 *            the DataBindingContext used for binding between the cell
	 *            editor and the viewer element.
	 * @param cellEditor
	 *            the cell editor
	 * @param cellEditorProperty
	 *            the cell editor property to be bound to the element.
	 * @param elementProperty
	 *            the element property to be bound to the cell editor.
	 * @return an ObservableValueEditingSupport instance using the given
	 *         arguments.
	 * @since 1.3
	 */
	public static EditingSupport create(ColumnViewer viewer,
			DataBindingContext dbc, final CellEditor cellEditor,
			final IValueProperty cellEditorProperty,
			final IValueProperty elementProperty) {
		return new ObservableValueEditingSupport(viewer, dbc) {
			@Override
			protected IObservableValue doCreateCellEditorObservable(
					CellEditor cellEditor) {
				return cellEditorProperty.observe(cellEditor);
			}

			@Override
			protected IObservableValue doCreateElementObservable(
					Object element, ViewerCell cell) {
				return elementProperty.observe(element);
			}

			@Override
			protected CellEditor getCellEditor(Object element) {
				return cellEditor;
			}
		};
	}

	/**
	 * Maintains references to the instances currently imployed while editing.
	 * Will be <code>null</code> when not editing.
	 */
	private EditingState editingState;

	private final ColumnViewerEditorActivationListenerHelper activationListener = new ColumnViewerEditorActivationListenerHelper();

	private ColumnViewer viewer;

	private DataBindingContext dbc;

	/**
	 * Constructs a new instance with the provided <code>viewer</code> and
	 * <code>dbc</code>.
	 *
	 * @param viewer
	 *            viewer to edit
	 * @param dbc
	 *            dbc to create <code>Bindings</code>
	 */
	public ObservableValueEditingSupport(ColumnViewer viewer,
			DataBindingContext dbc) {
		super(viewer);

		if (dbc == null) {
			throw new IllegalArgumentException("Parameter dbc was null."); //$NON-NLS-1$
		}

		this.viewer = viewer;
		this.dbc = dbc;
	}

	/**
	 * Default implementation always returns <code>true</code>.
	 *
	 * @see org.eclipse.jface.viewers.EditingSupport#canEdit(java.lang.Object)
	 */
	@Override
	protected boolean canEdit(Object element) {
		return true;
	}

	/**
	 * Default implementation always returns <code>null</code> as this will be
	 * handled by the Binding.
	 *
	 * @see org.eclipse.jface.viewers.EditingSupport#getValue(java.lang.Object)
	 */
	@Override
	protected Object getValue(Object element) {
		// no op
		return null;
	}

	/**
	 * Default implementation does nothing as this will be handled by the
	 * Binding.
	 *
	 * @see org.eclipse.jface.viewers.EditingSupport#setValue(java.lang.Object,
	 *      java.lang.Object)
	 */
	@Override
	protected void setValue(Object element, Object value) {
		// no op
	}

	/**
	 * Creates a {@link Binding} between the editor and the element to be
	 * edited. Invokes {@link #doCreateCellEditorObservable(CellEditor)},
	 * {@link #doCreateElementObservable(Object, ViewerCell)}, and then
	 * {@link #createBinding(IObservableValue, IObservableValue)}.
	 */
	@Override
	final protected void initializeCellEditorValue(CellEditor cellEditor,
			ViewerCell cell) {
		IObservableValue target = doCreateCellEditorObservable(cellEditor);
		Assert.isNotNull(target,
				"doCreateCellEditorObservable(...) did not return an observable"); //$NON-NLS-1$

		IObservableValue model = doCreateElementObservable(cell.getElement(),
				cell);
		Assert.isNotNull(model,
				"doCreateElementObservable(...) did not return an observable"); //$NON-NLS-1$

		dirty = false;

		Binding binding = createBinding(target, model);

		target.addChangeListener(new IChangeListener() {
			@Override
			public void handleChange(ChangeEvent event) {
				dirty = true;
			}
		});

		Assert.isNotNull(binding, "createBinding(...) did not return a binding"); //$NON-NLS-1$

		editingState = new EditingState(binding, target, model);

		getViewer().getColumnViewerEditor().addEditorActivationListener(
				activationListener);
	}

	/**
	 * Creates the observable value for the CellEditor.
	 *
	 * @param cellEditor
	 * @return observable value
	 */
	protected abstract IObservableValue doCreateCellEditorObservable(
			CellEditor cellEditor);

	/**
	 * Creates the observable value for the element.
	 *
	 * @param element
	 * @param cell
	 * @return observable value
	 */
	protected abstract IObservableValue doCreateElementObservable(
			Object element, ViewerCell cell);

	/**
	 * Creates a new binding for the provided <code>target</code> and
	 * <code>model</code>. Default {@link UpdateValueStrategy value update
	 * strategies} are used with the target to model updating on
	 * {@link UpdateValueStrategy#POLICY_CONVERT}.
	 *
	 * @param target
	 * @param model
	 * @return binding
	 */
	protected Binding createBinding(IObservableValue target,
			IObservableValue model) {
		return dbc.bindValue(target, model, new UpdateValueStrategy(
				UpdateValueStrategy.POLICY_CONVERT), null);
	}

	boolean dirty = false;

	/**
	 * Updates the model from the target.
	 */
	@Override
	final protected void saveCellEditorValue(CellEditor cellEditor,
			ViewerCell cell) {
		if (dirty) {
			editingState.binding.updateTargetToModel();
			dirty = false;
		}
	}

	private class ColumnViewerEditorActivationListenerHelper extends
			ColumnViewerEditorActivationListener {

		@Override
		public void afterEditorActivated(ColumnViewerEditorActivationEvent event) {
			// do nothing
		}

		@Override
		public void afterEditorDeactivated(
				ColumnViewerEditorDeactivationEvent event) {
			editingState.dispose();
			editingState = null;

			viewer.getColumnViewerEditor().removeEditorActivationListener(this);
		}

		@Override
		public void beforeEditorActivated(
				ColumnViewerEditorActivationEvent event) {
			// do nothing
		}

		@Override
		public void beforeEditorDeactivated(
				ColumnViewerEditorDeactivationEvent event) {
			// do nothing
		}
	}

	/**
	 * Maintains references to objects that only live for the length of the edit
	 * cycle.
	 */
	private static class EditingState {
		IObservableValue target;

		IObservableValue model;

		Binding binding;

		EditingState(Binding binding, IObservableValue target,
				IObservableValue model) {
			this.binding = binding;
			this.target = target;
			this.model = model;
		}

		void dispose() {
			binding.dispose();
			target.dispose();
			model.dispose();
		}
	}
}
