/*
 * Copyright (c) 2025 The Khronos Group Inc.
 * Copyright (c) 2025 Valve Corporation
 * Copyright (c) 2025 LunarG, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and/or associated documentation files (the "Materials"), to
 * deal in the Materials without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Materials, and to permit persons to whom the Materials are
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice(s) and this permission notice shall be included in
 * all copies or substantial portions of the Materials.
 *
 * THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 *
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE MATERIALS OR THE
 * USE OR OTHER DEALINGS IN THE MATERIALS.
 *
 */

#include "folder_manager.h"

#include <algorithm>
#include <fstream>

#include "gtest/gtest.h"

#include "util/json_writer.h"
#include "util/env_var_wrapper.h"

namespace fs {
Folder::Folder(std::filesystem::path root_path, std::string name) noexcept : folder((root_path / name).lexically_normal()) {
    clear();
    // Don't actually create the folder yet, as we will do it on demand
}
Folder::~Folder() noexcept { clear(); }
Folder::Folder(Folder&& other) noexcept : actually_created(other.actually_created), folder(other.folder) { other.folder.clear(); }
Folder& Folder::operator=(Folder&& other) noexcept {
    folder = other.folder;
    actually_created = other.actually_created;
    other.folder.clear();
    return *this;
}

void Folder::check_if_first_use() {
    if (!actually_created) {
        if (!::testing::internal::InDeathTestChild()) {
            std::error_code err;
            if (!std::filesystem::create_directories(folder, err)) {
                std::cerr << "Failed to create folder " << folder << " because " << err.message() << "\n";
            }
        }
        actually_created = true;
    }
}

std::filesystem::path Folder::write_manifest(std::filesystem::path const& name, std::string const& contents) {
    check_if_first_use();
    std::filesystem::path out_path = (folder / name).lexically_normal();
    if (!::testing::internal::InDeathTestChild()) {
        auto file = std::ofstream(out_path, std::ios_base::trunc | std::ios_base::out);
        if (!file) {
            std::cerr << "Failed to create manifest " << name << " at " << out_path << "\n";
            return out_path;
        }
        file << contents << std::endl;
    }
    insert_file_to_tracking(name);
    return out_path;
}

// close file handle, delete file, remove `name` from managed file list.
void Folder::remove(std::filesystem::path const& name) {
    check_if_first_use();
    std::filesystem::path out_path = folder / name;
    if (!::testing::internal::InDeathTestChild()) {
        std::error_code err;
        if (!std::filesystem::remove(out_path, err)) {
            std::cerr << "Failed to remove file " << name << " at " << out_path << " because " << err.message() << "\n";
        }
    }

    auto found = std::find(added_files.begin(), added_files.end(), name);
    if (found != added_files.end()) {
        added_files.erase(found);
    } else {
        std::cout << "File " << name << " not in tracked files of folder " << folder << ".\n";
    }
}

// copy file into this folder
std::filesystem::path Folder::copy_file(std::filesystem::path const& file, std::filesystem::path const& new_name) {
    check_if_first_use();
    insert_file_to_tracking(new_name);

    auto new_filepath = folder / new_name;
    if (!::testing::internal::InDeathTestChild()) {
        std::error_code err;
        if (!std::filesystem::copy_file(file, new_filepath, err)) {
            std::cerr << "Failed to copy file " << file << " to " << new_filepath << " because " << err.message() << "\n";
        }
    }
    return new_filepath;
}

std::vector<std::filesystem::path> Folder::get_files() const { return added_files; }

std::filesystem::path Folder::add_symlink(std::filesystem::path const& target, std::filesystem::path const& link_name) {
    check_if_first_use();

    if (!::testing::internal::InDeathTestChild()) {
        std::error_code err;
        std::filesystem::create_symlink(target, folder / link_name, err);
        if (err.value() != 0) {
            std::cerr << "Failed to create symlink with target" << target << " with name " << folder / link_name << " because "
                      << err.message() << "\n";
        }
    }
    insert_file_to_tracking(link_name);
    return folder / link_name;
}
void Folder::insert_file_to_tracking(std::filesystem::path const& name) {
    auto found = std::find(added_files.begin(), added_files.end(), name);
    if (found != added_files.end()) {
        std::cout << "Overwriting manifest " << name << ". Was this intended?\n";
    } else {
        added_files.emplace_back(name);
    }
}

void Folder::clear() const noexcept {
    if (!::testing::internal::InDeathTestChild()) {
        std::error_code err;
        std::filesystem::remove_all(folder, err);
        if (err.value() != 0) {
            std::cerr << "Failed to remove folder " << folder << " because " << err.message() << "\n";
        }
    }
}

std::filesystem::path category_path_name(ManifestCategory category) {
    if (category == ManifestCategory::settings) return "settings.d";
    if (category == ManifestCategory::implicit_layer) return "implicit_layer.d";
    if (category == ManifestCategory::explicit_layer)
        return "explicit_layer.d";
    else
        return "icd.d";
}

FileSystemManager::FileSystemManager()
    : root_folder(std::filesystem::path(TEST_EXECUTION_DIRECTORY),
                  std::string(::testing::UnitTest::GetInstance()->GetInstance()->current_test_suite()->name()) + "_" +
                      ::testing::UnitTest::GetInstance()->current_test_info()->name()) {
    // Clean out test folder in case a previous run's files are still around
    root_folder.clear();

    auto const& root = root_folder.location();
    folders.try_emplace(ManifestLocation::null, root, std::string("null_dir"));
    folders.try_emplace(ManifestLocation::driver, root, std::string("icd_manifests"));
    folders.try_emplace(ManifestLocation::driver_env_var, root, std::string("icd_env_vars_manifests"));
    folders.try_emplace(ManifestLocation::explicit_layer, root, std::string("explicit_layer_manifests"));
    folders.try_emplace(ManifestLocation::explicit_layer_env_var, root, std::string("explicit_env_var_layer_folder"));
    folders.try_emplace(ManifestLocation::explicit_layer_add_env_var, root, std::string("explicit_add_env_var_layer_folder"));
    folders.try_emplace(ManifestLocation::implicit_layer, root, std::string("implicit_layer_manifests"));
    folders.try_emplace(ManifestLocation::implicit_layer_env_var, root, std::string("implicit_env_var_layer_manifests"));
    folders.try_emplace(ManifestLocation::implicit_layer_add_env_var, root, std::string("implicit_add_env_var_layer_manifests"));
    folders.try_emplace(ManifestLocation::override_layer, root, std::string("override_layer_manifests"));
#if defined(_WIN32)
    folders.try_emplace(ManifestLocation::windows_app_package, root, std::string("app_package_manifests"));
#endif
#if defined(__APPLE__)
    folders.try_emplace(ManifestLocation::macos_bundle, root, std::string("macos_bundle"));
#endif
    folders.try_emplace(ManifestLocation::settings_location, root, std::string("settings_location"));
    folders.try_emplace(ManifestLocation::unsecured_driver, root, std::string("unsecured_driver"));
    folders.try_emplace(ManifestLocation::unsecured_explicit_layer, root, std::string("unsecured_explicit_layer"));
    folders.try_emplace(ManifestLocation::unsecured_implicit_layer, root, std::string("unsecured_implicit_layer"));
    folders.try_emplace(ManifestLocation::unsecured_settings, root, std::string("unsecured_settings"));
}

fs::Folder& FileSystemManager::get_folder(ManifestLocation location) noexcept { return folders.at(location); }
fs::Folder const& FileSystemManager::get_folder(ManifestLocation location) const noexcept { return folders.at(location); }

Folder* FileSystemManager::get_folder_for_given_path(std::string_view given_path) noexcept {
    for (auto const& [manifest_location, folder] : folders) {
        if (folder.location() == given_path) {
            return &folders.at(manifest_location);
        }
    }
    for (auto const& [redirected_path, redirected_location] : redirected_paths) {
        if (redirected_path == given_path) {
            return &folders.at(redirected_location);
        }
        const auto mismatch_pair =
            std::mismatch(given_path.begin(), given_path.end(), redirected_path.begin(), redirected_path.end());
        if (mismatch_pair.second == redirected_path.end()) return &folders.at(redirected_location);
    }
    return nullptr;
}

bool FileSystemManager::is_folder_path(std::filesystem::path const& path) const noexcept {
    for (auto const& [location, folder] : folders) {
        if (path == folder.location()) {
            return true;
        }
    }
    return false;
}

void FileSystemManager::add_path_redirect(std::filesystem::path const& redirected_path, ManifestLocation location) {
    redirected_paths[redirected_path.string()] = location;
}

std::filesystem::path FileSystemManager::get_real_path_of_redirected_path(std::filesystem::path const& path) const {
    if (redirected_paths.count(path.string()) > 0) {
        return folders.at(redirected_paths.at(path.lexically_normal().string())).location();
    }
    for (auto const& [redirected_path_str, redirected_location] : redirected_paths) {
        std::filesystem::path redirected_path = redirected_path_str;
        const auto mismatch_pair = std::mismatch(path.begin(), path.end(), redirected_path.begin(), redirected_path.end());
        if (mismatch_pair.second == redirected_path.end()) return folders.at(redirected_location).location();
    }
    return std::filesystem::path{};
}

std::filesystem::path FileSystemManager::get_path_redirect_by_manifest_location(ManifestLocation location) const {
    for (auto const& [path, redirected_location] : redirected_paths) {
        if (redirected_location == location) {
            return path;
        }
    }
    return folders.at(ManifestLocation::null).location();
}

}  // namespace fs
