#include "pch.h"

#include <vcpkg/archives.h>
#include <vcpkg/tools.h>
#include <vcpkg/vcpkgpaths.h>

#include <vcpkg/base/checks.h>
#include <vcpkg/base/downloads.h>
#include <vcpkg/base/files.h>
#include <vcpkg/base/optional.h>
#include <vcpkg/base/stringrange.h>
#include <vcpkg/base/strings.h>
#include <vcpkg/base/system.h>
#include <vcpkg/base/util.h>

namespace vcpkg
{
    struct ToolData
    {
        std::array<int, 3> version;
        fs::path exe_path;
        std::string url;
        fs::path download_path;
        bool is_archive;
        fs::path tool_dir_path;
        std::string sha512;
    };

    static Optional<std::array<int, 3>> parse_version_string(const std::string& version_as_string)
    {
        static const std::regex RE(R"###((\d+)\.(\d+)\.(\d+))###");

        std::match_results<std::string::const_iterator> match;
        const auto found = std::regex_search(version_as_string, match, RE);
        if (!found)
        {
            return {};
        }

        const int d1 = atoi(match[1].str().c_str());
        const int d2 = atoi(match[2].str().c_str());
        const int d3 = atoi(match[3].str().c_str());
        const std::array<int, 3> result = {d1, d2, d3};
        return result;
    }

    static ExpectedT<ToolData, std::string> parse_tool_data_from_xml(const VcpkgPaths& paths, const std::string& tool)
    {
#if defined(_WIN32)
        static constexpr StringLiteral OS_STRING = "windows";
#elif defined(__APPLE__)
        static constexpr StringLiteral OS_STRING = "osx";
#elif defined(__linux__)
        static constexpr StringLiteral OS_STRING = "linux";
#elif defined(__FreeBSD__)
        static constexpr StringLiteral OS_STRING = "freebsd";
#else
        return std::string("operating system is unknown");
#endif

#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__)
        static const std::string XML_VERSION = "2";
        static const fs::path XML_PATH = paths.scripts / "vcpkgTools.xml";
        static const std::regex XML_VERSION_REGEX {R"###(<tools[\s]+version="([^"]+)">)###"};
        static const std::string XML = paths.get_filesystem().read_contents(XML_PATH).value_or_exit(VCPKG_LINE_INFO);
        std::smatch match_xml_version;
        const bool has_xml_version = std::regex_search(XML.cbegin(), XML.cend(), match_xml_version, XML_VERSION_REGEX);
        Checks::check_exit(VCPKG_LINE_INFO,
                           has_xml_version,
                           R"(Could not find <tools version="%s"> in %s)",
                           XML_VERSION,
                           XML_PATH.u8string());
        Checks::check_exit(VCPKG_LINE_INFO,
                           XML_VERSION == match_xml_version[1],
                           "Expected %s version: [%s], but was [%s]. Please re-run bootstrap-vcpkg.",
                           XML_PATH.u8string(),
                           XML_VERSION,
                           match_xml_version[1]);

        const std::regex tool_regex {Strings::format(R"###(<tool[\s]+name="%s"[\s]+os="%s">)###", tool, OS_STRING)};
        std::smatch match_tool_entry;
        const bool has_tool_entry = std::regex_search(XML.cbegin(), XML.cend(), match_tool_entry, tool_regex);
        if (!has_tool_entry)
        {
            return Strings::format(
                "Could not find entry for tool %s in %s for os=%s", tool, XML_PATH.u8string(), OS_STRING);
        }

        const std::string tool_data =
            StringRange::find_exactly_one_enclosed(XML, match_tool_entry[0], "</tool>").to_string();
        const std::string version_as_string =
            StringRange::find_exactly_one_enclosed(tool_data, "<version>", "</version>").to_string();
        const std::string exe_relative_path =
            StringRange::find_exactly_one_enclosed(tool_data, "<exeRelativePath>", "</exeRelativePath>").to_string();
        const std::string url = StringRange::find_exactly_one_enclosed(tool_data, "<url>", "</url>").to_string();
        const std::string sha512 =
            StringRange::find_exactly_one_enclosed(tool_data, "<sha512>", "</sha512>").to_string();
        auto archive_name = StringRange::find_at_most_one_enclosed(tool_data, "<archiveName>", "</archiveName>");

        const Optional<std::array<int, 3>> version = parse_version_string(version_as_string);
        Checks::check_exit(VCPKG_LINE_INFO,
                           version.has_value(),
                           "Could not parse version for tool %s. Version string was: %s",
                           tool,
                           version_as_string);

        const std::string tool_dir_name = Strings::format("%s-%s-%s", tool, version_as_string, OS_STRING);
        const fs::path tool_dir_path = paths.tools / tool_dir_name;
        const fs::path exe_path = tool_dir_path / exe_relative_path;

        return ToolData {*version.get(),
                         exe_path,
                         url,
                         paths.downloads / archive_name.value_or(exe_relative_path).to_string(),
                         archive_name.has_value(),
                         tool_dir_path,
                         sha512};
#endif
    }

    struct PathAndVersion
    {
        fs::path path;
        std::string version;
    };

    struct ToolProvider
    {
        virtual const std::string& tool_data_name() const = 0;
        virtual const std::string& exe_stem() const = 0;
        virtual std::array<int, 3> default_min_version() const = 0;

        virtual void add_special_paths(std::vector<fs::path>& out_candidate_paths) const {}
        virtual Optional<std::string> get_version(const fs::path& path_to_exe) const = 0;
    };

    static Optional<PathAndVersion> find_first_with_sufficient_version(const Files::Filesystem& fs,
                                                                       const ToolProvider& tool_provider,
                                                                       const std::vector<fs::path>& candidates,
                                                                       const std::array<int, 3>& expected_version)
    {
        for (auto&& candidate : candidates)
        {
            if (!fs.exists(candidate)) continue;
            auto maybe_version = tool_provider.get_version(candidate);
            const auto version = maybe_version.get();
            if (!version) continue;
            const auto parsed_version = parse_version_string(*version);
            if (!parsed_version) continue;
            auto& actual_version = *parsed_version.get();
            const auto version_acceptable =
                actual_version[0] > expected_version[0] ||
                (actual_version[0] == expected_version[0] && actual_version[1] > expected_version[1]) ||
                (actual_version[0] == expected_version[0] && actual_version[1] == expected_version[1] &&
                 actual_version[2] >= expected_version[2]);
            if (!version_acceptable) continue;

            return PathAndVersion {candidate, *version};
        }

        return nullopt;
    }

    static fs::path fetch_tool(const VcpkgPaths& paths, const std::string& tool_name, const ToolData& tool_data)
    {
        const std::array<int, 3>& version = tool_data.version;
        const std::string version_as_string = Strings::format("%d.%d.%d", version[0], version[1], version[2]);
        Checks::check_exit(VCPKG_LINE_INFO,
                           !tool_data.url.empty(),
                           "A suitable version of %s was not found (required v%s) and unable to automatically "
                           "download a portable one. Please install a newer version of %s.",
                           tool_name,
                           version_as_string,
                           tool_name);
        System::println("A suitable version of %s was not found (required v%s). Downloading portable %s v%s...",
                        tool_name,
                        version_as_string,
                        tool_name,
                        version_as_string);
        auto& fs = paths.get_filesystem();
        if (!fs.exists(tool_data.download_path))
        {
            System::println("Downloading %s...", tool_name);
            System::println("  %s -> %s", tool_data.url, tool_data.download_path.string());
            Downloads::download_file(fs, tool_data.url, tool_data.download_path, tool_data.sha512);
        }
        else
        {
            Downloads::verify_downloaded_file_hash(fs, tool_data.url, tool_data.download_path, tool_data.sha512);
        }

        if (tool_data.is_archive)
        {
            System::println("Extracting %s...", tool_name);
            Archives::extract_archive(paths, tool_data.download_path, tool_data.tool_dir_path);
        }
        else
        {
            std::error_code ec;
            fs.create_directories(tool_data.exe_path.parent_path(), ec);
            fs.rename(tool_data.download_path, tool_data.exe_path, ec);
        }

        Checks::check_exit(VCPKG_LINE_INFO,
                           fs.exists(tool_data.exe_path),
                           "Expected %s to exist after fetching",
                           tool_data.exe_path.u8string());

        return tool_data.exe_path;
    }

    static PathAndVersion fetch_tool(const VcpkgPaths& paths,
                                     const ToolProvider& tool_provider,
                                     const ToolData& tool_data)
    {
        const auto downloaded_path = fetch_tool(paths, tool_provider.tool_data_name(), tool_data);
        const auto downloaded_version = tool_provider.get_version(downloaded_path).value_or_exit(VCPKG_LINE_INFO);
        return {downloaded_path, downloaded_version};
    }

    static PathAndVersion get_path(const VcpkgPaths& paths, const ToolProvider& tool)
    {
        auto& fs = paths.get_filesystem();

        std::array<int, 3> min_version = tool.default_min_version();

        std::vector<fs::path> candidate_paths;
        auto maybe_tool_data = parse_tool_data_from_xml(paths, tool.tool_data_name());
        if (auto tool_data = maybe_tool_data.get())
        {
            candidate_paths.push_back(tool_data->exe_path);
            min_version = tool_data->version;
        }

        auto& exe_stem = tool.exe_stem();
        if (!exe_stem.empty())
        {
            auto paths_from_path = fs.find_from_PATH(exe_stem);
            candidate_paths.insert(candidate_paths.end(), paths_from_path.cbegin(), paths_from_path.cend());
        }

        tool.add_special_paths(candidate_paths);

        const auto maybe_path = find_first_with_sufficient_version(fs, tool, candidate_paths, min_version);
        if (const auto p = maybe_path.get())
        {
            return *p;
        }
        if (auto tool_data = maybe_tool_data.get())
        {
            return fetch_tool(paths, tool, *tool_data);
        }

        Checks::exit_with_message(VCPKG_LINE_INFO, maybe_tool_data.error());
    }

    struct CMakeProvider : ToolProvider
    {
        std::string m_exe = "cmake";

        virtual const std::string& tool_data_name() const override { return m_exe; }
        virtual const std::string& exe_stem() const override { return m_exe; }
        virtual std::array<int, 3> default_min_version() const override { return {3, 5, 1}; }

        virtual void add_special_paths(std::vector<fs::path>& out_candidate_paths) const override
        {
#if defined(_WIN32)
            const auto& program_files = System::get_program_files_platform_bitness();
            if (const auto pf = program_files.get()) out_candidate_paths.push_back(*pf / "CMake" / "bin" / "cmake.exe");
            const auto& program_files_32_bit = System::get_program_files_32_bit();
            if (const auto pf = program_files_32_bit.get())
                out_candidate_paths.push_back(*pf / "CMake" / "bin" / "cmake.exe");
#endif
        }
        virtual Optional<std::string> get_version(const fs::path& path_to_exe) const override
        {
            const std::string cmd = Strings::format(R"("%s" --version)", path_to_exe.u8string());
            const auto rc = System::cmd_execute_and_capture_output(cmd);
            if (rc.exit_code != 0)
            {
                return nullopt;
            }

            /* Sample output:
cmake version 3.10.2

CMake suite maintained and supported by Kitware (kitware.com/cmake).
                */
            return StringRange::find_exactly_one_enclosed(rc.output, "cmake version ", "\n").to_string();
        }
    };

    static fs::path get_7za_path(const VcpkgPaths& paths)
    {
#if defined(_WIN32)
        static const ToolData TOOL_DATA = parse_tool_data_from_xml(paths, "7zip").value_or_exit(VCPKG_LINE_INFO);
        if (!paths.get_filesystem().exists(TOOL_DATA.exe_path))
        {
            return fetch_tool(paths, "7zip", TOOL_DATA);
        }
        return TOOL_DATA.exe_path;
#else
        Checks::exit_with_message(VCPKG_LINE_INFO, "Cannot download 7zip for non-Windows platforms.");
#endif
    }

    struct NinjaProvider : ToolProvider
    {
        std::string m_exe = "ninja";

        virtual const std::string& tool_data_name() const override { return m_exe; }
        virtual const std::string& exe_stem() const override { return m_exe; }
        virtual std::array<int, 3> default_min_version() const override { return {3, 5, 1}; }

        virtual Optional<std::string> get_version(const fs::path& path_to_exe) const override
        {
            const std::string cmd = Strings::format(R"("%s" --version)", path_to_exe.u8string());
            const auto rc = System::cmd_execute_and_capture_output(cmd);
            if (rc.exit_code != 0)
            {
                return nullopt;
            }

            /* Sample output:
1.8.2
                */
            return rc.output;
        }
    };

    struct NuGetProvider : ToolProvider
    {
        std::string m_exe = "nuget";

        virtual const std::string& tool_data_name() const override { return m_exe; }
        virtual const std::string& exe_stem() const override { return m_exe; }
        virtual std::array<int, 3> default_min_version() const override { return {4, 6, 2}; }

        virtual Optional<std::string> get_version(const fs::path& path_to_exe) const override
        {
            const std::string cmd = Strings::format(R"("%s")", path_to_exe.u8string());
            const auto rc = System::cmd_execute_and_capture_output(cmd);
            if (rc.exit_code != 0)
            {
                return nullopt;
            }

            /* Sample output:
NuGet Version: 4.6.2.5055
usage: NuGet <command> [args] [options]
Type 'NuGet help <command>' for help on a specific command.

[[[List of available commands follows]]]
                */
            return StringRange::find_exactly_one_enclosed(rc.output, "NuGet Version: ", "\n").to_string();
        }
    };

    struct GitProvider : ToolProvider
    {
        std::string m_exe = "git";

        virtual const std::string& tool_data_name() const override { return m_exe; }
        virtual const std::string& exe_stem() const override { return m_exe; }
        virtual std::array<int, 3> default_min_version() const override { return {2, 7, 4}; }

        virtual void add_special_paths(std::vector<fs::path>& out_candidate_paths) const override
        {
#if defined(_WIN32)
            const auto& program_files = System::get_program_files_platform_bitness();
            if (const auto pf = program_files.get()) out_candidate_paths.push_back(*pf / "git" / "cmd" / "git.exe");
            const auto& program_files_32_bit = System::get_program_files_32_bit();
            if (const auto pf = program_files_32_bit.get())
                out_candidate_paths.push_back(*pf / "git" / "cmd" / "git.exe");
#endif
        }

        virtual Optional<std::string> get_version(const fs::path& path_to_exe) const override
        {
            const std::string cmd = Strings::format(R"("%s" --version)", path_to_exe.u8string());
            const auto rc = System::cmd_execute_and_capture_output(cmd);
            if (rc.exit_code != 0)
            {
                return nullopt;
            }

            /* Sample output:
git version 2.17.1.windows.2
                */
            const auto idx = rc.output.find("git version ");
            Checks::check_exit(
                VCPKG_LINE_INFO, idx != std::string::npos, "Unexpected format of git version string: %s", rc.output);
            return rc.output.substr(idx);
        }
    };

    struct IfwInstallerBaseProvider : ToolProvider
    {
        std::string m_exe = "";
        std::string m_toolname = "installerbase";

        virtual const std::string& tool_data_name() const override { return m_toolname; }
        virtual const std::string& exe_stem() const override { return m_exe; }
        virtual std::array<int, 3> default_min_version() const override { return {0, 0, 0}; }

        virtual void add_special_paths(std::vector<fs::path>& out_candidate_paths) const override
        {
            // TODO: Uncomment later
            // const std::vector<fs::path> from_path = Files::find_from_PATH("installerbase");
            // candidate_paths.insert(candidate_paths.end(), from_path.cbegin(), from_path.cend());
            // candidate_paths.push_back(fs::path(System::get_environment_variable("HOMEDRIVE").value_or("C:")) /
            // "Qt" / "Tools" / "QtInstallerFramework" / "3.1" / "bin" / "installerbase.exe");
            // candidate_paths.push_back(fs::path(System::get_environment_variable("HOMEDRIVE").value_or("C:")) /
            // "Qt" / "QtIFW-3.1.0" / "bin" / "installerbase.exe");
        }

        virtual Optional<std::string> get_version(const fs::path& path_to_exe) const override
        {
            const std::string cmd = Strings::format(R"("%s" --framework-version)", path_to_exe.u8string());
            const auto rc = System::cmd_execute_and_capture_output(cmd);
            if (rc.exit_code != 0)
            {
                return nullopt;
            }

            /* Sample output:
3.1.81
                */
            return rc.output;
        }
    };

    struct ToolCacheImpl final : ToolCache
    {
        vcpkg::Cache<std::string, fs::path> path_only_cache;
        vcpkg::Cache<std::string, PathAndVersion> path_version_cache;

        virtual const fs::path& get_tool_path(const VcpkgPaths& paths, const std::string& tool) const override
        {
            return path_only_cache.get_lazy(tool, [&]() {
                // First deal with specially handled tools.
                // For these we may look in locations like Program Files, the PATH etc as well as the auto-downloaded
                // location.
                if (tool == Tools::SEVEN_ZIP) return get_7za_path(paths);
                if (tool == Tools::CMAKE || tool == Tools::GIT || tool == Tools::NINJA || tool == Tools::NUGET ||
                    tool == Tools::IFW_INSTALLER_BASE)
                    return get_tool_pathversion(paths, tool).path;
                if (tool == Tools::IFW_BINARYCREATOR)
                    return get_tool_path(paths, Tools::IFW_INSTALLER_BASE).parent_path() / "binarycreator.exe";
                if (tool == Tools::IFW_REPOGEN)
                    return get_tool_path(paths, Tools::IFW_INSTALLER_BASE).parent_path() / "repogen.exe";

                // For other tools, we simply always auto-download them.
                const ToolData tool_data = parse_tool_data_from_xml(paths, tool).value_or_exit(VCPKG_LINE_INFO);
                if (paths.get_filesystem().exists(tool_data.exe_path))
                {
                    return tool_data.exe_path;
                }
                return fetch_tool(paths, tool, tool_data);
            });
        }

        const PathAndVersion& get_tool_pathversion(const VcpkgPaths& paths, const std::string& tool) const
        {
            return path_version_cache.get_lazy(tool, [&]() -> PathAndVersion {
                if (tool == Tools::CMAKE)
                {
                    if (System::get_environment_variable("VCPKG_FORCE_SYSTEM_BINARIES").has_value())
                    {
                        return {"cmake", "0"};
                    }
                    return get_path(paths, CMakeProvider());
                }
                if (tool == Tools::GIT)
                {
                    if (System::get_environment_variable("VCPKG_FORCE_SYSTEM_BINARIES").has_value())
                    {
                        return {"git", "0"};
                    }
                    return get_path(paths, GitProvider());
                }
                if (tool == Tools::NINJA)
                {
                    if (System::get_environment_variable("VCPKG_FORCE_SYSTEM_BINARIES").has_value())
                    {
                        return {"ninja", "0"};
                    }
                    return get_path(paths, NinjaProvider());
                }
                if (tool == Tools::NUGET) return get_path(paths, NuGetProvider());
                if (tool == Tools::IFW_INSTALLER_BASE) return get_path(paths, IfwInstallerBaseProvider());

                Checks::exit_with_message(VCPKG_LINE_INFO, "Finding version for %s is not implemented yet.", tool);
            });
        }

        virtual const std::string& get_tool_version(const VcpkgPaths& paths, const std::string& tool) const override
        {
            return get_tool_pathversion(paths, tool).version;
        }
    };

    std::unique_ptr<ToolCache> get_tool_cache() { return std::make_unique<ToolCacheImpl>(); }
}
