/*
 * Copyright (c) 2015, the Dart project authors.
 *
 * Licensed under the Eclipse Public License v1.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.eclipse.org/legal/epl-v10.html
 *
 * 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.dartlang.vm.service;

import com.google.gson.JsonObject;
import org.dartlang.vm.service.consumer.*;
import org.dartlang.vm.service.element.*;
import org.dartlang.vm.service.logging.Logger;
import org.dartlang.vm.service.logging.Logging;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;

public class VmServiceTest {
  private static File dartVm;
  private static File sampleDart;
  private static File sampleDartWithException;
  private static int vmPort = 7575;
  private static Process process;
  private static VmService vmService;
  private static SampleOutPrinter sampleOut;
  private static SampleOutPrinter sampleErr;
  private static int actualVmServiceVersionMajor;

  public static void main(String[] args) {
    setupLogging();
    parseArgs(args);

    try {
      echoDartVmVersion();
      runSample();
      runSampleWithException();
      System.out.println("Test Complete");
    } finally {
      vmDisconnect();
      stopSample();
    }
  }

  private static void echoDartVmVersion() {
    // Echo Dart VM version
    List<String> processArgs = new ArrayList<>();
    processArgs.add(dartVm.getAbsolutePath());
    processArgs.add("--version");
    ProcessBuilder processBuilder = new ProcessBuilder(processArgs);
    try {
      process = processBuilder.start();
    } catch (IOException e) {
      throw new RuntimeException("Failed to launch Dart VM", e);
    }
    new SampleOutPrinter("version output", process.getInputStream());
    new SampleOutPrinter("version output", process.getErrorStream());
  }

  private static void finishExecution(SampleVmServiceListener vmListener, ElementList<IsolateRef> isolates) {
    // Finish execution
    vmResume(isolates.get(0), null);
    vmListener.waitFor(VmService.DEBUG_STREAM_ID, EventKind.Resume);

    // VM pauses on exit and must be resumed to cleanly terminate process
    vmListener.waitFor(VmService.DEBUG_STREAM_ID, EventKind.PauseExit);
    vmResume(isolates.get(0), null);
    vmListener.waitFor(VmService.ISOLATE_STREAM_ID, EventKind.IsolateExit);
    waitForProcessExit();

    sampleOut.assertLastLine("exiting");
    // TODO(devoncarew):
    //   vm-service: isolate(544050040) 'sample_main.dart:main()' has no debugger attached and is paused at start.
    //sampleErr.assertLastLine(null);
    process = null;
  }

  private static boolean isWindows() {
    return System.getProperty("os.name").startsWith("Win");
  }

  private static void parseArgs(String[] args) {
    if (args.length != 1) {
      showErrorAndExit("Expected absolute path to Dart SDK");
    }
    File sdkDir = new File(args[0]);
    if (!sdkDir.isDirectory()) {
      showErrorAndExit("Specified directory does not exist: " + sdkDir);
    }
    File binDir = new File(sdkDir, "bin");
    dartVm = new File(binDir, isWindows() ? "dart.exe" : "dart");
    if (!dartVm.isFile()) {
      showErrorAndExit("Cannot find Dart VM in SDK: " + dartVm);
    }
    File currentDir = new File(".").getAbsoluteFile();
    File projDir = currentDir;
    String projName = "vm_service";
    while (!projDir.getName().equals(projName)) {
      projDir = projDir.getParentFile();
      if (projDir == null) {
        showErrorAndExit("Cannot find project " + projName + " from " + currentDir);
        return;
      }
    }
    sampleDart = new File(projDir, "java/example/sample_main.dart".replace("/", File.separator));
    if (!sampleDart.isFile()) {
      showErrorAndExit("Cannot find sample: " + sampleDart);
    }
    sampleDartWithException = new File(projDir,
            "java/example/sample_exception.dart".replace("/", File.separator));
    if (!sampleDartWithException.isFile()) {
      showErrorAndExit("Cannot find sample: " + sampleDartWithException);
    }
    System.out.println("Using Dart SDK: " + sdkDir);
  }

  /**
   * Exercise VM service with "normal" sample.
   */
  private static void runSample() {
    SampleVmServiceListener vmListener = startSampleAndConnect(sampleDart);
    vmGetVersion();
    ElementList<IsolateRef> isolates = vmGetVmIsolates();
    Isolate sampleIsolate = vmGetIsolate(isolates.get(0));
    Library rootLib = vmGetLibrary(sampleIsolate, sampleIsolate.getRootLib());
    vmGetScript(sampleIsolate, rootLib.getScripts().get(0));
    vmCallServiceExtension(sampleIsolate);

    // Run to breakpoint on line "foo(1);"
    vmAddBreakpoint(sampleIsolate, rootLib.getScripts().get(0), 25);
    vmListener.waitFor(VmService.DEBUG_STREAM_ID, EventKind.BreakpointAdded);
    vmResume(isolates.get(0), null);
    vmListener.waitFor(VmService.DEBUG_STREAM_ID, EventKind.Resume);
    vmListener.waitFor(VmService.DEBUG_STREAM_ID, EventKind.PauseBreakpoint);
    sampleOut.assertLastLine("hello");

    // Get stack trace
    vmGetStack(sampleIsolate);

    // Evaluate
    vmEvaluateInFrame(sampleIsolate, 0, "deepList[0]");

    // Get coverage information
    vmGetSourceReport(sampleIsolate);

    // Step over line "foo(1);"
    vmResume(isolates.get(0), StepOption.Over);
    vmListener.waitFor(VmService.DEBUG_STREAM_ID, EventKind.Resume);
    vmListener.waitFor(VmService.DEBUG_STREAM_ID, EventKind.PauseBreakpoint);
    sampleOut.assertLastLine("val: 1");

    finishExecution(vmListener, isolates);
  }

  /**
   * Exercise VM service with sample that throws exceptions.
   */
  private static void runSampleWithException() {
    SampleVmServiceListener vmListener = startSampleAndConnect(sampleDartWithException);
    ElementList<IsolateRef> isolates = vmGetVmIsolates();
    Isolate sampleIsolate = vmGetIsolate(isolates.get(0));

    // Run until exception occurs
    vmPauseOnException(isolates.get(0), ExceptionPauseMode.All);
    vmResume(isolates.get(0), null);
    vmListener.waitFor(VmService.DEBUG_STREAM_ID, EventKind.Resume);
    Event event = vmListener.waitFor(VmService.DEBUG_STREAM_ID, EventKind.PauseException);
    InstanceRefToString convert = new InstanceRefToString(sampleIsolate, vmService, new OpLatch());
    System.out.println("Received PauseException event");
    System.out.println("  Exception: " + convert.toString(event.getException()));
    System.out.println("  Top Frame:");
    showFrame(convert, event.getTopFrame());
    sampleOut.assertLastLine("hello");

    finishExecution(vmListener, isolates);
  }

  private static void setupLogging() {
    Logging.setLogger(new Logger() {
      @Override
      public void logError(String message) {
        System.out.println("Log error: " + message);
      }

      @Override
      public void logError(String message, Throwable exception) {
        System.out.println("Log error: " + message);
        if (exception != null) {
          System.out.println("Log error exception: " + exception);
          exception.printStackTrace();
        }
      }

      @Override
      public void logInformation(String message) {
        System.out.println("Log info: " + message);
      }

      @Override
      public void logInformation(String message, Throwable exception) {
        System.out.println("Log info: " + message);
        if (exception != null) {
          System.out.println("Log info exception: " + exception);
          exception.printStackTrace();
        }
      }
    });
  }

  private static void showErrorAndExit(String errMsg) {
    System.out.println(errMsg);
    System.out.flush();
    sleep(10);
    System.out.println("Usage: VmServiceTest /path/to/Dart/SDK");
    System.exit(1);
  }

  private static void showFrame(InstanceRefToString convert, Frame frame) {
    System.out.println("    #" + frame.getIndex() + " " + frame.getFunction().getName() + " ("
            + frame.getLocation().getScript().getUri() + ")");
    for (BoundVariable var : frame.getVars()) {
      InstanceRef instanceRef = (InstanceRef)var.getValue();
      System.out.println("      " + var.getName() + " = " + convert.toString(instanceRef));
    }
  }

  private static void showRPCError(RPCError error) {
    System.out.println(">>> Received error response");
    System.out.println("  Code: " + error.getCode());
    System.out.println("  Message: " + error.getMessage());
    System.out.println("  Details: " + error.getDetails());
    System.out.println("  Request: " + error.getRequest());
  }

  private static void showSentinel(Sentinel sentinel) {
    System.out.println(">>> Received sentinel response");
    System.out.println("  Sentinel kind: " + sentinel.getKind());
    System.out.println("  Sentinel value: " + sentinel.getValueAsString());
  }

  private static void sleep(int milliseconds) {
    try {
      Thread.sleep(milliseconds);
    } catch (InterruptedException e) {
      // ignored
    }
  }

  private static void startSample(File dartFile) {
    List<String> processArgs;
    ProcessBuilder processBuilder;

    // Use new port to prevent race conditions
    // between one sample releasing a port
    // and the next sample using it.
    ++vmPort;

    processArgs = new ArrayList<>();
    processArgs.add(dartVm.getAbsolutePath());
    processArgs.add("--pause_isolates_on_start");
    processArgs.add("--observe");
    processArgs.add("--enable-vm-service=" + vmPort);
    processArgs.add("--disable-service-auth-codes");
    processArgs.add(dartFile.getAbsolutePath());
    processBuilder = new ProcessBuilder(processArgs);
    System.out.println("=================================================");
    System.out.println("Launching sample: " + dartFile);
    try {
      process = processBuilder.start();
    } catch (IOException e) {
      throw new RuntimeException("Failed to launch Dart sample", e);
    }
    // Echo sample application output to System.out
    sampleOut = new SampleOutPrinter("stdout", process.getInputStream());
    sampleErr = new SampleOutPrinter("stderr", process.getErrorStream());
    System.out.println("Dart process started - port " + vmPort);
  }

  private static SampleVmServiceListener startSampleAndConnect(File dartFile) {
    startSample(dartFile);
    sleep(1000);
    vmConnect();
    SampleVmServiceListener vmListener = new SampleVmServiceListener(
            new HashSet<>(Collections.singletonList(EventKind.BreakpointResolved)));
    vmService.addVmServiceListener(vmListener);
    vmStreamListen(VmService.DEBUG_STREAM_ID);
    vmStreamListen(VmService.ISOLATE_STREAM_ID);
    return vmListener;
  }

  private static void stopSample() {
    if (process == null) {
      return;
    }
    final Process processToStop = process;
    process = null;
    long endTime = System.currentTimeMillis() + 5000;
    while (System.currentTimeMillis() < endTime) {
      try {
        int exit = processToStop.exitValue();
        if (exit != 0) {
          System.out.println("Sample exit code: " + exit);
        }
        return;
      } catch (IllegalThreadStateException e) {
        //$FALL-THROUGH$
      }
      try {
        Thread.sleep(20);
      } catch (InterruptedException e) {
        //$FALL-THROUGH$
      }
    }
    processToStop.destroy();
    System.out.println("Terminated sample process");
  }

  @SuppressWarnings("SameParameterValue")
  private static void vmAddBreakpoint(Isolate isolate, ScriptRef script, int lineNum) {
    final OpLatch latch = new OpLatch();
    vmService.addBreakpoint(isolate.getId(), script.getId(), lineNum, new AddBreakpointConsumer() {
      @Override
      public void onError(RPCError error) {
        showRPCError(error);
      }

      @Override
      public void received(Breakpoint response) {
        System.out.println("Received Breakpoint response");
        System.out.println("  BreakpointNumber:" + response.getBreakpointNumber());
        latch.opComplete();
      }

      @Override
      public void received(Sentinel response) {
        showSentinel(response);
      }
    });
    latch.waitAndAssertOpComplete();
  }

  private static void vmConnect() {
    try {
      vmService = VmService.localConnect(vmPort);
    } catch (IOException e) {
      throw new RuntimeException("Failed to connect to the VM vmService service", e);
    }
  }

  private static void vmDisconnect() {
    if (vmService != null) {
      vmService.disconnect();
    }
  }

  @SuppressWarnings("SameParameterValue")
  private static void vmEvaluateInFrame(Isolate isolate, int frameIndex, String expression) {
    System.out.println("Evaluating: " + expression);
    final ResultLatch<InstanceRef> latch = new ResultLatch<>();
    vmService.evaluateInFrame(isolate.getId(), frameIndex, expression, new EvaluateInFrameConsumer() {
      @Override
      public void onError(RPCError error) {
        showRPCError(error);
      }

      @Override
      public void received(ErrorRef response) {
        showErrorAndExit(response.getMessage());
      }

      public void received(Sentinel response) {
        System.out.println(response.getValueAsString());
      }

      @Override
      public void received(InstanceRef response) {
        System.out.println("Received InstanceRef response");
        System.out.println("  Id: " + response.getId());
        System.out.println("  Kind: " + response.getKind());
        System.out.println("  Json: " + response.getJson());
        latch.setValue(response);
      }
    });
    InstanceRef instanceRef = latch.getValue();
    InstanceRefToString convert = new InstanceRefToString(isolate, vmService, latch);
    System.out.println("Result: " + convert.toString(instanceRef));
  }

  private static SourceReport vmGetSourceReport(Isolate isolate) {
    System.out.println("Getting coverage information for " + isolate.getId());
    final long startTime = System.currentTimeMillis();
    final ResultLatch<SourceReport> latch = new ResultLatch<>();
    vmService.getSourceReport(isolate.getId(), Collections.singletonList(SourceReportKind.Coverage), new GetSourceReportConsumer() {
      @Override
      public void onError(RPCError error) {
        showRPCError(error);
      }

      @Override
      public void received(SourceReport response) {
        System.out.println("Received SourceReport response (" + (System.currentTimeMillis() - startTime) + "ms)");
        System.out.println("  Script count: " + response.getScripts().size());
        System.out.println("  Range count: " + response.getRanges().size());
        latch.setValue(response);
      }

      @Override
      public void received(Sentinel response) {
        showSentinel(response);
      }
    });
    return latch.getValue();
  }

  private static Isolate vmGetIsolate(IsolateRef isolate) {
    final ResultLatch<Isolate> latch = new ResultLatch<>();
    vmService.getIsolate(isolate.getId(), new GetIsolateConsumer() {
      @Override
      public void onError(RPCError error) {
        showRPCError(error);
      }

      @Override
      public void received(Isolate response) {
        System.out.println("Received Isolate response");
        System.out.println("  Id: " + response.getId());
        System.out.println("  Name: " + response.getName());
        System.out.println("  Number: " + response.getNumber());
        System.out.println("  Start Time: " + response.getStartTime());
        System.out.println("  RootLib Id: " + response.getRootLib().getId());
        System.out.println("  RootLib Uri: " + response.getRootLib().getUri());
        System.out.println("  RootLib Name: " + response.getRootLib().getName());
        System.out.println("  RootLib Json: " + response.getRootLib().getJson());
        System.out.println("  Isolate: " + response);
        latch.setValue(response);
      }

      @Override
      public void received(Sentinel response) {
        showSentinel(response);
      }
    });
    return latch.getValue();
  }

  private static Library vmGetLibrary(Isolate isolateId, LibraryRef library) {
    final ResultLatch<Library> latch = new ResultLatch<>();
    vmService.getLibrary(isolateId.getId(), library.getId(), new GetLibraryConsumer() {
      @Override
      public void onError(RPCError error) {
        showRPCError(error);
      }

      @Override
      public void received(Library response) {
        System.out.println("Received GetLibrary library");
        System.out.println("  uri: " + response.getUri());
        latch.setValue(response);
      }
    });
    return latch.getValue();
  }

  private static void vmGetScript(Isolate isolate, ScriptRef scriptRef) {
    final ResultLatch<Script> latch = new ResultLatch<>();
    vmService.getObject(isolate.getId(), scriptRef.getId(), new GetObjectConsumer() {
      @Override
      public void onError(RPCError error) {
        showRPCError(error);
      }

      @Override
      public void received(Obj response) {
        if (response instanceof Script) {
          latch.setValue((Script) response);
        } else {
          RPCError.unexpected("Script", response);
        }
      }

      @Override
      public void received(Sentinel response) {
        RPCError.unexpected("Script", response);
      }
    });
    Script script = latch.getValue();
    System.out.println("Received Script");
    System.out.println("  Id: " + script.getId());
    System.out.println("  Uri: " + script.getUri());
    System.out.println("  Source: " + script.getSource());
    System.out.println("  TokenPosTable: " + script.getTokenPosTable());
    if (script.getTokenPosTable() == null) {
      showErrorAndExit("Expected TokenPosTable to be non-null");
    }
  }

  private static void vmGetStack(Isolate isolate) {
    final ResultLatch<Stack> latch = new ResultLatch<>();
    vmService.getStack(isolate.getId(), new GetStackConsumer() {
      @Override
      public void onError(RPCError error) {
        showRPCError(error);
      }

      @Override
      public void received(Stack stack) {
        latch.setValue(stack);
      }

      @Override
      public void received(Sentinel response) {
        showSentinel(response);
      }
    });
    Stack stack = latch.getValue();
    System.out.println("Received Stack response");
    System.out.println("  Messages:");
    for (Message message : stack.getMessages()) {
      System.out.println("    " + message.getName());
    }
    System.out.println("  Frames:");
    InstanceRefToString convert = new InstanceRefToString(isolate, vmService, latch);
    for (Frame frame : stack.getFrames()) {
      showFrame(convert, frame);
    }
  }

  private static void vmGetVersion() {
    final OpLatch latch = new OpLatch();
    vmService.getVersion(new VersionConsumer() {
      @Override
      public void onError(RPCError error) {
        showRPCError(error);
      }

      @Override
      public void received(Version response) {
        System.out.println("Received Version response");
        actualVmServiceVersionMajor = response.getMajor();
        System.out.println("  Major: " + actualVmServiceVersionMajor);
        System.out.println("  Minor: " + response.getMinor());
        System.out.println(response.getJson());
        latch.opComplete();
      }
    });
    latch.waitAndAssertOpComplete();
  }

  private static void vmCallServiceExtension(Isolate isolateId) {
    final OpLatch latch = new OpLatch();
    vmService.callServiceExtension(isolateId.getId(), "getIsolate", new ServiceExtensionConsumer() {
      @Override
      public void onError(RPCError error) {
        showRPCError(error);
      }

      @Override
      public void received(JsonObject result) {
        System.out.println("Received response: " + result);
        latch.opComplete();
      }
    });
    latch.waitAndAssertOpComplete();
  }

  private static ElementList<IsolateRef> vmGetVmIsolates() {
    final ResultLatch<ElementList<IsolateRef>> latch = new ResultLatch<>();
    vmService.getVM(new VMConsumer() {
      @Override
      public void onError(RPCError error) {
        showRPCError(error);
      }

      @Override
      public void received(VM response) {
        System.out.println("Received VM response");
        System.out.println("  ArchitectureBits: " + response.getArchitectureBits());
        System.out.println("  HostCPU: " + response.getHostCPU());
        System.out.println("  TargetCPU: " + response.getTargetCPU());
        System.out.println("  Pid: " + response.getPid());
        System.out.println("  StartTime: " + response.getStartTime());
        for (IsolateRef isolate : response.getIsolates()) {
          System.out.println("  Isolate " + isolate.getNumber() + ", " + isolate.getId() + ", "
              + isolate.getName());
        }
        latch.setValue(response.getIsolates());
      }
    });
    return latch.getValue();
  }

  private static void vmPauseOnException(IsolateRef isolate, ExceptionPauseMode mode) {
    System.out.println("Request pause on exception: " + mode);
    final OpLatch latch = new OpLatch();
    vmService.setIsolatePauseMode(isolate.getId(), mode, /*shouldPauseOnExit=*/true, new SetIsolatePauseModeConsumer() {
        @Override
        public void onError(RPCError error) {
            showRPCError(error);
        }

        @Override
        public void received(Success response) {
            System.out.println("Successfully set pause on exception");
            latch.opComplete();
        }

        @Override
        public void received(Sentinel response) {
          showSentinel(response);
        }
    });
    latch.waitAndAssertOpComplete();
  }

  private static void vmResume(IsolateRef isolateRef, final StepOption step) {
    final String id = isolateRef.getId();
    vmService.resume(id, step, null, new ResumeConsumer() {
      @Override
      public void onError(RPCError error) {
        showRPCError(error);
      }

      @Override
      public void received(Success response) {
        if (step == null) {
          System.out.println("Resumed isolate " + id);
        } else {
          System.out.println("Step " + step + " isolate " + id);
        }
      }

      @Override
      public void received(Sentinel response) {
        showSentinel(response);
      }
    });
    // Do not wait for confirmation, but display error if it occurs
  }

  private static void vmStreamListen(String streamId) {
    final OpLatch latch = new OpLatch();
    vmService.streamListen(streamId, new SuccessConsumer() {
      @Override
      public void onError(RPCError error) {
        showRPCError(error);
      }

      @Override
      public void received(Success response) {
        System.out.println("Subscribed to debug event stream");
        latch.opComplete();
      }
    });
    latch.waitAndAssertOpComplete();
  }

  private static void waitForProcessExit() {
    if (actualVmServiceVersionMajor == 2) {
      // Don't wait for VM 1.12 - protocol 2.1
      return;
    }
    long end = System.currentTimeMillis() + 5000;
    while (true) {
      try {
        System.out.println("Exit code: " + process.exitValue());
        return;
      } catch (IllegalThreadStateException e) {
        // fall through to wait for exit
      }
      if (System.currentTimeMillis() >= end) {
        throw new RuntimeException("Expected child process to finish");
      }
      sleep(10);
    }
  }
}
