// 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 java.util.ArrayList;
import java.util.List;

import org.chromium.debug.core.ChromiumDebugPlugin;
import org.chromium.debug.core.model.BreakpointSynchronizer.Direction;
import org.chromium.sdk.util.Destructable;
import org.chromium.sdk.util.DestructingGuard;
import org.eclipse.core.resources.IMarkerDelta;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.debug.core.DebugEvent;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.IBreakpointListener;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.model.IBreakpoint;
import org.eclipse.debug.core.model.IDebugTarget;
import org.eclipse.debug.core.model.IDisconnect;
import org.eclipse.debug.core.model.IMemoryBlock;
import org.eclipse.debug.core.model.IProcess;
import org.eclipse.debug.core.model.ISuspendResume;
import org.eclipse.debug.core.model.ITerminate;
import org.eclipse.debug.core.model.IThread;

/**
 * An IDebugTarget implementation for remote JavaScript debugging.
 * This class is essentially a thin wrapper that uses its internal state object
 * as implementation. The at first target is in 'initialize' state, later
 * it should transfer into 'normal' state.
 */

public class DebugTargetImpl extends DebugElementImpl implements IDebugTarget {
  /**
   * Loads browser tabs, consults the {@code selector} which of the tabs to
   * attach to, and if any has been selected, requests an attachment to the tab.
   *
   * @param debugTargetImpl target that is attached
   * @param remoteServer embedding application we are connected with
   * @param destructingGuard guard that should gain any destructable value -- a caller
   *      will dispose everything if this method fails
   * @param attachCallback to invoke on successful attachment, can fail to be called
   * @param monitor to report the progress to
   * @return false if user canceled attach (via tab selection dialog) or true otherwise
   */
  public static boolean attach(DebugTargetImpl debugTargetImpl,
      JavascriptVmEmbedder.ConnectionToRemote remoteServer,
      DestructingGuard destructingGuard, Runnable attachCallback,
      IProgressMonitor monitor) throws CoreException {

    monitor.beginTask("", 2); //$NON-NLS-1$
    JavascriptVmEmbedder.VmConnector connector = remoteServer.selectVm();
    if (connector == null) {
      return false;
    }

    monitor.worked(1);

    ConnectedTargetData.TargetInnerState connectedState;
    ConnectedTargetData connectedData;

    ListenerBlock listenerBlock = new ListenerBlock();
    try {
      connectedState = ConnectedTargetData.create(debugTargetImpl, listenerBlock);
      connectedData = connectedState.getConnectedTargetData();

      final JavascriptVmEmbedder embedder = connector.attach(connectedData.getEmbedderListener(),
              connectedData.getDebugEventListener());
      // From this moment V8 may call our listeners. We block them by listenerBlock for a while.

      Destructable embedderDestructor = new Destructable() {
        public void destruct() {
          embedder.getJavascriptVm().detach();
        }
      };

      destructingGuard.addValue(embedderDestructor);

      connectedData.setVmEmbedder(embedder);

      debugTargetImpl.setInnerState(connectedState);

      connectedData.fireBecameConnectedEvents();

      listenerBlock.setProperlyInitialized();
    } finally {
      listenerBlock.unblock();
    }

    connectedData.initListeners();

    try {
      if (attachCallback != null) {
        attachCallback.run();
      }
    } catch (Exception e) {
      ChromiumDebugPlugin.log(e);
    }

    return true;
  }

  /**
   * Defines an actual state of target. It is who implements virtually all operations
   * of {@link DebugTargetImpl}.
   */
  static abstract class State {
    abstract ITerminate getTerminate();
    abstract ISuspendResume getSuspendResume();
    abstract IDisconnect getDisconnect();
    abstract IBreakpointListener getBreakpointListner();
    abstract IThread[] getThreads() throws DebugException;
    abstract String getName();
    abstract String getVmStatus();
    abstract boolean supportsBreakpoint(IBreakpoint breakpoint);
    abstract EvaluateContext getEvaluateContext();
    abstract ConnectedTargetData getConnectedTargetDataOrNull();
  }

  static final IThread[] EMPTY_THREADS = new IThread[0];

  private final WorkspaceBridge.Factory workspaceBridgeFactory;

  private final SourceWrapSupport sourceWrapSupport;

  private final ILaunch launch;
  private final BreakpointSynchronizer.Direction presetSyncDirection;
  private volatile State currentState = new TargetInitializeState(this);

  public DebugTargetImpl(ILaunch launch, WorkspaceBridge.Factory workspaceBridgeFactory,
      SourceWrapSupport sourceWrapSupport, BreakpointSynchronizer.Direction presetSyncDirection) {
    this.launch = launch;
    this.workspaceBridgeFactory = workspaceBridgeFactory;
    this.sourceWrapSupport = sourceWrapSupport;
    this.presetSyncDirection = presetSyncDirection;
  }

  public void fireTargetCreated() {
    fireDebugEvent(new DebugEvent(this, DebugEvent.CREATE));
  }

  void setInnerState(State state) {
    currentState = state;
  }

  ConnectedTargetData getConnectedDataOrNull() {
    return currentState.getConnectedTargetDataOrNull();
  }

  WorkspaceBridge.Factory getWorkspaceBridgeFactory() {
    return workspaceBridgeFactory;
  }

  @Override
  public DebugTargetImpl getDebugTarget() {
    return this;
  }

  @Override
  public ILaunch getLaunch() {
    return launch;
  }

  @Override
  public boolean canTerminate() {
    return currentState.getTerminate().canTerminate();
  }

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

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

  @Override
  public boolean canResume() {
    return currentState.getSuspendResume().canResume();
  }

  @Override
  public boolean canSuspend() {
    return currentState.getSuspendResume().canSuspend();
  }

  @Override
  public boolean isSuspended() {
    return currentState.getSuspendResume().isSuspended();
  }

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

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

  @Override
  public void breakpointAdded(IBreakpoint breakpoint) {
    currentState.getBreakpointListner().breakpointAdded(breakpoint);
  }

  @Override
  public void breakpointRemoved(IBreakpoint breakpoint, IMarkerDelta delta) {
    currentState.getBreakpointListner().breakpointRemoved(breakpoint, delta);
  }

  @Override
  public void breakpointChanged(IBreakpoint breakpoint, IMarkerDelta delta) {
    currentState.getBreakpointListner().breakpointChanged(breakpoint, delta);
  }

  @Override
  public boolean canDisconnect() {
    return currentState.getDisconnect().canDisconnect();
  }

  @Override
  public void disconnect() throws DebugException {
    currentState.getDisconnect().disconnect();
  }

  @Override
  public boolean isDisconnected() {
    return currentState.getDisconnect().isDisconnected();
  }

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

  @Override
  public IMemoryBlock getMemoryBlock(long startAddress, long length) throws DebugException {
    return null;
  }

  @Override
  public IProcess getProcess() {
    return null;
  }

  @Override
  public IThread[] getThreads() throws DebugException {
    return currentState.getThreads();
  }

  @Override
  public boolean hasThreads() throws DebugException {
    return getThreads().length != 0;
  }

  @Override
  public String getName() {
    return currentState.getName();
  }

  @Override
  public boolean supportsBreakpoint(IBreakpoint breakpoint) {
    return currentState.supportsBreakpoint(breakpoint);
  }

  public String getChromiumModelIdentifier() {
    return workspaceBridgeFactory.getDebugModelIdentifier();
  }

  public WorkspaceBridge.JsLabelProvider getLabelProvider() {
    return workspaceBridgeFactory.getLabelProvider();
  }

  public String getVmStatus() {
    return currentState.getVmStatus();
  }

  public ConnectedTargetData getConnectedOrNull() {
    return currentState.getConnectedTargetDataOrNull();
  }

  public SourceWrapSupport getSourceWrapSupport() {
    return sourceWrapSupport;
  }

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

  public static List<ConnectedTargetData> getAllConnectedTargetDatas() {
    IDebugTarget[] array = DebugPlugin.getDefault().getLaunchManager().getDebugTargets();
    List<ConnectedTargetData> result = new ArrayList<ConnectedTargetData>(array.length);
    for (IDebugTarget target : array) {
      if (target instanceof DebugTargetImpl == false) {
        continue;
      }
      if (target.getLaunch().isTerminated()) {
        continue;
      }
      DebugTargetImpl debugTargetImpl = (DebugTargetImpl) target;

      ConnectedTargetData connectedData = debugTargetImpl.getConnectedDataOrNull();

      if (connectedData == null) {
        continue;
      }

      result.add(connectedData);
    }
    return result;
  }

  static class ListenerBlock {
    private volatile boolean isBlocked = true;
    private volatile boolean hasBeenProperlyInitialized = false;
    private final Object monitor = new Object();
    void waitUntilReady() {
      if (isBlocked) {
        synchronized (monitor) {
          while (isBlocked) {
            try {
              monitor.wait();
            } catch (InterruptedException e) {
              throw new RuntimeException(e);
            }
          }
        }
      }
      if (!hasBeenProperlyInitialized) {
        throw new RuntimeException("DebugTarget has not been properly initialized"); //$NON-NLS-1$
      }
    }
    void setProperlyInitialized() {
      hasBeenProperlyInitialized = true;
    }
    void unblock() {
      isBlocked = false;
      synchronized (monitor) {
        monitor.notifyAll();
      }
    }
  }

  /**
   * Fires a debug event
   *
   * @param event to be fired
   */
  public static void fireDebugEvent(DebugEvent event) {
    DebugPlugin debugPlugin = DebugPlugin.getDefault();
    if (debugPlugin != null) {
      debugPlugin.fireDebugEventSet(new DebugEvent[] { event });
    }
  }

  public BreakpointSynchronizer.Direction getPresetSyncDirection() {
    return presetSyncDirection;
  }
}
