import { DebugProtocol } from "@vscode/debugprotocol";
import { DebuggerWorker } from "./debugger-worker";
import { Breakpoint, Command, InitArguments, Stackframe, Variables, Location, Trace } from "./types";

export class DebuggerClient {
  worker: Worker | DebuggerWorker
  seq: number;
  initialized: boolean;
  resolverMap: Map<number, (value: unknown) => void>;
  finished: boolean;
  requestTimeout = 60000;
  createWorker: () => Worker | DebuggerWorker;
  private readonly commandsReturnLocation: string[] = [
    Command.StepOver,
    Command.StepInto,
    Command.StepNext,
    Command.StepOut,
    Command.StepInstruction,
    Command.ContinueUntilBreakpoint,
    Command.Reset,
    Command.Step,
  ];

  private readonly breakpointCommads: string[] = [
    Command.AddBreakpoint,
    Command.RemoveAllBreakpoints,
    Command.RemoveBreakpoint
  ];

  constructor(createWorker?: () => Worker) {
    this.seq = 0;
    this.createWorker = createWorker?? (() => new DebuggerWorker());
    this.worker = this.createWorker();
    this.initialized = false;
    this.resolverMap = new Map();
  }

  public async initialize(init_arguments: InitArguments): Promise<Location> {
    this.finished = false;
    return await this.sendRequest(Command.Initialize, init_arguments);
  }

  public async reset(): Promise<Location> {
    this.finished = false;
    return await this.sendRequest(Command.Reset);
  }

  public async stepOver(): Promise<Location> {
    return await this.sendRequest(Command.StepOver)
  }

  public async stepInto(): Promise<Location> {
    return await this.sendRequest(Command.StepInto);
  }

  public async stepOut(): Promise<Location> {
    return await this.sendRequest(Command.StepOut);
  }

  public async stepNext(): Promise<Location> {
    return await this.sendRequest(Command.StepNext);
  }

  public async advance(steps?: number): Promise<Location> {
    return await this.sendRequest(Command.StepInstruction, steps);
  }

  public async continueUntilBreakpoint(): Promise<Location> {
    return await this.sendRequest(Command.ContinueUntilBreakpoint);
  }

  public async addBreakpoint(breakpoint: Breakpoint) {
    await this.sendRequest(Command.AddBreakpoint, breakpoint);
  }

  public async removeBreakpoint(breakpoint: Breakpoint) {
    await this.sendRequest(Command.RemoveAllBreakpoints, breakpoint);
  }

  public async removeAllBreakpoints() {
    await this.sendRequest(Command.RemoveAllBreakpoints)
  }

  public async requireVariables(): Promise<Variables> {
    return await this.sendRequest(Command.Variables);
  }

  public async requireStacktrace(): Promise<Stackframe[]> {
    return await this.sendRequest(Command.Stacktrace);
  }

  public async evaluateExpression(expression: string) {
    return await this.sendRequest(Command.Evaluate, expression);
  }

  public async runToEnd() {
    await this.sendRequest(Command.RunToEnd);
  }

  public async requireCurrentStep(): Promise<any> {
    return await this.sendRequest(Command.Step);
  }

  public async getCallTrace(): Promise<Trace[]> {
    return await this.sendRequest(Command.CallTrace);
  }

  private sendRequest(command: Command, args?: any): Promise<any> {
    // wrap the timeout
    return this.timeoutPromise(this.requestTimeout, this.internalSendRequest(command, args));
  }

  private internalSendRequest(command: Command, args?: any): Promise<any> {
    return new Promise((res, rej) => {
      const channel = new MessageChannel();
      channel.port1.onmessage = ({data}) => {
        channel.port1.close();
        const resp = data as DebugProtocol.Response;
        if (resp.command === Command.Initialize) {
          this.initialized = true;
        }
        if (this.commandsReturnLocation.includes(resp.command) && resp.body == null) {
          this.finished = true;
        }
        if (resp.success) {
          res(resp.body);
        } else {
          rej(resp.message);
        }
      };
      if (this.finished && !this.breakpointCommads.includes(command)) {
        rej("Trace already finishes.");
      } else {
        const request: DebugProtocol.Request = {
          command: command,
          seq: this.seq++,
          arguments: args,
          type: "request"
        }
        this.worker.postMessage(request, [channel.port2]);
      }
    });
  }

  private timeoutPromise(ms: number, promise: Promise<any>) {
    return new Promise((resolve, reject) => {
      const timeoutId = setTimeout(async () => {
        // Re-init the worker since the previous worker hangs
        this.worker.terminate();
        this.worker = this.createWorker();
        reject(new Error("promise timeout"))
      }, ms);
      promise.then(
        (res) => {
          clearTimeout(timeoutId);
          resolve(res);
        },
        (err) => {
          clearTimeout(timeoutId);
          reject(err);
        }
      );
    })
  }
}
