// file      : odb/generator.cxx
// copyright : Copyright (c) 2009-2012 Code Synthesis Tools CC
// license   : GNU GPL v3; see accompanying LICENSE file

#include <cctype>  // std::toupper, std::is{alpha,upper,lower}
#include <string>
#include <memory>  // std::auto_ptr
#include <fstream>
#include <iostream>

#include <cutl/fs/auto-remove.hxx>

#include <cutl/compiler/code-stream.hxx>
#include <cutl/compiler/cxx-indenter.hxx>
#include <cutl/compiler/sloc-counter.hxx>

#include <odb/version.hxx>
#include <odb/context.hxx>
#include <odb/generator.hxx>

#include <odb/generate.hxx>
#include <odb/relational/generate.hxx>

using namespace std;
using namespace cutl;

using semantics::path;
typedef vector<path> paths;

namespace
{
  static char const cxx_file_header[] =
    "// This file was generated by ODB, object-relational mapping (ORM)\n"
    "// compiler for C++.\n"
    "//\n\n";

  static char const sql_file_header[] =
    "/* This file was generated by ODB, object-relational mapping (ORM)\n"
    " * compiler for C++.\n"
    " */\n\n";

  void
  open (ifstream& ifs, string const& path)
  {
    ifs.open (path.c_str (), ios_base::in | ios_base::binary);

    if (!ifs.is_open ())
    {
      cerr << path << ": error: unable to open in read mode" << endl;
      throw generator::failed ();
    }
  }

  void
  append (ostream& os, vector<string> const& text, string const& file)
  {
    for (vector<string>::const_iterator i (text.begin ());
         i != text.end (); ++i)
    {
      os << *i << endl;
    }

    if (!file.empty ())
    {
      ifstream ifs;
      open (ifs, file);
      os << ifs.rdbuf ();
    }
  }
}

void generator::
generate (options const& ops,
          features& fts,
          semantics::unit& unit,
          path const& p,
          paths const& inputs)
{
  try
  {
    // First create the database model.
    //
    cutl::shared_ptr<semantics::relational::model> model;

    if (ops.generate_schema ())
    {
      auto_ptr<context> ctx (create_context (cerr, unit, ops, fts, 0));

      switch (ops.database ())
      {
      case database::mssql:
      case database::mysql:
      case database::oracle:
      case database::pgsql:
      case database::sqlite:
        {
          model = relational::model::generate ();
          break;
        }
      }
    }

    // Output files.
    //
    path file (ops.output_name ().empty ()
               ? p.leaf ()
               : path (ops.output_name ()).leaf ());
    string base (file.base ().string ());

    fs::auto_removes auto_rm;

    string hxx_name (base + ops.odb_file_suffix () + ops.hxx_suffix ());
    string ixx_name (base + ops.odb_file_suffix () + ops.ixx_suffix ());
    string cxx_name (base + ops.odb_file_suffix () + ops.cxx_suffix ());
    string sch_name (base + ops.schema_file_suffix () + ops.cxx_suffix ());
    string sql_name (base + ops.sql_suffix ());

    path hxx_path (hxx_name);
    path ixx_path (ixx_name);
    path cxx_path (cxx_name);
    path sch_path (sch_name);
    path sql_path (sql_name);

    if (!ops.output_dir ().empty ())
    {
      path dir (ops.output_dir ());
      hxx_path = dir / hxx_path;
      ixx_path = dir / ixx_path;
      cxx_path = dir / cxx_path;
      sch_path = dir / sch_path;
      sql_path = dir / sql_path;
    }

    bool gen_cxx (!ops.generate_schema_only ());

    //
    //
    ofstream hxx;

    if (gen_cxx)
    {
      hxx.open (hxx_path.string ().c_str (), ios_base::out);

      if (!hxx.is_open ())
      {
        cerr << "error: unable to open '" << hxx_path << "' in write mode"
             << endl;
        throw failed ();
      }

      auto_rm.add (hxx_path);
    }

    //
    //
    ofstream ixx;

    if (gen_cxx)
    {
      ixx.open (ixx_path.string ().c_str (), ios_base::out);

      if (!ixx.is_open ())
      {
        cerr << "error: unable to open '" << ixx_path << "' in write mode"
             << endl;
        throw failed ();
      }

      auto_rm.add (ixx_path);
    }

    //
    //
    ofstream cxx;

    if (gen_cxx)
    {
      cxx.open (cxx_path.string ().c_str (), ios_base::out);

      if (!cxx.is_open ())
      {
        cerr << "error: unable to open '" << cxx_path << "' in write mode"
             << endl;
        throw failed ();
      }

      auto_rm.add (cxx_path);
    }

    //
    //
    bool gen_sql_schema (ops.generate_schema () &&
                         ops.schema_format ().count (schema_format::sql));
    ofstream sql;

    if (gen_sql_schema)
    {
      sql.open (sql_path.string ().c_str (), ios_base::out);

      if (!sql.is_open ())
      {
        cerr << "error: unable to open '" << sql_path << "' in write mode"
             << endl;
        throw failed ();
      }

      auto_rm.add (sql_path);
    }

    //
    //
    bool gen_sep_schema (gen_cxx &&
                         ops.generate_schema () &&
                         ops.schema_format ().count (schema_format::separate));

    ofstream sch;

    if (gen_sep_schema)
    {
      sch.open (sch_path.string ().c_str (), ios_base::out);

      if (!sch.is_open ())
      {
        cerr << "error: unable to open '" << sch_path << "' in write mode"
             << endl;
        throw failed ();
      }

      auto_rm.add (sch_path);
    }

    // Print C++ headers.
    //
    if (gen_cxx)
    {
      hxx << cxx_file_header;
      ixx << cxx_file_header;
      cxx << cxx_file_header;
    }

    if (gen_sep_schema)
      sch << cxx_file_header;

    if (gen_sql_schema)
      sql << sql_file_header;

    typedef compiler::ostream_filter<compiler::cxx_indenter, char> ind_filter;
    typedef compiler::ostream_filter<compiler::sloc_counter, char> sloc_filter;

    size_t sloc_total (0);

    // Include settings.
    //
    string gp (ops.guard_prefix ());
    if (!gp.empty () && gp[gp.size () - 1] != '_')
      gp.append ("_");

    // HXX
    //
    if (gen_cxx)
    {
      auto_ptr<context> ctx (
        create_context (hxx, unit, ops, fts, model.get ()));

      sloc_filter sloc (ctx->os);

      string guard (ctx->make_guard (gp + hxx_name));

      hxx << "#ifndef " << guard << endl
          << "#define " << guard << endl
          << endl;

      // Version check.
      //
      hxx << "#include <odb/version.hxx>" << endl
          << endl
          << "#if (ODB_VERSION != " << ODB_VERSION << "UL)" << endl
          << "#error ODB runtime version mismatch" << endl
          << "#endif" << endl
          << endl;

      hxx << "#include <odb/pre.hxx>" << endl
          << endl;

      // Copy prologue.
      //
      if (!ops.hxx_prologue ().empty () || !ops.hxx_prologue_file ().empty ())
      {
        hxx << "// Begin prologue." << endl
            << "//" << endl;
        append (hxx, ops.hxx_prologue (), ops.hxx_prologue_file ());
        hxx << "//" << endl
            << "// End prologue." << endl
            << endl;
      }

      // Include main file(s).
      //
      for (paths::const_iterator i (inputs.begin ()); i != inputs.end (); ++i)
        hxx << "#include " <<
          ctx->process_include_path (i->leaf ().string ()) << endl;

      hxx << endl;


      {
        // We don't want to indent prologues/epilogues.
        //
        ind_filter ind (ctx->os);

        // There are no -odb.hxx includes if we are generating code for
        // everything.
        //
        if (!ops.at_once ())
          include::generate (true);

        header::generate ();

        switch (ops.database ())
        {
        case database::mssql:
        case database::mysql:
        case database::oracle:
        case database::pgsql:
        case database::sqlite:
          {
            relational::header::generate ();
            break;
          }
        }
      }

      hxx << "#include " << ctx->process_include_path (ixx_name) << endl
          << endl;

      // Copy epilogue.
      //
      if (!ops.hxx_epilogue ().empty () || !ops.hxx_epilogue_file ().empty ())
      {
        hxx << "// Begin epilogue." << endl
            << "//" << endl;
        append (hxx, ops.hxx_epilogue (), ops.hxx_epilogue_file ());
        hxx << "//" << endl
            << "// End epilogue." << endl
            << endl;
      }

      hxx << "#include <odb/post.hxx>" << endl
          << endl;

      hxx << "#endif // " << guard << endl;

      if (ops.show_sloc ())
        cerr << hxx_name << ": " << sloc.stream ().count () << endl;

      sloc_total += sloc.stream ().count ();
    }

    // IXX
    //
    if (gen_cxx)
    {
      auto_ptr<context> ctx (
        create_context (ixx, unit, ops, fts, model.get ()));

      sloc_filter sloc (ctx->os);

      // Copy prologue.
      //
      if (!ops.ixx_prologue ().empty () || !ops.ixx_prologue_file ().empty ())
      {
        ixx << "// Begin prologue." << endl
            << "//" << endl;
        append (ixx, ops.ixx_prologue (), ops.ixx_prologue_file ());
        ixx << "//" << endl
            << "// End prologue." << endl
            << endl;
      }

      {
        // We don't want to indent prologues/epilogues.
        //
        ind_filter ind (ctx->os);

        inline_::generate ();

        switch (ops.database ())
        {
        case database::mssql:
        case database::mysql:
        case database::oracle:
        case database::pgsql:
        case database::sqlite:
          {
            relational::inline_::generate ();
            break;
          }
        }
      }

      // Copy epilogue.
      //
      if (!ops.ixx_epilogue ().empty () || !ops.ixx_epilogue_file ().empty ())
      {
        ixx << "// Begin epilogue." << endl
            << "//" << endl;
        append (ixx, ops.ixx_epilogue (), ops.ixx_epilogue_file ());
        ixx << "//" << endl
            << "// End epilogue." << endl;
      }

      if (ops.show_sloc ())
        cerr << ixx_name << ": " << sloc.stream ().count () << endl;

      sloc_total += sloc.stream ().count ();
    }

    // CXX
    //
    if (gen_cxx)
    {
      auto_ptr<context> ctx (
        create_context (cxx, unit, ops, fts, model.get ()));

      sloc_filter sloc (ctx->os);

      cxx << "#include <odb/pre.hxx>" << endl
          << endl;

      // Copy prologue.
      //
      if (!ops.cxx_prologue ().empty () || !ops.cxx_prologue_file ().empty ())
      {
        cxx << "// Begin prologue." << endl
            << "//" << endl;
        append (cxx, ops.cxx_prologue (), ops.cxx_prologue_file ());
        cxx << "//" << endl
            << "// End prologue." << endl
            << endl;
      }

      cxx << "#include " << ctx->process_include_path (hxx_name) << endl
          << endl;

      {
        // We don't want to indent prologues/epilogues.
        //
        ind_filter ind (ctx->os);

        // There are no -odb.hxx includes if we are generating code for
        // everything.
        //
        if (!ops.at_once ())
          include::generate (false);

        switch (ops.database ())
        {
        case database::mssql:
        case database::mysql:
        case database::oracle:
        case database::pgsql:
        case database::sqlite:
          {
            relational::source::generate ();
            break;
          }
        }
      }

      // Copy epilogue.
      //
      if (!ops.cxx_epilogue ().empty () || !ops.cxx_epilogue_file ().empty ())
      {
        cxx << "// Begin epilogue." << endl
            << "//" << endl;
        append (cxx, ops.cxx_epilogue (), ops.cxx_epilogue_file ());
        cxx << "//" << endl
            << "// End epilogue." << endl
            << endl;
      }

      cxx << "#include <odb/post.hxx>" << endl;

      if (ops.show_sloc ())
        cerr << cxx_name << ": " << sloc.stream ().count () << endl;

      sloc_total += sloc.stream ().count ();
    }

    // SCH
    //
    if (gen_sep_schema)
    {
      auto_ptr<context> ctx (
        create_context (sch, unit, ops, fts, model.get ()));

      sloc_filter sloc (ctx->os);

      sch << "#include <odb/pre.hxx>" << endl
          << endl;

      // Copy prologue.
      //
      if (!ops.schema_prologue ().empty () ||
          !ops.schema_prologue_file ().empty ())
      {
        sch << "// Begin prologue." << endl
            << "//" << endl;
        append (sch, ops.schema_prologue (), ops.schema_prologue_file ());
        sch << "//" << endl
            << "// End prologue." << endl
            << endl;
      }

      sch << "#include " << ctx->process_include_path (hxx_name) << endl
          << endl;

      {
        // We don't want to indent prologues/epilogues.
        //
        ind_filter ind (ctx->os);

        switch (ops.database ())
        {
        case database::mssql:
        case database::mysql:
        case database::oracle:
        case database::pgsql:
        case database::sqlite:
          {
            relational::schema_source::generate ();
            break;
          }
        }
      }

      // Copy epilogue.
      //
      if (!ops.schema_epilogue ().empty () ||
          !ops.schema_epilogue_file ().empty ())
      {
        sch << "// Begin epilogue." << endl
            << "//" << endl;
        append (sch, ops.schema_epilogue (), ops.schema_epilogue_file ());
        sch << "//" << endl
            << "// End epilogue." << endl
            << endl;
      }

      sch << "#include <odb/post.hxx>" << endl;

      if (ops.show_sloc ())
        cerr << sch_name << ": " << sloc.stream ().count () << endl;

      sloc_total += sloc.stream ().count ();
    }

    // SQL
    //
    if (gen_sql_schema)
    {
      auto_ptr<context> ctx (
        create_context (sql, unit, ops, fts, model.get ()));

      switch (ops.database ())
      {
      case database::mssql:
      case database::mysql:
      case database::oracle:
      case database::pgsql:
      case database::sqlite:
        {
          relational::schema::generate_prologue ();

          // Copy prologue.
          //
          if (!ops.sql_prologue ().empty () ||
              !ops.sql_prologue_file ().empty ())
          {
            sql << "/* Begin prologue." << endl
                << " */" << endl;
            append (sql, ops.sql_prologue (), ops.sql_prologue_file ());
            sql << "/*" << endl
                << " * End prologue. */" << endl
                << endl;
          }

          if (!ops.omit_drop ())
            relational::schema::generate_drop ();

          // Copy interlude.
          //
          if (!ops.sql_interlude ().empty () ||
              !ops.sql_interlude_file ().empty ())
          {
            sql << "/* Begin interlude." << endl
                << " */" << endl;
            append (sql, ops.sql_interlude (), ops.sql_interlude_file ());
            sql << "/*" << endl
                << " * End interlude. */" << endl
                << endl;
          }

          if (!ops.omit_create ())
            relational::schema::generate_create ();

          // Copy epilogue.
          //
          if (!ops.sql_epilogue ().empty () ||
              !ops.sql_epilogue_file ().empty ())
          {
            sql << "/* Begin epilogue." << endl
                << " */" << endl;
            append (sql, ops.sql_epilogue (), ops.sql_epilogue_file ());
            sql << "/*" << endl
                << " * End epilogue. */" << endl
                << endl;
          }

          relational::schema::generate_epilogue ();
          break;
        }
      }
    }

    // Communicate the sloc count to the driver. This is necessary to
    // correctly handle the total if we are compiling multiple files in
    // one invocation.
    //
    if (ops.show_sloc () || ops.sloc_limit_specified ())
      cout << "odb:sloc:" << sloc_total << endl;

    auto_rm.cancel ();
  }
  catch (operation_failed const&)
  {
    // Code generation failed. Diagnostics has already been issued.
    //
    throw failed ();
  }
  catch (semantics::invalid_path const& e)
  {
    cerr << "error: '" << e.path () << "' is not a valid filesystem path"
         << endl;
    throw failed ();
  }
  catch (fs::error const&)
  {
    // Auto-removal of generated files failed. Ignore it.
    //
    throw failed ();
  }
}
