/** @file

  Simple path and file utilities.

  @section license License

  Licensed to the Apache Software Foundation (ASF) under one
  or more contributor license agreements.  See the NOTICE file
  distributed with this work for additional information
  regarding copyright ownership.  The ASF licenses this file
  to you under the Apache License, Version 2.0 (the
  "License"); you may not use this file except in compliance
  with the License.  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
 */

#pragma once

#include <string>
#include <string_view>
#include <array>
#include <system_error>
#include <sys/stat.h>
#include "tscore/ink_memory.h"
#include "tscpp/util/TextView.h"
#include "tscore/BufferWriter.h"

namespace ts
{
namespace file
{
  /** Utility class for file system paths.
   */
  class path
  {
    using self_type = path;

  public:
    using value_type                          = char;
    using string_type                         = std::string;
    static constexpr char preferred_separator = value_type{'/'};

    /// Default construct empty path.
    path() = default;

    /// Copy constructor - copies the path.
    path(const self_type &that) = default;

    /// Move constructor.
    path(self_type &&that) = default;

    /// Construct from a null terminated string.
    explicit path(const char *src);

    /// Construct from a string view.
    path(std::string_view src);
    //  template < typename ... Args > explicit path(std::string_view base, Args... rest);

    /// Move from an existing string
    path(std::string &&that);

    /// Replace the path with a copy of @a that.
    self_type &operator=(const self_type &that) = default;

    /// Replace the path with the contents of @a that.
    self_type &operator=(self_type &&that) = default;

    /// Assign @a p as the path.
    self_type &operator=(std::string_view p);

    /** Append or replace path with @a that.
     *
     * If @a that is absolute, it replaces @a this. Otherwise @a that is appended with exactly one
     * separator.
     *
     * @param that Filesystem path.
     * @return @a this
     */
    self_type &operator/=(const self_type &that);
    self_type &operator/=(std::string_view that);

    /// Check if the path is empty.
    bool empty() const;

    /// Check if the path is absolute.
    bool is_absolute() const;

    /// Check if the path is not absolute.
    bool is_relative() const;

    /// Access the path explicitly.
    char const *c_str() const;

    /// Get a view of the path.
    std::string_view view() const;

    /// Get a copy of the path.
    std::string string() const;

    /// Get relative path
    self_type relative_path();

    /// Get parent path
    path parent_path();

  protected:
    std::string _path; ///< File path.
  };

  /// Information about a file.
  class file_status
  {
    using self_type = file_status;

  public:
  protected:
    struct ::stat _stat; ///< File information.

    friend self_type status(const path &, std::error_code &) noexcept;

    friend int file_type(const self_type &);
    friend time_t modification_time(const file_status &fs);
    friend uintmax_t file_size(const self_type &);
    friend bool is_regular_file(const file_status &);
    friend bool is_dir(const file_status &);
    friend bool is_char_device(const file_status &);
    friend bool is_block_device(const file_status &);
  };

  /** Get the status of the file at @a p.
   *
   * @param p Path to file.
   * @param ec Error code return.
   * @return Status of the file.
   */
  file_status status(const path &p, std::error_code &ec) noexcept;

  // Related free functions.
  // These are separate because they are not part of std::filesystem::path.

  /// Return the file type value.
  int file_type(const file_status &fs);

  /// Return modification time
  time_t modification_time(const file_status &fs);

  /// Check if the path is to a regular file.
  bool is_regular_file(const file_status &fs);

  /// Check if the path is to a directory.
  bool is_dir(const file_status &p);

  /// Check if the path is to a character device.
  bool is_char_device(const file_status &fs);

  /// Check if the path is to a block device.
  bool is_block_device(const file_status &fs);

  /// Size of the file or block device.
  uintmax_t file_size(const file_status &fs);

  /// Check if file is readable.
  bool is_readable(const path &s);

  // Get directory location suitable for temporary files
  path temp_directory_path();

  // Returns current path.
  path current_path();

  // Returns return the canonicalized absolute pathname
  path canonical(const path &p, std::error_code &ec);

  // Return the filename derived from path p.
  //
  // This is made to match the std::filesystem::path::filename behavior:
  //   https://en.cppreference.com/w/cpp/filesystem/path/filename
  //
  // Examples:
  //   given "/foo/bar.txt", this returns "bar.txt"
  //   given "/foo/bar", this returns "bar"
  //   given "/foo/bar/", this returns ""
  //   given "/", this returns ""
  path filename(const path &p);

  // Checks if the file/directory exists
  bool exists(const path &p);

  // Create directories
  bool create_directories(const path &p, std::error_code &ec, mode_t mode = 0775) noexcept;

  // Copy files ("from" cannot be directory). @todo make it more generic
  bool copy(const path &from, const path &to, std::error_code &ec);

  // Removes files and directories recursively
  bool remove(const path &path, std::error_code &ec);

  /** Load the file at @a p into a @c std::string
   *
   * @param p Path to file
   * @return The contents of the file.
   */
  std::string load(const path &p, std::error_code &ec);
  /* ------------------------------------------------------------------- */

  inline path::path(char const *src) : _path(src) {}

  inline path::path(std::string_view base) : _path(base) {}

  inline path::path(std::string &&that) : _path(std::move(that)) {}

  inline path &
  path::operator=(std::string_view p)
  {
    _path.assign(p);
    return *this;
  }

  inline char const *
  path::c_str() const
  {
    return _path.c_str();
  }

  inline std::string_view
  path::view() const
  {
    return _path;
  }

  inline std::string
  path::string() const
  {
    return _path;
  }

  inline bool
  path::empty() const
  {
    return _path.empty();
  }

  inline bool
  path::is_absolute() const
  {
    return !_path.empty() && preferred_separator == _path[0];
  }

  inline bool
  path::is_relative() const
  {
    return !this->is_absolute();
  }

  inline path &
  path::operator/=(const self_type &that)
  {
    return *this /= std::string_view(that._path);
  }

  inline path
  path::relative_path()
  {
    return (this->is_absolute()) ? path(_path.substr(sizeof(preferred_separator))) : *this;
  }

  inline path
  path::parent_path()
  {
    if (this->is_absolute() && _path.substr(sizeof(preferred_separator)).empty()) {
      // No relative path
      return *this;
    }

    const size_t last_slash_idx = _path.find_last_of(preferred_separator);
    if (std::string::npos != last_slash_idx) {
      return path(_path.substr(0, last_slash_idx));
    } else {
      return path();
    }
  }

  /** Combine two strings as file paths.

       @return A @c path with the combined path.
  */
  inline path
  operator/(const path &lhs, const path &rhs)
  {
    return path(lhs) /= rhs;
  }

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

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

  inline path
  operator/(path &&lhs, std::string_view rhs)
  {
    return path(std::move(lhs)) /= rhs;
  }

  inline bool
  operator==(const path &lhs, const path &rhs)
  {
    return lhs.string() == rhs.string();
  }

  inline bool
  operator!=(const path &lhs, const path &rhs)
  {
    return lhs.string() != rhs.string();
  }

  /* ------------------------------------------------------------------- */
} // namespace file
} // namespace ts
/* ------------------------------------------------------------------- */
