/*******************************************************************************
 * Copyright (c) 2007, 2016 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
 *     Ericsson			  - Modified for caching commands corresponding to multiple execution contexts
 *******************************************************************************/

package org.eclipse.cdt.dsf.debug.service.command;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.DsfRunnable;
import org.eclipse.cdt.dsf.concurrent.IDsfStatusConstants;
import org.eclipse.cdt.dsf.concurrent.ImmediateExecutor;
import org.eclipse.cdt.dsf.datamodel.DMContexts;
import org.eclipse.cdt.dsf.datamodel.IDMContext;
import org.eclipse.cdt.dsf.internal.DsfPlugin;
import org.eclipse.cdt.dsf.service.DsfSession;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;

/**
 * This is a utility class for caching results of commands--typically commands
 * sent to an external engine via some protocol (GDB/MI, e.g.). Commands are
 * sent to their intended target through the supplied {@link ICommandControl},
 * and their results are cached so as to quickly service future identical
 * commands.
 *
 * The cache has to be reset by the client that owns and uses the cache. A reset
 * should be done when an event occurs that alters the state of a context such
 * that commands on that context may no longer yield the same outcome as they
 * did before the event. A reset can be done on the entire cache or on a per
 * context basis.
 *
 * @since 1.0
 */

public class CommandCache implements ICommandListener {
	static enum CommandStyle {
		COALESCED, NONCOALESCED
	}

	/**
	 * Holds cache information for a given command.
	 * @param <V> Type matches the result type associated with the command.
	 */
	class CommandInfo {

		/*
		 *  Control variables.
		 */

		/** List of the request monitors associated with this command */
		private final List<DataRequestMonitor<ICommandResult>> fCurrentRequestMonitors;

		/** Original command. Need for reference from Queue completion notification */
		private final ICommand<ICommandResult> fCommand;

		/** Style of this command ( internal coalesced or not) */
		private final CommandStyle fCmdStyle;

		/** Command being processed for this command */
		private CommandInfo fCoalescedCmd;

		private ICommandToken fToken;

		public CommandInfo(CommandStyle cmdstyle, ICommand<ICommandResult> cmd, DataRequestMonitor<ICommandResult> rm) {
			fCmdStyle = cmdstyle;
			fCommand = cmd;
			fCurrentRequestMonitors = new LinkedList<>();
			fCurrentRequestMonitors.add(rm);
			fCoalescedCmd = null;
		}

		public CommandStyle getCommandstyle() {
			return fCmdStyle;
		}

		public List<DataRequestMonitor<ICommandResult>> getRequestMonitorList() {
			return fCurrentRequestMonitors;
		}

		public ICommand<ICommandResult> getCommand() {
			return fCommand;
		}

		public CommandInfo getCoalescedCmd() {
			return fCoalescedCmd;
		}

		public void setCoalescedCmd(CommandInfo cmd) {
			fCoalescedCmd = cmd;
		}

		@Override
		public boolean equals(Object other) {
			if (!(other instanceof CommandInfo))
				return false;
			CommandInfo otherCmd = (CommandInfo) other;

			return otherCmd.fCommand.equals(fCommand);
		}

		@Override
		public int hashCode() {
			return fCommand.hashCode();
		}
	}

	class CommandResultInfo {
		private final ICommandResult fData;
		private final IStatus fStatus;

		public CommandResultInfo(ICommandResult data, IStatus status) {
			fData = data;
			fStatus = status;
		}

		public ICommandResult getData() {
			return fData;
		}

		public IStatus getStatus() {
			return fStatus;
		}
	}

	private DsfSession fSession;

	/**
	 * The command control to be used to send commands to the backend.
	 * In certain cases, a {@link BufferedCommandControl} should be used
	 * to artificially delay the processing of command results; these
	 * artificial delays are important to keep events and command results
	 * ordered as received by the backend.
	 */
	private ICommandControl fCommandControl;

	/*
	 *  This class contains 5 significant lists.
	 *
	 *  Cached Results :
	 *
	 *      Contains a mapping of commands and their completed results. Until the cached
	 *      results are cleared by the owner of the cache.
	 *
	 *  Pending Commands Not Queued :
	 *
	 *      The Control object has not yet indicated that it has recognized the command
	 *      yet. The user is not allowed to interrogate these objects until the Control
	 *      object indicates they have been queued ( commandQueued notification ).
	 *
	 *  Pending Commands Unsent :
	 *
	 *      This is the list of commands  which have been  issued to the Control object but
	 *      have not been actually issued to the backend. These commands represent coalesce
	 *      options.  They may be compared against the Queued list  being maintained by the
	 *      Control object until told otherwise - commandSent notification ).
	 *
	 *  Pending Commands Sent :
	 *
	 *      This is a list  of commands which have been issued to the Control object and
	 *      have also been sent to the backend. It is not possible use these objects for
	 *      coalescents.
	 *
	 *  Coalesced Pending Q :
	 *
	 *      These represent original commands  for which a new coalesced command has been
	 *      created. When the coalesced commands completes the results will be decomposed
	 *      when back into individual results from this command.
	 */
	private Set<IDMContext> fAvailableContexts = new HashSet<>();

	private Map<IDMContext, HashMap<CommandInfo, CommandResultInfo>> fCachedContexts = new HashMap<>();

	private ArrayList<CommandInfo> fPendingQCommandsSent = new ArrayList<>();

	private ArrayList<CommandInfo> fPendingQCommandsNotYetSent = new ArrayList<>();

	private ArrayList<CommandInfo> fPendingQWaitingForCoalescedCompletion = new ArrayList<>();

	private static boolean DEBUG = false;
	private static final String CACHE_TRACE_IDENTIFIER = " [CHE]"; //$NON-NLS-1$
	private static String BLANK_CACHE_TRACE_IDENTIFIER = ""; //$NON-NLS-1$
	static {
		DEBUG = Boolean.parseBoolean(Platform.getDebugOption("org.eclipse.cdt.dsf/debugCache")); //$NON-NLS-1$
		for (int i = 0; i < CACHE_TRACE_IDENTIFIER.length(); i++) {
			BLANK_CACHE_TRACE_IDENTIFIER += " "; //$NON-NLS-1$
		}
	}

	private void debug(String message) {
		debug(message, ""); //$NON-NLS-1$
	}

	private void debug(String message, String prefix) {
		if (DEBUG) {
			// The message can span more than one line
			String[] multiLine = message.split("\n"); //$NON-NLS-1$

			// Create a blank prefix for proper alignment
			String blankPrefix = ""; //$NON-NLS-1$
			for (int i = 0; i < prefix.length(); i++) {
				blankPrefix += " "; //$NON-NLS-1$
			}

			for (int i = 0; i < multiLine.length; i++) {
				String traceIdentifier;
				if (i == 0) {
					// For the first line we prepend the cache identifier string
					traceIdentifier = CACHE_TRACE_IDENTIFIER + prefix;

				} else {
					// For all other lines we prepend a blank prefix for proper alignment
					traceIdentifier = BLANK_CACHE_TRACE_IDENTIFIER + blankPrefix;
				}

				message = DsfPlugin.getDebugTime() + traceIdentifier + " " + multiLine[i]; //$NON-NLS-1$

				// Make sure our lines are not too long
				while (message.length() > 100) {
					String partial = message.substring(0, 100) + "\\"; //$NON-NLS-1$
					message = message.substring(100);
					System.out.println(partial);
				}
				System.out.println(message);
			}
		}
	}

	public CommandCache(DsfSession session, ICommandControl control) {
		fSession = session;
		fCommandControl = control;

		/*
		 * We listen for the notifications that the commands have been sent to
		 * their intended target via the ICommandControl service.
		 */
		fCommandControl.addCommandListener(this);
	}

	/*
	 * Constructs a coalesced command if possible.
	 */
	private CommandInfo getCoalescedCommand(CommandInfo cmd) {

		for (CommandInfo currentUnsentEntry : new ArrayList<>(fPendingQCommandsNotYetSent)) {
			/*
			 *  Get the current unsent entry to determine if we can coalesced with it.
			 */
			ICommand<?> unsentCommand = currentUnsentEntry.getCommand();

			/*
			 * Check if we can so construct a new COALESCED command from scratch.
			 */

			// For sanity's sake, cast the generic ?'s to concrete types in the cache implementation.
			@SuppressWarnings("unchecked")
			ICommand<ICommandResult> coalescedCmd = (ICommand<ICommandResult>) unsentCommand
					.coalesceWith(cmd.getCommand());

			if (coalescedCmd != null) {
				CommandInfo coalescedCmdInfo = new CommandInfo(CommandStyle.COALESCED, coalescedCmd, null);

				if (currentUnsentEntry.getCommandstyle() == CommandStyle.COALESCED) {
					/*
					 *  We matched a command which is itself already a COALESCED command.  So
					 *  we need to run through  the reference list  and point all the current
					 *  command which are referencing the command we just subsumed and change
					 *  them to point to the new super command.
					 */

					for (CommandInfo waitingEntry : new ArrayList<>(fPendingQWaitingForCoalescedCompletion)) {

						if (waitingEntry.getCoalescedCmd() == currentUnsentEntry) {
							/*
							 *  This referenced the old command change it to point to the new one.
							 */
							waitingEntry.setCoalescedCmd(coalescedCmdInfo);
						}
					}
				} else {
					/*
					 *  This currently unsent entry needs to go into the coalescing list. To
					 *  be completed when the coalesced command comes back with a result.
					 */
					fPendingQWaitingForCoalescedCompletion.add(currentUnsentEntry);
					currentUnsentEntry.setCoalescedCmd(coalescedCmdInfo);
				}

				/*
				 *  Either way we want to take the command back from the Control object so it
				 *  does not continue to process it.
				 */
				fPendingQCommandsNotYetSent.remove(currentUnsentEntry);
				fCommandControl.removeCommand(currentUnsentEntry.fToken);

				return (coalescedCmdInfo);
			}
		}

		return null;
	}

	/**
	 * Executes given ICommand, or retrieves the cached result if known.
	 * @param command Command to execute.
	 * @param rm Return token, contains the retrieved MIInfo object as
	 * well as its cache status.
	 */
	public <V extends ICommandResult> void execute(ICommand<V> command, DataRequestMonitor<V> rm) {
		assert fSession.getExecutor().isInExecutorThread();

		// Cast the generic ?'s to concrete types in the cache implementation.
		@SuppressWarnings("unchecked")
		final ICommand<ICommandResult> genericCommand = (ICommand<ICommandResult>) command;
		@SuppressWarnings("unchecked")
		final DataRequestMonitor<ICommandResult> genericDone = (DataRequestMonitor<ICommandResult>) rm;

		CommandInfo cachedCmd = new CommandInfo(CommandStyle.NONCOALESCED, genericCommand, genericDone);

		final IDMContext context = genericCommand.getContext();

		/*
		 * If command is already cached, just return the cached data.
		 */
		if (fCachedContexts.get(context) != null && fCachedContexts.get(context).containsKey(cachedCmd)) {
			CommandResultInfo result = fCachedContexts.get(context).get(cachedCmd);
			debug(command.toString().trim());
			if (result.getStatus().getSeverity() <= IStatus.INFO) {
				@SuppressWarnings("unchecked")
				V v = (V) result.getData();
				rm.setData(v);
				debug(v.toString());
			} else {
				rm.setStatus(result.getStatus());
				debug(result.getStatus().toString());
			}
			rm.done();
			return;
		}

		/*
		 *  Return an error if the target is available anymore.
		 */
		if (!isTargetAvailable(command.getContext())) {
			debug(command.toString().trim(), "[N/A]"); //$NON-NLS-1$

			rm.setStatus(new Status(IStatus.ERROR, DsfPlugin.PLUGIN_ID, IDsfStatusConstants.INVALID_STATE,
					"Target not available.", null)); //$NON-NLS-1$
			rm.done();
			return;
		}

		/*
		 *  If we are already waiting for this command to complete,
		 *  add this request monitor to list of waiting monitors.
		 */
		for (CommandInfo sentCommand : fPendingQCommandsSent) {
			if (sentCommand.equals(cachedCmd)) {
				sentCommand.getRequestMonitorList().add(genericDone);
				debug(command.toString().trim(), "[SNT]"); //$NON-NLS-1$
				return;
			}
		}
		for (CommandInfo notYetSentCommand : fPendingQCommandsNotYetSent) {
			if (notYetSentCommand.equals(cachedCmd)) {
				notYetSentCommand.getRequestMonitorList().add(genericDone);
				debug(command.toString().trim(), "[SND]"); //$NON-NLS-1$
				return;
			}
		}

		/*
		 *  We see if this command can be combined into a coalesced one. The
		 *  coalesce routine will take care of the already enqueued one which
		 *  this command is being coalesced with.
		 */

		CommandInfo coalescedCmd = getCoalescedCommand(cachedCmd);

		if (coalescedCmd != null) {
			/*
			 *  The original command we were handed needs to go into the waiting QUEUE.
			 *  We also need to point it it to the coalesced command.
			 */
			fPendingQWaitingForCoalescedCompletion.add(cachedCmd);
			cachedCmd.setCoalescedCmd(coalescedCmd);
			cachedCmd = coalescedCmd;
		}

		/*
		 *  Now we have a command to send ( coalesced or not ).  Put it in the cannot touch
		 *  it list and give it to the Control object. Our state handlers will move it into
		 *  the proper list as the Control object deals with it.
		 */
		final CommandInfo finalCachedCmd = cachedCmd;
		fPendingQCommandsNotYetSent.add(finalCachedCmd);

		finalCachedCmd.fToken = fCommandControl.queueCommand(finalCachedCmd.getCommand(),
				new DataRequestMonitor<ICommandResult>(ImmediateExecutor.getInstance(), null) {
					@Override
					public synchronized void done() {
						// protect against the cache being called in non-session thread, but at
						// the same time avoid adding extra dispatch cycles to command processing.
						if (fSession.getExecutor().isInExecutorThread()) {

							super.done();
						} else {
							fSession.getExecutor().execute(new DsfRunnable() {
								@Override
								public void run() {
									superDone();
								}
							});
						}
					}

					private void superDone() {
						super.done();
					}

					@Override
					public void handleCompleted() {

						/*
						 *  Match this up with a command set we know about.
						 */
						if (!fPendingQCommandsSent.remove(finalCachedCmd)) {
							/*
							 * This can happen if the call to queueCommand() completes without
							 * actually having to send the command.  In that case, we should find
							 * our command in the fPendingQCommandsNotYetSent queue.
							 * For example, upon termination some queueCommand() implementation
							 * may complete immediately with an error status. Bug 309309
							 */
							if (!fPendingQCommandsNotYetSent.remove(finalCachedCmd)) {
								assert false : "Missing command"; //$NON-NLS-1$
								return;
							}
						}

						ICommandResult result = getData();
						IStatus status = getStatus();

						if (finalCachedCmd.getCommandstyle() == CommandStyle.COALESCED) {
							/*
							 *  We matched a command which is itself  already a COALESCED command.  So
							 *  we need to go through the list of unsent commands which were not sent
							 *  because  the coalesced command represented it.  For each match we find
							 *  we create a new result from the coalesced command for it.
							 */

							for (CommandInfo waitingEntry : new ArrayList<>(fPendingQWaitingForCoalescedCompletion)) {

								if (waitingEntry.getCoalescedCmd() == finalCachedCmd) {

									/*
									 *  Remove this entry from the list since we can complete it.
									 */
									fPendingQWaitingForCoalescedCompletion.remove(waitingEntry);

									// Cast the calculated result back to the requested type.
									@SuppressWarnings("unchecked")
									V subResult = (V) result.getSubsetResult(waitingEntry.getCommand());
									CommandResultInfo subResultInfo = new CommandResultInfo(subResult, status);

									if (fCachedContexts.get(context) != null) {
										fCachedContexts.get(context).put(waitingEntry, subResultInfo);
									} else {
										HashMap<CommandInfo, CommandResultInfo> map = new HashMap<>();
										map.put(waitingEntry, subResultInfo);
										fCachedContexts.put(context, map);
									}

									if (!isSuccess()) {

										/*
										 *  We had some form of error with the original command. So notify the
										 *  original requesters of the issues.
										 */
										for (DataRequestMonitor<?> pendingRM : waitingEntry.getRequestMonitorList()) {
											pendingRM.setStatus(status);
											pendingRM.done();
										}
									} else {
										assert subResult != null;

										/*
										 *  Notify the original requesters of the positive results.
										 */
										for (DataRequestMonitor<? extends ICommandResult> pendingRM : waitingEntry
												.getRequestMonitorList()) {
											// Cast the pending return token to match the requested type.
											@SuppressWarnings("unchecked")
											DataRequestMonitor<V> vPendingRM = (DataRequestMonitor<V>) pendingRM;

											vPendingRM.setData(subResult);
											vPendingRM.done();
										}
									}
								}
							}
						} else {
							// Save the command result in cache, but only if the command's context
							// is still available.  Otherwise an error may get cached incorrectly.
							if (isTargetAvailable(context)) {
								CommandResultInfo resultInfo = new CommandResultInfo(result, status);

								if (fCachedContexts.get(context) != null) {
									fCachedContexts.get(context).put(finalCachedCmd, resultInfo);
								} else {
									HashMap<CommandInfo, CommandResultInfo> map = new HashMap<>();
									map.put(finalCachedCmd, resultInfo);
									fCachedContexts.put(context, map);
								}
							}
							// This is an original request which completed. Indicate success or
							// failure to the original requesters.
							if (!isSuccess()) {
								/*
								 *  We had some form of error with the original command. So notify the
								 *  original requesters of the issues.
								 */
								for (DataRequestMonitor<?> pendingRM : finalCachedCmd.getRequestMonitorList()) {
									pendingRM.setStatus(status);
									pendingRM.done();
								}
							} else {
								// Cast the calculated result back to the requested type.
								@SuppressWarnings("unchecked")
								V vResult = (V) result;

								for (DataRequestMonitor<? extends ICommandResult> pendingRM : finalCachedCmd
										.getRequestMonitorList()) {
									// Cast the pending return token to match the requested type.
									@SuppressWarnings("unchecked")
									DataRequestMonitor<V> vPendingRM = (DataRequestMonitor<V>) pendingRM;

									vPendingRM.setData(vResult);
									vPendingRM.done();
								}
							}
						}
					}
				});
	}

	/**
	 * TODO
	 */
	public void setContextAvailable(IDMContext context, boolean isAvailable) {
		if (isAvailable) {
			fAvailableContexts.add(context);
		} else {
			fAvailableContexts.remove(context);
			for (Iterator<IDMContext> itr = fAvailableContexts.iterator(); itr.hasNext();) {
				if (DMContexts.isAncestorOf(context, itr.next())) {
					itr.remove();
				}
			}
		}
	}

	/**
	 * TODO
	 * @see #setContextAvailable(IDMContext, boolean)
	 */
	public boolean isTargetAvailable(IDMContext context) {
		for (IDMContext availableContext : fAvailableContexts) {
			if (context.equals(availableContext) || DMContexts.isAncestorOf(context, availableContext)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Clears all the cache data. Equivalent to <code>reset(null)</code>.
	 */
	public void reset() {
		fCachedContexts.clear();
	}

	@Override
	public void commandRemoved(ICommandToken token) {
		/*
		 *  Do nothing.
		 */
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.dsf.debug.service.command.ICommandListener#commandQueued(org.eclipse.cdt.dsf.debug.service.command.ICommandToken)
	 */
	@Override
	public void commandQueued(ICommandToken token) {
		/*
		 *  Do nothing.
		 */
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.dsf.debug.service.command.ICommandListener#commandDone(org.eclipse.cdt.dsf.debug.service.command.ICommandToken, org.eclipse.cdt.dsf.debug.service.command.ICommandResult)
	 */
	@Override
	public void commandDone(ICommandToken token, ICommandResult result) {
		/*
		 *  We handle the done with a runnable where we initiated the command
		 *  so there is nothing to do here.
		 */
	}

	/*
	 * Move the command into our internal sent list. This means we can no longer look at
	 * this command for possible coalescence since it has been given to the debug engine
	 * and is currently being processed.
	 *
	 * @see org.eclipse.cdt.dsf.debug.service.command.ICommandListener#commandSent(org.eclipse.cdt.dsf.debug.service.command.ICommandToken)
	 */
	@Override
	public void commandSent(ICommandToken token) {

		// Cast the generic ?'s to concrete types in the cache implementation.
		@SuppressWarnings("unchecked")
		ICommand<ICommandResult> genericCommand = (ICommand<ICommandResult>) token.getCommand();

		CommandInfo cachedCmd = new CommandInfo(CommandStyle.NONCOALESCED, genericCommand, null);

		// It is important to actually fetch the content of the fPendingQCommandsNotYetSent map
		// instead of only using 'cachedCmd'.  This is because although cachedCmd can be considered
		// equal to unqueuedCommand, it is not identical and we need the full content of unqueuedCommand.
		// For instance, cachedCmd does not have the list of requestMonitors that unqueuedCommand has.
		for (CommandInfo unqueuedCommand : new ArrayList<>(fPendingQCommandsNotYetSent)) {
			if (unqueuedCommand.equals(cachedCmd)) {
				fPendingQCommandsNotYetSent.remove(unqueuedCommand);
				fPendingQCommandsSent.add(unqueuedCommand);
				break;
			}
		}
	}

	/**
	 * Clears the cache entries for given context.  Clears the whole cache if
	 * context parameter is null.
	 */
	public void reset(IDMContext dmc) {
		if (dmc == null) {
			fCachedContexts.clear();
			return;
		}
		for (Iterator<IDMContext> itr = fCachedContexts.keySet().iterator(); itr.hasNext();) {
			IDMContext keyDmc = itr.next();
			if (keyDmc != null && (dmc.equals(keyDmc) || DMContexts.isAncestorOf(keyDmc, dmc))) {
				itr.remove();
			}
		}
	}
}
