/*
 * Copyright 2016 WebAssembly Community Group participants
 *
 * Licensed 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.
 */

var WABT_OK = 0;
var WABT_ERROR = 1;

/// features and default enabled state
const FEATURES = Object.freeze({
  'exceptions': false,
  'mutable_globals': true,
  'sat_float_to_int': true,
  'sign_extension': true,
  'simd': true,
  'threads': false,
  'function_references': false,
  'multi_value': true,
  'tail_call': false,
  'bulk_memory': true,
  'reference_types': true,
  'annotations': false,
  'code_metadata': false,
  'gc': false,
  'memory64': false,
  'multi_memory': false,
  'extended_const': false,
  'relaxed_simd': false,
});

/// If value is not undefined, return it. Otherwise return default_.
function maybeDefault(value, default_) {
  if (value === undefined) {
    return default_;
  }
  return value;
}

/// Coerce value to boolean if not undefined. Otherwise return default_.
function booleanOrDefault(value, default_) {
  return !!maybeDefault(value, default_);
}

/// Allocate memory in the Module.
function malloc(size) {
  var addr = Module._malloc(size);
  if (addr == 0) {
    throw new Error('out of memory');
  }
  return addr;
}

/// Convert an ArrayBuffer/TypedArray/string into a buffer that can be
/// used by the Module.
function allocateBuffer(buf) {
  var addr;
  var size;
  if (buf instanceof ArrayBuffer) {
    size = buf.byteLength;
    addr = malloc(size);
    (new Uint8Array(HEAP8.buffer, addr, size)).set(new Uint8Array(buf))
  } else if (ArrayBuffer.isView(buf)) {
    size = buf.buffer.byteLength;
    addr = malloc(size);
    (new Uint8Array(HEAP8.buffer, addr, size)).set(buf);
  } else if (typeof buf == 'string') {
    size = buf.length;
    addr = malloc(size);
    writeAsciiToMemory(buf, addr, true);  // don't null-terminate
  } else {
    throw new Error('unknown buffer type: ' + buf);
  }
  return {addr: addr, size: size};
}

function allocateCString(s) {
  var size = s.length;
  var addr = malloc(size);
  writeAsciiToMemory(s, addr);
  return {addr: addr, size: size};
}


/// Features
function Features(obj) {
  this.addr = Module._wabt_new_features();
  for ([f, v] of Object.entries(FEATURES)) {
    this[f] = booleanOrDefault(obj[f], v);
  }
}
Features.prototype = Object.create(Object.prototype);

Features.prototype.destroy = function() {
  Module._wabt_destroy_features(this.addr);
};

Object.keys(FEATURES).forEach(function(feature) {
  Object.defineProperty(Features.prototype, feature, {
    enumerable: true,
    get: function() {
      return Module['_wabt_' + feature + '_enabled'](this.addr);
    },
    set: function(newValue) {
      Module['_wabt_set_' + feature + '_enabled'](this.addr, newValue | 0);
    }
  });
});


/// Lexer
function Lexer(filename, buffer, errors) {
  this.filenameObj = allocateCString(filename);
  this.bufferObj = allocateBuffer(buffer);
  this.addr = Module._wabt_new_wast_buffer_lexer(
      this.filenameObj.addr, this.bufferObj.addr, this.bufferObj.size,
      errors.addr);
}
Lexer.prototype = Object.create(Object.prototype);

Lexer.prototype.destroy = function() {
  Module._wabt_destroy_wast_lexer(this.addr);
  Module._free(this.bufferObj.addr);
  Module._free(this.filenameObj.addr);
};


/// OutputBuffer
function OutputBuffer(addr) {
  this.addr = addr;
}
OutputBuffer.prototype = Object.create(Object.prototype);

OutputBuffer.prototype.toTypedArray = function() {
  if (!this.addr) {
    return null;
  }

  var addr = Module._wabt_output_buffer_get_data(this.addr);
  var size = Module._wabt_output_buffer_get_size(this.addr);
  var buffer = new Uint8Array(size);
  buffer.set(new Uint8Array(HEAPU8.buffer, addr, size));
  return buffer;
};

OutputBuffer.prototype.toString = function() {
  if (!this.addr) {
    return '';
  }

  var addr = Module._wabt_output_buffer_get_data(this.addr);
  var size = Module._wabt_output_buffer_get_size(this.addr);
  return UTF8ToString(addr, size);
};

OutputBuffer.prototype.destroy = function() {
  Module._wabt_destroy_output_buffer(this.addr);
};


/// Errors
function Errors(kind) {
  this.kind = kind;
  this.addr = Module._wabt_new_errors();
}
Errors.prototype = Object.create(Object.prototype);

Errors.prototype.format = function() {
  var buffer;
  switch (this.kind) {
    case 'text':
      buffer = new OutputBuffer(
          Module._wabt_format_text_errors(this.addr, this.lexer.addr));
      break;
    case 'binary':
      buffer = new OutputBuffer(Module._wabt_format_binary_errors(this.addr));
      break;
    default:
      throw new Error('Invalid Errors kind: ' + this.kind);
  }
  var message = buffer.toString();
  buffer.destroy();
  return message;
};

Errors.prototype.destroy = function() {
  Module._wabt_destroy_errors(this.addr);
  if (this.lexer) {
    this.lexer.destroy();
  }
};


/// parseWat
function parseWat(filename, buffer, options) {
  var errors = new Errors('text');
  var lexer = new Lexer(filename, buffer, errors);
  errors.lexer = lexer;
  var features = new Features(options || {});

  try {
    var parseResult_addr =
        Module._wabt_parse_wat(lexer.addr, features.addr, errors.addr);

    var result = Module._wabt_parse_wat_result_get_result(parseResult_addr);
    if (result !== WABT_OK) {
      throw new Error('parseWat failed:\n' + errors.format());
    }

    var module_addr =
        Module._wabt_parse_wat_result_release_module(parseResult_addr);
    var result = new WasmModule(module_addr, errors);
    // Clear errors so it isn't destroyed below.
    errors = null;
    return result;
  } finally {
    Module._wabt_destroy_parse_wat_result(parseResult_addr);
    features.destroy();
    if (errors) {
      errors.destroy();
    }
  }
}


// readWasm
function readWasm(buffer, options) {
  var bufferObj = allocateBuffer(buffer);
  var errors = new Errors('binary');
  var readDebugNames = booleanOrDefault(options.readDebugNames, false);
  var features = new Features(options);

  try {
    var readBinaryResult_addr = Module._wabt_read_binary(
        bufferObj.addr, bufferObj.size, readDebugNames, features.addr,
        errors.addr);

    var result =
        Module._wabt_read_binary_result_get_result(readBinaryResult_addr);
    if (result !== WABT_OK) {
      throw new Error('readWasm failed:\n' + errors.format());
    }

    var module_addr =
        Module._wabt_read_binary_result_release_module(readBinaryResult_addr);
    var result = new WasmModule(module_addr, errors);
    // Clear errors so it isn't destroyed below.
    errors = null;
    return result;
  } finally {
    Module._wabt_destroy_read_binary_result(readBinaryResult_addr);
    features.destroy();
    if (errors) {
      errors.destroy();
    }
    Module._free(bufferObj.addr);
  }
}


// WasmModule (can't call it Module because emscripten has claimed it.)
function WasmModule(module_addr, errors) {
  this.module_addr = module_addr;
  this.errors = errors;
}
WasmModule.prototype = Object.create(Object.prototype);

WasmModule.prototype.validate = function(options) {
  var features = new Features(options || {});
  try {
    var result = Module._wabt_validate_module(
        this.module_addr, features.addr, this.errors.addr);
    if (result !== WABT_OK) {
      throw new Error('validate failed:\n' + this.errors.format());
    }
  } finally {
    features.destroy();
  }
};

WasmModule.prototype.resolveNames = function() {
  // No-op, this is now part of text parsing.
};

WasmModule.prototype.generateNames = function() {
  var result = Module._wabt_generate_names_module(this.module_addr);
  if (result !== WABT_OK) {
    throw new Error('generateNames failed.');
  }
};

WasmModule.prototype.applyNames = function() {
  var result = Module._wabt_apply_names_module(this.module_addr);
  if (result !== WABT_OK) {
    throw new Error('applyNames failed.');
  }
};

WasmModule.prototype.toText = function(options) {
  var foldExprs = booleanOrDefault(options.foldExprs, false);
  var inlineExport = booleanOrDefault(options.inlineExport, false);

  var writeModuleResult_addr =
      Module._wabt_write_text_module(this.module_addr, foldExprs, inlineExport);

  var result =
      Module._wabt_write_module_result_get_result(writeModuleResult_addr);

  try {
    if (result !== WABT_OK) {
      throw new Error('toText failed.');
    }

    var outputBuffer =
        new OutputBuffer(Module._wabt_write_module_result_release_output_buffer(
            writeModuleResult_addr));

    return outputBuffer.toString();

  } finally {
    if (outputBuffer) {
      outputBuffer.destroy();
    }
    Module._wabt_destroy_write_module_result(writeModuleResult_addr);
  }
};

WasmModule.prototype.toBinary = function(options) {
  var log = booleanOrDefault(options.log, false);
  var canonicalize_lebs = booleanOrDefault(options.canonicalize_lebs, true);
  var relocatable = booleanOrDefault(options.relocatable, false);
  var write_debug_names = booleanOrDefault(options.write_debug_names, false);

  var writeModuleResult_addr = Module._wabt_write_binary_module(
      this.module_addr, log, canonicalize_lebs, relocatable, write_debug_names);

  var result =
      Module._wabt_write_module_result_get_result(writeModuleResult_addr);

  try {
    if (result !== WABT_OK) {
      throw new Error('toBinary failed.');
    }

    var binaryOutputBuffer =
        new OutputBuffer(Module._wabt_write_module_result_release_output_buffer(
            writeModuleResult_addr));
    var logOutputBuffer = new OutputBuffer(
        Module._wabt_write_module_result_release_log_output_buffer(
            writeModuleResult_addr));

    return {
      buffer: binaryOutputBuffer.toTypedArray(),
      log: logOutputBuffer.toString()
    };

  } finally {
    if (binaryOutputBuffer) {
      binaryOutputBuffer.destroy();
    }
    if (logOutputBuffer) {
      logOutputBuffer.destroy();
    }
    Module._wabt_destroy_write_module_result(writeModuleResult_addr);
  }
};

WasmModule.prototype.destroy = function() {
  Module._wabt_destroy_module(this.module_addr);
  if (this.errors) {
    this.errors.destroy();
  }
};

Module['parseWat'] = parseWat;
Module['readWasm'] = readWasm;
Module['FEATURES'] = FEATURES;
