// file      : cutl/path -*- C++ -*-
// copyright : Copyright (c) 2014-2015 Code Synthesis Tools CC
// license   : MIT; see accompanying LICENSE file

#ifndef BUILD_PATH
#define BUILD_PATH

#include <string>
#include <ostream>
#include <utility> // move
#include <exception>

namespace build
{
  template <typename C>
  class basic_path;

  template <typename C>
  struct path_traits
  {
    typedef std::basic_string<C> string_type;
    typedef typename string_type::size_type size_type;

    // Canonical directory and path seperators.
    //
#ifdef _WIN32
    static C const directory_separator = '\\';
    static C const path_separator = ';';
#else
    static C const directory_separator = '/';
    static C const path_separator = ':';
#endif

    // Directory separator tests. On some platforms there
    // could be multiple seperators. For example, on Windows
    // we check for both '/' and '\'.
    //
    static bool
    is_separator (C c)
    {
#ifdef _WIN32
      return c == '\\' || c == '/';
#else
      return c == '/';
#endif
    }

    static size_type
    find_separator (string_type const& s, size_type pos = 0)
    {
      for (size_type n (s.size ()); pos < n; ++pos)
      {
        if (is_separator (s[pos]))
          return pos;
      }

      return string_type::npos;
    }

    static size_type
    rfind_separator (string_type const& s, size_type pos = string_type::npos)
    {
      if (pos == string_type::npos)
        pos = s.size ();
      else
        pos++;

      for (; pos > 0; --pos)
      {
        if (is_separator (s[pos - 1]))
          return pos - 1;
      }

      return string_type::npos;
    }

    // Return the position of '.' or npos if there is no extension.
    //
    static size_type
    find_extension (string_type const& s)
    {
      size_type i (s.size ());

      for (; i > 0; --i)
      {
        C c (s[i - 1]);

        if (c == '.')
          break;

        if (is_separator (c))
        {
          i = 0;
          break;
        }
      }

      // Weed out paths like ".txt" (and "/.txt") and "txt.".
      //
      if (i > 1 && !is_separator (s[i - 2]) && i != s.size ())
        return i - 1;
      else
        return string_type::npos;
    }

    static int
    compare (string_type const& l, string_type const& r)
    {
      size_type ln (l.size ()), rn (r.size ()), n (ln < rn ? ln : rn);
      for (size_type i (0); i != n; ++i)
      {
#ifdef _WIN32
        C lc (tolower (l[i])), rc (tolower (r[i]));
#else
        C lc (l[i]), rc (r[i]);
#endif
        if (is_separator (lc) && is_separator (rc))
          continue;

        if (lc < rc) return -1;
        if (lc > rc) return 1;
      }

      return ln < rn ? -1 : (ln > rn ? 1 : 0);
    }

  private:
#ifdef _WIN32
    static C
    tolower (C);
#endif
  };

  template <typename C>
  class invalid_basic_path;

  typedef basic_path<char> path;
  typedef invalid_basic_path<char> invalid_path;

  typedef basic_path<wchar_t> wpath;
  typedef invalid_basic_path<wchar_t> invalid_wpath;

  //
  //
  class invalid_path_base: std::exception
  {
  public:
    virtual char const*
    what () const throw ();
  };

  template <typename C>
  class invalid_basic_path: public invalid_path_base
  {
  public:
    typedef std::basic_string<C> string_type;

    invalid_basic_path (C const* p): path_ (p) {}
    invalid_basic_path (string_type const& p): path_ (p) {}
    ~invalid_basic_path () throw () {}

    string_type const&
    path () const
    {
      return path_;
    }

  private:
    string_type path_;
  };

  template <typename C>
  class basic_path
  {
  public:
    typedef std::basic_string<C> string_type;
    typedef typename string_type::size_type size_type;

    typedef path_traits<C> traits;

    // Construct special empty path.
    //
    basic_path ()
    {
    }

    explicit
    basic_path (C const* s)
        : path_ (s)
    {
      init ();
    }

    basic_path (C const* s, size_type n)
        : path_ (s, n)
    {
      init ();
    }

    explicit
    basic_path (string_type s)
        : path_ (std::move (s))
    {
      init ();
    }

    basic_path (const string_type& s, size_type n)
        : path_ (s, 0, n)
    {
      init ();
    }

    basic_path (const string_type& s, size_type p, size_type n)
        : path_ (s, p, n)
    {
      init ();
    }

    void
    swap (basic_path& p)
    {
      path_.swap (p.path_);
    }

    void
    clear ()
    {
      path_.clear ();
    }

    static basic_path
    current ();

    static void
    current (basic_path const&);

  public:
    bool
    empty () const
    {
      return path_.empty ();
    }

    bool
    absolute () const;

    bool
    relative () const
    {
      return !absolute ();
    }

    bool
    root () const;

    // Return true if *this is a sub-path of the specified path (i.e.,
    // the specified path is a prefix). Expects both paths to be
    // normalized.
    //
    bool
    sub (const basic_path& p) const
    {
      return path_.compare (0, p.path_.size (), p.path_) == 0;
    }

  public:
    // Return the path without the directory part.
    //
    basic_path
    leaf () const;

    // Return the path without the specified directory part. Throws
    // invalid_path if the directory is not a prefix of *this. Expects
    // both paths to be normalized.
    //
    basic_path
    leaf (basic_path const&) const;

    // Return the directory part of the path or empty path if
    // there is no directory.
    //
    basic_path
    directory () const;

    // Return the directory part of the path without the specified
    // leaf part. Throws invalid_path if the leaf is not a suffix of
    // *this. Expects both paths to be normalized.
    //
    basic_path
    directory (basic_path const&) const;

    // Return the root directory of the path or empty path if
    // the directory is not absolute.
    //
    basic_path
    root_directory () const;

    // Return the path without the extension, if any.
    //
    basic_path
    base () const;

    // Return the extension or NULL if not present.
    //
    const C*
    extension () const;

    // Return a path relative to the specified path that is equivalent
    // to *this. Throws invalid_path if a relative path cannot be derived
    // (e.g., paths are on different drives on Windows).
    //
    basic_path
    relative (basic_path) const;

  public:
    // Normalize the path. This includes collapsing the '.' and '..'
    // directories if possible, collapsing multiple directory
    // separators, and converting all directory separators to the
    // canonical form. Returns *this.
    //
    basic_path&
    normalize ();

    // Make the path absolute using the current directory unless
    // it is already absolute.
    //
    basic_path&
    complete ();

  public:
    basic_path
    operator/ (basic_path const& x) const
    {
      basic_path r (*this);
      r /= x;
      return r;
    }

    basic_path&
    operator/= (basic_path const&);

    basic_path
    operator+ (string_type const& s) const
    {
      return basic_path (path_ + s);
    }

    basic_path
    operator+ (C c) const
    {
      return basic_path (path_ + c);
    }

    basic_path&
    operator+= (string_type const& s)
    {
      path_ += s;
      return *this;
    }

    basic_path&
    operator+= (C c)
    {
      path_ += c;
      return *this;
    }

    // Note that comparison is case-insensitive if the filesystem is
    // not case-sensitive (e.g., Windows).
    //
    bool
    operator== (basic_path const& x) const
    {
      return traits::compare (path_, x.path_) == 0;
    }

    bool
    operator!= (basic_path const& x) const
    {
      return !(*this == x);
    }

    bool
    operator< (basic_path const& x) const
    {
      return traits::compare (path_, x.path_) < 0;
    }

  public:
    const string_type&
    string () const
    {
      return path_;
    }

    // If possible, return a POSIX representation of the path. For example,
    // for a Windows path in the form foo\bar this function will return
    // foo/bar. If it is not possible to create a POSIX representation for
    // this path (e.g., c:\foo), this function will throw the invalid_path
    // exception.
    //
    string_type
    posix_string () const;

  private:
    void
    init ();

  private:
    string_type path_;
  };

  /*
  template <typename C>
  inline std::basic_ostream<C>&
  operator<< (std::basic_ostream<C>& os, basic_path<C> const& p)
  {
    return os << p.string ();
  }
  */
}

namespace std
{
  template <typename C>
  struct hash<build::basic_path<C>>: hash<basic_string<C>>
  {
    size_t
    operator() (const build::basic_path<C>& p) const noexcept
    {
      return hash<basic_string<C>>::operator() (p.string ());
    }
  };
}

#include <build/path.ixx>
#include <build/path.txx>

#endif // BUILD_PATH