/*******************************************************************************
 * Copyright (c) 2005, 2018 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.ui.internal.services;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import org.eclipse.core.commands.Command;
import org.eclipse.core.commands.IParameter;
import org.eclipse.core.commands.Parameterization;
import org.eclipse.core.commands.ParameterizedCommand;
import org.eclipse.core.commands.common.NotDefinedException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.commands.ICommandService;
import org.eclipse.ui.internal.WorkbenchPlugin;

/**
 * <p>
 * A manager for items parsed from the preference store. This attaches a
 * listener to the registry after the first read, and monitors the preference
 * for changes from that point on. When {@link #dispose()} is called, the
 * listener is detached.
 * </p>
 * <p>
 * This class is only intended for internal use within the
 * <code>org.eclipse.ui.workbench</code> plug-in.
 * </p>
 *
 * @since 3.2
 */
public abstract class PreferencePersistence extends RegistryPersistence {

	/**
	 * Inserts the given element into the indexed two-dimensional array in the array
	 * at the index. The array is grown as necessary.
	 *
	 * @param elementToAdd The element to add to the indexed array; may be
	 *                     <code>null</code>
	 * @param indexedArray The two-dimensional array that is indexed by element
	 *                     type; must not be <code>null</code>.
	 * @param index        The index at which the element should be added; must be a
	 *                     valid index.
	 * @param currentCount The current number of items in the array at the index.
	 */
	protected static final void addElementToIndexedArray(final IMemento elementToAdd, final IMemento[][] indexedArray,
			final int index, final int currentCount) {
		final IMemento[] elements;
		if (currentCount == 0) {
			elements = new IMemento[1];
			indexedArray[index] = elements;
		} else if (currentCount >= indexedArray[index].length) {
			final IMemento[] copy = new IMemento[indexedArray[index].length * 2];
			System.arraycopy(indexedArray[index], 0, copy, 0, currentCount);
			elements = copy;
			indexedArray[index] = elements;
		} else {
			elements = indexedArray[index];
		}
		elements[currentCount] = elementToAdd;
	}

	/**
	 * Adds a warning to be logged at some later point in time.
	 *
	 * @param warningsToLog The collection of warnings to be logged; must not be
	 *                      <code>null</code>.
	 * @param message       The message to log; must not be <code>null</code>.
	 */
	protected static final void addWarning(final List<IStatus> warningsToLog, final String message) {
		addWarning(warningsToLog, message, null, null, null);
	}

	/**
	 * Adds a warning to be logged at some later point in time. This logs the
	 * identifier of the item.
	 *
	 * @param warningsToLog The collection of warnings to be logged; must not be
	 *                      <code>null</code>.
	 * @param message       The message to log; must not be <code>null</code>.
	 * @param id            The identifier of the item for which a warning is being
	 *                      logged; may be <code>null</code>.
	 */
	protected static final void addWarning(final List<IStatus> warningsToLog, final String message, final String id) {
		addWarning(warningsToLog, message, id, null, null);
	}

	/**
	 * Adds a warning to be logged at some later point in time. This logs the
	 * identifier of the item, as well as an extra attribute.
	 *
	 * @param warningsToLog       The collection of warnings to be logged; must not
	 *                            be <code>null</code>.
	 * @param message             The message to log; must not be <code>null</code>.
	 * @param id                  The identifier of the item for which a warning is
	 *                            being logged; may be <code>null</code>.
	 * @param extraAttributeName  The name of extra attribute to be logged; may be
	 *                            <code>null</code>.
	 * @param extraAttributeValue The value of the extra attribute to be logged; may
	 *                            be <code>null</code>.
	 */
	protected static final void addWarning(final List<IStatus> warningsToLog, final String message, final String id,
			final String extraAttributeName, final String extraAttributeValue) {
		String statusMessage = message;
		if (id != null) {
			statusMessage = statusMessage + ": id='" + id + '\''; //$NON-NLS-1$
		}
		if (extraAttributeName != null) {
			if (id != null) {
				statusMessage = statusMessage + ',';
			} else {
				statusMessage = statusMessage + ':';
			}
			statusMessage = statusMessage + ' ' + extraAttributeName + "='" //$NON-NLS-1$
					+ extraAttributeValue + '\'';
		}

		final IStatus status = new Status(IStatus.WARNING, WorkbenchPlugin.PI_WORKBENCH, 0, statusMessage, null);
		warningsToLog.add(status);
	}

	/**
	 * Reads a boolean attribute from a memnto.
	 *
	 * @param memento      The memento from which to read the attribute; must not be
	 *                     <code>null</code>.
	 * @param attribute    The attribute to read; must not be <code>null</code>.
	 * @param defaultValue The default boolean value.
	 * @return The attribute's value; may be <code>null</code> if none.
	 */
	protected static final boolean readBoolean(final IMemento memento, final String attribute,
			final boolean defaultValue) {
		final String value = memento.getString(attribute);
		if (value == null) {
			return defaultValue;
		}

		if (defaultValue) {
			return !value.equalsIgnoreCase("false"); //$NON-NLS-1$
		}

		return !value.equalsIgnoreCase("true"); //$NON-NLS-1$
	}

	/**
	 * Reads an optional attribute from a memento. This converts zero-length strings
	 * into <code>null</code>.
	 *
	 * @param memento   The memento from which to read the attribute; must not be
	 *                  <code>null</code>.
	 * @param attribute The attribute to read; must not be <code>null</code>.
	 * @return The attribute's value; may be <code>null</code> if none.
	 */
	protected static final String readOptional(final IMemento memento, final String attribute) {
		String value = memento.getString(attribute);
		if ((value != null) && (value.isEmpty())) {
			value = null;
		}

		return value;
	}

	/**
	 * Reads the parameterized command from a parent memento. This is used to read
	 * the parameter sub-elements from a key element, as well as the command id.
	 * Each parameter is guaranteed to be valid. If invalid parameters are found,
	 * then a warning status will be appended to the <code>warningsToLog</code>
	 * list. The command id is required, or a warning will be logged.
	 *
	 * @param memento        The memento from which the parameters should be read;
	 *                       must not be <code>null</code>.
	 * @param commandService The service providing commands for the workbench; must
	 *                       not be <code>null</code>.
	 * @param warningsToLog  The list of warnings found during parsing. Warnings
	 *                       found will parsing the parameters will be appended to
	 *                       this list. This value must not be <code>null</code>.
	 * @param message        The message to print if the command identifier is not
	 *                       present; must not be <code>null</code>.
	 * @return The array of parameters found for this configuration element;
	 *         <code>null</code> if none can be found.
	 */
	protected static final ParameterizedCommand readParameterizedCommand(final IMemento memento,
			final ICommandService commandService, final List<IStatus> warningsToLog, final String message,
			final String id) {
		final String commandId = readRequired(memento, ATT_COMMAND_ID, warningsToLog, message, id);
		if (commandId == null) {
			return null;
		}

		final Command command = commandService.getCommand(commandId);
		return readParameters(memento, warningsToLog, command);
	}

	/**
	 * Reads the parameters from a parent memento. This is used to read the
	 * parameter sub-elements from a key element. Each parameter is guaranteed to be
	 * valid. If invalid parameters are found, then a warning status will be
	 * appended to the <code>warningsToLog</code> list.
	 *
	 * @param memento       The memento from which the parameters should be read;
	 *                      must not be <code>null</code>.
	 * @param warningsToLog The list of warnings found during parsing. Warnings
	 *                      found will parsing the parameters will be appended to
	 *                      this list. This value must not be <code>null</code>.
	 * @param command       The command around which the parameterization should be
	 *                      created; must not be <code>null</code>.
	 * @return The array of parameters found for this memento; <code>null</code> if
	 *         none can be found.
	 */
	protected static final ParameterizedCommand readParameters(final IMemento memento,
			final List<IStatus> warningsToLog, final Command command) {
		final IMemento[] parameterMementos = memento.getChildren(TAG_PARAMETER);
		if ((parameterMementos == null) || (parameterMementos.length == 0)) {
			return new ParameterizedCommand(command, null);
		}

		final Collection<Parameterization> parameters = new ArrayList<>();
		for (final IMemento parameterMemento : parameterMementos) {
			// Read out the id.
			final String id = parameterMemento.getString(ATT_ID);
			if ((id == null) || (id.isEmpty())) {
				// The name should never be null. This is invalid.
				addWarning(warningsToLog, "Parameters need a name"); //$NON-NLS-1$
				continue;
			}

			// Find the parameter on the command.
			IParameter parameter = null;
			try {
				final IParameter[] commandParameters = command.getParameters();
				if (commandParameters != null) {
					for (final IParameter currentParameter : commandParameters) {
						if (Objects.equals(currentParameter.getId(), id)) {
							parameter = currentParameter;
							break;
						}
					}

				}
			} catch (final NotDefinedException e) {
				// This should not happen.
			}
			if (parameter == null) {
				// The name should never be null. This is invalid.
				addWarning(warningsToLog, "Could not find a matching parameter", id); //$NON-NLS-1$
				continue;
			}

			// Read out the value.
			final String value = parameterMemento.getString(ATT_VALUE);
			if ((value == null) || (value.isEmpty())) {
				// The name should never be null. This is invalid.
				addWarning(warningsToLog, "Parameters need a value", id); //$NON-NLS-1$
				continue;
			}

			parameters.add(new Parameterization(parameter, value));
		}

		if (parameters.isEmpty()) {
			return new ParameterizedCommand(command, null);
		}

		return new ParameterizedCommand(command, parameters.toArray(new Parameterization[parameters.size()]));
	}

	/**
	 * Reads a required attribute from the memento.
	 *
	 * @param memento       The memento from which to read; must not be
	 *                      <code>null</code>.
	 * @param attribute     The attribute to read; must not be <code>null</code>.
	 * @param warningsToLog The list of warnings; must not be <code>null</code>.
	 * @param message       The warning message to use if the attribute is missing;
	 *                      must not be <code>null</code>.
	 * @return The required attribute; may be <code>null</code> if missing.
	 */
	protected static final String readRequired(final IMemento memento, final String attribute,
			final List<IStatus> warningsToLog, final String message) {
		return readRequired(memento, attribute, warningsToLog, message, null);
	}

	/**
	 * Reads a required attribute from the memento. This logs the identifier of the
	 * item if this required element cannot be found.
	 *
	 * @param memento       The memento from which to read; must not be
	 *                      <code>null</code>.
	 * @param attribute     The attribute to read; must not be <code>null</code>.
	 * @param warningsToLog The list of warnings; must not be <code>null</code>.
	 * @param message       The warning message to use if the attribute is missing;
	 *                      must not be <code>null</code>.
	 * @param id            The identifier of the element for which this is a
	 *                      required attribute; may be <code>null</code>.
	 * @return The required attribute; may be <code>null</code> if missing.
	 */
	protected static final String readRequired(final IMemento memento, final String attribute,
			final List<IStatus> warningsToLog, final String message, final String id) {
		final String value = memento.getString(attribute);
		if ((value == null) || (value.isEmpty())) {
			addWarning(warningsToLog, message, id);
			return null;
		}

		return value;
	}

	/**
	 * Whether the preference and registry change listeners have been attached yet.
	 */
	protected boolean preferenceListenerAttached = false;

	/**
	 * The registry change listener for this class.
	 */
	private final IPropertyChangeListener preferenceChangeListener;

	/**
	 * Detaches the preference change listener from the registry.
	 */
	@Override
	public final void dispose() {
		super.dispose();

		final IPreferenceStore store = WorkbenchPlugin.getDefault().getPreferenceStore();
		store.removePropertyChangeListener(preferenceChangeListener);
	}

	/**
	 * Checks whether the preference change could affect this persistence class.
	 *
	 * @param event The event indicating the preference change; must not be
	 *              <code>null</code>.
	 * @return <code>true</code> if the persistence instance is affected by this
	 *         change; <code>false</code> otherwise.
	 */
	protected abstract boolean isChangeImportant(final PropertyChangeEvent event);

	/**
	 * Reads the various elements from the registry. Subclasses should extend, but
	 * must not override.
	 */
	@Override
	protected void read() {
		super.read();

		if (!preferenceListenerAttached) {
			final IPreferenceStore store = WorkbenchPlugin.getDefault().getPreferenceStore();
			store.addPropertyChangeListener(preferenceChangeListener);
		}
	}

	/**
	 * Constructs a new instance of {@link PreferencePersistence}. A preference
	 * change listener is created.
	 */
	protected PreferencePersistence() {
		super();

		preferenceChangeListener = event -> {
			if (isChangeImportant(event)) {
				read();
			}
		};
	}
}
