/*******************************************************************************
 * 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
 *     Bjorn Freeman-Benson - initial API and implementation
 *     Pawel Piech (Wind River) - ported PDA Virtual Machine to Java (Bug 261400)
 *******************************************************************************/
package org.eclipse.debug.examples.core.pda.model;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.eclipse.debug.core.DebugEvent;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.model.IBreakpoint;
import org.eclipse.debug.core.model.IStackFrame;
import org.eclipse.debug.core.model.IThread;
import org.eclipse.debug.core.model.IValue;
import org.eclipse.debug.core.model.IVariable;
import org.eclipse.debug.examples.core.pda.protocol.PDADataCommand;
import org.eclipse.debug.examples.core.pda.protocol.PDADropFrameCommand;
import org.eclipse.debug.examples.core.pda.protocol.PDAEvent;
import org.eclipse.debug.examples.core.pda.protocol.PDAListResult;
import org.eclipse.debug.examples.core.pda.protocol.PDANoSuchLabelEvent;
import org.eclipse.debug.examples.core.pda.protocol.PDAPopDataCommand;
import org.eclipse.debug.examples.core.pda.protocol.PDAPushDataCommand;
import org.eclipse.debug.examples.core.pda.protocol.PDAResumeCommand;
import org.eclipse.debug.examples.core.pda.protocol.PDAResumedEvent;
import org.eclipse.debug.examples.core.pda.protocol.PDARunControlEvent;
import org.eclipse.debug.examples.core.pda.protocol.PDAStackCommand;
import org.eclipse.debug.examples.core.pda.protocol.PDAStackCommandResult;
import org.eclipse.debug.examples.core.pda.protocol.PDAStepCommand;
import org.eclipse.debug.examples.core.pda.protocol.PDASuspendCommand;
import org.eclipse.debug.examples.core.pda.protocol.PDASuspendedEvent;
import org.eclipse.debug.examples.core.pda.protocol.PDAUnimplementedInstructionEvent;
import org.eclipse.debug.examples.core.pda.protocol.PDAVMResumedEvent;
import org.eclipse.debug.examples.core.pda.protocol.PDAVMSuspendedEvent;

/**
 * A PDA thread. A PDA VM is single threaded.
 */
public class PDAThread extends PDADebugElement implements IThread, IPDAEventListener {

    /**
     * ID of this thread as reported by PDA.
     */
    private final int fThreadId;

	/**
	 * Breakpoint this thread is suspended at or <code>null</code>
	 * if none.
	 */
	private IBreakpoint fBreakpoint;

	/**
	 * Whether this thread is stepping
	 */
	private boolean fStepping = false;

	/**
	 * Whether this thread is suspended
	 */
	private boolean fSuspended = false;

	/**
	 * Most recent error event or <code>null</code>
	 */
	private String fErrorEvent;

	/**
	 * Table mapping stack frames to current variables
	 */
	private Map<IStackFrame, IVariable[]> fVariables = Collections.synchronizedMap(new HashMap<IStackFrame, IVariable[]>());

	/**
	 * Constructs a new thread for the given target
	 *
	 * @param target VM
	 */
	public PDAThread(PDADebugTarget target, int threadId) {
		super(target);
		fThreadId = threadId;
	}

	/**
	 * Called by the debug target after the thread is created.
	 *
	 * @since 3.5
	 */
	void start() {
	    fireCreationEvent();
        getPDADebugTarget().addEventListener(this);
	}

    /**
     * Called by the debug target before the thread is removed.
     *
     * @since 3.5
     */
	void exit() {
        getPDADebugTarget().removeEventListener(this);
        fireTerminateEvent();
	}

	@Override
	public IStackFrame[] getStackFrames() throws DebugException {
		if (isSuspended()) {
		    PDAStackCommandResult result = (PDAStackCommandResult)sendCommand(new PDAStackCommand(fThreadId));
            IStackFrame[] frames = new IStackFrame[result.fFrames.length];
		    for (int i = 0; i < result.fFrames.length; i++) {
		        frames[frames.length - i - 1] = new PDAStackFrame(this, result.fFrames[i], i);
			}
            return frames;
		}
		return new IStackFrame[0];
	}

	@Override
	public boolean hasStackFrames() throws DebugException {
		return isSuspended();
	}

	@Override
	public int getPriority() throws DebugException {
		return 0;
	}

	@Override
	public IStackFrame getTopStackFrame() throws DebugException {
		IStackFrame[] frames = getStackFrames();
		if (frames.length > 0) {
			return frames[0];
		}
		return null;
	}

	@Override
	public String getName() {
		return "Main thread"; //$NON-NLS-1$
	}

	@Override
	public synchronized IBreakpoint[] getBreakpoints() {
		if (fBreakpoint == null) {
			return new IBreakpoint[0];
		}
		return new IBreakpoint[]{fBreakpoint};
	}

	/**
	 * Notifies this thread it has been suspended by the given breakpoint.
	 *
	 * @param breakpoint breakpoint
	 */
	public synchronized void suspendedBy(IBreakpoint breakpoint) {
		fBreakpoint = breakpoint;
		suspended(DebugEvent.BREAKPOINT);
	}

	@Override
	public boolean canResume() {
		return isSuspended() && !getDebugTarget().isSuspended();
	}

	@Override
	public boolean canSuspend() {
		return !isSuspended();
	}

	@Override
	public boolean isSuspended() {
	    if (getDebugTarget().isTerminated()) {
	        return false;
	    }
	    if (getDebugTarget().isSuspended()) {
	        return true;
	    }
	    synchronized (this) {
	        return fSuspended;
	    }
	}

	@Override
	public void resume() throws DebugException {
		//#ifdef ex2
//#		// TODO: Exercise 2 - send resume request to interpreter
		//#else
		sendCommand(new PDAResumeCommand(fThreadId));
		//#endif
	}

	@Override
	public void suspend() throws DebugException {
		//#ifdef ex2
//#		// TODO: Exercise 2 - send suspend request to interpreter
		//#else
	    sendCommand(new PDASuspendCommand(fThreadId));
		//#endif
	}

	@Override
	public boolean canStepInto() {
		return false;
	}

	@Override
	public boolean canStepOver() {
		return isSuspended();
	}

	@Override
	public boolean canStepReturn() {
		return false;
	}

	@Override
	public boolean isStepping() {
		return fStepping;
	}

	@Override
	public void stepInto() throws DebugException {
	}

	@Override
	public void stepOver() throws DebugException {
		sendCommand(new PDAStepCommand(fThreadId));
	}

	@Override
	public void stepReturn() throws DebugException {
	}

	@Override
	public boolean canTerminate() {
		return !isTerminated();
	}

	@Override
	public boolean isTerminated() {
		return getDebugTarget().isTerminated();
	}

	@Override
	public void terminate() throws DebugException {
	    getDebugTarget().terminate();
	}

	/**
	 * Sets whether this thread is stepping
	 *
	 * @param stepping whether stepping
	 */
	private synchronized void setStepping(boolean stepping) {
		fStepping = stepping;
	}

	/**
	 * Sets whether this thread is suspended
	 *
	 * @param suspended whether suspended
	 */
	private synchronized  void setSuspended(boolean suspended) {
		fSuspended = suspended;
	}

	/**
	 * Sets the most recent error event encountered, or <code>null</code>
	 * to clear the most recent error
	 *
	 * @param event one of 'unimpinstr' or 'nosuchlabel' or <code>null</code>
	 */
	private synchronized void setError(String event) {
		fErrorEvent = event;
	}

	/**
	 * Returns the most recent error event encountered since the last
	 * suspend, or <code>null</code> if none.
	 *
	 * @return the most recent error event encountered since the last
	 * suspend, or <code>null</code> if none
	 */
	public Object getError() {
		return fErrorEvent;
	}

	@Override
	public void handleEvent(PDAEvent _event) {
	    if (_event instanceof PDARunControlEvent && fThreadId == ((PDARunControlEvent)_event).fThreadId) {
	        PDARunControlEvent event = (PDARunControlEvent)_event;
    		// clear previous state
    		fBreakpoint = null;
    		setStepping(false);

    		// handle events
    		if (event instanceof PDAResumedEvent || event instanceof PDAVMResumedEvent) {
    			setSuspended(false);
    			if ("step".equals(event.fReason)) { //$NON-NLS-1$
    				setStepping(true);
    				resumed(DebugEvent.STEP_OVER);
    			//#ifdef ex2
    //#			}
    //#			// TODO: Exercise 2 - handle/fire "client" resume event
    			//#else
    			} else if ("client".equals(event.fReason)) { //$NON-NLS-1$
    				resumed(DebugEvent.CLIENT_REQUEST);
    			}
    			//#endif
    			//#ifdef ex5
    //#			// TODO: Exercise 5 - handle start of drop event
    			//#else
    			else if ("drop".equals(event.fReason)) { //$NON-NLS-1$
    				resumed(DebugEvent.STEP_RETURN);
    			}
    			//#endif
    		} else if (event instanceof PDASuspendedEvent || event instanceof PDAVMSuspendedEvent) {
    			setSuspended(true);
    			//#ifdef ex2
    //#			// TODO: Exercise 2 - handle/fire "client" suspend event
    //#			if (event.endsWith("step")) {
    //#				suspended(DebugEvent.STEP_END);
    //#			} else if (event.startsWith("suspended event") && getError() != null) {
    //#				exceptionHit();
    //#			}
    			//#else
    			if ("client".equals(event.fReason)) { //$NON-NLS-1$
    				suspended(DebugEvent.CLIENT_REQUEST);
    			} else if ("step".equals(event.fReason)) { //$NON-NLS-1$
    				suspended(DebugEvent.STEP_END);
    			} else if ("event".equals(event.fReason) && getError() != null) { //$NON-NLS-1$
    				exceptionHit();
    			}
    			//#endif
    			//#ifdef ex5
    //#			// TODO: Exercise 5 - handle end of drop event
    			//#else
    			else if ("drop".equals(event.fReason)) { //$NON-NLS-1$
    				suspended(DebugEvent.STEP_END);
    			}
    			//#endif
    		} else if (_event instanceof PDANoSuchLabelEvent ||
    		           _event instanceof PDAUnimplementedInstructionEvent)
    		{
    			setError(event.fMessage);
    		}
	    }
	}

	/**
	 * Notification the target has resumed for the given reason.
	 * Clears any error condition that was last encountered and
	 * fires a resume event, and clears all cached variables
	 * for stack frames.
	 *
	 * @param detail reason for the resume
	 */
	private void resumed(int detail) {
		setError(null);
		fVariables.clear();
		fireResumeEvent(detail);
	}

	/**
	 * Notification the target has suspended for the given reason
	 *
	 * @param detail reason for the suspend
	 */
	private void suspended(int detail) {
		fireSuspendEvent(detail);
	}

	/**
     * Notification an error was encountered. Fires a breakpoint
     * suspend event.
     */
    private void exceptionHit() {
    	suspended(DebugEvent.BREAKPOINT);
    }

	/**
	 * Sets the current variables for the given stack frame. Called
	 * by PDA stack frame when it is created.
	 *
	 * @param frame
	 * @param variables
	 */
	protected void setVariables(IStackFrame frame, IVariable[] variables) {
		synchronized (fVariables) {
			fVariables.put(frame, variables);
		}
	}

	/**
	 * Returns the current variables for the given stack frame, or
	 * <code>null</code> if none.
	 *
	 * @param frame stack frame
	 * @return variables or <code>null</code>
	 */
	protected IVariable[] getVariables(IStackFrame frame) {
		synchronized (fVariables) {
			IVariable[] variables = fVariables.get(frame);
			if (variables == null) {
				return new IVariable[0];
			}
			return variables;
		}
	}

	/**
	 * Pops the top frame off the callstack.
	 *
	 * @throws DebugException
     *
     * @since 3.5
	 */
	public void popFrame() throws DebugException {
		//#ifdef ex5
//#		// TODO: Exercise 5 - send drop request
		//#else
		sendCommand(new PDADropFrameCommand(fThreadId));
		//#endif
	}

	/**
	 * Returns whether this thread can pop the top stack frame.
	 *
	 * @return whether this thread can pop the top stack frame
	 *
	 * @since 3.5
	 */
	public boolean canPopFrame() {
		//#ifdef ex5
//#		// TODO: Exercise 5 - allow pop if there is more than 1 frame on the stack
		//#else
		try {
			return getStackFrames().length > 1;
		} catch (DebugException e) {
		}
		//#endif
		return false;
	}

    /**
     * Returns the values on the data stack (top down)
     *
     * @return the values on the data stack (top down)
     *
     * @since 3.5
     */
    public IValue[] getDataStack() throws DebugException {
        PDAListResult result = (PDAListResult)sendCommand(new PDADataCommand(fThreadId));
        if (result.fValues.length > 0) {
            IValue[] values = new IValue[result.fValues.length];
            for (int i = 0; i < result.fValues.length; i++) {
                values[values.length - i - 1] = new PDAStackValue(this, result.fValues[i], i);
            }
            return values;
        }
        return new IValue[0];
    }

    /**
     * Returns whether popping the data stack is currently permitted
     *
     * @return whether popping the data stack is currently permitted
     *
     * @since 3.5
     */
    public boolean canPopData() {
        try {
            return !isTerminated() && isSuspended() && getDataStack().length > 0;
        } catch (DebugException e) {
        }
        return false;
    }

    /**
     * Pops and returns the top of the data stack
     *
     * @return the top value on the stack
     * @throws DebugException if the stack is empty or the request fails
     *
     * @since 3.5
     */
    public IValue popData() throws DebugException {
        IValue[] dataStack = getDataStack();
        if (dataStack.length > 0) {
            sendCommand(new PDAPopDataCommand(fThreadId));
            return dataStack[0];
        }
        requestFailed("Empty stack", null); //$NON-NLS-1$
        return null;
    }

    /**
     * Returns whether pushing a value is currently supported.
     *
     * @return whether pushing a value is currently supported
     *
     * @since 3.5
     */
    public boolean canPushData() {
        return !isTerminated() && isSuspended();
    }

    /**
     * Pushes a value onto the stack.
     *
     * @param value value to push
     * @throws DebugException on failure
     *
     * @since 3.5
     */
    public void pushData(String value) throws DebugException {
        sendCommand(new PDAPushDataCommand(fThreadId, value));
    }

    /**
     * Returns this thread's unique identifier
     *
     * @return this thread's unique identifier
     *
     * @since 3.5
     */
	public int getIdentifier() {
	    return fThreadId;
	}
}
