/*******************************************************************************
 *  Copyright (c) 2007, 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
 *     Landmark Graphics Corporation - bug 397183
 *******************************************************************************/
package org.eclipse.equinox.internal.p2.engine;

import java.util.*;
import org.eclipse.core.runtime.*;
import org.eclipse.equinox.internal.p2.core.helpers.LogHelper;
import org.eclipse.equinox.internal.provisional.p2.core.eventbus.IProvisioningEventBus;
import org.eclipse.equinox.p2.engine.IProfile;
import org.eclipse.equinox.p2.engine.ProvisioningContext;
import org.eclipse.equinox.p2.engine.spi.ProvisioningAction;
import org.eclipse.equinox.p2.engine.spi.Touchpoint;
import org.eclipse.osgi.util.NLS;

public abstract class Phase {
	protected static final String PARM_OPERAND = "operand"; //$NON-NLS-1$
	protected static final String PARM_PHASE_ID = "phaseId"; //$NON-NLS-1$
	protected static final String PARM_PROFILE = "profile"; //$NON-NLS-1$
	protected static final String PARM_PROFILE_DATA_DIRECTORY = "profileDataDirectory"; //$NON-NLS-1$
	protected static final String PARM_CONTEXT = "context"; //$NON-NLS-1$
	/**
	 * Internal property.
	 */
	protected static final String PARM_AGENT = "agent"; //$NON-NLS-1$
	protected static final String PARM_FORCED = "forced"; //$NON-NLS-1$
	protected static final String PARM_TOUCHPOINT = "touchpoint"; //$NON-NLS-1$
	protected static final String LAST_RESULT_INTERNAL_NAME = "_p2_internal_last_result_variable_"; //$NON-NLS-1$ //
	protected static final String LAST_RESULT_PUBLIC_NAME = "lastResult"; //$NON-NLS-1$

	protected final String phaseId;
	protected final int weight;
	protected final boolean forced;
	protected int prePerformWork = 1000;
	protected int mainPerformWork = 10000;
	protected int postPerformWork = 1000;
	private Map<String, Object> operandParameters = null;
	private Map<String, Object> phaseParameters = new HashMap<>();
	private Map<Touchpoint, Map<String, Object>> touchpointToTouchpointPhaseParameters = new HashMap<>();
	private Map<Touchpoint, Map<String, Object>> touchpointToTouchpointOperandParameters = new HashMap<>();
	ActionManager actionManager; // injected from phaseset
	protected boolean isPaused = false;

	protected Phase(String phaseId, int weight, boolean forced) {
		if (phaseId == null || phaseId.length() == 0)
			throw new IllegalArgumentException(Messages.phaseid_not_set);
		if (weight <= 0)
			throw new IllegalArgumentException(Messages.phaseid_not_positive);
		this.weight = weight;
		this.phaseId = phaseId;
		this.forced = forced;
	}

	protected Phase(String phaseId, int weight) {
		this(phaseId, weight, false);
	}

	final protected ActionManager getActionManager() {
		return actionManager;
	}

	@Override
	public String toString() {
		return getClass().getName() + " - " + this.weight; //$NON-NLS-1$
	}

	private void broadcastPhaseEvent(EngineSession session, Operand[] operands, int type) {
		IProvisioningEventBus bus = session.getAgent().getService(IProvisioningEventBus.class);
		if (bus != null) {
			bus.publishEvent(getPhaseEvent(operands, type));
		}
	}

	protected PhaseEvent getPhaseEvent(final Operand[] operands, int type) {
		return new PhaseEvent(phaseId, operands, type);
	}

	void perform(MultiStatus status, EngineSession session, Operand[] operands, IProgressMonitor monitor) {
		SubMonitor subMonitor = SubMonitor.convert(monitor, prePerformWork + mainPerformWork + postPerformWork);
		session.recordPhaseEnter(this);
		broadcastPhaseEvent(session, operands, PhaseEvent.TYPE_START);
		prePerform(status, session, subMonitor.newChild(prePerformWork));
		if (status.matches(IStatus.ERROR | IStatus.CANCEL))
			return;
		session.recordPhaseStart(this);

		subMonitor.setWorkRemaining(mainPerformWork + postPerformWork);
		mainPerform(status, session, operands, subMonitor.newChild(mainPerformWork));
		if (status.matches(IStatus.ERROR | IStatus.CANCEL))
			return;

		session.recordPhaseEnd(this);
		subMonitor.setWorkRemaining(postPerformWork);
		postPerform(status, session, subMonitor.newChild(postPerformWork));
		phaseParameters.clear();
		if (status.matches(IStatus.ERROR | IStatus.CANCEL))
			return;
		broadcastPhaseEvent(session, operands, PhaseEvent.TYPE_END);
		session.recordPhaseExit(this);
		subMonitor.done();
	}

	void prePerform(MultiStatus status, EngineSession session, IProgressMonitor monitor) {
		IProfile profile = session.getProfile();
		phaseParameters.put(PARM_PROFILE, profile);
		phaseParameters.put(PARM_PROFILE_DATA_DIRECTORY, session.getProfileDataDirectory());
		phaseParameters.put(PARM_CONTEXT, session.getProvisioningContext());
		phaseParameters.put(PARM_PHASE_ID, phaseId);
		phaseParameters.put(PARM_FORCED, Boolean.toString(forced));
		phaseParameters.put(PARM_AGENT, session.getAgent());
		mergeStatus(status, initializePhase(monitor, profile, phaseParameters));
	}

	private void mainPerform(MultiStatus status, EngineSession session, Operand[] operands, SubMonitor subMonitor) {
		IProfile profile = session.getProfile();
		subMonitor.beginTask(null, operands.length);
		for (int i = 0; i < operands.length; i++) {
			subMonitor.setWorkRemaining(operands.length - i);
			if (subMonitor.isCanceled())
				throw new OperationCanceledException();
			while (isPaused) {
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					mergeStatus(status, new Status(IStatus.ERROR, EngineActivator.ID, NLS.bind(Messages.phase_thread_interrupted_error, phaseId), e));
					return;
				}
				if (subMonitor.isCanceled())
					throw new OperationCanceledException();
			}
			Operand operand = operands[i];
			if (!isApplicable(operand))
				continue;

			session.recordOperandStart(operand);
			List<ProvisioningAction> actions = getActions(operand);
			operandParameters = new HashMap<>(phaseParameters);
			operandParameters.put(PARM_OPERAND, operand);
			mergeStatus(status, initializeOperand(profile, operand, operandParameters, subMonitor));
			if (status.matches(IStatus.ERROR | IStatus.CANCEL)) {
				operandParameters = null;
				return;
			}

			Touchpoint operandTouchpoint = (Touchpoint) operandParameters.get(PARM_TOUCHPOINT);
			if (operandTouchpoint != null) {
				mergeStatus(status, initializeTouchpointParameters(profile, operand, operandTouchpoint, subMonitor));
				if (status.matches(IStatus.ERROR | IStatus.CANCEL))
					return;

				operandParameters = touchpointToTouchpointOperandParameters.get(operandTouchpoint);
			}

			if (actions != null) {
				Object lastResult = null;
				for (ProvisioningAction action : actions) {
					Map<String, Object> parameters = operandParameters;
					Touchpoint touchpoint = action.getTouchpoint();
					if (touchpoint != null) {
						mergeStatus(status, initializeTouchpointParameters(profile, operand, touchpoint, subMonitor));
						if (status.matches(IStatus.ERROR | IStatus.CANCEL))
							return;

						parameters = touchpointToTouchpointOperandParameters.get(touchpoint);
					}
					if (lastResult != null) {
						parameters = new HashMap<>(parameters);
						parameters.put(LAST_RESULT_INTERNAL_NAME, lastResult);
					}
					parameters = Collections.unmodifiableMap(parameters);

					IStatus actionStatus = null;
					try {
						session.recordActionExecute(action, parameters);
						actionStatus = action.execute(parameters);
						lastResult = action.getResult();
					} catch (RuntimeException e) {
						if (!forced)
							throw e;
						// "action.execute" calls user code and might throw an unchecked exception
						// we catch the error here to gather information on where the problem occurred.
						actionStatus = new Status(IStatus.ERROR, EngineActivator.ID, NLS.bind(Messages.forced_action_execute_error, action.getClass().getName()), e);
					} catch (LinkageError e) {
						if (!forced)
							throw e;
						// Catch linkage errors as these are generally recoverable but let other Errors propagate (see bug 222001)
						actionStatus = new Status(IStatus.ERROR, EngineActivator.ID, NLS.bind(Messages.forced_action_execute_error, action.getClass().getName()), e);
					}
					if (forced && actionStatus != null && actionStatus.matches(IStatus.ERROR)) {
						MultiStatus result = new MultiStatus(EngineActivator.ID, IStatus.ERROR, getProblemMessage(), null);
						result.add(new Status(IStatus.ERROR, EngineActivator.ID, session.getContextString(this, operand, action), null));
						LogHelper.log(result);
						actionStatus = Status.OK_STATUS;
					}
					mergeStatus(status, actionStatus);
					if (status.matches(IStatus.ERROR | IStatus.CANCEL))
						return;
				}
			}
			mergeStatus(status, touchpointCompleteOperand(profile, operand, operandParameters, subMonitor));
			mergeStatus(status, completeOperand(profile, operand, operandParameters, subMonitor));
			if (status.matches(IStatus.ERROR | IStatus.CANCEL))
				return;
			operandParameters = null;
			session.recordOperandEnd(operand);
			subMonitor.worked(1);
		}
	}

	private IStatus initializeTouchpointParameters(IProfile profile, Operand operand, Touchpoint touchpoint, IProgressMonitor monitor) {
		if (touchpointToTouchpointOperandParameters.containsKey(touchpoint))
			return Status.OK_STATUS;

		Map<String, Object> touchpointPhaseParameters = touchpointToTouchpointPhaseParameters.get(touchpoint);
		if (touchpointPhaseParameters == null) {
			touchpointPhaseParameters = new HashMap<>(phaseParameters);
			IStatus status = touchpoint.initializePhase(monitor, profile, phaseId, touchpointPhaseParameters);
			if (status != null && status.matches(IStatus.ERROR | IStatus.CANCEL))
				return status;
			touchpointToTouchpointPhaseParameters.put(touchpoint, touchpointPhaseParameters);
		}

		Map<String, Object> touchpointOperandParameters = new HashMap<>(touchpointPhaseParameters);
		touchpointOperandParameters.putAll(operandParameters);
		IStatus status = touchpoint.initializeOperand(profile, touchpointOperandParameters);
		if (status != null && status.matches(IStatus.ERROR | IStatus.CANCEL))
			return status;
		touchpointToTouchpointOperandParameters.put(touchpoint, touchpointOperandParameters);
		return Status.OK_STATUS;
	}

	/**
	 * Merges a given IStatus into a MultiStatus
	 */
	protected static void mergeStatus(MultiStatus multi, IStatus status) {
		if (status != null && !status.isOK())
			multi.merge(status);
	}

	void postPerform(MultiStatus status, EngineSession session, IProgressMonitor monitor) {
		IProfile profile = session.getProfile();
		mergeStatus(status, touchpointCompletePhase(monitor, profile, phaseParameters));
		mergeStatus(status, completePhase(monitor, profile, phaseParameters));
	}

	void undo(MultiStatus status, EngineSession session, IProfile profile, Operand operand, ProvisioningAction[] actions, ProvisioningContext context) {
		if (operandParameters == null) {
			operandParameters = new HashMap<>(phaseParameters);
			operandParameters.put(PARM_OPERAND, operand);
			mergeStatus(status, initializeOperand(profile, operand, operandParameters, new NullProgressMonitor()));
			Touchpoint operandTouchpoint = (Touchpoint) operandParameters.get(PARM_TOUCHPOINT);
			if (operandTouchpoint != null) {
				mergeStatus(status, initializeTouchpointParameters(profile, operand, operandTouchpoint, new NullProgressMonitor()));
				if (status.matches(IStatus.ERROR | IStatus.CANCEL))
					return;

				operandParameters = touchpointToTouchpointOperandParameters.get(operandTouchpoint);
			}
			operandParameters = Collections.unmodifiableMap(operandParameters);
		}
		for (ProvisioningAction action : actions) {
			Map<String, Object> parameters = operandParameters;
			Touchpoint touchpoint = action.getTouchpoint();
			if (touchpoint != null) {
				mergeStatus(status, initializeTouchpointParameters(profile, operand, touchpoint, new NullProgressMonitor()));
				if (status.matches(IStatus.ERROR))
					return;

				parameters = touchpointToTouchpointOperandParameters.get(touchpoint);
			}
			IStatus actionStatus = null;
			try {
				session.recordActionUndo(action, parameters);
				actionStatus = action.undo(parameters);
			} catch (RuntimeException e) {
				// "action.undo" calls user code and might throw an unchecked exception
				// we catch the error here to gather information on where the problem occurred.
				actionStatus = new Status(IStatus.ERROR, EngineActivator.ID, NLS.bind(Messages.action_undo_error, action.getClass().getName()), e);
			} catch (LinkageError e) {
				// Catch linkage errors as these are generally recoverable but let other Errors propagate (see bug 222001)
				actionStatus = new Status(IStatus.ERROR, EngineActivator.ID, NLS.bind(Messages.action_undo_error, action.getClass().getName()), e);
			}
			if (actionStatus != null && actionStatus.matches(IStatus.ERROR)) {
				MultiStatus result = new MultiStatus(EngineActivator.ID, IStatus.ERROR, getProblemMessage(), null);
				result.add(new Status(IStatus.ERROR, EngineActivator.ID, session.getContextString(this, operand, action), null));
				result.merge(actionStatus);
			}
		}
		mergeStatus(status, touchpointCompleteOperand(profile, operand, operandParameters, new NullProgressMonitor()));
		mergeStatus(status, completeOperand(profile, operand, operandParameters, new NullProgressMonitor()));
		operandParameters = null;
	}

	public boolean isApplicable(Operand operand) {
		return true;
	}

	protected IStatus initializePhase(IProgressMonitor monitor, IProfile profile, Map<String, Object> parameters) {
		return Status.OK_STATUS;
	}

	protected IStatus completePhase(IProgressMonitor monitor, IProfile profile, Map<String, Object> parameters) {
		return Status.OK_STATUS;
	}

	IStatus touchpointCompletePhase(IProgressMonitor monitor, IProfile profile, Map<String, Object> parameters) {
		if (touchpointToTouchpointPhaseParameters.isEmpty())
			return Status.OK_STATUS;

		MultiStatus status = new MultiStatus(EngineActivator.ID, IStatus.OK, null, null);
		for (Map.Entry<Touchpoint, Map<String, Object>> entry : touchpointToTouchpointPhaseParameters.entrySet()) {
			Touchpoint touchpoint = entry.getKey();
			Map<String, Object> touchpointParameters = entry.getValue();
			mergeStatus(status, touchpoint.completePhase(monitor, profile, phaseId, touchpointParameters));
		}
		touchpointToTouchpointPhaseParameters.clear();
		return status;
	}

	protected IStatus completeOperand(IProfile profile, Operand operand, Map<String, Object> parameters, IProgressMonitor monitor) {
		return Status.OK_STATUS;
	}

	IStatus touchpointCompleteOperand(IProfile profile, Operand operand, Map<String, Object> parameters, IProgressMonitor monitor) {
		if (touchpointToTouchpointOperandParameters.isEmpty())
			return Status.OK_STATUS;

		MultiStatus status = new MultiStatus(EngineActivator.ID, IStatus.OK, null, null);
		for (Map.Entry<Touchpoint, Map<String, Object>> entry : touchpointToTouchpointOperandParameters.entrySet()) {
			Touchpoint touchpoint = entry.getKey();
			Map<String, Object> touchpointParameters = entry.getValue();
			mergeStatus(status, touchpoint.completeOperand(profile, touchpointParameters));
		}
		touchpointToTouchpointOperandParameters.clear();
		return status;
	}

	protected IStatus initializeOperand(IProfile profile, Operand operand, Map<String, Object> parameters, IProgressMonitor monitor) {
		return Status.OK_STATUS;
	}

	protected abstract List<ProvisioningAction> getActions(Operand operand);

	/**
	 * Returns a human-readable message to be displayed in case of an error performing
	 * this phase. Subclasses should override.
	 */
	protected String getProblemMessage() {
		return NLS.bind(Messages.phase_error, getClass().getName());
	}

	protected void setPaused(boolean isPaused) {
		this.isPaused = isPaused;
	}
}
