#pragma once

#include <cassert>
#include <array>
#include <limits>
#include <climits>
#include <cstdint>
#include <string>
#include <string_view>
#include <map>
#include <vector>
#include <set>
#include <filesystem>
#include <fstream>
#include <regex>

#if defined(_WIN32) || defined(_WIN64)
#include <windows.h>
#include <shlwapi.h>
#include <xmllite.h>
#endif

namespace cppwinrt
{
#if defined(_WIN32) || defined(_WIN64)
    struct registry_key
    {
        HKEY handle{};

        registry_key(registry_key const&) = delete;
        registry_key& operator=(registry_key const&) = delete;

        explicit registry_key(HKEY handle) :
            handle(handle)
        {
        }

        ~registry_key() noexcept
        {
            if (handle)
            {
                RegCloseKey(handle);
            }
        }
    };

    template <typename T>
    struct com_ptr
    {
        T* ptr{};

        com_ptr(com_ptr const&) = delete;
        com_ptr& operator=(com_ptr const&) = delete;

        com_ptr() noexcept = default;

        ~com_ptr() noexcept
        {
            if (ptr)
            {
                ptr->Release();
            }
        }

        auto operator->() const noexcept
        {
            return ptr;
        }
    };

    static void check_xml(HRESULT result)
    {
        if (result < 0)
        {
            throw std::invalid_argument("Could not read the Windows SDK's Platform.xml");
        }
    }

    enum class xml_requirement
    {
        required = 0,
        optional
    };

    inline void add_files_from_xml(
        std::set<std::string>& files,
        std::string const& sdk_version,
        std::filesystem::path const& xml_path,
        std::filesystem::path const& sdk_path,
        xml_requirement xml_path_requirement)
    {
        com_ptr<IStream> stream;

        auto streamResult = SHCreateStreamOnFileW(
            xml_path.c_str(),
            STGM_READ, &stream.ptr);
        if (xml_path_requirement == xml_requirement::optional &&
            (streamResult == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) ||
             streamResult == HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND)))
        {
            return;
        }
        check_xml(streamResult);

        com_ptr<IXmlReader> reader;

        check_xml(CreateXmlReader(
            __uuidof(IXmlReader),
            reinterpret_cast<void**>(&reader.ptr),
            nullptr));

        check_xml(reader->SetInput(stream.ptr));
        XmlNodeType node_type = XmlNodeType_None;

        while (S_OK == reader->Read(&node_type))
        {
            if (node_type != XmlNodeType_Element)
            {
                continue;
            }

            wchar_t const* value{ nullptr };
            check_xml(reader->GetLocalName(&value, nullptr));

            if (0 != wcscmp(value, L"ApiContract"))
            {
                continue;
            }

            auto path = sdk_path;
            path /= L"References";
            path /= sdk_version;

            check_xml(reader->MoveToAttributeByName(L"name", nullptr));
            check_xml(reader->GetValue(&value, nullptr));
            path /= value;

            check_xml(reader->MoveToAttributeByName(L"version", nullptr));
            check_xml(reader->GetValue(&value, nullptr));
            path /= value;

            check_xml(reader->MoveToAttributeByName(L"name", nullptr));
            check_xml(reader->GetValue(&value, nullptr));
            path /= value;

            path += L".winmd";
            files.insert(path.string());
        }
    }

    inline registry_key open_sdk()
    {
        HKEY key;

        if (0 != RegOpenKeyExW(
            HKEY_LOCAL_MACHINE,
            L"SOFTWARE\\Microsoft\\Windows Kits\\Installed Roots",
            0,
            // https://task.ms/29349404 - The SDK sometimes stores the 64 bit location into KitsRoot10 which is wrong,
            // this breaks 64-bit cppwinrt.exe, so work around this by forcing to use the WoW64 hive.
            KEY_READ | KEY_WOW64_32KEY, 
            &key))
        {
            throw std::invalid_argument("Could not find the Windows SDK in the registry");
        }

        return registry_key{ key };
    }

    inline std::filesystem::path get_sdk_path()
    {
        auto key = open_sdk();

        DWORD path_size = 0;

        if (0 != RegQueryValueExW(
            key.handle,
            L"KitsRoot10",
            nullptr,
            nullptr,
            nullptr,
            &path_size))
        {
            throw std::invalid_argument("Could not find the Windows SDK path in the registry");
        }

        std::wstring root((path_size / sizeof(wchar_t)) - 1, L'?');

        RegQueryValueExW(
            key.handle,
            L"KitsRoot10",
            nullptr,
            nullptr,
            reinterpret_cast<BYTE*>(root.data()),
            &path_size);

        return root;
    }

    inline std::string get_module_path()
    {
        std::string path(100, '?');

        while (true)
        {
            DWORD actual_size = GetModuleFileNameA(nullptr, path.data(), 1 + static_cast<uint32_t>(path.size()));

            if (actual_size < 1 + path.size())
            {
                path.resize(actual_size);
                break;
            }
            else
            {
                path.resize(path.size() * 2, '?');
            }
        }

        return path;
    }

    inline std::string get_sdk_version()
    {
        auto module_path = get_module_path();
        std::regex rx(R"(((\d+)\.(\d+)\.(\d+)\.(\d+)))");
        std::cmatch match;
        auto sdk_path = get_sdk_path();

        if (std::regex_search(module_path.c_str(), match, rx))
        {
            auto path = sdk_path / "Platforms\\UAP" / match[1].str() / "Platform.xml";

            if (std::filesystem::exists(path))
            {
                return match[1].str();
            }
        }

        auto key = open_sdk();
        uint32_t index{};
        std::array<char, 100> subkey;
        std::array<unsigned long, 4> version_parts{};
        std::string result;

        while (0 == RegEnumKeyA(key.handle, index++, subkey.data(), static_cast<uint32_t>(subkey.size())))
        {
            if (!std::regex_match(subkey.data(), match, rx))
            {
                continue;
            }

            auto path = sdk_path / "Platforms\\UAP" / match[1].str() / "Platform.xml";
            if (!std::filesystem::exists(path))
            {
                continue;
            }

            char* next_part = subkey.data();
            bool force_newer = false;

            for (size_t i = 0; ; ++i)
            {
                auto version_part = strtoul(next_part, &next_part, 10);

                if ((version_part < version_parts[i]) && !force_newer)
                {
                    break;
                }
                else if (version_part > version_parts[i])
                {
                    // E.g. ensure something like '2.1' is considered newer than '1.2'
                    force_newer = true;
                }

                version_parts[i] = version_part;

                if (i == std::size(version_parts) - 1)
                {
                    result = subkey.data();
                    break;
                }

                if (!next_part)
                {
                    break;
                }

                ++next_part;
            }
        }

        if (result.empty())
        {
            throw std::invalid_argument("Could not find the Windows SDK");
        }

        return result;
    }
#endif /* defined(_WIN32) || defined(_WIN64) */

    [[noreturn]] inline void throw_invalid(std::string const& message)
    {
        throw std::invalid_argument(message);
    }

    template <typename...T>
    [[noreturn]] inline void throw_invalid(std::string message, T const&... args)
    {
        (message.append(args), ...);
        throw std::invalid_argument(message);
    }

    struct option
    {
        static constexpr uint32_t no_min = 0;
        static constexpr uint32_t no_max = UINT_MAX;
        
        std::string_view name;
        uint32_t min{ no_min };
        uint32_t max{ no_max };
        std::string_view arg{};
        std::string_view desc{};
    };

    struct reader
    {
        template <typename C, typename V, size_t numOptions>
        reader(C const argc, V const argv, const option(& options)[numOptions])
        {
#ifdef _DEBUG
            {
                std::set<std::string_view> unique;

                for (auto&& option : options)
                {
                    // If this assertion fails it means there are duplicate options.
                    assert(unique.insert(option.name).second);
                }
            }
#endif

            if (argc < 2)
            {
                return;
            }

            auto last{ std::end(options) };

            for (C i = 1; i < argc; ++i)
            {
                extract_option(argv[i], options, last);
            }

            for (auto&& option : options)
            {
                auto args = m_options.find(option.name);
                std::size_t const count = args == m_options.end() ? 0 : args->second.size();

                if (option.min == 0 && option.max == 0 && count > 0)
                {
                    throw_invalid("Option '", option.name, "' does not accept a value");
                }
                else if (option.max == option.min && count != option.max)
                {
                    throw_invalid("Option '", option.name, "' requires exactly ", std::to_string(option.max), " value(s)");
                }
                else if (count < option.min)
                {
                    throw_invalid("Option '", option.name, "' requires at least ", std::to_string(option.min), " value(s)");
                }
                else if (count > option.max)
                {
                    throw_invalid("Option '", option.name, "' accepts at most ", std::to_string(option.max), " value(s)");
                }
            }
        }

        explicit operator bool() const noexcept
        {
            return !m_options.empty();
        }

        bool exists(std::string_view const& name) const noexcept
        {
            return m_options.count(name);
        }

        auto const& values(std::string_view const& name) const noexcept
        {
            auto result = m_options.find(name);

            if (result == m_options.end())
            {
                static std::vector<std::string> empty{};
                return empty;
            }

            return result->second;
        }

        auto value(std::string_view const& name, std::string_view const& default_value = {}) const
        {
            auto result = m_options.find(name);

            if (result == m_options.end() || result->second.empty())
            {
                return std::string{ default_value };
            }

            return result->second.front();
        }

        template <typename F>
        auto files(std::string_view const& name, F directory_filter) const
        {
            std::set<std::string> files;

            auto add_directory = [&](auto&& path)
            {
                for (auto&& file : std::filesystem::directory_iterator(path))
                {
                    if (std::filesystem::is_regular_file(file))
                    {
                        auto filename = file.path().string();

                        if (directory_filter(filename))
                        {
                            files.insert(filename);
                        }
                    }
                }
            };

            for (auto&& path : values(name))
            {
                if (std::filesystem::is_directory(path))
                {
                    add_directory(std::filesystem::canonical(path));
                    continue;
                }

                if (std::filesystem::is_regular_file(path))
                {
                    files.insert(std::filesystem::canonical(path).string());
                    continue;
                }
                if (path == "local")
                {
#if defined(_WIN32) || defined(_WIN64)
                    std::array<char, 260> local{};
#ifdef _WIN64
                    ExpandEnvironmentStringsA("%windir%\\System32\\WinMetadata", local.data(), static_cast<uint32_t>(local.size()));
#else
                    ExpandEnvironmentStringsA("%windir%\\SysNative\\WinMetadata", local.data(), static_cast<uint32_t>(local.size()));
#endif
                    add_directory(local.data());
#else /* defined(_WIN32) || defined(_WIN64) */
                    throw_invalid("Spec '", path, "' not supported outside of Windows");
#endif /* defined(_WIN32) || defined(_WIN64) */
                    continue;
                }

                std::string sdk_version;

                if (path == "sdk" || path == "sdk+")
                {
#if defined(_WIN32) || defined(_WIN64)
                    sdk_version = get_sdk_version();
#else /* defined(_WIN32) || defined(_WIN64) */
                    throw_invalid("Spec '", path, "' not supported outside of Windows");
#endif /* defined(_WIN32) || defined(_WIN64) */
                }
                else
                {
                    std::regex rx(R"(((\d+)\.(\d+)\.(\d+)\.(\d+))\+?)");
                    std::smatch match;

                    if (std::regex_match(path, match, rx))
                    {
                        sdk_version = match[1].str();
                    }
                }

                if (!sdk_version.empty())
                {
#if defined(_WIN32) || defined(_WIN64)
                    auto sdk_path = get_sdk_path();
                    auto xml_path = sdk_path;
                    xml_path /= L"Platforms\\UAP";
                    xml_path /= sdk_version;
                    xml_path /= L"Platform.xml";

                    add_files_from_xml(files, sdk_version, xml_path, sdk_path, xml_requirement::required);

                    if (path.back() != '+')
                    {
                        continue;
                    }

                    for (auto&& item : std::filesystem::directory_iterator(sdk_path / L"Extension SDKs"))
                    {
                        xml_path = item.path() / sdk_version;
                        xml_path /= L"SDKManifest.xml";

                        // Not all Extension SDKs include an SDKManifest.xml file; ignore those which do not (e.g. WindowsIoT).
                        add_files_from_xml(files, sdk_version, xml_path, sdk_path, xml_requirement::optional);
                    }
#else /* defined(_WIN32) || defined(_WIN64) */
                    throw_invalid("Spec '", path, "' not supported outside of Windows");
#endif /* defined(_WIN32) || defined(_WIN64) */

                    continue;
                }

                throw_invalid("Path '", path, "' is not a file or directory");
            }

            return files;
        }

        auto files(std::string_view const& name) const
        {
            return files(name, [](auto&&) {return true; });
        }

    private:

        inline bool starts_with(std::string_view const& value, std::string_view const& match) noexcept
        {
            return 0 == value.compare(0, match.size(), match);
        }

        template<typename O>
        auto find(O const& options, std::string_view const& arg)
        {
            for (auto current = std::begin(options); current != std::end(options); ++current)
            {
                if (starts_with(current->name, arg))
                {
                    return current;
                }
            }

            return std::end(options);
        }

        std::map<std::string_view, std::vector<std::string>> m_options;

        template<typename O, typename L>
        void extract_option(std::string_view arg, O const& options, L& last)
        {
            if (arg[0] == '-'
#if defined(_WIN32) || defined(_WIN64)
                || arg[0] == '/'
#endif
            )
            {
                arg.remove_prefix(1);
                last = find(options, arg);

                if (last == std::end(options))
                {
                    throw_invalid("Option '-", arg, "' is not supported");
                }

                m_options.try_emplace(last->name);
            }
            else if (arg[0] == '@')
            {
                arg.remove_prefix(1);
                extract_response_file(arg, options, last);
            }
            else if (last == std::end(options))
            {
                throw_invalid("Value '", arg, "' is not supported");
            }
            else
            {
                m_options[last->name].push_back(std::string{ arg });
            }
        }

        template<typename O, typename L>
        void extract_response_file(std::string_view const& arg, O const& options, L& last)
        {
            std::filesystem::path response_path{ std::string{ arg } };
            std::string extension = response_path.extension().generic_string();
            std::transform(extension.begin(), extension.end(), extension.begin(),
                [](auto c) { return static_cast<unsigned char>(::tolower(c)); });

            // Check if misuse of @ prefix, so if directory or metadata file instead of response file.
            if (is_directory(response_path) || extension == ".winmd")
            {
                throw_invalid("'@' is reserved for response files");
            }
            std::string line_buf;
            std::ifstream response_file(absolute(response_path));
            while (getline(response_file, line_buf))
            {
                size_t argc = 0;
                std::vector<std::string> argv;

                parse_command_line(line_buf.data(), argv, &argc);

                for (size_t i = 0; i < argc; i++)
                {
                    extract_option(argv[i], options, last);
                }
            }
        }

        template <typename Character>
        static void parse_command_line(Character* cmdstart, std::vector<std::string>& argv, size_t* argument_count)
        {

            std::string arg;
            bool copy_character;
            unsigned backslash_count;
            bool in_quotes;
            bool first_arg;

            Character* p = cmdstart;
            in_quotes = false;
            first_arg = true;
            *argument_count = 0;

            while (true)
            {
                if (*p)
                {
                    while (*p == ' ' || *p == '\t')
                        ++p;
                }

                if (!first_arg)
                {
                    argv.emplace_back(arg);
                    arg.clear();
                    ++*argument_count;
                }

                if (*p == '\0')
                    break;

                while (true)
                {
                    copy_character = true;

                    // Rules:
                    // 2N     backslashes   + " ==> N backslashes and begin/end quote
                    // 2N + 1 backslashes   + " ==> N backslashes + literal "
                    // N      backslashes       ==> N backslashes
                    backslash_count = 0;

                    while (*p == '\\')
                    {
                        ++p;
                        ++backslash_count;
                    }

                    if (*p == '"')
                    {
                        // if 2N backslashes before, start/end quote, otherwise
                        // copy literally:
                        if (backslash_count % 2 == 0)
                        {
                            if (in_quotes && p[1] == '"')
                            {
                                p++; // Double quote inside quoted string
                            }
                            else
                            {
                                // Skip first quote char and copy second:
                                copy_character = false;
                                in_quotes = !in_quotes;
                            }
                        }

                        backslash_count /= 2;
                    }

                    while (backslash_count--)
                    {
                        arg.push_back('\\');
                    }

                    if (*p == '\0' || (!in_quotes && (*p == ' ' || *p == '\t')))
                        break;

                    if (copy_character)
                    {
                        arg.push_back(*p);
                    }

                    ++p;
                }

                first_arg = false;
            }
        }
    };
}
