// file      : build/scope -*- C++ -*-
// copyright : Copyright (c) 2014-2015 Code Synthesis Ltd
// license   : MIT; see accompanying LICENSE file

#ifndef BUILD_SCOPE
#define BUILD_SCOPE

#include <functional>    // function
#include <unordered_set>
#include <unordered_map>

#include <butl/path-map>

#include <build/types>
#include <build/module>
#include <build/variable>
#include <build/prerequisite>
#include <build/target-type>
#include <build/rule-map>
#include <build/operation>

namespace build
{
  class scope
  {
  public:
    const dir_path&
    path () const {return *path_;} // Absolute and normalized.

    const dir_path&
    src_path () const {return *src_path_;} // Corresponding src path.

    const dir_path* src_path_ {nullptr}; // Cached src_{root,base} var value.

    scope*
    parent_scope () const {return parent_;}

    // Root scope of this scope or NULL if this scope is not (yet)
    // in any (known) project. Note that if the scope itself is
    // root, then this function return this. To get to the outer
    // root, query the root scope of the parent.
    //
    scope*
    root_scope () const {return root_;}

    // Root scope of a strong amalgamation of this scope or NULL if
    // this scope is not (yet) in any (known) project. If there is
    // no strong amalgamation, then this function returns the root
    // scope of the project (in other words, in this case a project
    // is treated as its own strong amalgamation).
    //
    scope*
    strong_scope () const
    {
      return root_ != nullptr
        ? root_->strong_ != nullptr ? root_->strong_ : root_
        : nullptr;
    }

    bool
    root () const {return root_ == this;}

    // Variables.
    //
  public:
    variable_map vars;

    // Lookup, including in outer scopes. If you only want to lookup
    // in this scope, do it on the the variables map directly.
    //
    lookup<const value>
    operator[] (const variable&) const;

    lookup<const value>
    operator[] (const std::string& name) const
    {
      return operator[] (variable_pool.find (name));
    }

    // Return a value suitable for assignment (or append if you only
    // want to append to the value from this scope). If the variable
    // does not exist in this scope's map, then a new one with the
    // NULL value is added and returned. Otherwise the existing value
    // is returned.
    //
    value&
    assign (const variable& var) {return vars.assign (var).first.get ();}

    value&
    assign (const std::string& name) {return vars.assign (name).first.get ();}

    // Return a value suitable for appending. If the variable does not
    // exist in this scope's map, then outer scopes are searched for
    // the same variable. If found then a new variable with the found
    // value is added to this scope and returned. Otherwise this
    // function proceeds as assign().
    //
    value&
    append (const variable&);

    value&
    append (const std::string& name)
    {
      return append (variable_pool.find (name));
    }

    // Target type/pattern-specific variables.
    //
    variable_type_map target_vars;

    // Prerequisite cache.
    //
  public:
    prerequisite_set prerequisites;

    // Meta/operations supported by this project (set on the root
    // scope only).
    //
    build::meta_operations meta_operations;
    build::operations operations;

    typedef build::path path_type;

    // Set of buildfiles already loaded for this scope. The included
    // buildfiles are checked against the project's root scope while
    // imported -- against the global scope (global_scope).
    //
    std::unordered_set<path_type> buildfiles;

    // Target types.
    //
  public:
    target_type_map target_types;

    const target_type*
    find_target_type (const char*, const scope** = nullptr) const;

    // Given a name, figure out its type, taking into account extensions,
    // special names (e.g., '.' and '..'), or anything else that might be
    // relevant. Also process the name (in place) by extracting the
    // extension, adjusting dir/value, etc., (note that the dir is not
    // necessarily normalized). Return NULL if not found.
    //
    const target_type*
    find_target_type (name&, const std::string*& ext) const;

    // Rules.
    //
  public:
    rule_map rules;

    // Modules.
    //
  public:
    loaded_module_map modules; // Only on root scope.

  private:
    friend class scope_map;
    friend class temp_scope;

    // These two from <build/file> set strong_.
    //
    friend void create_bootstrap_outer (scope&);
    friend scope& create_bootstrap_inner (scope&, const dir_path&);

    scope () = default;

    const dir_path* path_; // Pointer to the key in scope_map.
    scope* parent_;
    scope* root_;
    scope* strong_ = nullptr; // Only set on root sopes.
                              // NULL means no strong amalgamtion.
  };

  // Temporary scope. The idea is to be able to create a temporary
  // scope in order not to change the variables in the current scope.
  // Such a scope is not entered in to the scope map. As a result it
  // can only be used as a temporary set of variables. In particular,
  // defining targets/prerequisites directly in such a scope will surely
  // end up badly. Defining any nested scopes will be as if defining
  // such a scope in the parent (since path() returns parent's path).
  //
  class temp_scope: public scope
  {
  public:
    temp_scope (scope& p)
    {
      path_ = p.path_;
      parent_ = &p;
      root_ = p.root_;
      // No need to copy strong_ since we are never root scope.
    }
  };

  using scope_map_base = butl::dir_path_map<scope>;
  class scope_map: public scope_map_base
  {
  public:
    // Note that we assume the first insertion into the map is that
    // of the global scope.
    //
    std::pair<scope&, bool>
    insert (const dir_path&, bool root);

    scope&
    operator[] (const dir_path& p) {return insert (p, false).first;}

    // Find the most qualified scope that encompasses this path.
    //
    scope&
    find (const dir_path&);

    scope&
    find (const path& p)
    {
      // Natural thing to do here would be to call find (p.directory ()).
      // However, there could be a situation where the passed path is a
      // directory (i.e., the calling code does not know what it is dealing
      // with), so let's use the whole path.
      //
      return find (dir_path (p.string ()));
    }
  };

  extern scope_map scopes;
  extern scope* global_scope;
}

#endif // BUILD_SCOPE