// 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.sdk.internal.v8native;

import java.util.Collections;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.chromium.sdk.Breakpoint;
import org.chromium.sdk.DebugContext;
import org.chromium.sdk.DebugEventListener;
import org.chromium.sdk.InvalidContextException;
import org.chromium.sdk.JavascriptVm;
import org.chromium.sdk.JavascriptVm.ScriptsCallback;
import org.chromium.sdk.JavascriptVm.SuspendCallback;
import org.chromium.sdk.RelayOk;
import org.chromium.sdk.SyncCallback;
import org.chromium.sdk.Version;
import org.chromium.sdk.internal.v8native.InternalContext.ContextDismissedCheckedException;
import org.chromium.sdk.internal.v8native.protocol.V8ProtocolUtil;
import org.chromium.sdk.internal.v8native.protocol.input.CommandResponse;
import org.chromium.sdk.internal.v8native.protocol.input.SuccessCommandResponse;
import org.chromium.sdk.internal.v8native.protocol.output.ContextlessDebuggerMessage;
import org.chromium.sdk.internal.v8native.protocol.output.DebuggerMessageFactory;
import org.chromium.sdk.util.AsyncFuture;
import org.chromium.sdk.util.AsyncFuture.Callback;
import org.chromium.sdk.util.AsyncFutureRef;
import org.chromium.sdk.util.MethodIsBlockingException;
import org.chromium.sdk.util.RelaySyncCallback;

/**
 * A class that holds and administers main parts of debug protocol implementation.
 */
public class DebugSession {
  private static final Logger LOGGER = Logger.getLogger(DebugSession.class.getName());

  /** The script manager for the associated tab. */
  private final ScriptManager scriptManager;

  private final V8CommandProcessor v8CommandProcessor;

  private final ContextBuilder contextBuilder;

  /** Our manager. */
  private DebugSessionManager sessionManager;

  /** Context owns breakpoint manager. */
  private final BreakpointManager breakpointManager;

  private final ScriptManagerProxy scriptManagerProxy = new ScriptManagerProxy(this);

  private final DefaultResponseHandler defaultResponseHandler;

  private final JavascriptVm javascriptVm;

  private volatile Version vmVersion = null;

  public DebugSession(DebugSessionManager sessionManager, V8ContextFilter contextFilter,
      V8CommandOutput v8CommandOutput, JavascriptVm javascriptVm) {
    this.scriptManager = new ScriptManager(contextFilter, this);
    this.sessionManager = sessionManager;
    this.javascriptVm = javascriptVm;
    this.breakpointManager = new BreakpointManager(this);

    this.defaultResponseHandler = new DefaultResponseHandler(this);
    this.v8CommandProcessor = new V8CommandProcessor(v8CommandOutput, defaultResponseHandler,
        this);
    this.contextBuilder = new ContextBuilder(this);
  }

  public ScriptManager getScriptManager() {
    return scriptManager;
  }

  public V8CommandProcessor getV8CommandProcessor() {
    return v8CommandProcessor;
  }

  public DebugSessionManager getSessionManager() {
    return sessionManager;
  }

  public void onDebuggerDetached() {
    getSessionManager().onDebuggerDetached();
    getScriptManager().reset();
    contextBuilder.forceCancelContext();
  }

  /**
   * Sends V8 command messages, but only those which doesn't depend on context.
   * Use {@code InternalContext} if you need to send context-specific commands.
   * @return
   */
  public RelayOk sendMessageAsync(ContextlessDebuggerMessage message, boolean isImmediate,
      V8CommandProcessor.V8HandlerCallback commandCallback, SyncCallback syncCallback) {
    return v8CommandProcessor.sendV8CommandAsync(message, isImmediate,
        commandCallback, syncCallback);
  }

  JavascriptVm getJavascriptVm() {
    return javascriptVm;
  }

  public Version getVmVersion() {
    return vmVersion;
  }

  /**
   * @return the DebugEventListener associated with this context
   */
  public DebugEventListener getDebugEventListener() {
    return getSessionManager().getDebugEventListener();
  }

  public BreakpointManager getBreakpointManager() {
    return breakpointManager;
  }

  public ScriptManagerProxy getScriptManagerProxy() {
    return scriptManagerProxy;
  }

  public ContextBuilder getContextBuilder() {
    return contextBuilder;
  }

  /**
   * Drops current context and creates a new one. This is useful if context is known to have changed
   * (e.g. experimental feature LiveEdit may change current stack while execution is suspended).
   * The method is asynchronous and returns immediately.
   * Does nothing if currently there is no active context. Otherwise dismisses current context,
   * invokes {@link DebugEventListener#resumed()} and initiates downloading stack frame descriptions
   * and building new context. When the context is built,
   * calls {@link DebugEventListener#suspended(DebugContext)}.
   * <p>
   * Must be called from Dispatch Thread.
   * @return true if context has been actually dropped.
   */
  public boolean recreateCurrentContext() {
    ContextBuilder.ExpectingBacktraceStep step = contextBuilder.startRebuildCurrentContext();
    if (step == null) {
      return false;
    }
    defaultResponseHandler.getBreakpointProcessor().processNextStep(step);
    return true;
  }

  public void suspend(final SuspendCallback suspendCallback) {
    V8CommandProcessor.V8HandlerCallback v8Callback = new V8CommandCallbackBase() {
      @Override
      public void failure(String message) {
        if (suspendCallback != null) {
          suspendCallback.failure(new Exception(message));
        }
      }
      @Override
      public void success(SuccessCommandResponse successResponse) {
        if (suspendCallback != null) {
          suspendCallback.success();
        }

        ContextBuilder.ExpectingBreakEventStep step1 = contextBuilder.buildNewContextWhenIdle();
        if (step1 == null) {
          return;
        }
        ContextBuilder.ExpectingBacktraceStep step2 =
            step1.setContextState(Collections.<Breakpoint>emptyList(), null);
        defaultResponseHandler.getBreakpointProcessor().processNextStep(step2);
      }
    };
    sendMessageAsync(DebuggerMessageFactory.suspend(), true, v8Callback, null);
  }

  /**
   * A proxy to script manager that makes sure that all scripts have been pre-loaded from remote.
   * This is done only once per debug session.
   * TODO: consider loading all scripts synchronously on session start.
   */
  public static class ScriptManagerProxy {
    private final DebugSession debugSession;
    private final AsyncFutureRef<Void> scriptsLoadedFuture = new AsyncFutureRef<Void>();

    ScriptManagerProxy(DebugSession debugSession) {
      this.debugSession = debugSession;
    }

    public RelayOk getAllScripts(final ScriptsCallback callback, SyncCallback syncCallback) {
      if (!scriptsLoadedFuture.isInitialized()) {
        scriptsLoadedFuture.initializeRunning(new ScriptsRequester());
      }

      // Operation is multi-step, so make sure that syncCallback won't be left uncalled.
      RelaySyncCallback relay = new RelaySyncCallback(syncCallback);
      final RelaySyncCallback.Guard guard = relay.newGuard();

      Callback<Void> futureCallback = new Callback<Void>() {
        @Override public void done(Void res) {
          if (callback != null) {
            RelayOk relayOk = getAllScriptsAsync(callback, guard.getRelay());
            guard.discharge(relayOk);
          }
        }
      };

      return scriptsLoadedFuture.getAsync(futureCallback, guard.asSyncCallback());
    }

    private RelayOk getAllScriptsAsync(final ScriptsCallback callback, RelaySyncCallback relay) {
      // We should call the callback from Dispatch thread (so that the whole collection
      // kept fresh during the call-back).
      return debugSession.getV8CommandProcessor().runInDispatchThread(
          new Runnable() {
            @Override
            public void run() {
              callback.success(debugSession.getScriptManager().allScripts());
            }
          },
          relay.getUserSyncCallback());
    }

    private class ScriptsRequester implements AsyncFuture.Operation<Void> {
      @Override
      public RelayOk start(final Callback<Void> requestCallback,
          SyncCallback syncCallback) {
        V8Helper.ScriptLoadCallback scriptLoadCallback = new V8Helper.ScriptLoadCallback() {
          @Override
          public void success() {
            requestCallback.done(null);
          }

          @Override
          public void failure(final String message) {
            LOGGER.log(Level.SEVERE, null,
                new Exception("Failed to load scripts from remote: " + message));
            requestCallback.done(null);
          }
        };
        return V8Helper.reloadAllScriptsAsync(debugSession, scriptLoadCallback, syncCallback);
      }
    }
  }

  /**
   * Checks version of V8 and check if it in running state.
   */
  public void startCommunication() throws MethodIsBlockingException {
    V8BlockingCallback<Void> callback = new V8BlockingCallback<Void>() {
      @Override
      public Void messageReceived(CommandResponse response) {
        SuccessCommandResponse successResponse = response.asSuccess();
        if (successResponse == null) {
          return null;
        }
        Version vmVersion = V8ProtocolUtil.parseVersionResponse(successResponse);
        DebugSession.this.vmVersion = vmVersion;

        if (V8VersionFeatures.isRunningAccurate(vmVersion)) {
          Boolean running = successResponse.running();
          if (running == Boolean.FALSE) {
            ContextBuilder.ExpectingBreakEventStep step1 = contextBuilder.buildNewContextWhenIdle();
            // If step is not null -- we are already in process of building a context.
            if (step1 != null) {
              ContextBuilder.ExpectingBacktraceStep step2 =
                  step1.setContextState(Collections.<Breakpoint>emptyList(), null);

              defaultResponseHandler.getBreakpointProcessor().processNextStep(step2);
            }
          }
        }
        return null;
      }

      @Override
      protected Void handleSuccessfulResponse(SuccessCommandResponse response) {
        throw new UnsupportedOperationException();
      }
    };

    V8Helper.callV8Sync(this.v8CommandProcessor, DebuggerMessageFactory.version(), callback);
  }

  public RelayOk sendLoopbackMessage(Runnable callback, SyncCallback syncCallback) {
    return this.v8CommandProcessor.runInDispatchThread(callback, syncCallback);
  }

  public void maybeRethrowContextException(ContextDismissedCheckedException e) {
    // TODO(peter.rybin): make some kind of option out of this
    final boolean strictPolicy = true;
    if (strictPolicy) {
      throw new InvalidContextException(e);
    }
  }
}
