/*******************************************************************************
 * Copyright (c) 2010 Wind River Systems 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:
 *     Wind River Systems - initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.dsf.concurrent;

import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.cdt.dsf.internal.DsfPlugin;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;

/**
 * A type of {@link Sequence} which uses reflection and annotations to
 * declare its different {@link Sequence.Step}.  It can be used to make
 * larger DSF sequences more readable and easier to override.
 *
 * The order of execution of the {@code @Execute} methods is determined by
 * the {@link #getExecutionOrder()} method.
 *
 * {@code @Execute} methods can be grouped in a hierarchical set of groups,
 * which should be included in the result of {@link #getExecutionOrder()}.
 * Using groups can make overriding slightly simpler.
 *
 * A usage example follows: <code><pre>
 *    public class MyReflectionSequence extends ReflectionSequence {
 *
 *        public MyReflectionSequence(DsfExecutor executor) {
 *            super(executor);
 *        }
 *
 *        protected static final String GROUP_INIT = "GROUP_INIT";
 *
 *        {@code @Override}
 *        protected String[] getExecutionOrder(String group) {
 *           if (GROUP_TOP_LEVEL.equals(group)) {
 *               // This is the top level group which contains
 *               // all sub-groups, or steps that are not in
 *               // other groups.
 *               return new String[] { GROUP_INIT, "step3", "step4" };
 *           }
 *
 *           // Now deal with the content of sub-groups
 *           if (GROUP_INIT.equals(group)) {
 *               return new String[] { "step1", "step2" };
 *           }
 *
 *           // An invalid group was requested
 *           return null;
 *        }
 *
 *        {@code @Execute}
 *        public void step1(RequestMonitor rm) {
 *            // Do something
 *            rm.done();
 *        }
 *
 *        {@code @RollBack("step1")}
 *        public void rollBack1(RequestMonitor rm) {
 *        	// Rollback what was done in step1()
 *        	rm.done();
 *        }
 *
 *        {@code @Execute}
 *        public void step2(RequestMonitor rm) {
 *            // Do something else
 *            rm.done();
 *        }
 *
 *        {@code @Execute}
 *        public void step3(RequestMonitor rm) {
 *            // Do something else
 *            rm.done();
 *        }
 *
 *        {@code @Execute}
 *        public void step4(RequestMonitor rm) {
 *            // Do something else
 *            rm.done();
 *        }
 *    }
 * </pre></code>
 *
 * @since 2.2
 */
abstract public class ReflectionSequence extends Sequence {

	/**
	 * The top-level group in which all sub-groups or steps that
	 * are not part of any sub-groups are contained.  This group
	 * identifier is the one that will be used in the first call to
	 * {@link #getExecutionOrder()}.
	 */
	public static final String GROUP_TOP_LEVEL = "GROUP_TOP_LEVEL"; //$NON-NLS-1$

	private Step[] fReflectionSteps;

	/**
	 * Annotation used to indicate that a method corresponds to an
	 * {@link Sequence.Step#execute()} method of a {@link Sequence.Step}.
	 * The annotated method must be declared public.
	 */
	@Inherited
	@Retention(RetentionPolicy.RUNTIME)
	@Target(ElementType.METHOD)
	public static @interface Execute {
	}

	/**
	 * Annotation used to indicate that a method corresponds to a
	 * {@link Sequence.Step#rollBack()} method of a {@link Sequence.Step}.
	 * Declaring such a method is optional.  If declared, the annotated
	 * method must be declared public.
	 */
	@Inherited
	@Retention(RetentionPolicy.RUNTIME)
	@Target(ElementType.METHOD)
	public static @interface RollBack {
		/**
		 * Name of the method tagged with the {@link Execute} annotation that this method rolls back.
		 */
		String value();
	}

	private class ReflectionStep extends Step {
		final private Method fExecuteMethod;
		final private Method fRollbackMethod;

		private ReflectionStep(Method executeMethod, Method rollbackMethod) {
			assert executeMethod != null;

			fExecuteMethod = executeMethod;

			fRollbackMethod = rollbackMethod;
		}

		@Override
		public void execute(RequestMonitor rm) {
			try {
				fExecuteMethod.invoke(ReflectionSequence.this, rm);
			} catch (Exception e) {
				rm.setStatus(new Status(IStatus.ERROR, DsfPlugin.PLUGIN_ID, IDsfStatusConstants.INTERNAL_ERROR,
						"Error executing step execute method: " + fExecuteMethod.getName(), e)); //$NON-NLS-1$
				rm.done();
			}
		}

		@Override
		public void rollBack(RequestMonitor rm) {
			if (fRollbackMethod == null) {
				super.rollBack(rm);
			} else {
				try {
					fRollbackMethod.invoke(ReflectionSequence.this, rm);
				} catch (Exception e) {
					rm.setStatus(new Status(IStatus.ERROR, DsfPlugin.PLUGIN_ID, IDsfStatusConstants.INTERNAL_ERROR,
							"Error executing step rollback method: " + fRollbackMethod.getName(), e)); //$NON-NLS-1$
					rm.done();
				}
			}
		}
	}

	public ReflectionSequence(DsfExecutor executor) {
		super(executor);
	}

	public ReflectionSequence(DsfExecutor executor, RequestMonitor rm) {
		super(executor, rm);
	}

	public ReflectionSequence(DsfExecutor executor, IProgressMonitor pm, String taskName, String rollbackTaskName) {
		super(executor, pm, taskName, rollbackTaskName);
	}

	public ReflectionSequence(DsfExecutor executor, RequestMonitorWithProgress rm, String taskName,
			String rollbackTaskName) {
		super(executor, rm, taskName, rollbackTaskName);
	}

	/**
	 * This method must return the execution order of {@code @Execute} methods and/or groups.
	 *
	 * @param groupName The name of a group for which the list of {@code @Execute} methods
	 *                  or sub-groups should be returned in the order they should be executed.
	 *                  If the concept of groups is not used, then this parameter can be ignored,
	 *                  at the top-level ordering should be returned.
	 *
	 * @return An array containing the list of @Execute methods and groups in the order
	 *         they should be executed, or null if the specified groupName is unknown.
	 */
	abstract protected String[] getExecutionOrder(String groupName);

	@Override
	public Step[] getSteps() {
		if (fReflectionSteps == null) {
			Map<String, Method> executeMethods = getAnnotatedMethods(Execute.class);
			Map<String, Method> rollBackMethods = getAnnotatedMethods(RollBack.class);
			List<Step> steps = getGroupSteps(GROUP_TOP_LEVEL, executeMethods, rollBackMethods);
			fReflectionSteps = steps.toArray(new ReflectionStep[steps.size()]);
		}
		return fReflectionSteps;
	}

	private List<Step> getGroupSteps(String groupId, Map<String, Method> executeMethods,
			Map<String, Method> rollBackMethods) {
		List<Step> steps = new ArrayList<>(executeMethods.size());

		String[] order = getExecutionOrder(groupId);
		if (order == null) {
			throw new RuntimeException("Unknown group in sequence: " + groupId); //$NON-NLS-1$
		}

		for (String name : order) {
			Method executeMethod = executeMethods.get(name);
			if (executeMethod == null) {
				// name is a group id
				steps.addAll(getGroupSteps(name, executeMethods, rollBackMethods));
			} else {
				steps.add(new ReflectionStep(executeMethod, rollBackMethods.get(executeMethod.getName())));
			}
		}
		return steps;
	}

	private Map<String, Method> getAnnotatedMethods(Class<? extends Annotation> annotationType) {
		Map<String, Method> retVal = new HashMap<>();
		try {
			Method[] methods = getClass().getMethods();
			for (Method method : methods) {
				if (method.isAnnotationPresent(annotationType)) {
					Class<?>[] paramTypes = method.getParameterTypes();
					if (paramTypes.length != 1) { // must have one and only param, the RequestMonitor
						throw new IllegalArgumentException("Method " + //$NON-NLS-1$
								method.getDeclaringClass().getSimpleName() + "#" + method.getName() + //$NON-NLS-1$
								" must have a single parameter"); //$NON-NLS-1$
					} else {
						if (annotationType.equals(Execute.class)) {
							retVal.put(method.getName(), method);
						} else {// @Rollback
							retVal.put(method.getAnnotation(RollBack.class).value(), method);
						}
					}
				}
			}
		} catch (SecurityException e) {
			throw new IllegalArgumentException("No permission to access ReflectionSequence method"); //$NON-NLS-1$
		}
		return retVal;
	}

}
