/*
 * Copyright 2019 by its authors. See AUTHORS.
 *
 * 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 <iostream>
#include <fstream>

#include <stdlib.h>
#include <unistd.h>
#include <getopt.h>
#include <libgen.h>

#include <string>
#include <map>
#include <algorithm>
#include <stdexcept>
#include <iosfwd>
#include <type_traits>
#include <cassert>

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <Eolian.h>

#include <Eina.hh>
#include <Eolian_Cxx.hh>

#include <eolian/mono/logging.hh>
#include <eolian/mono/name_helpers.hh>
#include <eolian/mono/klass.hh>
#include <eolian/mono/enum_definition.hh>
#include <eolian/mono/struct_definition.hh>
#include <eolian/mono/type_impl.hh>
#include <eolian/mono/marshall_type_impl.hh>
#include <eolian/mono/marshall_annotation.hh>
#include <eolian/mono/function_pointer.hh>
#include <eolian/mono/alias_definition.hh>
#include <eolian/mono/variable_definition.hh>

namespace eolian_mono {

/// Program options.
struct options_type
{
   std::vector<std::string> include_dirs;
   std::string in_file;
   std::string out_file;
   std::string examples_dir;
   std::string dllimport;
   mutable Eolian_State* state;
   mutable Eolian_Unit const* unit;
   int v_major;
   int v_minor;
   bool want_beta;
   bool want_partial;
   std::map<const std::string, std::string> references_map;
};

// Parses a CSV file in the format 'filename,library' (without trimming spaces around ',')
static std::vector<std::pair<std::string, std::string> >
parse_reference(std::string filename)
{
    std::vector<std::pair<std::string, std::string> > ret;
    std::string delimiter = ",";
    std::ifstream infile(filename);
    std::string line;

    while (std::getline(infile, line))
      {
         size_t pos = line.find(delimiter);

         if (pos == std::string::npos)
           throw std::invalid_argument("Malformed mapping. Must be 'filename,library'");

         std::string eo_filename = line.substr(0, pos);
         std::string library = line.substr(pos + 1);
         library[0] = std::toupper(library[0]);
         ret.push_back(std::pair<std::string, std::string>(eo_filename, library));
      }
    return ret;
}

static bool
opts_check(eolian_mono::options_type const& opts)
{
   if (opts.in_file.empty())
     {
        EINA_CXX_DOM_LOG_ERR(eolian_mono::domain)
          << "Nothing to generate?" << std::endl;
     }
   else if (opts.out_file.empty())
     {
        EINA_CXX_DOM_LOG_ERR(eolian_mono::domain)
          << "Nowhere to generate?" << std::endl;
     }
   else
     return true; // valid.
   return false;
}

static void
run(options_type const& opts)
{
   const Eolian_Class *klass = NULL;
   Eina_Iterator *aliases = NULL;
   const Eolian_Typedecl *tp = NULL;
   char* dup = strdup(opts.in_file.c_str());
   std::string basename_input = basename(dup);
   klass = ::eolian_state_class_by_file_get(opts.state, basename_input.c_str());
   aliases= ::eolian_state_aliases_by_file_get(opts.state, basename_input.c_str());
   free(dup);

   std::string class_file_name = opts.out_file;

   std::ofstream output_file;
   std::ostream_iterator<char> iterator
     {[&]
     {
       if(opts.out_file == "-")
         return std::ostream_iterator<char>(std::cout);
       else
         {
           output_file.open(class_file_name);
           if (!output_file.good())
             {
               EINA_CXX_DOM_LOG_ERR(eolian_mono::domain)
                 << "Can't open output file: " << class_file_name << std::endl;
               throw std::runtime_error("");
             }
           return std::ostream_iterator<char>(output_file);
         }
     }()};

   if (!as_generator(
              "/*\n"
              " * Copyright 2019 by its authors. See AUTHORS.\n"
              " *\n"
              " * Licensed under the Apache License, Version 2.0 (the \"License\");\n"
              " * you may not use this file except in compliance with the License.\n"
              " * You may obtain a copy of the License at\n"
              " *\n"
              " *     http://www.apache.org/licenses/LICENSE-2.0\n"
              " *\n"
              " * Unless required by applicable law or agreed to in writing, software\n"
              " * distributed under the License is distributed on an \"AS IS\" BASIS,\n"
              " * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n"
              " * See the License for the specific language governing permissions and\n"
              " * limitations under the License.\n"
              " */\n\n"
        ).generate(iterator, attributes::unused, efl::eolian::grammar::context_null()))
     throw std::runtime_error("Failed to generate license notice");

   if (!as_generator("#pragma warning disable CS1591\n").generate(iterator, efl::eolian::grammar::attributes::unused, efl::eolian::grammar::context_null()))
     throw std::runtime_error("Failed to generate pragma to disable missing docs");

   if (!as_generator("using System;\n"
                     "using System.Runtime.InteropServices;\n"
                     "using System.Collections.Generic;\n"
                     "using System.Linq;\n"
                     "using System.Threading;\n"
                     "using System.ComponentModel;\n"
                     "using System.Diagnostics.CodeAnalysis;\n"
                     "using System.Diagnostics.Contracts;\n")
     .generate(iterator, efl::eolian::grammar::attributes::unused, efl::eolian::grammar::context_null()))
     {
        throw std::runtime_error("Failed to generate file preamble");
     }

   using efl::eolian::grammar::context_add_tag;

   auto context = context_add_tag(eolian_mono::indentation_context{0},
                  context_add_tag(eolian_mono::eolian_state_context{opts.state},
                  context_add_tag(eolian_mono::options_context{opts.want_beta,
                                                               opts.examples_dir},
                  context_add_tag(eolian_mono::library_context{opts.dllimport,
                                                               opts.v_major,
                                                               opts.v_minor,
                                                               opts.references_map},
                                  efl::eolian::grammar::context_null()))));

   EINA_ITERATOR_FOREACH(aliases, tp)
     {
         if (eolian_typedecl_type_get(tp) == EOLIAN_TYPEDECL_FUNCTION_POINTER)
           {
              const Eolian_Function *fp = eolian_typedecl_function_pointer_get(tp);
              efl::eolian::grammar::attributes::function_def function_def(fp, EOLIAN_FUNCTION_POINTER, tp, opts.unit);
              if (!eolian_mono::function_pointer.generate(iterator, function_def, context))
                throw std::runtime_error("Failed to generate function pointer wrapper");
           }
         else // Regular aliases
           {
              efl::eolian::grammar::attributes::alias_def alias(tp, opts.unit);
              auto alias_cxt = context_add_tag(class_context{class_context::alias}, context);

              if (!eolian_mono::alias_definition.generate(iterator, alias, alias_cxt))
                throw std::runtime_error("Failed to generate alias.");
           }
     }
   ::eina_iterator_free(aliases);


   // Constants
   {
      auto var_cxt = context_add_tag(class_context{class_context::variables}, context);
      for (efl::eina::iterator<const Eolian_Constant> var_iterator( ::eolian_state_constants_by_file_get(opts.state, basename_input.c_str()))
              , var_last; var_iterator != var_last; ++var_iterator)
        {
           efl::eolian::grammar::attributes::constant_def var(&*var_iterator, opts.unit);
           if (!eolian_mono::constant_definition.generate(iterator, var, var_cxt))
             {
                throw std::runtime_error("Failed to generate enum");
             }
        }
   }

   if (klass)
     {
       efl::eolian::grammar::attributes::klass_def klass_def(klass, opts.unit);
       std::vector<efl::eolian::grammar::attributes::klass_def> klasses{klass_def};

       auto klass_gen = !opts.want_partial ? eolian_mono::klass
         : eolian_mono::klass(eolian_mono::class_partial);
       if (!klass_gen.generate(iterator, klass_def, context))
         {
            throw std::runtime_error("Failed to generate class");
         }
     }

   // Enums
   for (efl::eina::iterator<const Eolian_Typedecl> enum_iterator( ::eolian_state_enums_by_file_get(opts.state, basename_input.c_str()))
           , enum_last; enum_iterator != enum_last; ++enum_iterator)
     {
        efl::eolian::grammar::attributes::enum_def enum_(&*enum_iterator, opts.unit);
        auto enum_cxt = context_add_tag(class_context{class_context::enums}, context);
        if (!eolian_mono::enum_definition.generate(iterator, enum_, enum_cxt))
          {
             throw std::runtime_error("Failed to generate enum");
          }
     }

   // Structs
   for (efl::eina::iterator<const Eolian_Typedecl> struct_iterator( ::eolian_state_structs_by_file_get(opts.state, basename_input.c_str()))
           , struct_last; struct_iterator != struct_last; ++struct_iterator)
     {
        efl::eolian::grammar::attributes::struct_def struct_(&*struct_iterator, opts.unit);
        auto structs_cxt = context_add_tag(class_context{class_context::structs}, context);
        if (!eolian_mono::struct_entities.generate(iterator, struct_, structs_cxt))
          {
             throw std::runtime_error("Failed to generate struct");
          }
     }
}

static void
database_load(options_type const& opts)
{
   for (auto src : opts.include_dirs)
     {
        if (!::eolian_state_directory_add(opts.state, src.c_str()))
          {
             EINA_CXX_DOM_LOG_WARN(eolian_mono::domain)
               << "Couldn't load eolian from '" << src << "'.";
          }
     }
   if (!::eolian_state_all_eot_files_parse(opts.state))
     {
        EINA_CXX_DOM_LOG_ERR(eolian_mono::domain)
          << "Eolian failed parsing eot files";
        assert(false && "Error parsing eot files");
     }
   if (opts.in_file.empty())
     {
       EINA_CXX_DOM_LOG_ERR(eolian_mono::domain)
         << "No input file.";
       assert(false && "Error parsing input file");
     }
   if (!::eolian_state_file_path_parse(opts.state, opts.in_file.c_str()))
     {
       EINA_CXX_DOM_LOG_ERR(eolian_mono::domain)
         << "Failed parsing: " << opts.in_file << ".";
       assert(false && "Error parsing input file");
     }
}

} // namespace eolian_mono {

static void
_print_version()
{
   std::cerr
     << "Eolian C++ Binding Generator (EFL "
     << PACKAGE_VERSION << ")" << std::endl;
}

static void
_usage(const char *progname)
{
   std::cerr
     << progname
     << " [options] [file.eo]" << std::endl
     << " A single input file must be provided (unless -a is specified)." << std::endl
     << "Options:" << std::endl
     << "  -a, --all               Generate bindings for all Eo classes." << std::endl
     << "  -c, --class <name>      The Eo class name to generate code for." << std::endl
     << "  -D, --out-dir <dir>     Output directory where generated code will be written." << std::endl
     << "  -I, --in <file/dir>     The source containing the .eo descriptions." << std::endl
     << "  -o, --out-file <file>   The output file name. [default: <classname>.eo.cs]" << std::endl
     << "  -n, --namespace <ns>    Wrap generated code in a namespace. [Eg: Efl.Ui.Widget]" << std::endl
     << "  -r, --recurse           Recurse input directories loading .eo files." << std::endl
     << "  -v, --version           Print the version." << std::endl
     << "  -b, --beta              Enable @beta methods." << std::endl
     << "  -e, --example-dir <dir> Folder to search for example files." << std::endl
     << "  -p, --partial           Create class as a partial class" << std::endl
     << "  -h, --help              Print this help." << std::endl;
   exit(EXIT_FAILURE);
}

static void
_assert_not_dup(std::string option, std::string value)
{
   if (value != "")
     {
        EINA_CXX_DOM_LOG_ERR(eolian_mono::domain) <<
          "Option -" + option + " already set (" + value + ")";
     }
}

static eolian_mono::options_type
opts_get(int argc, char **argv)
{
   eolian_mono::options_type opts{};

   const struct option long_options[] =
     {
       { "in",        required_argument, 0,  'I' },
       { "out-file",  required_argument, 0,  'o' },
       { "version",   no_argument,       0,  'v' },
       { "help",      no_argument,       0,  'h' },
       { "dllimport",      required_argument,       0,  'l' },
       { "vmaj", required_argument, 0, 'M' },
       { "vmin", required_argument, 0, 'm' },
       { "references", required_argument, 0, 'r'},
       { "beta", no_argument, 0, 'b'},
       { "example-dir", required_argument, 0,  'e' },
       { "partial", no_argument, 0,  'p' },
       { 0,           0,                 0,   0  }
     };
   const char* options = "I:D:o:c:M:m:ar:vhbpe:";

   int c, idx;
   while ( (c = getopt_long(argc, argv, options, long_options, &idx)) != -1)
     {
        if (c == 'I')
          {
             opts.include_dirs.push_back(optarg);
          }
        else if (c == 'o')
          {
             _assert_not_dup("o", opts.out_file);
             opts.out_file = optarg;
          }
        else if (c == 'h')
          {
           _usage(argv[0]);
          }
        else if (c == 'l')
          {
            opts.dllimport = optarg;
          }
        else if (c == 'M')
          {
            opts.v_major = std::stoi(optarg);
          }
        else if (c == 'm')
          {
            opts.v_minor = std::stoi(optarg);
          }
        else if (c == 'r')
          {
             try
               {
                  std::vector<std::pair<std::string, std::string> > names = eolian_mono::parse_reference(optarg);
                 for (auto&& p : names)
                   {
                      opts.references_map[p.first] = p.second;
                   }
               }
             catch (const std::invalid_argument &e)
               {
                   std::cerr << "Invalid argument processing argument " << optarg << std::endl;
                   _usage(argv[0]);
                   assert(false && e.what());
               }
          }
        else if (c == 'v')
          {
             _print_version();
             if (argc == 2) exit(EXIT_SUCCESS);
          }
        else if (c == 'b')
          {
             opts.want_beta = true;
          }
        else if (c == 'e')
          {
             opts.examples_dir = optarg;
             if (!opts.examples_dir.empty() && opts.examples_dir.back() != '/') opts.examples_dir += "/";
          }
        else if (c == 'p')
          {
             opts.want_partial = true;
          }
     }
   if (optind == argc-1)
     {
        opts.in_file = argv[optind];
     }

   if (!eolian_mono::opts_check(opts))
     {
        _usage(argv[0]);
        assert(false && "Wrong options passed in command-line");
     }

   return opts;
}

int main(int argc, char **argv)
{
   try
     {
        efl::eina::eina_init eina_init;
        efl::eolian::eolian_init eolian_init;
        efl::eolian::eolian_state eolian_state;
        eolian_mono::options_type opts = opts_get(argc, argv);
        opts.state = eolian_state.value;
        opts.unit = eolian_state.as_unit();
        eolian_mono::database_load(opts);
        eolian_mono::run(opts);
     }
   catch(std::exception const& e)
     {
       std::cerr << "EOLCXX: Eolian C++ failed generation for the following reason: " << e.what() << std::endl;
       std::cout << "EOLCXX: Eolian C++ failed generation for the following reason: " << e.what() << std::endl;
       return -1;
     }
   return 0;
}


