// file      : libbuild2/cxx/init.cxx -*- C++ -*-
// license   : MIT; see accompanying LICENSE file

#include <libbuild2/cxx/init.hxx>

#include <libbuild2/scope.hxx>
#include <libbuild2/diagnostics.hxx>

#include <libbuild2/cc/guess.hxx>
#include <libbuild2/cc/module.hxx>

#include <libbuild2/cxx/target.hxx>

#ifndef BUILD2_DEFAULT_CXX
#  ifdef BUILD2_NATIVE_CXX
#    define BUILD2_DEFAULT_CXX BUILD2_NATIVE_CXX
#  else
#    define BUILD2_DEFAULT_CXX ""
#  endif
#endif

using namespace std;
using namespace butl;

namespace build2
{
  namespace cxx
  {
    using cc::compiler_id;
    using cc::compiler_type;
    using cc::compiler_class;
    using cc::compiler_info;

    class config_module: public cc::config_module
    {
    public:
      explicit
      config_module (config_data&& d): cc::config_module (move (d)) {}

      virtual strings
      translate_std (const compiler_info&,
                     const target_triplet&,
                     scope&,
                     const string*) const override;
    };

    using cc::module;

    strings config_module::
    translate_std (const compiler_info& ci,
                   const target_triplet& tt,
                   scope& rs,
                   const string* v) const
    {
      strings r;

      compiler_type ct (ci.id.type);
      compiler_class cl (ci.class_);
      uint64_t mj (ci.version.major);
      uint64_t mi (ci.version.minor);
      uint64_t p (ci.version.patch);

      // Besides various `c++NN` we have two special values: `latest` and
      // `experimental`.
      //
      // The semantics of the `latest` value is the latest available standard
      // that is not necessarily complete or final but is practically usable.
      // In other words, a project that uses this value and does not rely on
      // any unstable/bleeding edge parts of the standard (or takes care to
      // deal with them, for example, using feature test macros), can be
      // reasonably expected to work. In particular, this is the value we use
      // by default in projects created by bdep-new(1) as well as to build the
      // build2 toolchain itself.
      //
      // The `experimental` value, as the name suggests, is the latest
      // available standard that is not necessarily usable in real projects.
      // By definition, `experimental` >= `latest`.
      //
      // In addition to the `experimental` value itself we have a number of
      // feature flags that can be used to enable or disable certain major
      // parts (such as modules, concepts, etc) in this mode. They are also
      // used to signal back to the project whether a particular feature is
      // available. A feature flag set by the user has a tri-state semantics:
      //
      // - false        - disabled
      // - unspecified  - enabled if practically usable
      // - true         - enabled even if practically unusable
      //
      bool latest       (v != nullptr && *v == "latest");
      bool experimental (v != nullptr && *v == "experimental");

      // Feature flags.
      //
      auto& vp (rs.var_pool ());

      //bool concepts (false);
      //auto& v_c (vp.insert<bool> ("cxx.features.concepts"));

      bool modules (false);
      auto& v_m (vp.insert<bool> ("cxx.features.modules"));

      // NOTE: see also module sidebuild subproject if changing anything about
      // modules here.

      string o;

      switch (cl)
      {
      case compiler_class::msvc:
        {
          // C++ standard-wise, with VC you got what you got up until 14.2.
          // Starting with 14.3 there is now the /std: switch which defaults
          // to c++14 but can be set to c++latest. And from 15.3 it can be
          // c++17.
          //
          bool v16_0 (         mj > 19 || (mj == 19 && mi >= 20));
          bool v15_3 (v16_0 || (mj == 19 && mi >= 11));
          bool v14_3 (v15_3 || (mj == 19 && (mi > 0 || (mi == 0 && p >= 24215))));

          // The question is also whether we should verify that the requested
          // standard is provided by this VC version. And if so, from which
          // version should we say VC supports 11, 14, and 17? We should
          // probably be as loose as possible here since the author will
          // always be able to tighten (but not loosen) this in the buildfile
          // (i.e., detect unsupported versions).
          //
          // For now we are not going to bother doing this for C++03.
          //
          if (experimental)
          {
            if (v14_3)
              o = "/std:c++latest";
          }
          else if (latest)
          {
            // We used to map `latest` to `c++latest` but starting from 16.1,
            // VC seem to have adopted the "move fast and break things" motto
            // for this mode. So starting from 16 we only enable it in
            // `experimental`.
            //
            if (v16_0)
              o = "/std:c++17";
            else if (v14_3)
              o = "/std:c++latest";
          }
          else if (v == nullptr)
            ;
          else if (*v != "98" && *v != "03")
          {
            bool sup (false);

            if      (*v == "11") // C++11 since VS2010/10.0.
            {
              sup = mj >= 16;
            }
            else if (*v == "14") // C++14 since VS2015/14.0.
            {
              sup = mj >= 19;
            }
            else if (*v == "17") // C++17 since VS2015/14.0u2.
            {
              // Note: the VC15 compiler version is 19.10.
              //
              sup = (mj > 19 ||
                     (mj == 19 && (mi > 0 || (mi == 0 && p >= 23918))));
            }

            if (!sup)
              fail << "C++" << *v << " is not supported by " << ci.signature <<
                info << "required by " << project (rs) << '@' << rs;

            if (v15_3)
            {
              if      (*v == "14") o = "/std:c++14";
              else if (*v == "17") o = "/std:c++17";
            }
            else if (v14_3)
            {
              if      (*v == "14") o = "/std:c++14";
              else if (*v == "17") o = "/std:c++latest";
            }
          }

          if (!o.empty ())
            r.push_back (move (o));

          break;
        }
      case compiler_class::gcc:
        {
          if (latest || experimental)
          {
            switch (ct)
            {
            case compiler_type::gcc:
              {
                if      (mj >= 8)            o = "-std=c++2a"; // 20
                else if (mj >= 5)            o = "-std=c++1z"; // 17
                else if (mj == 4 && mi >= 8) o = "-std=c++1y"; // 14
                else if (mj == 4 && mi >= 4) o = "-std=c++0x"; // 11

                break;
              }
            case compiler_type::clang:
              {
                // Clang 10.0.0 targeting MSVC 16.4 and 16.5 (preview) in the
                // c++2a mode uncovers some Concepts-related bugs in MSVC
                // (LLVM bug #44956). So in this case we for now map `latest`
                // to c++17. @@ TMP
                //
                // Note that if 10.0.0 is released without a workaround, then
                // we will need to carry this forward at least for 16.4 since
                // this version is unlikely to ever be fixed. Which means we
                // will somehow need to pass the version of MSVC Clang is
                // targeting.
                //
                if (latest && mj == 10 && tt.system == "win32-msvc")
                {
                  o = "-std=c++17";
                }
                else if  (mj >= 5)                         o = "-std=c++2a";
                else if  (mj >  3 || (mj == 3 && mi >= 5)) o = "-std=c++1z";
                else if  (mj == 3 && mi >= 4)              o = "-std=c++1y";
                else     /* ??? */                         o = "-std=c++0x";

                break;
              }
            case compiler_type::icc:
              {
                if      (mj >= 17)                         o = "-std=c++1z";
                else if (mj >  15 || (mj == 15 && p >= 3)) o = "-std=c++1y";
                else    /* ??? */                          o = "-std=c++0x";

                break;
              }
            default:
              assert (false);
            }
          }
          else if (v == nullptr)
            ;
          else
          {
            // Translate 11 to 0x, 14 to 1y, 17 to 1z, 20 to 2a, and 23 to 2b
            // for compatibility with older versions of the compilers.
            //
            o = "-std=";

            if      (*v == "23") o += "c++2b";
            else if (*v == "20") o += "c++2a";
            else if (*v == "17") o += "c++1z";
            else if (*v == "14") o += "c++1y";
            else if (*v == "11") o += "c++0x";
            else if (*v == "03") o += "c++03";
            else if (*v == "98") o += "c++98";
            else o += *v; // In case the user specifies `gnu++NN` or some such.
          }

          if (!o.empty ())
            r.push_back (move (o));

          break;
        }
      }

      if (experimental)
      {
        switch (ct)
        {
        case compiler_type::msvc:
          {
            // Starting with 15.5 (19.12) Visual Studio-created projects
            // default to the strict mode. However, this flag currently tends
            // to trigger too many compiler bugs. So for now we leave it to
            // the experimenters to enjoy.
            //
            if (mj > 19 || (mj == 19 && mi >= 12))
              r.push_back ("/permissive-");

            break;
          }
        default:
          break;
        }

        // Unless disabled by the user, try to enable C++ modules.
        //
        lookup l;
        if (!(l = rs[v_m]) || cast<bool> (l))
        {
          switch (ct)
          {
          case compiler_type::msvc:
            {
              // While modules are supported in VC 15.0 (19.10), there is a
              // bug in the separate interface/implementation unit support
              // which makes them pretty much unusable. This has been fixed in
              // 15.3 (19.11). And 15.5 (19.12) supports the `export module
              // M;` syntax. And 16.4 (19.24) supports the global module
              // fragment.
              //
              if (mj > 19 || (mj == 19 && mi >= (l ? 10 : 12)))
              {
                r.push_back (
                  mj > 19  || mi >= 24     ?
                  "/D__cpp_modules=201810" : // p1103 (merged modules)
                  mj == 19 || mi >= 12     ?
                  "/D__cpp_modules=201704" : // p0629r0 (export module M;)
                  "/D__cpp_modules=201703"); // n4647   (       module M;)

                r.push_back ("/experimental:module");
                modules = true;
              }
              break;
            }
          case compiler_type::gcc:
            {
              // We now use extended/experimental module mapper support which
              // is currently only available in our c++-modules-ex branch.
              // But let's allow forcing it to plain c++-modules in case
              // things got merged or the user feels adventurous.
              //
              if (mj >= 10 &&
                  ci.version.build.find (l
                                         ? "c++-modules"
                                         : "c++-modules-ex") != string::npos)
              {
                // Currently defines __cpp_modules=201810 which is said to
                // correspond to p1103 (merged modules).
                //
                r.push_back ("-fmodules-ts");
                modules = true;
              }
              break;
            }
          case compiler_type::clang:
            {
              // Enable starting with Clang 6.0.0.
              //
              // Note that we are using Apple to vanilla Clang version re-map
              // from above so may need to update things there as well.
              //
              // Also see Clang modules support hack in cc::compile.
              //
              // @@ Clang 9 enables modules by default in C++2a. We should
              //    probably reflect this in the modules value.
              //
              if (mj >= 6)
              {
                r.push_back ("-D__cpp_modules=201704"); // p0629r0
                r.push_back ("-fmodules-ts");
                modules = true;
              }
              break;
            }
          case compiler_type::icc:
            break; // No modules support yet.
          }
        }
      }

      rs.assign (v_m) = modules;
      //rs.assign (v_c) = concepts;

      return r;
    }

    static const char* const hinters[] = {"c", nullptr};

    // See cc::module for details on guess_init vs config_init.
    //
    bool
    guess_init (scope& rs,
                scope& bs,
                const location& loc,
                bool,
                bool,
                module_init_extra& extra)
    {
      tracer trace ("cxx::guess_init");
      l5 ([&]{trace << "for " << bs;});

      // We only support root loading (which means there can only be one).
      //
      if (&rs != &bs)
        fail (loc) << "cxx.guess module must be loaded in project root";

      // Load cc.core.vars so that we can cache all the cc.* variables.
      //
      load_module (rs, rs, "cc.core.vars", loc);

      // Enter all the variables and initialize the module data.
      //
      auto& vp (rs.var_pool ());

      cc::config_data d {
        cc::lang::cxx,

        "cxx",
        "c++",
        BUILD2_DEFAULT_CXX,
        ".ii",

        hinters,

        // NOTE: remember to update documentation if changing anything here.
        //
        vp.insert<strings> ("config.cxx"),
        vp.insert<string>  ("config.cxx.id"),
        vp.insert<string>  ("config.cxx.version"),
        vp.insert<string>  ("config.cxx.target"),
        vp.insert<string>  ("config.cxx.std"),
        vp.insert<strings> ("config.cxx.poptions"),
        vp.insert<strings> ("config.cxx.coptions"),
        vp.insert<strings> ("config.cxx.loptions"),
        vp.insert<strings> ("config.cxx.aoptions"),
        vp.insert<strings> ("config.cxx.libs"),

        // List of translatable headers. Inclusions of such headers are
        // translated to the corresponding header unit imports.
        //
        // A header can be specified either as an absolute and normalized path
        // or as a <>-style include name. The latter kind is automatically
        // translated to the absolute form based on the compiler's system (as
        // opposed to -I) header search paths. Note also that all entries must
        // be specified before loading the cxx module.
        //
        &vp.insert<strings> ("config.cxx.translatable_headers"),

        vp.insert<process_path> ("cxx.path"),
        vp.insert<strings>      ("cxx.mode"),
        vp.insert<dir_paths>    ("cxx.sys_lib_dirs"),
        vp.insert<dir_paths>    ("cxx.sys_inc_dirs"),

        vp.insert<string>   ("cxx.std"),

        vp.insert<strings>  ("cxx.poptions"),
        vp.insert<strings>  ("cxx.coptions"),
        vp.insert<strings>  ("cxx.loptions"),
        vp.insert<strings>  ("cxx.aoptions"),
        vp.insert<strings>  ("cxx.libs"),

        &vp.insert<strings> ("cxx.translatable_headers"),

        vp["cc.poptions"],
        vp["cc.coptions"],
        vp["cc.loptions"],
        vp["cc.aoptions"],
        vp["cc.libs"],

        vp.insert<strings>      ("cxx.export.poptions"),
        vp.insert<strings>      ("cxx.export.coptions"),
        vp.insert<strings>      ("cxx.export.loptions"),
        vp.insert<vector<name>> ("cxx.export.libs"),

        vp["cc.export.poptions"],
        vp["cc.export.coptions"],
        vp["cc.export.loptions"],
        vp["cc.export.libs"],

        vp.insert<string> ("cxx.stdlib"),

        vp["cc.runtime"],
        vp["cc.stdlib"],

        vp["cc.type"],
        vp["cc.system"],
        vp["cc.module_name"],
        vp["cc.reprocess"],

        // Ability to signal that source is already (partially) preprocessed.
        // Valid values are 'none' (not preprocessed), 'includes' (no #include
        // directives in source), 'modules' (as above plus no module
        // declaration depends on preprocessor, e.g., #ifdef, etc), and 'all'
        // (the source is fully preprocessed). Note that for 'all' the source
        // can still contain comments and line continuations. Note also that
        // for some compilers (e.g., VC) there is no way to signal that the
        // source is already preprocessed.
        //
        // What about header unit imports? Well, they are in a sense
        // standardized precompiled headers so we treat them as includes.
        //
        vp.insert<string>   ("cxx.preprocessed"),

        nullptr, // cxx.features.symexport (set in init() below).

        vp.insert<string>   ("cxx.id"),
        vp.insert<string>   ("cxx.id.type"),
        vp.insert<string>   ("cxx.id.variant"),

        vp.insert<string>   ("cxx.class"),

        &vp.insert<string>   ("cxx.version"),
        &vp.insert<uint64_t> ("cxx.version.major"),
        &vp.insert<uint64_t> ("cxx.version.minor"),
        &vp.insert<uint64_t> ("cxx.version.patch"),
        &vp.insert<string>   ("cxx.version.build"),

        &vp.insert<string>   ("cxx.variant_version"),
        &vp.insert<uint64_t> ("cxx.variant_version.major"),
        &vp.insert<uint64_t> ("cxx.variant_version.minor"),
        &vp.insert<uint64_t> ("cxx.variant_version.patch"),
        &vp.insert<string>   ("cxx.variant_version.build"),

        vp.insert<string>   ("cxx.signature"),
        vp.insert<string>   ("cxx.checksum"),

        vp.insert<string>   ("cxx.pattern"),

        vp.insert<target_triplet> ("cxx.target"),

        vp.insert<string>   ("cxx.target.cpu"),
        vp.insert<string>   ("cxx.target.vendor"),
        vp.insert<string>   ("cxx.target.system"),
        vp.insert<string>   ("cxx.target.version"),
        vp.insert<string>   ("cxx.target.class")
      };

      // Alias some cc. variables as cxx.
      //
      vp.insert_alias (d.c_runtime,     "cxx.runtime");
      vp.insert_alias (d.c_module_name, "cxx.module_name");

      auto& m (extra.set_module (new config_module (move (d))));
      m.guess (rs, loc, extra.hints);

      return true;
    }

    bool
    config_init (scope& rs,
                 scope& bs,
                 const location& loc,
                 bool,
                 bool,
                 module_init_extra& extra)
    {
      tracer trace ("cxx::config_init");
      l5 ([&]{trace << "for " << bs;});

      // We only support root loading (which means there can only be one).
      //
      if (&rs != &bs)
        fail (loc) << "cxx.config module must be loaded in project root";

      // Load cxx.guess and share its module instance as ours.
      //
      extra.module = load_module (rs, rs, "cxx.guess", loc, extra.hints);
      extra.module_as<config_module> ().init (rs, loc, extra.hints);

      return true;
    }

    static const target_type* const hdr[] =
    {
      &hxx::static_type,
      &ixx::static_type,
      &txx::static_type,
      &mxx::static_type,
      nullptr
    };

    static const target_type* const inc[] =
    {
      &hxx::static_type,
      &h::static_type,
      &ixx::static_type,
      &txx::static_type,
      &mxx::static_type,
      &cxx::static_type,
      &c::static_type,
      nullptr
    };

    bool
    init (scope& rs,
          scope& bs,
          const location& loc,
          bool,
          bool,
          module_init_extra& extra)
    {
      tracer trace ("cxx::init");
      l5 ([&]{trace << "for " << bs;});

      // We only support root loading (which means there can only be one).
      //
      if (&rs != &bs)
        fail (loc) << "cxx module must be loaded in project root";

      // Load cxx.config.
      //
      auto& cm (
        load_module<config_module> (rs, rs, "cxx.config", loc, extra.hints));

      auto& vp (rs.var_pool ());

      bool modules (cast<bool> (rs["cxx.features.modules"]));

      bool symexport (false);
      if (modules)
      {
        auto& var (vp.insert<bool> ("cxx.features.symexport"));
        symexport = cast_false<bool> (rs[var]);
        cm.x_symexport = &var;
      }

      cc::data d {
        cm,

        "cxx.compile",
        "cxx.link",
        "cxx.install",
        "cxx.uninstall",

        cm.x_info->id.type,
        cm.x_info->id.variant,
        cm.x_info->class_,
        cm.x_info->version.major,
        cm.x_info->version.minor,
        cast<process_path>   (rs[cm.x_path]),
        cast<strings>        (rs[cm.x_mode]),
        cast<target_triplet> (rs[cm.x_target]),

        cm.tstd,

        modules,
        symexport,

        cast<dir_paths> (rs[cm.x_sys_lib_dirs]),
        cast<dir_paths> (rs[cm.x_sys_inc_dirs]),
        cm.x_info->sys_mod_dirs ? &cm.x_info->sys_mod_dirs->first : nullptr,

        cm.sys_lib_dirs_mode,
        cm.sys_inc_dirs_mode,
        cm.sys_mod_dirs_mode,

        cm.sys_lib_dirs_extra,
        cm.sys_inc_dirs_extra,

        cxx::static_type,
        modules ? &mxx::static_type : nullptr,
        hdr,
        inc
      };

      auto& m (extra.set_module (new module (move (d))));
      m.init (rs, loc, extra.hints);

      return true;
    }

    static const module_functions mod_functions[] =
    {
      // NOTE: don't forget to also update the documentation in init.hxx if
      //       changing anything here.

      {"cxx.guess",  nullptr, guess_init},
      {"cxx.config", nullptr, config_init},
      {"cxx",        nullptr, init},
      {nullptr,      nullptr, nullptr}
    };

    const module_functions*
    build2_cxx_load ()
    {
      return mod_functions;
    }
  }
}