import { SentioProvider } from "./sentio-provider";
import { Breakpoint, InitArguments, Variables, Location, Stackframe } from "./types";
const truffle_debugger = require("@truffle/debugger");
let { data, evm, trace, session, sourcemapping, stacktrace, controller } = truffle_debugger.selectors;

export class Debugger {
  private static _instance: any;
  private truffle_debugger: any;
  private readonly sectionPrintouts = new Set(["bui", "glo", "con", "loc"]);

  private constructor() {

  }
  
  public initialized: boolean = false;

  public static get Instance() {
    return this._instance || (this._instance = new this())
  }

  public async initialize(init_arguments: InitArguments) {
    const provider = new SentioProvider(init_arguments.providerURL, { keepAlive: false });
    await provider.init(init_arguments.txnHash, init_arguments.chainId);
    this.truffle_debugger = await truffle_debugger.forTx(init_arguments.txnHash, { provider: provider, compilations: init_arguments.shimmedCompilations, lightMode: true });
    await this.truffle_debugger.startFullMode();
    this.initialized = true;
  }

  public async reset() {
    if (this.truffle_debugger.view(session.status.loaded)) {
      await this.truffle_debugger.reset();
    }
  }

  public async stepOver() {
    await this.truffle_debugger.stepOver();
  }

  public async stepInto() {
    await this.truffle_debugger.stepInto();
  }

  public async stepOut() {
    await this.truffle_debugger.stepOut();
  }

  public async stepNext() {
    await this.truffle_debugger.stepNext();
  }

  public async advance() {
    await this.truffle_debugger.advance();
  }

  public async continueUntilBreakpoint() {
    await this.truffle_debugger.continueUntilBreakpoint();
  }

  public async addBreakpoint(breakpoint: Breakpoint) {
    await this.truffle_debugger.addBreakpoint(this.determinBreakPoint(breakpoint));
  }

  public async removeBreakpoint(breakpoint: Breakpoint) {
    await this.truffle_debugger.removeBreakpoint(this.determinBreakPoint(breakpoint));
  }

  public async removeAllBreakpoints() {
    await this.truffle_debugger.removeAllBreakpoints();
  }

  public async variables(): Promise<Variables> {
    const values = await this.truffle_debugger.variables({ indicateUnknown: true });
    const sections = this.truffle_debugger.view(data.current.identifiers.sections);

    const sectionVars: Variables = {
      builtin: [],
      global: [],
      contract: [],
      local: []
    };

    for (const [section, variables] of Object.entries(sections)) {
      // only check the first 3 characters of each name given in the input sectionPrintouts
      // since each section name defined in the constructor contains 3 characters
      const includeThisSection = this.sectionPrintouts.has(section.slice(0, 3));
      //@ts-ignore
      if (includeThisSection && variables.length > 0) {
        //@ts-ignore
        for (const variable of variables) {
          const value = values[variable];
          //@ts-ignore
          sectionVars[section].push({
            name: variable,
            raw: value
          });
        }
      }
    }

    return sectionVars;
  }

  public stackTrace(): Stackframe[] {
    let final: boolean = false;
    if (this.truffle_debugger.view(session.status.loaded)) {
      //print final report if finished & failed, intermediate if not
      if (
        this.truffle_debugger.view(trace.finished) &&
        !this.truffle_debugger.view(evm.transaction.status)
      ) {
        final = true; //print final stack trace
      } else {
        final = false; //intermediate call stack
      }
    }
    let report = final
      ? this.truffle_debugger.view(stacktrace.current.finalReport)
      : this.truffle_debugger.view(stacktrace.current.report);
    return report;
  }

  public currentLocation(): Location | undefined {
    if (this.truffle_debugger.view(trace.finishedOrUnloaded)) {
      return undefined;
    }
    const currentLocation = this.truffle_debugger.view(controller.current.location);
    const currentLines = currentLocation.sourceRange
      ? currentLocation.sourceRange.lines
      : null;
    const currentLength = currentLocation.sourceRange
      ? currentLocation.sourceRange.length
      : null;
    const currentSourceId = currentLocation.source
      ? currentLocation.source.id
      : null;

    //search sources for given string
    let sources = Object.values(
      this.truffle_debugger.view(sourcemapping.views.sources)
    );
    //we will indeed need the sources here, not just IDs
    let matchingSources = sources.filter(source =>
      // @ts-ignore
      source.id === currentSourceId
    );

    return {
      // @ts-ignore
      compilationId: matchingSources[0].compilationId,
      lines: currentLines,
      length: currentLength,
      // @ts-ignore
      sourcePath: matchingSources[0].sourcePath,
    }
  }

  public async terminate() {
    this.truffle_debugger = null;
    this.initialized = false;
  }

  private determinBreakPoint(breakPoint: Breakpoint) {
    //search sources for given string
    let sources = Object.values(
      this.truffle_debugger.view(sourcemapping.views.sources)
    );
    //we will indeed need the sources here, not just IDs
    let matchingSources = sources.filter(source =>
      // @ts-ignore
      source.sourcePath === breakPoint.sourcePath && source.compilationId === breakPoint.compilationId
    );

    return {
        // @ts-ignore
        sourceId: matchingSources[0].id,
        line: breakPoint.line
      }
  }
}
