/*
 * Copyright 2007 Sascha Weinreuter
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.intellij.plugins.xsltDebugger;

import com.intellij.execution.process.ProcessHandler;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.util.EventDispatcher;
import com.intellij.xdebugger.XSourcePosition;
import org.intellij.plugins.xsltDebugger.impl.XsltBreakpointHandler;
import org.intellij.plugins.xsltDebugger.impl.XsltDebugProcess;
import org.intellij.plugins.xsltDebugger.rt.engine.Breakpoint;
import org.intellij.plugins.xsltDebugger.rt.engine.BreakpointManager;
import org.intellij.plugins.xsltDebugger.rt.engine.Debugger;
import org.intellij.plugins.xsltDebugger.rt.engine.DebuggerStoppedException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.net.MalformedURLException;
import java.net.SocketException;
import java.net.URI;
import java.net.URISyntaxException;
import java.rmi.RemoteException;
import java.util.EventListener;

/**
 * This is the main place that interacts with the debugged XSLT processor. Waits until the processor
 * hits a breakpoint, resumes execution, etc.
 */
public final class XsltDebuggerSession implements Disposable {
  private static final Logger LOG = Logger.getInstance(XsltDebuggerSession.class);

  private static final Key<XsltDebuggerSession> DEBUGGER_SESSION = Key.create("DEBUGGER_SESSION");

  private final Project myProject;
  private final ProcessHandler myProcess;
  private final Debugger myClient;
  private final EventDispatcher<Listener> myEventDispatcher = EventDispatcher.create(Listener.class);

  private Breakpoint myTempBreakpoint;

  private volatile Debugger.State myState;
  private boolean myClosed;

  private XsltDebuggerSession(Project project, ProcessHandler process, Debugger client) {
    myProject = project;
    myProcess = process;
    myClient = client;
    Disposer.register(XsltDebugProcess.getInstance(process), this);
  }

  public void start() {
    myClient.start();
    myState = Debugger.State.RUNNING;

    final BreakpointManager breakpointManager = myClient.getBreakpointManager();
    final Listener multicaster = myEventDispatcher.getMulticaster();
    try {
      if (!myClient.waitForDebuggee()) {
        multicaster.debuggerStopped();
        return;
      }
      myState = Debugger.State.SUSPENDED;
      do {
        if (myState == Debugger.State.SUSPENDED) {
          if (myTempBreakpoint != null) {
            breakpointManager.removeBreakpoint(myTempBreakpoint);
            myTempBreakpoint = null;
          }
          multicaster.debuggerSuspended();
        } else if (myState == Debugger.State.RUNNING) {
          multicaster.debuggerResumed();
        } else if (myState == Debugger.State.STOPPED) {
          break;
        }
      }
      while ((myState = myClient.waitForStateChange(myState)) != null);

      multicaster.debuggerStopped();
    } catch (DebuggerStoppedException e) {
      multicaster.debuggerStopped();
    } catch (RuntimeException e) {
      if (e.getCause() instanceof RemoteException) {
        if (e.getCause().getCause() instanceof SocketException) {
          multicaster.debuggerStopped();
          return;
        }
      }
      throw e;
    } finally {
      myState = Debugger.State.STOPPED;
      close();
    }
  }

  public void addListener(Listener listener) {
    myEventDispatcher.addListener(listener);
  }

  public void removeListener(Listener listener) {
    myEventDispatcher.removeListener(listener);
  }

  public Debugger getClient() {
    return myClient;
  }

  public Debugger.State getCurrentState() {
    return myState;
  }

  public void pause() {
    myClient.pause();
  }

  public void resume() {
    myClient.resume();
  }

  public void stop() {
    try {
      myClient.stop(false);
    } catch (DebuggerStoppedException ignore) {
    }
  }

  public void stepOver() {
    myClient.step();
  }

  public void stepInto() {
    myClient.stepInto();
  }

  public boolean canRunTo(final XSourcePosition position) {
    return XsltBreakpointHandler.getActualLineNumber(myProject, position) != -1;
  }

  public void runTo(final PsiFile file, final XSourcePosition position) {
    assert myTempBreakpoint == null;

    final int lineNumber = XsltBreakpointHandler.getActualLineNumber(myProject, position);
    final String uri = XsltBreakpointHandler.getFileURL(file.getVirtualFile());
    myTempBreakpoint = myClient.getBreakpointManager().setBreakpoint(uri, lineNumber);

    resume();
  }

  public static @Nullable Editor openLocation(Project project, @NotNull String uri, int lineNumber) {
    try {
      final VirtualFile file = VfsUtil.findFileByURL(new URI(uri).toURL());
      final OpenFileDescriptor descriptor = new OpenFileDescriptor(project, file, lineNumber, 0);
      descriptor.navigate(true);

      return FileEditorManager.getInstance(project).openTextEditor(descriptor, true);
    } catch (MalformedURLException | URISyntaxException e) {
      LOG.warn(e);
      return null;
    }
  }

  public synchronized void close() {
    if (myClosed) return;
    myClosed = true;

    try {
      myClient.stop(true);
    } catch (DebuggerStoppedException e) {
      // OK
    } finally {
      myProcess.destroyProcess();
    }
  }

  @Override
  public void dispose() {
    detach(myProcess);
  }

  public static @NotNull XsltDebuggerSession create(Project project, @NotNull ProcessHandler process, Debugger client) {
    final XsltDebuggerSession session = new XsltDebuggerSession(project, process, client);
    process.putUserData(DEBUGGER_SESSION, session);
    return session;
  }

  public static XsltDebuggerSession getInstance(@NotNull ProcessHandler process) {
    return process.getUserData(DEBUGGER_SESSION);
  }

  public static void detach(ProcessHandler processHandler) {
    processHandler.putUserData(DEBUGGER_SESSION, null);
  }

  public interface Listener extends EventListener {
    void debuggerSuspended();

    void debuggerResumed();

    void debuggerStopped();
  }
}
