// Copyright (c) 2009 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.debug.core.model;

import static org.chromium.sdk.util.BasicUtil.toArray;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

import org.chromium.debug.core.ChromiumDebugPlugin;
import org.chromium.sdk.Breakpoint;
import org.chromium.sdk.CallFrame;
import org.chromium.sdk.DebugContext;
import org.chromium.sdk.DebugContext.StepAction;
import org.chromium.sdk.ExceptionData;
import org.eclipse.core.runtime.IAdaptable;
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.ISuspendResume;
import org.eclipse.debug.core.model.IThread;
import org.eclipse.debug.core.model.IVariable;

/**
 * This class represents the only Chromium V8 VM thread.
 */
public class JavascriptThread extends DebugElementImpl.WithConnected
    implements IThread, IAdaptable {

  private final RemoteEventListener remoteEventListener = new RemoteEventListener();

  private volatile StepState currentStepState = new RunningState(ResumeReason.UNSPECIFIED);
  private final Object currentStepStateMonitor = new Object();

  private volatile SuspendReason expectedSuspendReason = SuspendReason.UNSPECIFIED;

  /**
   * Holds 'suspended' state of the thread. As such has a getter to {@link DebugContext}.
   * It also keep references to basic enclosing objects.
   */
  public interface SuspendedState {
    JavascriptThread getThread();

    DebugContext getDebugContext();

    /**
     * Unsafe asynchronous getter: may return false, while the actual value has become true.
     */
    boolean isDismissed();
  }

  /**
   * Visitor that is used to describe thread state in UI. It doesn't expose too much of internals.
   */
  public interface StateVisitor<R> {
    R visitResumed(ResumeReason resumeReason);
    R visitSuspended(IBreakpoint[] breakpoints, ExceptionData exceptionData);
  }

  /**
   * Constructs a new thread for the given target
   *
   * @param connectedTargetData this thread is created for
   */
  public JavascriptThread(ConnectedTargetData connectedTargetData) {
    super(connectedTargetData);
  }

  /**
   * @return a separated interface for all remote events dispatching
   */
  RemoteEventListener getRemoteEventListener() {
    return remoteEventListener;
  }

  ISuspendResume getSuspendResumeAspect() {
    return suspendResumeAspect;
  }

  public StackFrameBase[] getStackFrames() throws DebugException {
    return currentStepState.getStackFrames();
  }

  /**
   * @return expose some information about thread state for UI presentation
   */
  public <R> R describeState(StateVisitor<R> visitor) {
    return currentStepState.describeState(visitor);
  }

  private static StackFrameBase[] wrapStackFrames(JavascriptThread.SuspendedState threadState) {
    DebugContext debugContext = threadState.getDebugContext();
    List<? extends CallFrame> jsFrames = debugContext.getCallFrames();
    List<StackFrameBase> result = new ArrayList<StackFrameBase>(jsFrames.size() + 1);

    ExceptionData exceptionData = debugContext.getExceptionData();
    if (exceptionData != null) {
      // Add fake 'throw exception' frame.
      EvaluateContext evaluateContext =
          new EvaluateContext(debugContext.getGlobalEvaluateContext(), threadState);
      result.add(new ExceptionStackFrame(evaluateContext, exceptionData));
    }
    for (CallFrame jsFrame : jsFrames) {
      result.add(new StackFrame(threadState, jsFrame));
    }
    return toArray(result, StackFrameBase.class);
  }

  /**
   * A fake stackframe that represents 'throwing exception'. It's a frame that holds an exception
   * as its only variable. This might be the only means to expose exception value to user because
   * exception may be raised with no frames on stack (e.g. compile error).
   */
  private static class ExceptionStackFrame extends StackFrameBase {
    private final IVariable[] variables;
    private final ExceptionData exceptionData;

    private ExceptionStackFrame(EvaluateContext evaluateContext, ExceptionData exceptionData) {
      super(evaluateContext);
      this.exceptionData = exceptionData;

      Variable variable = Variable.forException(evaluateContext, exceptionData);
      variables = new IVariable[] { variable };
    }

    @Override
    public IVariable[] getVariables() throws DebugException {
      return variables;
    }

    @Override
    public boolean hasVariables() throws DebugException {
      return variables.length > 0;
    }

    @Override
    public int getLineNumber() throws DebugException {
      return -1;
    }

    @Override
    public int getCharStart() throws DebugException {
      return -1;
    }

    @Override
    public int getCharEnd() throws DebugException {
      return getCharStart();
    }

    @Override
    public String getName() throws DebugException {
      return "<throwing exception>";
    }

    @Override
    Object getObjectForEquals() {
      return exceptionData;
    }

    @Override
    boolean isRegularFrame() {
      return false;
    }
  }

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

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

  public IStackFrame getTopStackFrame() throws DebugException {
    // Do not return frames[0] if it's a fake 'exception throwing' frame.
    StackFrameBase[] frames = getStackFrames();
    if (frames.length == 0) {
      return null;
    }
    if (frames[0].isRegularFrame()) {
      return frames[0];
    }
    if (frames.length == 1) {
      return null;
    }
    return frames[1];
  }

  public String getName() throws DebugException {
    return getDebugTarget().getLabelProvider().getThreadLabel(this);
  }

  public IBreakpoint[] getBreakpoints() {
    return currentStepState.getBreakpoints();
  }

  public boolean canResume() {
    return suspendResumeAspect.canResume();
  }

  public boolean canSuspend() {
    return suspendResumeAspect.canSuspend();
  }

  public boolean isSuspended() {
    return suspendResumeAspect.isSuspended();
  }

  public void resume() throws DebugException {
    suspendResumeAspect.resume();
  }

  public void suspend() throws DebugException {
    suspendResumeAspect.suspend();
  }

  public boolean canStepInto() {
    return currentStepState.canStep();
  }

  public boolean canStepOver() {
    return currentStepState.canStep();
  }

  public boolean canStepReturn() {
    return currentStepState.canStep();
  }

  public boolean isStepping() {
    return currentStepState.isStepping();
  }

  public void stepInto() throws DebugException {
    currentStepState.step(StepAction.IN, ResumeReason.STEP_INTO);
  }

  public void stepOver() throws DebugException {
    currentStepState.step(StepAction.OVER, ResumeReason.STEP_OVER);
  }

  public void stepReturn() throws DebugException {
    currentStepState.step(StepAction.OUT, ResumeReason.STEP_RETURN);
  }

  public boolean canTerminate() {
    return getDebugTarget().canTerminate();
  }

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

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

  EvaluateContext getEvaluateContext() {
    return currentStepState.getEvaluateContext();
  }

  @Override
  @SuppressWarnings("unchecked")
  public Object getAdapter(Class adapter) {
    if (adapter == EvaluateContext.class) {
      return getEvaluateContext();
    }
    return super.getAdapter(adapter);
  }

  class RemoteEventListener {

    void suspended(DebugContext context) {
      SuspendedStateImpl suspendedState;
      synchronized (currentStepStateMonitor) {
        if (currentStepState.isSuspended()) {
          throw new IllegalStateException("Already in suspended state");
        }
        suspendedState = new SuspendedStateImpl(context);
        currentStepState = suspendedState;
      }

      WorkspaceBridge workspaceRelations = getConnectedData().getWorkspaceRelations();

      Collection<? extends IBreakpoint> uiBreakpointsHit;
      SuspendReason suspendedReason;

      if (context.getState() == DebugContext.State.EXCEPTION) {
        uiBreakpointsHit =
            workspaceRelations.getBreakpointHandler().exceptionBreakpointHit(
                context.getExceptionData().isUncaught());
        suspendedReason = SuspendReason.BREAKPOINT;
      } else {
        Collection<? extends Breakpoint> sdkBreakpointsHit = context.getBreakpointsHit();
        uiBreakpointsHit =
            workspaceRelations.getBreakpointHandler().breakpointsHit(sdkBreakpointsHit);
        if (sdkBreakpointsHit.isEmpty()) {
          suspendedReason = expectedSuspendReason;
        } else {
          suspendedReason = SuspendReason.BREAKPOINT;
        }
      }

      suspendedState.setBreakpoints(uiBreakpointsHit);

      int suspendedDetail;
      if (suspendedReason == null) {
        suspendedDetail = DebugEvent.UNSPECIFIED;
      } else {
        suspendedDetail = suspendedReason.detailCode;
      }
      getConnectedData().fireSuspendEvent(suspendedDetail);
    }

    void resumed(ResumeReason resumeReason) {
      synchronized (currentStepStateMonitor) {
        if (!currentStepState.isSuspended()) {
          // Ignore.
          return;
        }
        if (resumeReason == null) {
          resumeReason = ResumeReason.UNSPECIFIED;
        }
        currentStepState.dismiss();
        currentStepState = new RunningState(resumeReason);
      }
      getConnectedData().fireResumeEvent(resumeReason.detailCode);
    }

  }

  private static abstract class StepState {
    abstract EvaluateContext getEvaluateContext();

    abstract IBreakpoint[] getBreakpoints();

    abstract StackFrameBase[] getStackFrames();

    abstract boolean isSuspended();
    abstract void resume();
    abstract boolean canSuspend();
    abstract void suspend();

    abstract boolean isStepping();
    abstract void step(StepAction stepAction, ResumeReason resumeReason);
    abstract boolean canStep();

    abstract <R> R describeState(StateVisitor<R> visitor);

    abstract void dismiss();
  }

  private class RunningState extends StepState {
    private final ResumeReason resumeReason;

    RunningState(ResumeReason resumeReason) {
      this.resumeReason = resumeReason;
    }

    @Override boolean isSuspended() {
      return false;
    }

    @Override boolean canSuspend() {
      return true;
    }

    @Override
    void suspend() {
      expectedSuspendReason = SuspendReason.CLIENT_REQUEST;
      getConnectedData().getJavascriptVm().suspend(null);
    }

    @Override StackFrameBase[] getStackFrames() {
      return EMPTY_FRAMES;
    }

    @Override IBreakpoint[] getBreakpoints() {
      return EMPTY_BREAKPOINTS;
    }

    @Override boolean isStepping() {
      return resumeReason.isStepping;
    }

    @Override boolean canStep() {
      return false;
    }

    @Override void step(StepAction stepAction, ResumeReason resumeReason) {
      // Ignore.
    }

    @Override void resume() {
      // Ignore.
    }

    @Override EvaluateContext getEvaluateContext() {
      return null;
    }

    @Override void dismiss() {
    }

    @Override
    <R> R describeState(StateVisitor<R> visitor) {
      return visitor.visitResumed(resumeReason);
    }
  }

  private class SuspendedStateImpl extends StepState implements SuspendedState {
    private final DebugContext context;
    private volatile boolean isDismissed = false;

    /**
     * Breakpoints this thread is suspended at or empty array if none.
     */
    private volatile IBreakpoint[] breakpoints = EMPTY_BREAKPOINTS;

    /**
     * Cached stack
     */
    private final AtomicReference<StackFrameBase[]> stackFrames =
        new AtomicReference<StackFrameBase[]>(null);

    SuspendedStateImpl(DebugContext context) {
      this.context = context;
    }

    @Override public JavascriptThread getThread() {
      return JavascriptThread.this;
    }

    @Override public DebugContext getDebugContext() {
      return context;
    }

    @Override void dismiss() {
      isDismissed = true;
    }

    @Override public boolean isDismissed() {
      return isDismissed;
    }

    void setBreakpoints(Collection<? extends IBreakpoint> uiBreakpoints) {
      this.breakpoints = toArray(uiBreakpoints, IBreakpoint.class);
    }

    @Override boolean isSuspended() {
      return true;
    }

    @Override boolean canSuspend() {
      return false;
    }

    @Override void suspend() {
      // Ignore.
    }

    @Override boolean canStep() {
      return true;
    }

    @Override
    void resume() {
      continueVm(StepAction.CONTINUE, ResumeReason.CLIENT_REQUEST, SuspendReason.UNSPECIFIED);
    }

    @Override
    void step(StepAction stepAction, ResumeReason resumeReason) {
      continueVm(stepAction, resumeReason, SuspendReason.STEP_END);
    }

    private void continueVm(StepAction stepAction, final ResumeReason resumeReason,
        SuspendReason futureSuspendReason) {
      expectedSuspendReason = futureSuspendReason;

      DebugContext.ContinueCallback callback = new DebugContext.ContinueCallback() {
        @Override public void success() {
          remoteEventListener.resumed(resumeReason);
        }

        @Override public void failure(String errorMessage) {
          ChromiumDebugPlugin.log(new Exception("Failed to resume: " + errorMessage));
        }
      };

      context.continueVm(stepAction, 1, callback);
    }

    @Override
    StackFrameBase[] getStackFrames() {
      StackFrameBase[] result = stackFrames.get();
      if (result == null) {
        result = wrapStackFrames(this);
        stackFrames.compareAndSet(null, result);
        result = stackFrames.get();
      }
      return result;
    }

    @Override IBreakpoint[] getBreakpoints() {
      return breakpoints;
    }

    @Override boolean isStepping() {
      return false;
    }

    @Override EvaluateContext getEvaluateContext() {
      return new EvaluateContext(context.getGlobalEvaluateContext(), this);
    }

    @Override
    <R> R describeState(StateVisitor<R> visitor) {
      return visitor.visitSuspended(breakpoints, context.getExceptionData());
    }
  }

  private final ISuspendResume suspendResumeAspect = new ISuspendResume() {
    @Override public boolean canResume() {
      return !isDisconnected() && isSuspended();
    }

    @Override public boolean isSuspended() {
      return !isDisconnected() && currentStepState.isSuspended();
    }

    @Override public void resume() throws DebugException {
      currentStepState.resume();
    }

    @Override public boolean canSuspend() {
      return !isDisconnected() && currentStepState.canSuspend();
    }

    @Override public void suspend() throws DebugException {
      currentStepState.suspend();
    }

    private boolean isDisconnected() {
      return getConnectedData().isDisconnected();
    }
  };

  /**
   * Wraps Eclipse mixed-up constants in a dedicated enum type.
   */
  public enum ResumeReason {
    STEP_INTO(DebugEvent.STEP_INTO, true),
    STEP_OVER(DebugEvent.STEP_OVER, true),
    STEP_RETURN(DebugEvent.STEP_RETURN, true),
    CLIENT_REQUEST(DebugEvent.CLIENT_REQUEST, false),
    UNSPECIFIED(DebugEvent.UNSPECIFIED, false);

    private final int detailCode;
    private final boolean isStepping;

    ResumeReason(int detailCode, boolean isStepping) {
      this.detailCode = detailCode;
      this.isStepping = isStepping;
    }
  }

  /**
   * Wraps Eclipse mixed-up constants in a dedicated enum type.
   */
  private enum SuspendReason {
    STEP_END(DebugEvent.STEP_END),
    CLIENT_REQUEST(DebugEvent.CLIENT_REQUEST),
    BREAKPOINT(DebugEvent.BREAKPOINT),
    UNSPECIFIED(DebugEvent.UNSPECIFIED);

    final int detailCode;

    SuspendReason(int detailCode) {
      this.detailCode = detailCode;
    }
  }

  private static final StackFrame[] EMPTY_FRAMES = new StackFrame[0];
  private static final IBreakpoint[] EMPTY_BREAKPOINTS = new IBreakpoint[0];
}
