/*******************************************************************************
 * Copyright (c) 2014, 2017 itemis AG and others.
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Alexander Nyßen (itemis AG)  - initial API and implementation
 *     Matthias Wienand (itemis AG) - initial API and implementation
 *
 *******************************************************************************/
package org.eclipse.gef.fx.listeners;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

import org.eclipse.gef.fx.nodes.InfiniteCanvas;

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Bounds;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.transform.Transform;

/**
 * You can use a VisualChangeListener to register/unregister specific listeners
 * for catching changes in the visual representation of a JavaFX {@link Node}.
 * Depending on the changed property, either the
 * {@link #boundsInLocalChanged(Bounds, Bounds)} or the
 * {@link #localToParentTransformChanged(Node, Transform, Transform)} method is
 * called. A bounds-in-local change occurs when the target node's effect, clip,
 * stroke, local transformations, or geometric bounds change. A
 * local-to-parent-transform change occurs when the node undergoes a
 * transformation change. Transformation listeners are registered for all nodes
 * in the hierarchy up to a specific parent.
 *
 * @author anyssen
 * @author mwienand
 *
 */
public abstract class VisualChangeListener {

	private Node observed;
	private Node parent;
	private HashMap<ChangeListener<Transform>, Node> localToParentTransformListeners = new HashMap<>();
	private boolean layoutBoundsChanged = false;
	private boolean boundsInLocalChanged = false;
	private boolean boundsInParentChanged = false;
	private Bounds oldBoundsInLocal = null;
	private Bounds newBoundsInLocal = null;

	private ChangeListener<? super Bounds> layoutBoundsListener = new ChangeListener<Bounds>() {
		@Override
		public void changed(ObservableValue<? extends Bounds> observable,
				Bounds oldValue, Bounds newValue) {
			// only fire a visual change event if the new bounds are valid
			if (isValidBounds(newValue)) {
				layoutBoundsChanged = true;
				onBoundsChanged();
			}
		}
	};

	private final ChangeListener<? super Bounds> boundsInLocalListener = new ChangeListener<Bounds>() {
		@Override
		public void changed(ObservableValue<? extends Bounds> observable,
				Bounds oldValue, Bounds newValue) {
			// only fire a visual change event if the new bounds are valid
			if (isValidBounds(newValue)) {
				oldBoundsInLocal = oldValue;
				newBoundsInLocal = newValue;
				boundsInLocalChanged = true;
				onBoundsChanged();
			}
		}
	};

	private ChangeListener<? super Bounds> boundsInParentListener = new ChangeListener<Bounds>() {
		@Override
		public void changed(ObservableValue<? extends Bounds> observable,
				Bounds oldValue, Bounds newValue) {
			// only fire a visual change event if the new bounds are valid
			if (isValidBounds(newValue)) {
				boundsInParentChanged = true;
				onBoundsChanged();
			}
		}
	};

	/**
	 * This method is called upon a bounds-in-local change.
	 *
	 * @param oldBounds
	 *            The old {@link Bounds}.
	 * @param newBounds
	 *            The new {@link Bounds}.
	 */
	protected abstract void boundsInLocalChanged(Bounds oldBounds,
			Bounds newBounds);

	private Node getNearestCommonAncestor(Node source, Node target) {
		if (source == target) {
			return source;
		}

		Set<Node> parents = new HashSet<>();
		Node m = source;
		Node n = target;
		while (m != null || n != null) {
			if (m != null) {
				if (parents.contains(m)) {
					return m;
				}
				parents.add(m);
				if (n != null && parents.contains(n)) {
					return n;
				}
				m = m.getParent();
			}
			if (n != null) {
				if (parents.contains(n)) {
					return n;
				}
				parents.add(n);
				if (m != null && parents.contains(m)) {
					return m;
				}
				n = n.getParent();
			}
		}

		// could not find a common parent
		return null;
	}

	/**
	 * Returns <code>true</code> if this {@link VisualChangeListener} is
	 * currently registered, otherwise returns <code>false</code>.
	 *
	 * @return <code>true</code> if this {@link VisualChangeListener} is
	 *         currently registered, otherwise <code>false</code>.
	 */
	public boolean isRegistered() {
		return parent != null;
	}

	/**
	 * Checks if the given Bounds contain NaN values. Returns <code>true</code>
	 * if no NaN values are found, otherwise <code>false</code>.
	 *
	 * @param b
	 * @return
	 */
	private boolean isValidBounds(Bounds b) {
		if (Double.isNaN(b.getMinX()) || Double.isInfinite(b.getMinX())) {
			return false;
		}
		if (Double.isNaN(b.getMinY()) || Double.isInfinite(b.getMinY())) {
			return false;
		}
		if (Double.isNaN(b.getMaxX()) || Double.isInfinite(b.getMaxX())) {
			return false;
		}
		if (Double.isNaN(b.getMaxY()) || Double.isInfinite(b.getMaxY())) {
			return false;
		}
		return true;
	}

	/**
	 * Checks if the given Transform contains NaN values. Returns
	 * <code>true</code> if no NaN values are found, otherwise <code>false/
	 * <code>.
	 *
	 * @param t
	 * @return
	 */
	private boolean isValidTransform(Transform t) {
		if (Double.isNaN(t.getMxx()) || Double.isInfinite(t.getMxx())) {
			return false;
		}
		if (Double.isNaN(t.getMxy()) || Double.isInfinite(t.getMxy())) {
			return false;
		}
		if (Double.isNaN(t.getMxz()) || Double.isInfinite(t.getMxz())) {
			return false;
		}
		if (Double.isNaN(t.getMyx()) || Double.isInfinite(t.getMyx())) {
			return false;
		}
		if (Double.isNaN(t.getMyy()) || Double.isInfinite(t.getMyy())) {
			return false;
		}
		if (Double.isNaN(t.getMyz()) || Double.isInfinite(t.getMyz())) {
			return false;
		}
		if (Double.isNaN(t.getMzx()) || Double.isInfinite(t.getMzx())) {
			return false;
		}
		if (Double.isNaN(t.getMzy()) || Double.isInfinite(t.getMzy())) {
			return false;
		}
		if (Double.isNaN(t.getMzz()) || Double.isInfinite(t.getMzz())) {
			return false;
		}
		if (Double.isNaN(t.getTx()) || Double.isInfinite(t.getTx())) {
			return false;
		}
		if (Double.isNaN(t.getTy()) || Double.isInfinite(t.getTy())) {
			return false;
		}
		if (Double.isNaN(t.getTz()) || Double.isInfinite(t.getTz())) {
			return false;
		}
		return true;
	}

	/**
	 * This method is called upon a local-to-parent-transform change.
	 *
	 * @param observed
	 *            The {@link Node} whose local-to-parent-transform changed.
	 * @param oldTransform
	 *            The old {@link Transform}.
	 * @param newTransform
	 *            The new {@link Transform}.
	 */
	protected abstract void localToParentTransformChanged(Node observed,
			Transform oldTransform, Transform newTransform);

	/**
	 * Called upon changes to any of the following properties: "layout-bounds",
	 * "bounds-in-local", and "bounds-in-parent". Calls the
	 * {@link #boundsInLocalChanged(Bounds, Bounds)} method if all bounds
	 * properties are changed.
	 */
	protected void onBoundsChanged() {
		if (layoutBoundsChanged && boundsInLocalChanged
				&& boundsInParentChanged) {
			layoutBoundsChanged = false;
			boundsInLocalChanged = false;
			boundsInParentChanged = false;
			boundsInLocalChanged(oldBoundsInLocal, newBoundsInLocal);
		}
	}

	/**
	 * Registers this listener on the given pair of observed and observer nodes
	 * to recognize visual changes of the observed node relative to the common
	 * parent of observer and observed node.
	 * <p>
	 * In detail, two kind of changes will be reported as visual changes:
	 * <ul>
	 * <li>changes to the bounds-in-local property of the observed node (
	 * {@link #boundsInLocalChanged(Bounds, Bounds)}) itself</li>
	 * <li>changes to the local-to-parent-transform property of any node in the
	 * observed node hierarchy up to (but excluding) the common parent of the
	 * observed and observer nodes (
	 * {@link #localToParentTransformChanged(Node, Transform, Transform)}).</li>
	 * </ul>
	 * <p>
	 * The use of a visual change lister allows to react to relative transform
	 * changes only. If the common parent of both nodes is for instance nested
	 * below an {@link InfiniteCanvas}, this allows to ignore transform changes
	 * that result from scrolling, as these will (in most cases) not indicate a
	 * visual change.
	 *
	 * @param observed
	 *            The observed {@link Node} to be observed for visual changes,
	 *            which includes bounds-in-local changes for the source node
	 *            itself, as well as local-to-parent-transform changes for all
	 *            ancestor nodes (including the source node) up to (but
	 *            excluding) the common parent node of source and target.
	 * @param observer
	 *            A {@link Node} in the same {@link Scene} as the given observed
	 *            node, relative to which transform changes will be reported.
	 *            That is, local-to-parent-transform changes will only be
	 *            reported for all nodes in the hierarchy up to (but excluding)
	 *            the common parent of observed and observer.
	 */
	public void register(Node observed, Node observer) {
		if (observed == null) {
			throw new IllegalArgumentException("Observed may not be null.");
		}
		if (observer == null) {
			throw new IllegalArgumentException("Observer not be null.");
		}

		Node commonAncestor = getNearestCommonAncestor(observed, observer);
		if (commonAncestor == null) {
			throw new IllegalArgumentException(
					"Source and target do not share a common ancestor.");
		}

		Node tmp = observed;
		while (tmp != null && tmp != commonAncestor) {
			tmp = tmp.getParent();
		}
		if (tmp == null) {
			throw new IllegalArgumentException(
					"TransformReference needs to be ancestor of the given observed node.");
		}

		// unregister old listeners
		if (this.observed != null) {
			unregister();
		}

		// assign new nodes
		this.observed = observed;
		parent = commonAncestor;

		// add bounds listeners
		observed.layoutBoundsProperty().addListener(layoutBoundsListener);
		observed.boundsInLocalProperty().addListener(boundsInLocalListener);
		observed.boundsInParentProperty().addListener(boundsInParentListener);

		// add transform listeners
		tmp = observed;
		while (tmp != null && tmp != parent) {
			final Node current = tmp;
			ChangeListener<Transform> transformChangeListener = new ChangeListener<Transform>() {
				@Override
				public void changed(
						ObservableValue<? extends Transform> observable,
						Transform oldValue, Transform newValue) {
					// only fire a visual change event if the new transform is
					// valid
					if (isValidTransform(newValue)) {
						localToParentTransformChanged(current, oldValue,
								newValue);
					}
				}
			};
			tmp.localToParentTransformProperty()
					.addListener(transformChangeListener);
			localToParentTransformListeners.put(transformChangeListener, tmp);
			tmp = tmp.getParent();
		}

		// add transform listeners
		// FIXME: Duplicate code!
		tmp = observer;
		while (tmp != null && tmp != parent) {
			final Node current = tmp;
			ChangeListener<Transform> transformChangeListener = new ChangeListener<Transform>() {
				@Override
				public void changed(
						ObservableValue<? extends Transform> observable,
						Transform oldValue, Transform newValue) {
					// only fire a visual change event if the new transform is
					// valid
					if (isValidTransform(newValue)) {
						localToParentTransformChanged(current, oldValue,
								newValue);
					}
				}
			};
			tmp.localToParentTransformProperty()
					.addListener(transformChangeListener);
			localToParentTransformListeners.put(transformChangeListener, tmp);
			tmp = tmp.getParent();
		}
	}

	/**
	 * Unregisters all previously registered listeners.
	 */
	public void unregister() {
		if (!isRegistered()) {
			return;
		}

		// remove bounds listener
		observed.layoutBoundsProperty().removeListener(layoutBoundsListener);
		observed.boundsInLocalProperty().removeListener(boundsInLocalListener);
		observed.boundsInParentProperty()
				.removeListener(boundsInParentListener);

		// remove transform listeners
		for (ChangeListener<Transform> l : localToParentTransformListeners
				.keySet()) {
			localToParentTransformListeners.get(l)
					.localToParentTransformProperty().removeListener(l);
		}

		// reset fields
		parent = null;
		observed = null;
		localToParentTransformListeners.clear();
	}
}