/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
 *
 * 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.
 */
/**
 * Runtime environment that provide js libaries calls.
 */
import { Pointer } from "./ctypes";
import { LibraryProvider } from "./types";
import { assert } from "./support";
import * as ctypes from "./ctypes";

/**
 * Detect library provider from the importObject.
 *
 * @param importObject The import object.
 */
function detectLibraryProvider(
  importObject: Record<string, any>
): LibraryProvider | undefined {
  if (
    importObject["wasmLibraryProvider"] &&
    importObject["wasmLibraryProvider"]["start"] &&
    importObject["wasmLibraryProvider"]["imports"] !== undefined
  ) {
    const item = importObject as { wasmLibraryProvider: LibraryProvider };
    // create provider so that we capture imports in the provider.
    return {
      imports: item.wasmLibraryProvider.imports,
      start: (inst: WebAssembly.Instance): void => {
        item.wasmLibraryProvider.start(inst);
      },
    };
  } else if (importObject["imports"] && importObject["start"] !== undefined) {
    return importObject as LibraryProvider;
  } else if (importObject["wasiImport"] && importObject["start"] !== undefined) {
    // WASI
    return {
      imports: {
        "wasi_snapshot_preview1": importObject["wasiImport"],
      },
      start: (inst: WebAssembly.Instance): void => {
        importObject["start"](inst);
      }
    };
  } else {
    return undefined;
  }
}

/**
 * Environment to impelement most of the JS library functions.
 */
export class Environment implements LibraryProvider {
  logger: (msg: string) => void;
  imports: Record<string, any>;
  /**
   * Maintains a table of FTVMWasmPackedCFunc that the C part
   * can call via TVMWasmPackedCFunc.
   *
   * We maintain a separate table so that we can have un-limited amount
   * of functions that do not maps to the address space.
   */
  packedCFuncTable: Array<ctypes.FTVMFFIWasmSafeCallType | undefined> = [
    undefined,
  ];
  /**
   * Free table index that can be recycled.
   */
  packedCFuncTableFreeId: Array<number> = [];

  private libProvider?: LibraryProvider;

  constructor(
    importObject: Record<string, any> = {},
    logger: (msg: string) => void = console.log
  ) {
    this.logger = logger;
    this.libProvider = detectLibraryProvider(importObject);
    // get imports from the provider
    if (this.libProvider !== undefined) {
      this.imports = this.libProvider.imports;
    } else {
      this.imports = importObject;
    }
    // update with more functions
    this.imports.env = this.environment(this.imports.env);
  }

  /** Mark the start of the instance. */
  start(inst: WebAssembly.Instance): void {
    if (this.libProvider !== undefined) {
      this.libProvider.start(inst);
    }
  }

  private environment(initEnv: Record<string, any>): Record<string, any> {
    // default env can be overriden by libraries.
    const defaultEnv = {
      "__cxa_thread_atexit": (): void => {},
      "emscripten_notify_memory_growth": (index: number): void => {}
    };
    const wasmSafeCall: ctypes.FTVMFFIWasmSafeCallType = (
      self: Pointer,
      args: Pointer,
      num_args: number,
      result: Pointer
    ): number => {
      const cfunc = this.packedCFuncTable[self];
      assert(cfunc !== undefined);
      return cfunc(self, args, num_args, result);
    };

    const wasmFunctionDeleter: ctypes.FTVMFFIWasmFunctionDeleter = (
      self: Pointer
    ): void => {
      this.packedCFuncTable[self] = undefined;
      this.packedCFuncTableFreeId.push(self);
    };

    const newEnv = {
      "TVMFFIWasmSafeCall": wasmSafeCall,
      "TVMFFIWasmFunctionDeleter": wasmFunctionDeleter,
      "__console_log": (msg: string): void => {
        this.logger(msg);
      }
    };
    return Object.assign(defaultEnv, initEnv, newEnv);
  }
}
