/// \file main.cpp
/// \brief This file takes care of handling command-line parameters and loading
/// the appropriate flavour of libtinycode-*.so

//
// This file is distributed under the MIT License. See LICENSE.md for details.
//

// Standard includes
#include <cstdio>
#include <cstdlib>
#include <memory>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>

extern "C" {
#include <dlfcn.h>
#include <libgen.h>
#include <unistd.h>
}

// LLVM includes
#include "llvm/ADT/ArrayRef.h"
#include "llvm/Object/Binary.h"
#include "llvm/Object/ELF.h"

// Local includes
#include "argparse.h"
#include "binaryfile.h"
#include "codegenerator.h"
#include "debug.h"
#include "ptcinterface.h"
#include "revamb.h"

PTCInterface ptc = {}; ///< The interface with the PTC library.
static std::string LibTinycodePath;
static std::string LibHelpersPath;

struct ProgramParameters {
  const char *InputPath;
  const char *OutputPath;
  size_t EntryPointAddress;
  DebugInfoType DebugInfo;
  const char *DebugPath;
  const char *LinkingInfoPath;
  const char *CoveragePath;
  const char *BBSummaryPath;
  bool NoOSRA;
  bool UseSections;
  bool DetectFunctionsBoundaries;
  bool NoLink;
  bool External;
};

using LibraryDestructor = GenericFunctor<decltype(&dlclose), &dlclose>;
using LibraryPointer = std::unique_ptr<void, LibraryDestructor>;

static const char *const Usage[] = {
  "revamb [options] [--] INFILE OUTFILE",
  nullptr,
};

static void findQemu(const char *Architecture) {
  // TODO: make this optional
  char *FullPath = realpath("/proc/self/exe", nullptr);
  assert(FullPath != nullptr);
  std::string Directory(dirname(FullPath));
  free(FullPath);

  // TODO: add other search paths?
  std::vector<std::string> SearchPaths;
#ifdef QEMU_INSTALL_PATH
  SearchPaths.push_back(std::string(QEMU_INSTALL_PATH) + "/lib");
#endif
#ifdef INSTALL_PATH
  SearchPaths.push_back(std::string(INSTALL_PATH) + "/lib");
#endif
  SearchPaths.push_back(Directory + "/../lib");

  for (auto &Path : SearchPaths) {
    std::stringstream LibraryPath;
    LibraryPath << Path << "/libtinycode-" << Architecture << ".so";
    std::stringstream HelpersPath;
    HelpersPath << Path << "/libtinycode-helpers-" << Architecture << ".ll";
    if (access(LibraryPath.str().c_str(), F_OK) != -1
        && access(HelpersPath.str().c_str(), F_OK) != -1) {
      LibTinycodePath = LibraryPath.str();
      LibHelpersPath = HelpersPath.str();
      return;
    }

  }

  assert(false && "Couldn't find libtinycode and the helpers");
}

/// Given an architecture name, loads the appropriate version of the PTC library,
/// and initializes the PTC interface.
///
/// \param Architecture the name of the architecture, e.g. "arm".
/// \param PTCLibrary a reference to the library handler.
///
/// \return EXIT_SUCCESS if the library has been successfully loaded.
static int loadPTCLibrary(LibraryPointer& PTCLibrary) {
  ptc_load_ptr_t ptc_load = nullptr;
  void *LibraryHandle = nullptr;

  // Look for the library in the system's paths
  LibraryHandle = dlopen(LibTinycodePath.c_str(), RTLD_LAZY);

  if (LibraryHandle == nullptr) {
    fprintf(stderr, "Couldn't load the PTC library: %s\n", dlerror());
    return EXIT_FAILURE;
  }

  // The library has been loaded, initialize the pointer, the caller will take
  // care of dlclose it from now on
  PTCLibrary.reset(LibraryHandle);

  // Obtain the address of the ptc_load entry point
  ptc_load = (ptc_load_ptr_t) dlsym(LibraryHandle, "ptc_load");

  if (ptc_load == nullptr) {
    fprintf(stderr, "Couldn't find ptc_load: %s\n", dlerror());
    return EXIT_FAILURE;
  }

  // Initialize the ptc interface
  if (ptc_load(LibraryHandle, &ptc) != 0) {
    fprintf(stderr, "Couldn't find PTC functions.\n");
    return EXIT_FAILURE;
  }

  return EXIT_SUCCESS;
}

/// Parses the input arguments to the program.
///
/// \param Argc number of arguments.
/// \param Argv array of strings containing the arguments.
/// \param Parameters where to store the parsed parameters.
///
/// \return EXIT_SUCCESS if the parameters have been successfully parsed.
static int parseArgs(int Argc, const char *Argv[],
                     ProgramParameters *Parameters) {
  const char *DebugString = nullptr;
  const char *DebugLoggingString = nullptr;
  const char *EntryPointAddressString = nullptr;
  long long EntryPointAddress = 0;

  // Initialize argument parser
  struct argparse Arguments;
  struct argparse_option Options[] = {
    OPT_HELP(),
    OPT_GROUP("Input description"),
    OPT_STRING('e', "entry",
               &EntryPointAddressString,
               "virtual address of the entry point where to start."),
    OPT_STRING('s', "debug-path",
               &Parameters->DebugPath,
               "destination path for the generated debug source."),
    OPT_STRING('c', "coverage-path",
               &Parameters->CoveragePath,
               "destination path for the CSV containing translated ranges."),
    OPT_STRING('i', "linking-info",
               &Parameters->LinkingInfoPath,
               "destination path for the CSV containing linking info."),
    OPT_STRING('g', "debug-info",
               &DebugString,
               "emit debug information. Possible values are 'none' for no debug"
               " information, 'asm' for debug information referring to the"
               " assembly of the input file, 'ptc' for debug information"
               " referred to the Portable Tiny Code, or 'll' for debug"
               " information referred to the LLVM IR."),
    OPT_STRING('d', "debug",
               &DebugLoggingString,
               "enable verbose logging."),
    OPT_BOOLEAN('O', "no-osra", &Parameters->NoOSRA,
                "disable OSRA."),
    OPT_BOOLEAN('L', "no-link", &Parameters->NoLink,
                "do not link the output to QEMU helpers."),
    OPT_BOOLEAN('E', "external", &Parameters->External,
                "set CSVs linkage to external, useful for debugging purposes."),
    OPT_BOOLEAN('S', "use-sections", &Parameters->UseSections,
                "use section informations, if available."),
    OPT_STRING('b', "bb-summary",
               &Parameters->BBSummaryPath,
               "destination path for the CSV containing the statistics about "
               "the translated basic blocks."),
    OPT_BOOLEAN('f', "functions-boundaries",
                &Parameters->DetectFunctionsBoundaries,
                "enable functions boundaries detection."),
    OPT_END(),
  };

  argparse_init(&Arguments, Options, Usage, 0);
  argparse_describe(&Arguments, "\nrevamb.",
                    "\nTranslates a binary into a program for a different "
                    "architecture.\n");
  Argc = argparse_parse(&Arguments, Argc, Argv);

  // Handle positional arguments
  if (Argc != 2) {
    fprintf(stderr, "Too many arguments.\n");
    return EXIT_FAILURE;
  }

  Parameters->InputPath = Argv[0];
  Parameters->OutputPath = Argv[1];

  // Check parameters
  if (EntryPointAddressString != nullptr) {
    if (sscanf(EntryPointAddressString, "%lld", &EntryPointAddress) != 1) {
      fprintf(stderr, "Entry point parameter (-e, --entry) is not a"
              " number.\n");
      return EXIT_FAILURE;
    }

    Parameters->EntryPointAddress = (size_t) EntryPointAddress;
  }

  if (DebugString != nullptr) {
    if (strcmp("none", DebugString) == 0) {
      Parameters->DebugInfo = DebugInfoType::None;
    } else if (strcmp("asm", DebugString) == 0) {
      Parameters->DebugInfo = DebugInfoType::OriginalAssembly;
    } else if (strcmp("ptc", DebugString) == 0) {
      Parameters->DebugInfo = DebugInfoType::PTC;
    } else if (strcmp("ll", DebugString) == 0) {
      Parameters->DebugInfo = DebugInfoType::LLVMIR;
    } else {
      fprintf(stderr, "Unexpected value for the debug type parameter"
              " (-g, --debug).\n");
      return EXIT_FAILURE;
    }
  }

  if (DebugLoggingString != nullptr) {
    DebuggingEnabled = true;
    std::string Input(DebugLoggingString);
    std::stringstream Stream(Input);
    std::string Type;
    while (std::getline(Stream, Type, ','))
      enableDebugFeature(Type.c_str());
  }

  if (Parameters->DebugPath == nullptr)
    Parameters->DebugPath = "";

  if (Parameters->LinkingInfoPath == nullptr)
    Parameters->LinkingInfoPath = "";

  if (Parameters->CoveragePath == nullptr)
    Parameters->CoveragePath = "";

  if (Parameters->BBSummaryPath == nullptr)
    Parameters->BBSummaryPath = "";

  return EXIT_SUCCESS;
}

int main(int argc, const char *argv[]) {
  // Parse arguments
  ProgramParameters Parameters {};
  if (parseArgs(argc, argv, &Parameters) != EXIT_SUCCESS)
    return EXIT_FAILURE;

  BinaryFile TheBinary(Parameters.InputPath, Parameters.UseSections);

  findQemu(TheBinary.architecture().name());

  // Load the appropriate libtyncode version
  LibraryPointer PTCLibrary;
  if (loadPTCLibrary(PTCLibrary) != EXIT_SUCCESS)
    return EXIT_FAILURE;

  // Translate everything
  Architecture TargetArchitecture;
  CodeGenerator Generator(TheBinary,
                          TargetArchitecture,
                          std::string(Parameters.OutputPath),
                          LibHelpersPath,
                          Parameters.DebugInfo,
                          std::string(Parameters.DebugPath),
                          std::string(Parameters.LinkingInfoPath),
                          std::string(Parameters.CoveragePath),
                          std::string(Parameters.BBSummaryPath),
                          !Parameters.NoOSRA,
                          Parameters.DetectFunctionsBoundaries,
                          !Parameters.NoLink,
                          Parameters.External);

  Generator.translate(Parameters.EntryPointAddress);

  Generator.serialize();

  return EXIT_SUCCESS;
}