/*
 * Copyright 2015 Telenav Inc. All rights reserved.
 *
 * The interface is based on working draft for C++ standard filesystem library
 * (http://cplusplus.github.io/filesystem-ts/working-draft.html), though we're NOT
 * aiming for fully compliant implementation.
 *
 * The implementation is partially based on boost::filesystem.
 */
#ifndef TN_FOUNDATION_SIMPLE_FILESYSTEM_HPP
#define TN_FOUNDATION_SIMPLE_FILESYSTEM_HPP

#include <string>
#include <stdexcept>

#include "foundation/cxx11compat.hpp"
#include "foundation/stdint.hpp"
#include "foundation/noncopyable.hpp"

namespace tn {
namespace simple_filesystem {

class path;

class filesystem_error : public std::runtime_error {
 public:
  filesystem_error();
  filesystem_error(const std::string &what_arg);
  filesystem_error(const std::string &what_arg, int errcode);
  filesystem_error(const std::string &what_arg, const path &path1);
  filesystem_error(const std::string &what_arg, const path &path1, int errcode);
  filesystem_error(const std::string &what_arg, const path &path1, const path &path2);
  filesystem_error(const std::string &what_arg, const path &path1, const path &path2, int errcode);

  bool ok() const TN_NOEXCEPT { return *what() == '\0'; }

 private:
  static std::string format_msg(const std::string &what_arg, const path *path1, const path *path2, int *errcode);
};

enum file_type {
  no_file_type = 0, // type of the file has not been determined or an error occurred
  file_not_found = -1,
  regular_file = 1,
  directory_file = 2,
  symlink_file = 3,
  unknown_file = 8
};

struct space_info
{
  uintmax_t capacity;
  uintmax_t free;
  uintmax_t available;
};

class path {
 public:
  static const char preferred_separator;

  // constructors
  path() TN_NOEXCEPT {}
  path(const path &rhs) : pathname_(rhs.pathname_) {}
  path(const std::string &str) : pathname_(str) {}
  path(const char *str) : pathname_(str) {}

  // assignments
  path &operator=(const path &p) {
    pathname_ = p.pathname_;
    return *this;
  }

  // appends
  path &operator/=(const path &p) { return append(p); }
  path &operator/=(const std::string &s) { return append(path(s)); }
  path &operator/=(const char *s) { return append(path(s)); }
  path &append(const path &p);

  // modifiers
  void  clear() TN_NOEXCEPT {
    pathname_.clear();
  }

  // conversions
  const char *c_str() const TN_NOEXCEPT {
    return pathname_.c_str();
  }
  const std::string &string() const TN_NOEXCEPT {
    return pathname_;
  }

  // compare
  int compare(const path &p) const TN_NOEXCEPT {
    return pathname_.compare(p.pathname_);
  }
  bool operator<(const path &rhs) const TN_NOEXCEPT {
    return pathname_ < rhs.pathname_;
  }

  // decomposition
  path parent_path() const;
  path filename() const;

  // query
  bool empty() const TN_NOEXCEPT {
    return pathname_.empty();
  }

 private:
  size_t append_separator_if_needed();
  bool is_separator(char c) const;
  bool is_root_separator(const std::string& str, size_t pos) const;
  size_t filename_pos(const std::string &str, size_t end_pos) const;
  size_t root_directory_start(const std::string &path, size_t size) const;
  size_t parent_path_end() const;

  std::string pathname_;
};

inline path operator/(const path &lhs, const path &rhs)  {
  return path(lhs) /= rhs;
}

struct directory_iterator_native_data;

class directory_iterator : noncopyable {
 public:
  typedef path value_type;
  typedef const path *pointer;
  typedef const path &reference;
  typedef std::input_iterator_tag iterator_category;

  static directory_iterator end;

  directory_iterator() TN_NOEXCEPT;
  directory_iterator(const path& p, filesystem_error& err) TN_NOEXCEPT;
  ~directory_iterator();

#if _TN_FOUNDATION_HAS_EXCEPTIONS
  explicit directory_iterator(const path &p);
  directory_iterator &operator++();
#endif

  bool operator!=(const directory_iterator &rhs) const TN_NOEXCEPT {
    return nd_ != rhs.nd_;
  }
  bool operator==(const directory_iterator &rhs) const TN_NOEXCEPT {
    return nd_ == rhs.nd_;
  }

  const path &operator*() const {
    return curr_path;
  }
  
  const path *operator->() const {
    return &curr_path;
  }

  directory_iterator &increment(filesystem_error& err) TN_NOEXCEPT;
 
 private:
  void init(const path& p, filesystem_error& err) TN_NOEXCEPT;
  void close();

  directory_iterator_native_data *nd_;
  path curr_path;
};

/**
 * Create a directory, the parent must exist.
 *
 * @param p the path of the directory to be created.
 * @return true if a new directory was created, false otherwise (i.e. it already existed)
 * @throw filesystem_error on error
 */
#if _TN_FOUNDATION_HAS_EXCEPTIONS
  bool create_directory(const path &p);
#endif
bool create_directory(const path &p, filesystem_error& err) TN_NOEXCEPT;


/**
 * Create a directory creating parent directories if neccessary.
 *
 * @param p the path of the directory to be created.
 * @return true if a new directory was created, false otherwise (i.e. it already existed)
 * @throw filesystem_error on error
 */
#if _TN_FOUNDATION_HAS_EXCEPTIONS
  bool create_directories(const path &p);
#endif
bool create_directories(const path &p, filesystem_error& err) TN_NOEXCEPT;

/**
 * Check whether file exists
 *
 * @param p file path
 * @throw filesystem_error on error
 */
#if _TN_FOUNDATION_HAS_EXCEPTIONS
  bool exists(const path &p);
#endif
bool exists(const path &p, filesystem_error& err) TN_NOEXCEPT;

/**
 * Check whether file is directory.
 *
 * @param p file path
 * @throw filesystem_error on error
 */
#if _TN_FOUNDATION_HAS_EXCEPTIONS
  bool is_directory(const path &p);
#endif
bool is_directory(const path &p, filesystem_error& err) TN_NOEXCEPT;

/**
 * Check whether file is a regular file (i.e. not a symlink,
 * directory, etc.)
 *
 * @param p file path
 * @throw filesystem_error on error
 */
#if _TN_FOUNDATION_HAS_EXCEPTIONS
  bool is_regular_file(const path &p);
#endif
bool is_regular_file(const path &p, filesystem_error& err) TN_NOEXCEPT;

/**
 * Remove file or directory if it exists. Directory must be empty, use
 * remove_all API to recursively remove a directory with its contents.
 *
 * @param p path to remove
 * @return true if file or directory was removed, false otherwise (i.e. it did not exist)
 * @throw filesystem_error on error
 */
#if _TN_FOUNDATION_HAS_EXCEPTIONS
  bool remove(const path &p);
#endif
bool remove(const path &p, filesystem_error& err) TN_NOEXCEPT;

/**
 * Recursively deletes the contents of the path if it exists.
 *
 * @param p path to remove
 * @return number of files and directories deleted
 * @throw filesystem_error on error
 */
#if _TN_FOUNDATION_HAS_EXCEPTIONS
  uintmax_t remove_all(const path &p);
#endif
uintmax_t remove_all(const path &p, filesystem_error& err) TN_NOEXCEPT;

/**
 * Rename file or directory.
 *
 * @param old_p old path
 * @param new_p new path
 * @throw filesystem_error on error
 */
#if _TN_FOUNDATION_HAS_EXCEPTIONS
  void rename(const path &old_p, const path &new_p);
#endif
void rename(const path &old_p, const path &new_p, filesystem_error& err) TN_NOEXCEPT;

/**
 * Get file size in bytes.
 *
 * @param p: file path
 * @throw filesystem_error on error
 */
#if _TN_FOUNDATION_HAS_EXCEPTIONS
  uintmax_t file_size(const path &p);
#endif
uintmax_t file_size(const path &p, filesystem_error& err) TN_NOEXCEPT;

/**
 * Recursively copy files or directories. Destination path must not 
 * exist (use remove_all if you need to overwrite existing files)
 *
 * @param from copy from path
 * @param to copy to path
 * @throw filesystem_error on error
 *
 * Note: on WinCE copying does not preserve directory permissions 
 */
#if _TN_FOUNDATION_HAS_EXCEPTIONS
  void copy(const path& from, const path& to);
#endif
void copy(const path& from, const path& to, filesystem_error& err) TN_NOEXCEPT;

/**
 * Get free disk space at given mount point
 */
#if _TN_FOUNDATION_HAS_EXCEPTIONS
  space_info space(const path& p);
#endif
space_info space(const path& p, filesystem_error& err) TN_NOEXCEPT;

} // namespace simple_filesystem
} // namespace tn

#endif /* TN_FOUNDATION_SIMPLE_FILESYSTEM_HPP */
