// TODO(sven): add flow in here

import { isSignature, isNumberLiteral } from "@webassemblyjs/ast";
import { assert } from "mamacro";

export function moduleContextFromModuleAST(m) {
  const moduleContext = new ModuleContext();

  assert(m.type === "Module");

  m.fields.forEach(field => {
    switch (field.type) {
      case "Start": {
        moduleContext.setStart(field.index);
        break;
      }
      case "TypeInstruction": {
        moduleContext.addType(field);
        break;
      }
      case "Func": {
        moduleContext.addFunction(field);
        break;
      }
      case "Global": {
        moduleContext.defineGlobal(field);
        break;
      }
      case "ModuleImport": {
        switch (field.descr.type) {
          case "GlobalType": {
            moduleContext.importGlobal(
              field.descr.valtype,
              field.descr.mutability
            );
            break;
          }
          case "Memory": {
            moduleContext.addMemory(
              field.descr.limits.min,
              field.descr.limits.max
            );
            break;
          }
          case "FuncImportDescr": {
            moduleContext.importFunction(field.descr);
            break;
          }

          case "Table": {
            // FIXME(sven): not implemented yet
            break;
          }

          default:
            throw new Error(
              "Unsupported ModuleImport of type " +
                JSON.stringify(field.descr.type)
            );
        }
        break;
      }
      case "Memory": {
        moduleContext.addMemory(field.limits.min, field.limits.max);
        break;
      }
    }
  });

  return moduleContext;
}

/**
 * Module context for type checking
 */
export class ModuleContext {
  constructor() {
    this.funcs = [];
    this.funcsOffsetByIdentifier = [];

    this.types = [];

    this.globals = [];
    this.globalsOffsetByIdentifier = [];

    this.mems = [];

    // Current stack frame
    this.locals = [];
    this.labels = [];
    this.return = [];

    this.debugName = "unknown";

    this.start = null;
  }

  /**
   * Set start segment
   */
  setStart(index) {
    this.start = index.value;
  }

  /**
   * Get start function
   */
  getStart() {
    return this.start;
  }

  /**
   * Reset the active stack frame
   */
  newContext(debugName, expectedResult) {
    this.locals = [];
    this.labels = [expectedResult];
    this.return = expectedResult;
    this.debugName = debugName;
  }

  /**
   * Functions
   */
  addFunction(func /*: Func*/) {
    // eslint-disable-next-line prefer-const
    let { params: args = [], results: result = [] } = func.signature || {};

    args = args.map(arg => arg.valtype);

    this.funcs.push({ args, result });

    if (typeof func.name !== "undefined") {
      this.funcsOffsetByIdentifier[func.name.value] = this.funcs.length - 1;
    }
  }

  importFunction(funcimport) {
    if (isSignature(funcimport.signature)) {
      // eslint-disable-next-line prefer-const
      let { params: args, results: result } = funcimport.signature;
      args = args.map(arg => arg.valtype);

      this.funcs.push({ args, result });
    } else {
      assert(isNumberLiteral(funcimport.signature));

      const typeId = funcimport.signature.value;
      assert(this.hasType(typeId));

      const signature = this.getType(typeId);
      this.funcs.push({
        args: signature.params.map(arg => arg.valtype),
        result: signature.results
      });
    }

    if (typeof funcimport.id !== "undefined") {
      // imports are first, we can assume their index in the array
      this.funcsOffsetByIdentifier[funcimport.id.value] = this.funcs.length - 1;
    }
  }

  hasFunction(index) {
    return typeof this.getFunction(index) !== "undefined";
  }

  getFunction(index) {
    if (typeof index !== "number") {
      throw new Error("getFunction only supported for number index");
    }

    return this.funcs[index];
  }

  getFunctionOffsetByIdentifier(name) {
    assert(typeof name === "string");

    return this.funcsOffsetByIdentifier[name];
  }

  /**
   * Labels
   */
  addLabel(result) {
    this.labels.unshift(result);
  }

  hasLabel(index) {
    return this.labels.length > index && index >= 0;
  }

  getLabel(index) {
    return this.labels[index];
  }

  popLabel() {
    this.labels.shift();
  }

  /**
   * Locals
   */
  hasLocal(index) {
    return typeof this.getLocal(index) !== "undefined";
  }

  getLocal(index) {
    return this.locals[index];
  }

  addLocal(type) {
    this.locals.push(type);
  }

  /**
   * Types
   */
  addType(type) {
    assert(type.functype.type === "Signature");
    this.types.push(type.functype);
  }

  hasType(index) {
    return this.types[index] !== undefined;
  }

  getType(index) {
    return this.types[index];
  }

  /**
   * Globals
   */
  hasGlobal(index) {
    return this.globals.length > index && index >= 0;
  }

  getGlobal(index) {
    return this.globals[index].type;
  }

  getGlobalOffsetByIdentifier(name) {
    assert(typeof name === "string");

    return this.globalsOffsetByIdentifier[name];
  }

  defineGlobal(global /*: Global*/) {
    const type = global.globalType.valtype;
    const mutability = global.globalType.mutability;

    this.globals.push({ type, mutability });

    if (typeof global.name !== "undefined") {
      this.globalsOffsetByIdentifier[global.name.value] =
        this.globals.length - 1;
    }
  }

  importGlobal(type, mutability) {
    this.globals.push({ type, mutability });
  }

  isMutableGlobal(index) {
    return this.globals[index].mutability === "var";
  }

  isImmutableGlobal(index) {
    return this.globals[index].mutability === "const";
  }

  /**
   * Memories
   */
  hasMemory(index) {
    return this.mems.length > index && index >= 0;
  }

  addMemory(min, max) {
    this.mems.push({ min, max });
  }

  getMemory(index) {
    return this.mems[index];
  }
}
