// Copyright (c) 2013, Thomas Goyne <plorkyeran@aegisub.org>
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//
// Aegisub Project http://www.aegisub.org/

#include <libaegisub/exception.h>

#include <cstdint>
#include <ctime>
#include <filesystem>
#include <iterator>
#include <memory>
#include <string>

#undef CreateDirectory

namespace agi::fs {
using path = std::filesystem::path;

/// Define a filesystem error which takes a path or a string
#define DEFINE_FS_EXCEPTION(type, base, message) \
	struct type : public base { \
		type(path const& p) : base(message + p.string()) { } \
		type(std::string const& s) : base(s) { } \
		const char *GetName() const { return ""; } \
	}

/// @class agi::FileSystemError
/// @extends agi::Exception
/// @brief Base class for errors related to the file system
///
/// This base class can not be instantiated.
/// File system errors do not support inner exceptions, as they
/// are always originating causes for errors.
DEFINE_EXCEPTION(FileSystemError, Exception);

/// A file can't be accessed for some reason
DEFINE_FS_EXCEPTION(FileNotAccessible, FileSystemError, "File is not accessible: ");

/// A file can't be accessed because there's no file by the given name
DEFINE_FS_EXCEPTION(FileNotFound, FileNotAccessible, "File not found: ");

/// An error of some unknown type has occured
DEFINE_EXCEPTION(FileSystemUnknownError, FileSystemError);

/// The path exists, but isn't a file
DEFINE_FS_EXCEPTION(NotAFile, FileNotAccessible, "Path is not a file (and should be): ");

/// The path exists, but isn't a directory
DEFINE_FS_EXCEPTION(NotADirectory, FileNotAccessible, "Path is not a directory (and should be): ");

/// The given path is too long for the filesystem
DEFINE_FS_EXCEPTION(PathTooLog, FileSystemError, "Path is too long: ");

/// Insufficient free space to complete operation
DEFINE_FS_EXCEPTION(DriveFull, FileSystemError, "Insufficient free space to write file: ");

/// Base class for access denied errors
DEFINE_FS_EXCEPTION(AccessDenied, FileNotAccessible, "Access denied to path: ");

/// Trying to read the file gave an access denied error
DEFINE_FS_EXCEPTION(ReadDenied, AccessDenied, "Access denied when trying to read: ");

/// Trying to write the file gave an access denied error
DEFINE_FS_EXCEPTION(WriteDenied, AccessDenied, "Access denied when trying to write: ");

/// File exists and cannot be overwritten due to being read-only
DEFINE_FS_EXCEPTION(ReadOnlyFile, WriteDenied, "File is read-only: ");

bool Exists(path const& p);
bool FileExists(path const& file);
bool DirectoryExists(path const& dir);

/// Get the local-charset encoded shortname for a file
///
/// This is purely for compatibility with external libraries which do
/// not support unicode filenames on Windows. On all other platforms,
/// it is a no-op.
std::string ShortName(path const& file_path);

/// Check for amount of free space on a path
uintmax_t FreeSpace(path const& dir_path);

/// Get the size in bytes of the file at path
///
/// @throws agi::FileNotFound if path does not exist
/// @throws agi::acs::NotAFile if path is a directory
/// @throws agi::acs::Read if path exists but could not be read
uintmax_t Size(path const& file_path);

/// Get the modification time of the file at path
///
/// @throws agi::FileNotFound if path does not exist
/// @throws agi::acs::NotAFile if path is a directory
/// @throws agi::acs::Read if path exists but could not be read
std::filesystem::file_time_type ModifiedTime(path const& file_path);

/// Create a directory and all required intermediate directories
/// @throws agi::acs::Write if the directory could not be created.
///
/// Trying to create a directory which already exists is not an error.
bool CreateDirectory(path const& dir_path);

/// Touch the given path
///
/// Creates the file if it does not exist, or updates the modified
/// time if it does
void Touch(path const& file_path);

/// Rename a file or directory
/// @param from Source path
/// @param to   Destination path
void Rename(path const& from, path const& to);

/// Copy a file
/// @param from Source path
/// @param to   Destination path
///
/// The destination path will be created if it does not exist.
void Copy(path const& from, path const& to);

/// Delete a file
/// @param path Path to file to delete
/// @throws agi::FileNotAccessibleError if file exists but could not be deleted
bool Remove(path const& file);

/// Check if the file has the given extension
/// @param p Path to check
/// @param ext Case-insensitive extension, without leading dot
bool HasExtension(path const& p, std::string const& ext);

std::filesystem::path Canonicalize(std::filesystem::path const& path);

class DirectoryIterator {
	struct PrivData;
	std::shared_ptr<PrivData> privdata;
	std::string value;
public:
	typedef path value_type;
	typedef path* pointer;
	typedef path& reference;
	typedef size_t difference_type;
	typedef std::forward_iterator_tag iterator_category;

	bool operator==(DirectoryIterator const&) const;
	DirectoryIterator& operator++();
	std::string const& operator*() const { return value; }

	DirectoryIterator(path const& p, std::string const& filter);
	DirectoryIterator();
	~DirectoryIterator();

	template<typename T> void GetAll(T& cont);
};

static inline DirectoryIterator& begin(DirectoryIterator &it) { return it; }
static inline DirectoryIterator end(DirectoryIterator &) { return DirectoryIterator(); }

template<typename T>
inline void DirectoryIterator::GetAll(T& cont) {
	copy(*this, end(*this), std::back_inserter(cont));
}
} // namespace agi::fs
