/*******************************************************************************
 * Copyright (c) 2005, 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
 *******************************************************************************/
package org.eclipse.core.commands;

import org.eclipse.core.commands.common.HandleObject;
import org.eclipse.core.commands.common.NotDefinedException;
import org.eclipse.core.internal.commands.util.Util;

/**
 * <p>
 * Provides information about the type of a command parameter. Clients can use a
 * parameter type to check if an object matches the type of the parameter with
 * {@link #isCompatible(Object)} and can get an
 * {@link AbstractParameterValueConverter} to convert between objects matching
 * the parameter type and strings that encode the object's identity.
 * </p>
 * <p>
 * A command parameter is not required to declare a type. To determine if a
 * given parameter has a type, check if an {@link IParameter} implements
 * {@link ITypedParameter} and if so, use
 * {@link ITypedParameter#getParameterType()} like this:
 * </p>
 *
 * <pre>
 *                   IParameter parameter = // ... get IParameter from Command
 *                   if (parameter instanceof ITypedParameter) {
 *                     ParameterType type = ((ITypedParameter)parameter).getParameterType();
 *                     if (type != null) {
 *                       // this parameter has a ParameterType
 *                     }
 *                   }
 * </pre>
 *
 * @see IParameter
 * @see ITypedParameter#getParameterType()
 * @since 3.2
 */
@SuppressWarnings("rawtypes")
public final class ParameterType extends HandleObject implements Comparable {

	/**
	 * TODO: this was copied from
	 * org.eclipse.core.internal.expressions.Expressions is there a better place
	 * to reference this?
	 *
	 * @param element
	 *            The element to test; may be <code>null</code>.
	 * @param type
	 *            The type against which we are testing;may be <code>null</code>.
	 * @return <code>true</code> if the <code>element</code> is an instance
	 *         of <code>type</code>; <code>false</code> otherwise.
	 */
	private static final boolean isInstanceOf(final Object element,
			final String type) {
		// null isn't an instanceof of anything.
		if (element == null) {
			return false;
		}
		return isSubtype(element.getClass(), type);
	}

	/**
	 * TODO: this was copied from
	 * org.eclipse.core.internal.expressions.Expressions is there a better place
	 * to reference this?
	 *
	 * @param clazz
	 *            The class to match; may be <code>null</code>.
	 * @param type
	 *            The type against which we are testing;may be <code>null</code>.
	 * @return <code>true</code> if the <code>element</code> is an instance
	 *         of <code>type</code>; <code>false</code> otherwise.
	 */
	private static final boolean isSubtype(final Class<?> clazz, final String type) {
		if (clazz.getName().equals(type)) {
			return true;
		}
		final Class<?> superClass = clazz.getSuperclass();
		if (superClass != null && isSubtype(superClass, type)) {
			return true;
		}
		for (Class<?> classInterface : clazz.getInterfaces()) {
			if (isSubtype(classInterface, type)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * An {@link AbstractParameterValueConverter} for converting parameter
	 * values between objects and strings. This may be <code>null</code>.
	 */
	private transient AbstractParameterValueConverter parameterTypeConverter;

	/**
	 * A string specifying the object type of this parameter type. This will be
	 * <code>null</code> when the parameter type is undefined but never null
	 * when it is defined.
	 */
	private transient String type = null;

	/**
	 * Constructs a new instance based on the given identifier. When a parameter
	 * type is first constructed, it is undefined. Parameter types should only
	 * be constructed by the {@link CommandManager} to ensure that the
	 * identifier remains unique.
	 *
	 * @param id
	 *            The identifier for this type. This value must not be
	 *            <code>null</code>, and must be unique amongst all parameter
	 *            types.
	 */
	ParameterType(final String id) {
		super(id);
	}

	/**
	 * Adds a listener to this parameter type that will be notified when its
	 * state changes.
	 *
	 * @param listener
	 *            The listener to be added; must not be <code>null</code>.
	 */
	public final void addListener(final IParameterTypeListener listener) {
		addListenerObject(listener);
	}

	/**
	 * Compares this parameter type with another object by comparing each of the
	 * non-transient attributes.
	 *
	 * @param object
	 *            The object with which to compare; must be an instance of
	 *            {@link ParameterType}.
	 * @return A negative integer, zero or a positive integer, if the object is
	 *         greater than, equal to or less than this parameter type.
	 */
	@Override
	public final int compareTo(final Object object) {
		final ParameterType castedObject = (ParameterType) object;
		int compareTo = Util.compare(defined, castedObject.defined);
		if (compareTo == 0) {
			compareTo = Util.compare(id, castedObject.id);
		}
		return compareTo;
	}

	/**
	 * <p>
	 * Defines this parameter type, setting the defined property to
	 * <code>true</code>.
	 * </p>
	 * <p>
	 * Notification is sent to all listeners that something has changed.
	 * </p>
	 *
	 * @param type
	 *            a string identifying the Java object type for this parameter
	 *            type; <code>null</code> is interpreted as
	 *            <code>"java.lang.Object"</code>
	 * @param parameterTypeConverter
	 *            an {@link AbstractParameterValueConverter} to perform
	 *            string/object conversions for parameter values; may be
	 *            <code>null</code>
	 */
	public final void define(final String type,
			final AbstractParameterValueConverter parameterTypeConverter) {

		final boolean definedChanged = !this.defined;
		this.defined = true;

		this.type = (type == null) ? Object.class.getName() : type;
		this.parameterTypeConverter = parameterTypeConverter;

		fireParameterTypeChanged(new ParameterTypeEvent(this, definedChanged));
	}

	/**
	 * Notifies all listeners that this parameter type has changed. This sends
	 * the given event to all of the listeners, if any.
	 *
	 * @param event
	 *            The event to send to the listeners; must not be
	 *            <code>null</code>.
	 */
	private final void fireParameterTypeChanged(final ParameterTypeEvent event) {
		if (event == null) {
			throw new NullPointerException(
					"Cannot send a null event to listeners."); //$NON-NLS-1$
		}

		if (!isListenerAttached()) {
			return;
		}

		for (Object listener : getListeners()) {
			final IParameterTypeListener parameterTypeListener = (IParameterTypeListener) listener;
			parameterTypeListener.parameterTypeChanged(event);
		}
	}

	/**
	 * Returns the value converter associated with this parameter, if any.
	 *
	 * @return The parameter value converter, or <code>null</code> if there is
	 *         no value converter for this parameter.
	 * @throws NotDefinedException
	 *             if the parameter type is not currently defined
	 */
	public final AbstractParameterValueConverter getValueConverter()
			throws NotDefinedException {
		if (!isDefined()) {
			throw new NotDefinedException(
					"Cannot use getValueConverter() with an undefined ParameterType"); //$NON-NLS-1$
		}

		return parameterTypeConverter;
	}

	/**
	 * Returns whether the provided value is compatible with this parameter
	 * type. An object is compatible with a parameter type if the object is an
	 * instance of the class defined as the parameter's type class.
	 *
	 * @param value
	 *            an object to check for compatibility with this parameter type;
	 *            may be <code>null</code>.
	 * @return <code>true</code> if the value is compatible with this type,
	 *         <code>false</code> otherwise
	 * @throws NotDefinedException
	 *             if the parameter type is not currently defined
	 */
	public boolean isCompatible(Object value) throws NotDefinedException {
		if (!isDefined()) {
			throw new NotDefinedException(
					"Cannot use isCompatible() with an undefined ParameterType"); //$NON-NLS-1$
		}
		return isInstanceOf(value, type);
	}

	/**
	 * Unregisters listener for changes to properties of this parameter type.
	 *
	 * @param listener
	 *            the instance to unregister. Must not be <code>null</code>.
	 *            If an attempt is made to unregister an instance which is not
	 *            already registered with this instance, no operation is
	 *            performed.
	 */
	public final void removeListener(final IParameterTypeListener listener) {
		removeListenerObject(listener);
	}

	/**
	 * The string representation of this parameter type. For debugging purposes
	 * only. This string should not be shown to an end user.
	 *
	 * @return The string representation; never <code>null</code>.
	 */
	@Override
	public final String toString() {
		if (string == null) {
			final StringBuilder stringBuffer = new StringBuilder("ParameterType("); //$NON-NLS-1$
			stringBuffer.append(id);
			stringBuffer.append(',');
			stringBuffer.append(defined);
			stringBuffer.append(')');
			string = stringBuffer.toString();
		}
		return string;
	}

	/**
	 * Makes this parameter type become undefined. Notification is sent to all
	 * listeners.
	 */
	@Override
	public final void undefine() {
		string = null;

		final boolean definedChanged = defined;
		defined = false;

		type = null;
		parameterTypeConverter = null;

		fireParameterTypeChanged(new ParameterTypeEvent(this, definedChanged));
	}

}
