// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fcntl.h>
#include <string.h>
#include <unistd.h>

#include <fstream>
#include <iostream>
#include <string>
#include <vector>

#include "tools/cddl/codegen.h"
#include "tools/cddl/logging.h"
#include "tools/cddl/parse.h"
#include "tools/cddl/sema.h"

std::string ReadEntireFile(const std::string& filename) {
  std::ifstream input(filename);
  if (!input) {
    return {};
  }

  input.seekg(0, std::ios_base::end);
  size_t length = input.tellg();
  std::string input_data(length + 1, 0);

  input.seekg(0, std::ios_base::beg);
  input.read(const_cast<char*>(input_data.data()), length);
  input_data[length] = 0;

  return input_data;
}

struct CommandLineArguments {
  std::string header_filename;
  std::string cc_filename;
  std::string gen_dir;
  std::string cddl_filename;
};

CommandLineArguments ParseCommandLineArguments(int argc, char** argv) {
  --argc;
  ++argv;
  CommandLineArguments result;
  while (argc) {
    if (strcmp(*argv, "--header") == 0) {
      // Parse the filename of the output header file. This is also the name
      // that will be used for the include guard and as the  include path in the
      // source file.
      if (!result.header_filename.empty()) {
        return {};
      }
      if (!argc) {
        return {};
      }
      --argc;
      ++argv;
      result.header_filename = *argv;
    } else if (strcmp(*argv, "--cc") == 0) {
      // Parse the filename of the output source file.
      if (!result.cc_filename.empty()) {
        return {};
      }
      if (!argc) {
        return {};
      }
      --argc;
      ++argv;
      result.cc_filename = *argv;
    } else if (strcmp(*argv, "--gen-dir") == 0) {
      // Parse the directory prefix that should be added to the output header.
      // and source file
      if (!result.gen_dir.empty()) {
        return {};
      }
      if (!argc) {
        return {};
      }
      --argc;
      ++argv;
      result.gen_dir = *argv;
    } else if (!result.cddl_filename.empty()) {
      return {};
    } else {
      // The input file which contains the CDDL spec.
      result.cddl_filename = *argv;
    }
    --argc;
    ++argv;
  }

  // If one of the required properties is missed, return empty. Else, return
  // generated struct.
  if (result.header_filename.empty() || result.cc_filename.empty() ||
      result.gen_dir.empty() || result.cddl_filename.empty()) {
    return {};
  }
  return result;
}

int main(int argc, char** argv) {
  // Parse and validate all cmdline arguments.
  CommandLineArguments args = ParseCommandLineArguments(argc, argv);
  if (args.cddl_filename.empty()) {
    std::cerr << "Usage: " << std::endl
              << "cddl --header parsed.h --cc parsed.cc --gen-dir "
                 "output/generated input.cddl"
              << std::endl
              << "All flags are required." << std::endl
              << "Example: " << std::endl
              << "./cddl --header osp_messages.h --cc osp_messages.cc "
                 "--gen-dir gen/msgs ../../msgs/osp_messages.cddl"
              << std::endl;
    return 1;
  }

  size_t pos = args.cddl_filename.find_last_of('.');
  if (pos == std::string::npos) {
    return 1;
  }

  // Validate and open the provided header file.
  std::string header_filename = args.gen_dir + "/" + args.header_filename;
  int header_fd = open(header_filename.c_str(), O_CREAT | O_TRUNC | O_WRONLY,
                       S_IRUSR | S_IWUSR | S_IRGRP);
  if (header_fd == -1) {
    std::cerr << "failed to open " << args.header_filename << std::endl;
    return 1;
  }

  // Validate and open the provided output source file.
  std::string cc_filename = args.gen_dir + "/" + args.cc_filename;
  int cc_fd = open(cc_filename.c_str(), O_CREAT | O_TRUNC | O_WRONLY,
                   S_IRUSR | S_IWUSR | S_IRGRP);
  if (cc_fd == -1) {
    std::cerr << "failed to open " << args.cc_filename << std::endl;
    return 1;
  }

  // Read and parse the CDDL spec file.
  std::string data = ReadEntireFile(args.cddl_filename);
  if (data.empty()) {
    return 1;
  }

  Logger::Log("Successfully initialized CDDL Code generator!");

  // Parse the full CDDL into a graph structure.
  Logger::Log("Parsing CDDL input file...");
  ParseResult parse_result = ParseCddl(data);
  if (!parse_result.root) {
    Logger::Error("Failed to parse CDDL input file");
    return 1;
  }
  Logger::Log("Successfully parsed CDDL input file!");

  // Build the Symbol table from this graph structure.
  Logger::Log("Generating CDDL Symbol Table...");
  std::pair<bool, CddlSymbolTable> cddl_result =
      BuildSymbolTable(*parse_result.root);
  if (!cddl_result.first) {
    Logger::Error("Failed to generate CDDL symbol table");
    return 1;
  }
  Logger::Log("Successfully generated CDDL symbol table!");

  Logger::Log("Generating CPP symbol table...");
  std::pair<bool, CppSymbolTable> cpp_result =
      BuildCppTypes(cddl_result.second);
  if (!cpp_result.first) {
    Logger::Error("Failed to generate CPP symbol table");
    return 1;
  }
  Logger::Log("Successfully generated CPP symbol table!");

  // Validate that the provided CDDL doesnt have duplicated indices.
  if (!ValidateCppTypes(cpp_result.second)) {
    return 1;
  }

  // Create the C++ files from the Symbol table.

  Logger::Log("Writing Header prologue...");
  if (!WriteHeaderPrologue(header_fd, args.header_filename)) {
    Logger::Error("WriteHeaderPrologue failed");
    return 1;
  }
  Logger::Log("Successfully wrote header prologue!");

  Logger::Log("Writing type definitions...");
  if (!WriteTypeDefinitions(header_fd, &cpp_result.second)) {
    Logger::Error("WriteTypeDefinitions failed");
    return 1;
  }
  Logger::Log("Successfully wrote type definitions!");

  Logger::Log("Writing function declaration...");
  if (!WriteFunctionDeclarations(header_fd, &cpp_result.second)) {
    Logger::Error("WriteFunctionDeclarations failed");
    return 1;
  }
  Logger::Log("Successfully wrote function declarations!");

  Logger::Log("Writing header epilogue...");
  if (!WriteHeaderEpilogue(header_fd, args.header_filename)) {
    Logger::Error("WriteHeaderEpilogue failed");
    return 1;
  }
  Logger::Log("Successfully wrote header epilogue!");

  Logger::Log("Writing source prologue...");
  if (!WriteSourcePrologue(cc_fd, args.header_filename)) {
    Logger::Error("WriteSourcePrologue failed");
    return 1;
  }
  Logger::Log("Successfully wrote source prologue!");

  Logger::Log("Writing encoders...");
  if (!WriteEncoders(cc_fd, &cpp_result.second)) {
    Logger::Error("WriteEncoders failed");
    return 1;
  }
  Logger::Log("Successfully wrote encoders!");

  Logger::Log("Writing decoders...");
  if (!WriteDecoders(cc_fd, &cpp_result.second)) {
    Logger::Error("WriteDecoders failed");
    return 1;
  }
  Logger::Log("Successfully wrote decoders!");

  Logger::Log("Writing equality operators...");
  if (!WriteEqualityOperators(cc_fd, &cpp_result.second)) {
    Logger::Error("WriteStructEqualityOperators failed");
    return 1;
  }
  Logger::Log("Successfully wrote equality operators!");

  Logger::Log("Writing source epilogue...");
  if (!WriteSourceEpilogue(cc_fd)) {
    Logger::Error("WriteSourceEpilogue failed");
    return 1;
  }
  Logger::Log("Successfully wrote source epilogue!");

  close(header_fd);
  close(cc_fd);
  Logger::Log("SUCCESSFULLY COMPLETED ALL OPERATIONS");

  return 0;
}