// file      : libbuild2/functions-filesystem.cxx -*- C++ -*-
// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd
// license   : MIT; see accompanying LICENSE file

#include <libbutl/filesystem.mxx>

#include <libbuild2/function.hxx>
#include <libbuild2/variable.hxx>

using namespace std;

namespace build2
{
  // Return paths of filesystem entries that match the pattern. See
  // path_search() overloads (below) for details.
  //
  static names
  path_search (const path& pattern, const optional<dir_path>& start)
  {
    names r;
    auto add = [&r] (path&& p, const std::string&, bool interm) -> bool
    {
      // Canonicalizing paths seems to be the right thing to do. Otherwise, we
      // can end up with different separators in the same path on Windows.
      //
      if (!interm)
        r.emplace_back (
          value_traits<path>::reverse (move (p.canonicalize ())));

      return true;
    };

    // Print paths "as is" in the diagnostics.
    //
    try
    {
      if (pattern.absolute ())
        path_search (pattern, add);
      else
      {
        // An absolute start directory must be specified for the relative
        // pattern.
        //
        if (!start || start->relative ())
        {
          diag_record dr (fail);

          if (!start)
            dr << "start directory is not specified";
          else
            dr << "start directory '" << start->representation ()
               << "' is relative";

          dr << info << "pattern '" << pattern.representation ()
             << "' is relative";
        }

        path_search (pattern, add, *start);
      }
    }
    catch (const system_error& e)
    {
      diag_record d (fail);
      d << "unable to scan";

      // If the pattern is absolute, then the start directory is not used, and
      // so printing it would be misleading.
      //
      if (start && pattern.relative ())
        d << " '" << start->representation () << "'";

      d << ": " << e
        << info << "pattern: '" << pattern.representation () << "'";
    }

    return r;
  }

  using butl::path_match;

  // Return true if a path for a filesystem entry matches the pattern. See
  // path_match() overloads (below) for details.
  //
  static bool
  path_match (const path& pattern,
              const path& entry,
              const optional<dir_path>& start)
  {
    // If pattern and entry are both either absolute or relative and
    // non-empty, and the first pattern component is not a self-matching
    // wildcard, then ignore the start directory.
    //
    bool rel (pattern.relative () == entry.relative () &&
              !pattern.empty () && !entry.empty ());

    bool self (!pattern.empty () &&
               (*pattern.begin ()).find ("***") != string::npos);

    if (rel && !self)
      return path_match (pattern, entry);

    // The start directory must be specified and be absolute.
    //
    if (!start || start->relative ())
    {
      diag_record dr (fail);

      // Print paths "as is".
      //
      if (!start)
        dr << "start directory is not specified";
      else
        dr << "start directory path '" << start->representation ()
           << "' is relative";

      dr << info << "pattern: '" << pattern.representation () << "'"
         << info << "entry: '" << entry.representation () << "'";
    }

    return path_match (pattern, entry, *start);
  }

  void
  filesystem_functions (function_map& m)
  {
    function_family f (m, "filesystem");

    // path_search
    //
    // Return filesystem paths that match the pattern. If the pattern is an
    // absolute path, then the start directory is ignored (if present).
    // Otherwise, the start directory must be specified and be absolute.
    //
    f["path_search"] = [](path pattern, optional<dir_path> start)
    {
      return path_search (pattern, start);
    };

    f["path_search"] = [](path pattern, names start)
    {
      return path_search (pattern, convert<dir_path> (move (start)));
    };

    f["path_search"] = [](names pattern, optional<dir_path> start)
    {
      return path_search (convert<path> (move (pattern)), start);
    };

    f["path_search"] = [](names pattern, names start)
    {
      return path_search (convert<path>     (move (pattern)),
                          convert<dir_path> (move (start)));
    };

    // path_match
    //
    // Match a filesystem entry name against a name pattern (both are strings),
    // or a filesystem entry path against a path pattern. For the latter case
    // the start directory may also be required (see below). The semantics of
    // the pattern and name/entry arguments is determined according to the
    // following rules:
    //
    // - The arguments must be of the string or path types, or be untyped.
    //
    // - If one of the arguments is typed, then the other one must be of the
    //   same type or be untyped. In the later case, an untyped argument is
    //   converted to the type of the other argument.
    //
    // - If both arguments are untyped and the start directory is specified,
    //   then the arguments are converted to the path type.
    //
    // - If both arguments are untyped and the start directory is not
    //   specified, then, if one of the arguments is syntactically a path (the
    //   value contains a directory separator), convert them to the path type,
    //   otherwise to the string type (match as names).
    //
    // If pattern and entry paths are both either absolute or relative and
    // non-empty, and the first pattern component is not a self-matching
    // wildcard (doesn't contain ***), then the start directory is not
    // required, and is ignored if specified. Otherwise, the start directory
    // must be specified and be an absolute path.
    //
    // Name matching.
    //
    f["path_match"] = [](string pattern, string name)
    {
      return path_match (pattern, name);
    };

    // Path matching.
    //
    f["path_match"] = [](path pat, path ent, optional<dir_path> start)
    {
      return path_match (pat, ent, start);
    };

    // The semantics depends on the presence of the start directory or the
    // first two argument syntactic representation.
    //
    f["path_match"] = [](names pat, names ent, optional<names> start)
    {
      auto path_arg = [] (const names& a) -> bool
      {
        return a.size () == 1 &&
        (a[0].directory () ||
         a[0].value.find_first_of (path::traits_type::directory_separators) !=
         string::npos);
      };

      return start || path_arg (pat) || path_arg (ent)
        ? path_match (convert<path> (move (pat)),   // Match as paths.
                      convert<path> (move (ent)),
                      start
                      ? convert<dir_path> (move (*start))
                      : optional<dir_path> ())
        : path_match (convert<string> (move (pat)), // Match as strings.
                      convert<string> (move (ent)));
    };
  }
}