const fs = require('fs');

const TOTAL_STACK = 1024 * 1024; // 1MB
const TOTAL_MEMORY = 2 * 1024 * 1024; // 1MB
const WASM_PAGE_SIZE = 64 * 1024; // Defined in WebAssembly specs

const WASM_CODE = Buffer.from(require('./cephes.wasm.base64.json'), 'base64');

class CephesWrapper {
  constructor(sync) {
    // Initialize the runtime's memory
    this._wasmMemory = new WebAssembly.Memory({
      'initial': TOTAL_MEMORY / WASM_PAGE_SIZE,
      'maximum': TOTAL_MEMORY / WASM_PAGE_SIZE
    });

    this._HEAP8 = new Int8Array(this._wasmMemory.buffer);
    this._HEAP16 = new Int16Array(this._wasmMemory.buffer);
    this._HEAP32 = new Int32Array(this._wasmMemory.buffer);
    this._HEAPF32 = new Float32Array(this._wasmMemory.buffer);
    this._HEAPF64 = new Float64Array(this._wasmMemory.buffer);

    // Compile and export program
    if (sync) {
      // compile synchronously
      const program = this._compileSync();
      this._exportProgram(program);

      // create a dummy compile promise
      this.compiled = Promise.resolve();
    } else {
      // create a singleton compile promise
      this.compiled = this._compileAsync()
        .then((program) => this._exportProgram(program));
    }
  }

  _AsciiToString(ptr) {
    let str = '';
    while (1) {
      const ch = this._HEAP8[((ptr++)>>0)];
      if (ch === 0) return str;
      str += String.fromCharCode(ch);
    }
  }

  _mtherr(name /* char* */, code /* int */) {
    // from mtherr.c
    let codemsg = '';
    switch (code) {
      case 1: codemsg = 'argument domain error'; break;
      case 2: codemsg = 'function singularity'; break;
      case 3: codemsg = 'overflow range error'; break;
      case 4: codemsg = 'underflow range error'; break;
      case 5: codemsg = 'total loss of precision'; break;
      case 6: codemsg = 'partial loss of precision'; break;
      case 33: codemsg = 'Unix domain error code'; break;
      case 34: codemsg = 'Unix range error code'; break;
      default: codemsg = 'unknown error';
    }

    const fnname = this._AsciiToString(name);
    const message = 'cephes reports "' + codemsg + '" in ' + fnname;

    // Restore stack to the STACKTOP before throwing. This only works because
    // all the exported cephes functions are plain functions.
    this.stackRestore(0);

    if (code == 1) {
      throw new RangeError(message);
    } else {
      throw new Error(message);
    }
  }

  _wasmImports() {
    return {
      'env': {
        // cephes error handler
        "_mtherr": this._mtherr.bind(this),

        // memory
        "memory": this._wasmMemory,
        "STACKTOP": 0,
        "STACK_MAX": TOTAL_STACK
      }
    };
  }

  _compileSync() {
    return new WebAssembly.Instance(
      new WebAssembly.Module(WASM_CODE),
      this._wasmImports()
    );
  }

  _compileAsync() {
    return WebAssembly.instantiate(
      WASM_CODE,
      this._wasmImports()
    ).then((results) => results.instance);
  }

  _exportProgram(program) {
    // export cephes functions
    for (const key of Object.keys(program.exports)) {
      if (key.startsWith('_cephes_')) {
        this[key] = program.exports[key];
      }
    }

    // export special stack functions
    this.stackAlloc = program.exports.stackAlloc;
    this.stackRestore = program.exports.stackRestore;
    this.stackSave = program.exports.stackSave;
  }

  // export helper functions
  getValue(ptr, type) {
    type = type || 'i8';
    if (type.charAt(type.length-1) === '*') type = 'i32'; // pointers are 32-bit
      switch(type) {
        case 'i1': return this._HEAP8[((ptr)>>0)];
        case 'i8': return this._HEAP8[((ptr)>>0)];
        case 'i16': return this._HEAP16[((ptr)>>1)];
        case 'i32': return this._HEAP32[((ptr)>>2)];
        case 'i64': return this._HEAP32[((ptr)>>2)];
        case 'float': return this._HEAPF32[((ptr)>>2)];
        case 'double': return this._HEAPF64[((ptr)>>3)];
        default: throw new Error('invalid type for getValue: ' + type);
      }
    return null;
  }

  writeArrayToMemory(array, buffer) {
    this._HEAP8.set(array, buffer);
  }
}

module.exports = CephesWrapper;