#ifndef MONGOCRYPT_PATH_PRIVATE_H
#define MONGOCRYPT_PATH_PRIVATE_H

#include "./user-check.h"

#include "./str.h"

#include <inttypes.h>

#ifndef _WIN32
#include <unistd.h>
#else
#include "./windows-lean.h"
#endif

typedef enum mpath_format {
    /// The POSIX path format
    MPATH_POSIX = 'p',
    /// The Win32 path format
    MPATH_WIN32 = 'w',
    /// The native path format for the current platform
    MPATH_NATIVE =
#ifdef _WIN32
        MPATH_WIN32,
#else
        MPATH_POSIX,
#endif
} mpath_format;

/**
 * @brief Determine if the given character is a path separator for the given
 * path format
 *
 * @param c A path character
 * @param f The path format to use
 */
static inline bool mpath_is_sep(char c, mpath_format f) {
    if (f == MPATH_WIN32) {
        return c == '/' || c == '\\';
    } else {
        return c == '/';
    }
}

/**
 * @brief Obtain the preferred path separator character for the given format
 */
static inline char mpath_preferred_sep(mpath_format f) {
    if (f == MPATH_WIN32) {
        return '\\';
    } else {
        return '/';
    }
}

/**
 * @brief Obtain the path string denoting the application's current working
 * directory
 *
 * @return mstr A new string which must be freed with mstr_free()
 */
static inline mstr mpath_current_path(void) {
#if defined(_WIN32)
    while (1) {
        DWORD len = GetCurrentDirectoryW(0, NULL);
        wchar_t *wstr = calloc(sizeof(wchar_t), len);
        DWORD got_len = GetCurrentDirectoryW(len, wstr);
        if (got_len > len) {
            free(wstr);
            continue;
        }
        mstr_narrow_result nar = mstr_win32_narrow(wstr);
        free(wstr);
        assert(nar.error == 0);
        return nar.string;
    }
#else
    mstr_mut mut = mstr_new(8096);
    char *p = getcwd(mut.raw.data, mut.raw.len);
    if (p == NULL) {
        mstr_free(mut.mstr);
        return MSTR_NULL;
    }
    mstr ret = mstr_copy_cstr(mut.raw.data);
    mstr_free(mut.mstr);
    return ret;
#endif
}

/**
 * @brief Determine whether the given path string has a trailing path separator
 */
static inline bool mpath_has_trailing_sep(mstr_view path, mpath_format f) {
    return path.len && mpath_is_sep(path.data[path.len - 1], f);
}

/**
 * @brief Obtain the parent path of the given path.
 */
static inline mstr_view mpath_parent(mstr_view path, mpath_format f) {
    if (mpath_has_trailing_sep(path, f)) {
        // Remove trailing separators:
        while (mpath_has_trailing_sep(path, f)) {
            path = mstrv_remove_suffix(path, 1);
        }
        return path;
    }
    // Remove everything that isn't a path separator:
    while (path.len != 0 && !mpath_has_trailing_sep(path, f)) {
        path = mstrv_remove_suffix(path, 1);
    }
    // Remove trailing separators again
    while (path.len > 1 && mpath_has_trailing_sep(path, f)) {
        path = mstrv_remove_suffix(path, 1);
    }
    // The result is the parent path.
    return path;
}

/**
 * @brief Obtain the filename denoted by the given path.
 *
 * The returned path will include no directory separators. If the given path
 * ends with a directory separator, the single-dot '.' path is returned instead.
 */
static inline mstr_view mpath_filename(mstr_view path, mpath_format f) {
    if (!path.len) {
        return path;
    }
    const char *it = path.data + path.len;
    while (it != path.data && !mpath_is_sep(it[-1], f)) {
        --it;
    }
    size_t off = (size_t)(it - path.data);
    mstr_view fname = mstrv_subview(path, off, path.len);
    if (fname.len == 0) {
        return mstrv_lit(".");
    }
    return fname;
}

/**
 * @brief Join the two given paths into a single path
 *
 * The two strings will be combined into a single string with a path separator
 * between them. If either string is empty, the other string will be copied
 * without modification.
 *
 * @param base The left-hand of the join
 * @param suffix The right-hand of the join
 * @param f The path format to use
 * @return mstr A new string resulting from the join
 */
static inline mstr mpath_join(mstr_view base, mstr_view suffix, mpath_format f) {
    if (!base.len) {
        return mstr_copy(suffix);
    }
    if (mpath_has_trailing_sep(base, f)) {
        return mstr_append(base, suffix);
    }
    if (!suffix.len) {
        return mstr_copy(base);
    }
    if (mpath_is_sep(suffix.data[0], f)) {
        return mstr_append(base, suffix);
    }
    // We must insert a path separator between the two strings
    assert(base.len <= SIZE_MAX - suffix.len - 1u);
    mstr_mut r = mstr_new(base.len + suffix.len + 1);
    char *p = r.raw.data;
    memcpy(p, base.data, base.len);
    p += base.len;
    *p++ = mpath_preferred_sep(f);
    memcpy(p, suffix.data, suffix.len);
    return r.mstr;
}

/**
 * @brief Obtain the root name for the given path.
 *
 * For the Windows format, this will return the drive letter, if present.
 * Otherwise, this will return an empty string.
 */
static inline mstr_view mpath_root_name(mstr_view path, mpath_format f) {
    if (f == MPATH_WIN32 && path.len > 1) {
        char c = path.data[0];
        if (path.len > 2 && path.data[1] == ':' && ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))) {
            return mstrv_subview(path, 0, 2);
        }
    }
    return mstrv_subview(path, 0, 0);
}

/**
 * @brief Return the root directory of the given path, if present.
 *
 * @note This will not include the drive letter of a Win32 path.
 */
static inline mstr_view mpath_root_directory(mstr_view path, mpath_format f) {
    mstr_view rname = mpath_root_name(path, f);
    path = mstrv_subview(path, rname.len, path.len);
    if (path.len && mpath_is_sep(path.data[0], f)) {
        return mstrv_subview(path, 0, 1);
    } else {
        return mstrv_subview(path, 0, 0);
    }
}

/**
 * @brief Obtain the root filepath of the given path.
 *
 * This will include both the root name and the root filepath, if present.
 */
static inline mstr_view mpath_root_path(mstr_view path, mpath_format f) {
    mstr_view rname = mpath_root_name(path, f);
    mstr_view rdir = mpath_root_directory(path, f);
    assert(rname.len <= SIZE_MAX - rdir.len);
    return mstrv_subview(path, 0, rname.len + rdir.len);
}

/**
 * @brief Determine whether the given filepath designates a single unambiguous
 * filesystem location.
 *
 * @note A Win32 filepath without a drive letter is not absolute!
 */
static inline bool mpath_is_absolute(mstr_view path, mpath_format f) {
    if (f == MPATH_WIN32) {
        // Win32 requires both a root name and a root directory for an absolute
        // path
        return mpath_root_name(path, f).len && mpath_root_directory(path, f).len;
    } else {
        // POSIX doesn't have "root names"
        return mpath_root_directory(path, f).len;
    }
}

/**
 * @brief Obtain a relative path from the given filepath
 *
 * If the path has a root path, returns the content of the path following that
 * root path, otherwise returns the same path itself.
 */
static inline mstr_view mpath_relative_path(mstr_view path, mpath_format f) {
    mstr_view root = mpath_root_path(path, f);
    return mstrv_subview(path, root.len, path.len);
}

/**
 * @brief Convert the filepath from one format to a preferred form in another
 * format.
 *
 * @note The return value must be freed with mstr_free()
 */
static inline mstr mpath_to_format(mpath_format from, mstr_view path, mpath_format to) {
    mstr_mut ret = mstr_new(path.len);
    const char *p = path.data;
    char *out = ret.raw.data;
    const char *stop = path.data + path.len;
    for (; p != stop; ++p, ++out) {
        if (mpath_is_sep(*p, from)) {
            *out = mpath_preferred_sep(to);
        } else {
            *out = *p;
        }
    }
    return ret.mstr;
}

/**
 * @brief Determine whether the given path is relative (not absolute)
 */
static inline bool mpath_is_relative(mstr_view path, mpath_format f) {
    return !mpath_is_absolute(path, f);
}

/**
 * @brief Convert the given path to an absolute path, if it is not already.
 *
 * @note The return value must be freed with mstr_free()
 */
static inline mstr mpath_absolute(mstr_view path, mpath_format f);

/**
 * @brief Resolve a path to an absolute path from the given base path.
 *
 * @note This is not the same as mpath_join(): If the given path is already
 * absolute, returns that path unchanged. Otherwise, resolves that path as being
 * relative to `base`.
 *
 * @note If `base` is also a relative path, it will also be given to
 * mpath_absolute() to resolve it.
 */
static inline mstr mpath_absolute_from(mstr_view path, mstr_view base, mpath_format f) {
    mstr_view rname = mpath_root_name(path, f);
    mstr_view rdir = mpath_root_directory(path, f);
    if (rname.len) {
        if (rdir.len) {
            // The path is already fully absolute
            return mstr_copy(path);
        } else {
            mstr abs_base = mpath_absolute(base, f);
            mstr_view base_rdir = mpath_root_directory(abs_base.view, f);
            mstr_view base_relpath = mpath_relative_path(abs_base.view, f);
            mstr_view relpath = mpath_relative_path(path, f);
            mstr ret = mstr_copy(rname);
            mstr_assign(&ret, mpath_join(ret.view, base_rdir, f));
            mstr_assign(&ret, mpath_join(ret.view, base_relpath, f));
            mstr_assign(&ret, mpath_join(ret.view, relpath, f));
            mstr_free(abs_base);
            return ret;
        }
    } else {
        // No root name
        if (rdir.len) {
            if (f == MPATH_POSIX) {
                // No root name, but a root directory on a POSIX path indicates an
                // absolute path
                return mstr_copy(path);
            }
            mstr abs_base = mpath_absolute(base, f);
            mstr_view base_rname = mpath_root_name(abs_base.view, f);
            mstr ret = mpath_join(base_rname, path, f);
            mstr_free(abs_base);
            return ret;
        } else {
            mstr abs_base = mpath_absolute(base, f);
            mstr r = mpath_join(abs_base.view, path, f);
            mstr_free(abs_base);
            return r;
        }
    }
}

static inline mstr mpath_absolute(mstr_view path, mpath_format f) {
    if (mpath_is_absolute(path, f)) {
        return mstr_copy(path);
    }
    mstr cur = mpath_current_path();
    mstr ret = mpath_absolute_from(path, cur.view, MPATH_NATIVE);
    mstr_assign(&ret, mpath_to_format(MPATH_NATIVE, ret.view, f));
    mstr_free(cur);
    return ret;
}

#endif // MONGOCRYPT_PATH_PRIVATE_H
