// Copyright (c) 2011 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.Collection;

import org.chromium.debug.core.ChromiumDebugPlugin;
import org.chromium.debug.core.model.DebugTargetImpl.ListenerBlock;
import org.chromium.debug.core.model.DebugTargetImpl.State;
import org.chromium.debug.core.sourcemap.PositionMapBuilderImpl;
import org.chromium.debug.core.sourcemap.SourcePositionMap;
import org.chromium.debug.core.sourcemap.SourcePositionMapBuilder;
import org.chromium.debug.core.util.ChromiumDebugPluginUtil;
import org.chromium.sdk.DebugContext;
import org.chromium.sdk.DebugEventListener;
import org.chromium.sdk.JavascriptVm;
import org.chromium.sdk.Script;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
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.IBreakpointManager;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchListener;
import org.eclipse.debug.core.model.IBreakpoint;
import org.eclipse.debug.core.model.IDisconnect;
import org.eclipse.debug.core.model.ISuspendResume;
import org.eclipse.debug.core.model.ITerminate;
import org.eclipse.debug.core.model.IThread;
import org.eclipse.osgi.util.NLS;

/**
 * Contains state and behavior of 'connected' {@link DebugTargetImpl}. Its inner implementation of
 * {@link DebugTargetImpl#State} is for {@link DebugTargetImpl} only and is available externally
 * only at construction. The class can be used
 * only after {@link #setVmEmbedder} was called. Listeners can be used from the
 * beginning, but they are blocking until {@link #listenerBlock} is unblocked which is done
 * externally after {@link #setVmEmbedder} is called.
 * <p>
 * It corresponds to 'connected' state of target and post-terminated state.
 */
public class ConnectedTargetData {

  /**
   * Creates instance and returns its inner state class. This is the only moment when inner
   * state class gets available outside the class.
   * @param listenerBlock blocks 2 event listeners to let all infrastructure get initialized
   * @return inner state of data that provides getter to the data itself.
   */
  static TargetInnerState create(DebugTargetImpl debugTargetImpl, ListenerBlock listenerBlock) {
    ConnectedTargetData data = new ConnectedTargetData(debugTargetImpl, listenerBlock);

    // No public getter for stateImpl. We expose state only to one who creates us.
    return data.debugTargetState;
  }

  private final DebugTargetImpl debugTargetImpl;
  private final TargetInnerState debugTargetState = new TargetInnerState();
  private final JavascriptThread singleThread;
  private final JavascriptThread[] threadArray;
  private final SourcePositionMapBuilder sourcePositionMapBuilder = new PositionMapBuilderImpl();
  private final ListenerBlock listenerBlock;
  private final DebugEventListenerImpl debugEventListener = new DebugEventListenerImpl();

  private JavascriptVmEmbedder vmEmbedder = null;
  private WorkspaceBridge workspaceRelations = null;

  private volatile boolean isDisconnected = false;

  private ConnectedTargetData(DebugTargetImpl debugTargetImpl, ListenerBlock listenerBlock) {
    this.debugTargetImpl = debugTargetImpl;
    this.singleThread = new JavascriptThread(this);
    this.threadArray = new JavascriptThread[] { singleThread };
    this.listenerBlock = listenerBlock;
  }

  void setVmEmbedder(JavascriptVmEmbedder vmEmbedder) {
    ConnectedTargetData.this.vmEmbedder = vmEmbedder;

    initWorkspaceRelations();
  }

  public void initListeners() {
    IBreakpointManager breakpointManager = DebugPlugin.getDefault().getBreakpointManager();
    breakpointManager.addBreakpointListener(debugTargetState.getBreakpointListner());
    breakpointManager.addBreakpointManagerListener(workspaceRelations.getBreakpointHandler());
    workspaceRelations.getBreakpointHandler().initBreakpointManagerListenerState(
        breakpointManager);

    workspaceRelations.startInitialization();
  }

  public JavascriptVmEmbedder getJavascriptEmbedder() {
    return vmEmbedder;
  }

  public JavascriptVm getJavascriptVm() {
    return getJavascriptEmbedder().getJavascriptVm();
  }

  void fireBecameConnectedEvents() {
    setDisconnected(false);
    DebugTargetImpl.fireDebugEvent(new DebugEvent(debugTargetImpl, DebugEvent.CHANGE));
    fireEventForThread(DebugEvent.CREATE, DebugEvent.UNSPECIFIED);
  }

  void fireResumeEvent(int detail) {
    fireEventForThread(DebugEvent.RESUME, detail);
    DebugTargetImpl.fireDebugEvent(new DebugEvent(debugTargetImpl, DebugEvent.RESUME, detail));
  }

  public Collection<? extends VmResource> getVmResource(IFile resource) throws CoreException {
    return workspaceRelations.findVmResourcesFromWorkspaceFile(resource);
  }

  private final VmStatusListenerImpl vmStatusListener = new VmStatusListenerImpl();

  private class VmStatusListenerImpl implements DebugEventListener.VmStatusListener {
    private String currentRequest = null;
    private int numberOfEnqueued;

    public synchronized void busyStatusChanged(String currentRequest, int numberOfEnqueued) {
      this.currentRequest = currentRequest;
      this.numberOfEnqueued = numberOfEnqueued;
      DebugTargetImpl.fireDebugEvent(new DebugEvent(debugTargetImpl, DebugEvent.CHANGE));
    }

    public synchronized String getStatusString() {
      if (currentRequest == null) {
        return null;
      }
      return NLS.bind(Messages.DebugTargetImpl_BUSY_WITH, currentRequest, numberOfEnqueued);
    }
  }

  public void synchronizeBreakpoints(BreakpointSynchronizer.Direction direction,
      BreakpointSynchronizer.Callback callback) {
    workspaceRelations.synchronizeBreakpoints(direction, callback);
  }

  public DebugEventListenerImpl getDebugEventListener() {
    return debugEventListener;
  }

  public JavascriptVmEmbedder.Listener getEmbedderListener() {
    return embedderListener;
  }

  public WorkspaceBridge getWorkspaceRelations() {
    return workspaceRelations;
  }

  public SourcePositionMap getSourcePositionMap() {
    return sourcePositionMapBuilder.getSourcePositionMap();
  }

  public SourcePositionMapBuilder getSourcePositionMapBuilder() {
    return sourcePositionMapBuilder;
  }

  public SourceWrapSupport getSourceWrapSupport() {
    return debugTargetImpl.getSourceWrapSupport();
  }

  public DebugTargetImpl getDebugTarget() {
    return debugTargetImpl;
  }

  public String getName() {
    return debugTargetState.getName();
  }

  private JavascriptThread getThread() {
    return disconnectAspect.isDisconnected()
        ? null
        : singleThread;
  }

  private void fireEventForThread(int kind, int detail) {
    try {
      IThread[] threads = debugTargetState.getThreads();
      if (threads.length > 0) {
        DebugTargetImpl.fireDebugEvent(new DebugEvent(threads[0], kind, detail));
      }
    } catch (DebugException e) {
      // Actually, this is not thrown in our getThreads()
      return;
    }
  }

  private void fireTerminateEvent() {
    // TODO(peter.rybin): from Alexander Pavlov: I think you need to fire a terminate event after
    // this line, for consolePseudoProcess if one is not null.

    // Do not report on threads -- the children are gone when terminated.
    DebugTargetImpl.fireDebugEvent(
        new DebugEvent(debugTargetImpl, DebugEvent.TERMINATE, DebugEvent.UNSPECIFIED));
    DebugTargetImpl.fireDebugEvent(
        new DebugEvent(debugTargetImpl.getLaunch(), DebugEvent.TERMINATE, DebugEvent.UNSPECIFIED));
  }

  void fireSuspendEvent(int detail) {
    fireEventForThread(DebugEvent.SUSPEND, detail);
    DebugTargetImpl.fireDebugEvent(new DebugEvent(debugTargetImpl, DebugEvent.SUSPEND, detail));
  }

  private void setDisconnected(boolean disconnected) {
    isDisconnected = disconnected;
  }

  boolean isDisconnected() {
    return isDisconnected;
  }

  private void initWorkspaceRelations() {
    ConnectedTargetData.this.workspaceRelations =
        debugTargetImpl.getWorkspaceBridgeFactory().attachedToVm(ConnectedTargetData.this,
            vmEmbedder.getJavascriptVm());

    // We'd like to know when launch is removed to remove our project.
    DebugPlugin.getDefault().getLaunchManager().addLaunchListener(new ILaunchListener() {
      public void launchAdded(ILaunch launch) {
      }
      public void launchChanged(ILaunch launch) {
      }
      // TODO(peter.rybin): maybe have one instance of listener for all targets?
      public void launchRemoved(ILaunch launch) {
        if (launch != debugTargetImpl.getLaunch()) {
          return;
        }
        DebugPlugin.getDefault().getLaunchManager().removeLaunchListener(this);
        workspaceRelations.launchRemoved();
      }
    });
  }

  private final IDisconnect disconnectAspect = new IDisconnect() {
    public boolean canDisconnect() {
      return !isDisconnected();
    }

    public void disconnect() throws DebugException {
      if (!canDisconnect()) {
        return;
      }
      workspaceRelations.beforeDetach();
      if (!vmEmbedder.getJavascriptVm().detach()) {
        ChromiumDebugPlugin.logWarning(Messages.DebugTargetImpl_BadResultWhileDisconnecting);
      }
      // This is a duplicated call to disconnected().
      // The primary one comes from V8DebuggerToolHandler#onDebuggerDetached
      // but we want to make sure the target becomes disconnected even if
      // there is a browser failure and it does not respond.
      debugEventListener.disconnected();
    }

    public boolean isDisconnected() {
      return isDisconnected;
    }
  };

  private final ITerminate terminateAspect = new ITerminate() {
    public boolean canTerminate() {
      return !isTerminated();
    }

    public boolean isTerminated() {
      return disconnectAspect.isDisconnected();
    }

    public void terminate() throws DebugException {
      disconnectAspect.disconnect();
    }
  };

  private final JavascriptVmEmbedder.Listener embedderListener =
      new JavascriptVmEmbedder.Listener() {
    public void reset() {
      listenerBlock.waitUntilReady();
      workspaceRelations.handleVmResetEvent();
      DebugTargetImpl.fireDebugEvent(
          new DebugEvent(debugTargetImpl, DebugEvent.CHANGE, DebugEvent.CONTENT));
    }
    public void closed() {
      debugEventListener.disconnected();
    }
  };

  private class DebugEventListenerImpl implements DebugEventListener {

    public void disconnected() {
      if (!disconnectAspect.isDisconnected()) {
        setDisconnected(true);
        DebugPlugin.getDefault().getBreakpointManager().removeBreakpointManagerListener(
            workspaceRelations.getBreakpointHandler());
        DebugPlugin.getDefault().getBreakpointManager().removeBreakpointListener(
            debugTargetImpl);
        fireTerminateEvent();
        
//        final IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject("STANDALONE_V8");
//        ChromiumDebugPluginUtil.deleteVirtualProjectAsync(project);
      }
    }

    public void resumed() {
      listenerBlock.waitUntilReady();
      singleThread.getRemoteEventListener().resumed(null);
    }

    public void suspended(DebugContext context) {
      listenerBlock.waitUntilReady();
      singleThread.getRemoteEventListener().suspended(context);
    }

    public void scriptLoaded(Script newScript) {
      listenerBlock.waitUntilReady();
      workspaceRelations.scriptLoaded(newScript);
    }

    public void scriptCollected(Script script) {
      listenerBlock.waitUntilReady();
      workspaceRelations.scriptCollected(script);
    }

    public void scriptContentChanged(Script newScript) {
      listenerBlock.waitUntilReady();
      workspaceRelations.reloadScript(newScript);
    }

    public VmStatusListener getVmStatusListener() {
      return vmStatusListener;
    }
  }

  class TargetInnerState extends State {
    @Override
    boolean supportsBreakpoint(IBreakpoint breakpoint) {
      return workspaceRelations.getBreakpointHandler().supportsBreakpoint(breakpoint);
    }

    @Override
    String getVmStatus() {
      if (isDisconnected) {
        return null;
      }
      return vmStatusListener.getStatusString();
    }

    @Override
    IThread[] getThreads() throws DebugException {
      return disconnectAspect.isDisconnected()
          ? DebugTargetImpl.EMPTY_THREADS
          : threadArray;
    }

    @Override
    ConnectedTargetData getConnectedTargetDataOrNull() {
      return getConnectedTargetData();
    }

    ConnectedTargetData getConnectedTargetData() {
      return ConnectedTargetData.this;
    }

    @Override
    ISuspendResume getSuspendResume() {
      return singleThread.getSuspendResumeAspect();
    }

    @Override
    ITerminate getTerminate() {
      return terminateAspect;
    }

    @Override
    IDisconnect getDisconnect() {
      return disconnectAspect;
    }

    @Override
    String getName() {
      JavascriptVmEmbedder vmEmbedder = getJavascriptEmbedder();
      return vmEmbedder.getTargetName();
    }

    @Override
    EvaluateContext getEvaluateContext() {
      return getThread().getEvaluateContext();
    }

    @Override
    IBreakpointListener getBreakpointListner() {
      return workspaceRelations.getBreakpointHandler();
    }
  }
}