// Copyright 2006-2012 AdvancedTools. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.advancedtools.cpp.debugger;

import com.advancedtools.cpp.CppBundle;
import com.advancedtools.cpp.CppSupportLoader;
import com.advancedtools.cpp.CppSupportSettings;
import com.advancedtools.cpp.actions.CompileCppAction;
import com.advancedtools.cpp.build.BuildUtils;
import com.advancedtools.cpp.debugger.commands.*;
import com.advancedtools.cpp.debugger.remote.CppRemoteDebugParameters;
import com.advancedtools.cpp.facade.CppCodeFragment;
import com.advancedtools.cpp.psi.ICppCodeFragment;
import com.advancedtools.cpp.run.BaseCppConfiguration;
import com.advancedtools.cpp.run.BaseCppRunnerParameters;
import com.advancedtools.cpp.run.CppRunnerParameters;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.execution.ui.ConsoleView;
import com.intellij.execution.ui.ConsoleViewContentType;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.testFramework.LightVirtualFile;
import com.intellij.util.Alarm;
import com.intellij.xdebugger.XDebugProcess;
import com.intellij.xdebugger.XDebugSession;
import com.intellij.xdebugger.XSourcePosition;
import com.intellij.xdebugger.breakpoints.XBreakpointHandler;
import com.intellij.xdebugger.evaluation.EvaluationMode;
import com.intellij.xdebugger.evaluation.XDebuggerEditorsProvider;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.*;
import java.util.*;
import java.util.concurrent.LinkedBlockingDeque;

/**
 * User: maxim
 * Date: 28.03.2009
 * Time: 19:44:59
 */
public class CppDebugProcess<T extends BaseCppConfiguration> extends XDebugProcess {
  private final Process process;
  private final Alarm myOutputReadingAlarm;
  private final List<Closeable> myStreamsToClose = new ArrayList<Closeable>();
  private final LinkedBlockingDeque<DebuggerCommand> commandsToWrite = new LinkedBlockingDeque<DebuggerCommand>();

  private final CppDebuggerContext context;
  private ConsoleView myConsoleView;
  private final CppBreakpointManager myBreakpointManager;
  private CppBaseDebugRunner myRunner;
  private BaseCppConfiguration myConfiguration;
  private String commandLine = "";

  public CppDebugProcess(XDebugSession session, CppBaseDebugRunner<T> debugRunner, T runConfiguration) {
    super(session);

    BaseCppRunnerParameters cppRunnerParameters = runConfiguration.getRunnerParameters();
    List<String> commands = new ArrayList<String>();
    commands.add("--interpreter=mi");
    String toolCommandLine = CompileCppAction.getEscapedPathToFile(cppRunnerParameters.getExecutableName());
    commands.add(toolCommandLine);

    List<String> command = BuildUtils.buildGccToolCall(
      CppSupportSettings.getInstance().getGdbPath(),
      Arrays.asList(toolCommandLine)
    );

    myRunner = debugRunner;
    myConfiguration = runConfiguration;

    try {
      String path = debugRunner.getWorkingDirectory(runConfiguration);
      File dir = BaseCppConfiguration.isEmpty(path) ? null : new File(path);
      ProcessBuilder processBuilder = new ProcessBuilder(command);
      processBuilder.directory(dir);
      processBuilder.redirectErrorStream(true); // important for command request / response logic
      process = processBuilder.start();
      if (dir != null) commandLine += dir.getPath() + " > ";
    } catch (IOException ex) {
      Messages.showErrorDialog(getSession().getProject(), ex.getMessage(), CppBundle.message("cpp.debugger.startup.error"));

      throw new RuntimeException(ex);
    }

    commandLine += toolCommandLine;

    if (cppRunnerParameters instanceof CppRunnerParameters) {
      commandLine += " " + ((CppRunnerParameters)cppRunnerParameters).getExecutableParameters();
    } else {
      CppRemoteDebugParameters remoteParameters = (CppRemoteDebugParameters) cppRunnerParameters;
      if (!BaseCppConfiguration.isEmpty(remoteParameters.getHost())) {
        commandLine += "@" + remoteParameters.getHost() + ":" + remoteParameters.getPort();
      }
      commandLine += "#" + remoteParameters.getPid();
    }

    final OutputStream outputStream = process.getOutputStream();
    final InputStream inputStream = process.getInputStream();
    final InputStreamReader numberReader = new InputStreamReader(inputStream);

    myStreamsToClose.add(numberReader);
    myStreamsToClose.add(outputStream);

    context = new CppDebuggerContext() {
      public OutputStream getOutputStream() {
        return outputStream;
      }

      public InputStream getInputStream() {
        return inputStream;
      }

      public InputStreamReader getInputReader() {
        return numberReader;
      }

      public CppBreakpointManager getBreakpointManager() {
        return myBreakpointManager;
      }

      public void scheduleOutputReading() {
        myOutputReadingAlarm.addRequest(new Runnable() {
          public void run() {
            myOutputReadingAlarm.cancelAllRequests();
            try {
              if (inputStream.available() > 0) {
                sendCommand(new ReadOutputCommand());
              } else {
                myOutputReadingAlarm.addRequest(this, 100);
              }
            } catch (IOException ex) {
              // exits
            }
          }
        }, 100);
      }

      public void sendCommand(DebuggerCommand command) {
        CppDebugProcess.this.sendCommand(command);
      }

      public void sendAndProcessOneCommand(final DebuggerCommand command) {
        final DebuggerCommand delegatingCommand = new DebuggerCommand(command.getCommandText()) {
          @Override
          public String getCommandText() {
            return command.getCommandText();
          }

          @Override
          public void post(CppDebuggerContext context) throws IOException {
            command.post(context);
          }

          @Override
          public void readResponse(CppDebuggerContext context) throws IOException {
            command.readResponse(context);
          }
        };

        commandsToWrite.addFirst(delegatingCommand);
        try {
          processOneCommand();
        } catch (IOException ex) {
          ex.printStackTrace();
        }
      }

      public XDebugSession getSession() {
        return CppDebugProcess.this.getSession();
      }

      public ProcessHandler getProcessHandler() {
        return CppDebugProcess.this.getProcessHandler();
      }

      private StringBuilder builder = new StringBuilder();
      private final char[] buf = new char[8192];
      private static final String GDB_MARKER = "(gdb) ";
      private String myPreviousOutput;

      public String readLine(boolean tillMarker) throws IOException {
        String res = doRead(tillMarker);

        if (res != null) {
          builder.setLength(0);
          StringTokenizer tokenizer = new StringTokenizer(res, "\r\n");
          int start = 0;
          String defaultDelim = SystemInfo.isWindows ? "\r\n" : "\n";

          while(tokenizer.hasMoreElements()) {
            String next = tokenizer.nextToken();
            String delim = builder.length() == 0 ? "":defaultDelim;

            if (start > 0) {
              if (( next.length() > 2 &&  // detecting previous line carry over
                    next.charAt(0) == ' ' &&
                    next.charAt(1) == ' ' &&
                    next.charAt(2) == ' '
                  ) ||
                  ( next.length() > 0 && next.charAt(0) == '}')
              ) {
                delim = "";
              }
              else {
                start = 0;
              }
            }

            builder.append(delim);
            builder.append(next);
            ++start;
          }

          res = builder.toString();
        }
        return res;
      }

      private String doRead(boolean tillMarker) throws IOException {
        builder.setLength(0);

        if (myPreviousOutput != null) {
          int index = myPreviousOutput.indexOf(GDB_MARKER);

          if (index != -1) {
            String ret = myPreviousOutput.substring(0, index);
            myPreviousOutput = myPreviousOutput.substring(index + GDB_MARKER.length());
            if (myPreviousOutput.length() == 0) myPreviousOutput = null;
            return ret;
          } else {
            builder.append(myPreviousOutput);
            myPreviousOutput = null;
          }
        }

        InputStreamReader reader = context.getInputReader();

        while (true) {
          int read = reader.read(buf);
          if (read == -1) break;
          int builderLength = builder.length();
          builder.append(buf, 0, read);

          if (tillMarker) {
            int index = builder.indexOf(GDB_MARKER, builderLength);
            if (index != -1) {
              myPreviousOutput = builder.substring(index + GDB_MARKER.length());
              if (myPreviousOutput.length() == 0) myPreviousOutput = null;

              return builder.substring(0, index);
            }
          } else {
            return builder.toString();
          }

        }

        return null;
      }

      public void printToConsole(String s, ConsoleViewContentType contentType) {
        CppDebugProcess.this.printToConsole(s + "\n", contentType);
      }
    };

    myBreakpointManager = new CppBreakpointManager(context);

    sendCommand(new StartupCommand(debugRunner, runConfiguration));

    ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
        public void run() {
          try {
            while(true) {
              if (processOneCommand()) break;
            }
          } catch (IOException ex) {
            ex.printStackTrace();
          }

          closeStreams();
        }
    });

    myOutputReadingAlarm = new Alarm(Alarm.ThreadToUse.OWN_THREAD, getSession().getProject());
  }

  private boolean processOneCommand() throws IOException {
    final DebuggerCommand command = commandsToWrite.removeFirst();
    command.post(context);
    if (command instanceof QuitCommand) return true;
    command.readResponse(context);
    return false;
  }

  private void closeStreams() {
    for(Closeable c:myStreamsToClose) {
      try {
        c.close();
      } catch (IOException ex) {}
    }
  }

  @Override
  public void sessionInitialized() {
    super.sessionInitialized();

    myConsoleView = (ConsoleView)getSession().getRunContentDescriptor().getExecutionConsole();

    sendCommand(new DebuggerCommand(myRunner.getRunCommandText(myConfiguration, this)));

    context.scheduleOutputReading();
    printToConsole(commandLine + "\n", ConsoleViewContentType.SYSTEM_OUTPUT);
//    getSession().setPauseActionSupported(true); // TODO:
  }

  void sendCommand(DebuggerCommand command) {
    commandsToWrite.addLast(command);
  }

  @Override
  public XDebuggerEditorsProvider getEditorsProvider() {
    return new XDebuggerEditorsProvider() {
      @NotNull
      @Override
      public FileType getFileType() {
        return CppSupportLoader.CPP_FILETYPE;
      }

      @NotNull
      @Override
      public Document createDocument(@NotNull Project project, @NotNull String s, @Nullable XSourcePosition xSourcePosition, @NotNull EvaluationMode evaluationMode) {
        VirtualFile virtualFile = new LightVirtualFile("dummy.cpp", s);
        ICppCodeFragment file = new CppCodeFragment(new SingleRootFileViewProvider(PsiManager.getInstance(project), virtualFile, true));
        return PsiDocumentManager.getInstance(project).getDocument(file);
      }
    };
  }

  public void startStepOver() {
    commandsToWrite.addLast(new DebuggerCommand("next"));
  }

  public void startStepInto() {
    commandsToWrite.addLast(new DebuggerCommand("step"));
  }

  public void startStepOut() {
    commandsToWrite.addLast(new DebuggerCommand("finish"));
  }

  public void stop() {
    sendCommand(new QuitCommand(myRunner, myConfiguration));
  }

  public void resume() {
    sendCommand(new ContinueCommand());
  }

  @Override
  public void startPausing() {
// BuildUtils.buildGccToolCall("sh", "kill -s SIGINT "+ sessionData.getPid())
//    sendCommand(new DebuggerCommand(""));
  }

  public void runToPosition(@NotNull XSourcePosition position) {
  }

  @Override
  public XBreakpointHandler<?>[] getBreakpointHandlers() {
    return myBreakpointManager.getBreakpointHandlers();
  }

  public void printToConsole(String s, ConsoleViewContentType contentType) {
    myConsoleView.print(s, contentType);
  }

  public void addCloseableOnDebuggingEnd(Closeable file) {
    myStreamsToClose.add(file);
  }
}