/*
 * 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.
 */

#include <cassert>
#include <cstdarg>
#include <cstdint>
#include <cstdlib>
#include <cstdio>

#include "config.h"

#include "binary-writer.h"
#include "binary-writer-spec.h"
#include "common.h"
#include "ir.h"
#include "option-parser.h"
#include "resolve-names.h"
#include "source-error-handler.h"
#include "stream.h"
#include "validator.h"
#include "wast-parser.h"
#include "writer.h"

#define PROGRAM_NAME "wast2wasm"

using namespace wabt;

static const char* s_infile;
static const char* s_outfile;
static bool s_dump_module;
static int s_verbose;
static WriteBinaryOptions s_write_binary_options =
    WABT_WRITE_BINARY_OPTIONS_DEFAULT;
static WriteBinarySpecOptions s_write_binary_spec_options =
    WABT_WRITE_BINARY_SPEC_OPTIONS_DEFAULT;
static bool s_spec;
static bool s_validate = true;
static WastParseOptions s_parse_options;

static std::unique_ptr<FileStream> s_log_stream;

#define NOPE HasArgument::No
#define YEP HasArgument::Yes

enum {
  FLAG_VERBOSE,
  FLAG_HELP,
  FLAG_DUMP_MODULE,
  FLAG_OUTPUT,
  FLAG_RELOCATABLE,
  FLAG_SPEC,
  FLAG_NO_CANONICALIZE_LEB128S,
  FLAG_DEBUG_NAMES,
  FLAG_NO_CHECK,
  FLAG_EXCEPTIONS,
  FLAG_DEBUG_PARSER,
  NUM_FLAGS
};

static const char s_description[] =
    "  read a file in the wasm s-expression format, check it for errors, and\n"
    "  convert it to the wasm binary format.\n"
    "\n"
    "examples:\n"
    "  # parse and typecheck test.wast\n"
    "  $ wast2wasm test.wast\n"
    "\n"
    "  # parse test.wast and write to binary file test.wasm\n"
    "  $ wast2wasm test.wast -o test.wasm\n"
    "\n"
    "  # parse spec-test.wast, and write verbose output to stdout (including\n"
    "  # the meaning of every byte)\n"
    "  $ wast2wasm spec-test.wast -v\n"
    "\n"
    "  # parse spec-test.wast, and write files to spec-test.json. Modules are\n"
    "  # written to spec-test.0.wasm, spec-test.1.wasm, etc.\n"
    "  $ wast2wasm spec-test.wast --spec -o spec-test.json\n";

static Option s_options[] = {
    {FLAG_VERBOSE, 'v', "verbose", nullptr, NOPE,
     "use multiple times for more info"},
    {FLAG_HELP, 'h', "help", nullptr, NOPE, "print this help message"},
    {FLAG_DEBUG_PARSER, 0, "debug-parser", nullptr, NOPE,
     "Turn on debugging the parser of wast files"},
    {FLAG_DUMP_MODULE, 'd', "dump-module", nullptr, NOPE,
     "print a hexdump of the module to stdout"},
    {FLAG_EXCEPTIONS, 0, "future-exceptions", nullptr, NOPE,
     "Test future extension for exception handling"},
    {FLAG_OUTPUT, 'o', "output", "FILE", YEP, "output wasm binary file"},
    {FLAG_RELOCATABLE, 'r', nullptr, nullptr, NOPE,
     "create a relocatable wasm binary (suitable for linking with wasm-link)"},
    {FLAG_SPEC, 0, "spec", nullptr, NOPE,
     "parse a file with multiple modules and assertions, like the spec "
     "tests"},
    {FLAG_NO_CANONICALIZE_LEB128S, 0, "no-canonicalize-leb128s", nullptr, NOPE,
     "Write all LEB128 sizes as 5-bytes instead of their minimal size"},
    {FLAG_DEBUG_NAMES, 0, "debug-names", nullptr, NOPE,
     "Write debug names to the generated binary file"},
    {FLAG_NO_CHECK, 0, "no-check", nullptr, NOPE,
     "Don't check for invalid modules"}
};
WABT_STATIC_ASSERT(NUM_FLAGS == WABT_ARRAY_SIZE(s_options));

static void on_option(struct OptionParser* parser,
                      struct Option* option,
                      const char* argument) {
  switch (option->id) {
    case FLAG_VERBOSE:
      s_verbose++;
      s_log_stream = FileStream::CreateStdout();
      s_write_binary_options.log_stream = s_log_stream.get();
      break;

    case FLAG_HELP:
      print_help(parser, PROGRAM_NAME);
      exit(0);
      break;

    case FLAG_DUMP_MODULE:
      s_dump_module = true;
      break;

    case FLAG_OUTPUT:
      s_outfile = argument;
      break;

    case FLAG_RELOCATABLE:
      s_write_binary_options.relocatable = true;
      break;

    case FLAG_SPEC:
      s_spec = true;
      break;

    case FLAG_NO_CANONICALIZE_LEB128S:
      s_write_binary_options.canonicalize_lebs = false;
      break;

    case FLAG_DEBUG_NAMES:
      s_write_binary_options.write_debug_names = true;
      break;

    case FLAG_NO_CHECK:
      s_validate = false;
      break;

    case FLAG_EXCEPTIONS:
      s_parse_options.allow_exceptions = true;
      break;

    case FLAG_DEBUG_PARSER:
      s_parse_options.debug_parsing = true;
      break;
  }
}

static void on_argument(struct OptionParser* parser, const char* argument) {
  s_infile = argument;
}

static void on_option_error(struct OptionParser* parser,
                            const char* message) {
  WABT_FATAL("%s\n", message);
}

static void parse_options(int argc, char** argv) {
  OptionParser parser;
  WABT_ZERO_MEMORY(parser);
  parser.description = s_description;
  parser.options = s_options;
  parser.num_options = WABT_ARRAY_SIZE(s_options);
  parser.on_option = on_option;
  parser.on_argument = on_argument;
  parser.on_error = on_option_error;
  parse_options(&parser, argc, argv);

  if (!s_infile) {
    print_help(&parser, PROGRAM_NAME);
    WABT_FATAL("No filename given.\n");
  }
}

static void write_buffer_to_file(const char* filename,
                                 const OutputBuffer& buffer) {
  if (s_dump_module) {
    if (s_verbose)
      s_log_stream->Writef(";; dump\n");
    if (!buffer.data.empty()) {
      s_log_stream->WriteMemoryDump(buffer.data.data(), buffer.data.size());
    }
  }

  if (filename) {
    buffer.WriteToFile(filename);
  }
}

int ProgramMain(int argc, char** argv) {
  init_stdio();

  parse_options(argc, argv);

  std::unique_ptr<WastLexer> lexer = WastLexer::CreateFileLexer(s_infile);
  if (!lexer)
    WABT_FATAL("unable to read file: %s\n", s_infile);

  SourceErrorHandlerFile error_handler;
  Script* script;
  Result result = parse_wast(lexer.get(), &script, &error_handler,
                             &s_parse_options);

  if (WABT_SUCCEEDED(result)) {
    result = resolve_names_script(lexer.get(), script, &error_handler);

    if (WABT_SUCCEEDED(result) && s_validate)
      result = validate_script(lexer.get(), script, &error_handler);

    if (WABT_SUCCEEDED(result)) {
      if (s_spec) {
        s_write_binary_spec_options.json_filename = s_outfile;
        s_write_binary_spec_options.write_binary_options =
            s_write_binary_options;
        result = write_binary_spec_script(script, s_infile,
                                          &s_write_binary_spec_options);
      } else {
        MemoryWriter writer;
        const Module* module = script->GetFirstModule();
        if (module) {
          result =
              write_binary_module(&writer, module, &s_write_binary_options);
        } else {
          WABT_FATAL("no module found\n");
        }

        if (WABT_SUCCEEDED(result))
          write_buffer_to_file(s_outfile, writer.output_buffer());
      }
    }
  }

  delete script;
  return result != Result::Ok;
}

int main(int argc, char** argv) {
  WABT_TRY
  return ProgramMain(argc, argv);
  WABT_CATCH_BAD_ALLOC_AND_EXIT
}
