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

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

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

  constructor(useWebWorker: boolean = false, worker?: Worker) {
    this.seq = 0;
    if (useWebWorker) {
      this.worker = worker ?? new Worker("./worker/dist/truffle-debugger-worker.js");
    } else {
      this.worker = new DebuggerWorker();
    }
    this.initialized = false;
    this.resolverMap = new Map();
    this.worker.onmessage = (ev: any) => {
      this.handleResponse(ev.data)
    }
  }

  public async initialize(init_arguments: InitArguments) {
    this.finished = false;
    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.StepOut);
  }

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

  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);
  }

  private sendRequest(command: Command, args?: any): Promise<any> {
    if (this.finished && !this.breakpointCommads.includes(command)) {
      throw new Error("Trace already finishes.");
    }
    const request: DebugProtocol.Request = {
      command: command,
      seq: this.seq,
      arguments: args,
      type: "request"
    }
    this.worker.postMessage(request);
    return new Promise((resolve, reject) => {
      this.resolverMap.set(this.seq, resolve);
      this.seq ++;
    })
  }

  private handleResponse(resp: DebugProtocol.Response) {
    if (resp.command === Command.Initialize) {
      this.initialized = true;
    }
    if (this.commandsReturnLocation.includes(resp.command) && resp.body == null) {
      this.finished = true;
    }
    if (this.resolverMap.has(resp.request_seq)) {
      this.resolverMap.get(resp.request_seq)!(resp.body);
      this.resolverMap.delete(resp.request_seq);
    }
  }
}
