#ifdef _WIN32
#include "registry.h"
#include "win-unicode.h"
#include <windows.h>
#include <processenv.h>
#include <winreg.h>
#include <winerror.h>

#include <vector>

namespace registry
{
    inline REGSAM internalGetRegistryViewFlag(RegistryView view)
    {
        switch (view)
        {
            case RegistryView::kRegistryView32:
                return KEY_WOW64_32KEY;
            case RegistryView::kRegistryView64:
                return KEY_WOW64_64KEY;
            //case RegistryView::kRegistryViewDefault:
            default:
                return 0;
        }
    }

    HKEY internalOpenKey(HKEY root, LPCWSTR path, RegistryView view)
    {
        HKEY hkey;
        DWORD res = ::RegOpenKeyExW(root, path, 0, KEY_QUERY_VALUE | internalGetRegistryViewFlag(view), &hkey);
        if (res == ERROR_SUCCESS)
            return hkey;
        // if that failed, try reading from 64 bit registry (but only if default registry view was requested)
        if (view != RegistryView::kRegistryViewDefault)
            return NULL;
        res = ::RegOpenKeyExW(root, path, 0, KEY_QUERY_VALUE | KEY_WOW64_64KEY, &hkey);
        if (res == ERROR_SUCCESS)
            return hkey;
        return NULL;
    }

    HKEY internalOpenForDeleteKey(HKEY root, LPCWSTR path, RegistryView view)
    {
        HKEY hkey;
        DWORD res = ::RegOpenKeyExW(root, path, 0, KEY_SET_VALUE | internalGetRegistryViewFlag(view), &hkey);
        if (res == ERROR_SUCCESS)
            return hkey;
        // if that failed, try reading from 64 bit registry (but only if default registry view was requested)
        if (view != RegistryView::kRegistryViewDefault)
            return NULL;
        res = ::RegOpenKeyExW(root, path, 0, KEY_SET_VALUE | KEY_WOW64_64KEY, &hkey);
        if (res == ERROR_SUCCESS)
            return hkey;
        return NULL;
    }

    bool internalGetString(HKEY root, LPCWSTR path, LPCWSTR key, std::wstring& outVal, RegistryView view)
    {
        HKEY hkey = internalOpenKey(root, path, view);
        if (!hkey)
            return false;

        // The key is opened. Query the value.
        WCHAR buffer[2048];

        DWORD dataType;
        DWORD dataLength = sizeof(buffer);
        DWORD res = ::RegQueryValueExW(hkey, key, NULL, &dataType, (LPBYTE)buffer, &dataLength);
        ::RegCloseKey(hkey);
        // In MULTI_SZ case only the first string will be returned
        if ((dataType == REG_SZ || dataType == REG_MULTI_SZ) && dataLength >= sizeof(WCHAR) && res == ERROR_SUCCESS)
        {
            const DWORD outLength = dataLength / sizeof(WCHAR) - 1;
            outVal.assign(buffer, buffer + outLength);
            return true;
        }
        if ((dataType == REG_EXPAND_SZ) && dataLength >= 1 && res == ERROR_SUCCESS)
        {
            // expand the string
            WCHAR exBuffer[ARRAYSIZE(buffer)];
            if (!::ExpandEnvironmentStringsW(buffer, exBuffer, ARRAYSIZE(exBuffer) - 1))
            {
                // just return the unexpanded string
                const DWORD outLength = dataLength / sizeof(WCHAR) - 1;
                outVal.assign(buffer, buffer + outLength);
                return true;
            }
            else
            {
                // return the expanded buffer
                outVal = exBuffer;
                return true;
            }
        }
        return false;
    }

    bool internalGetUInt32(HKEY root, LPCWSTR path, LPCWSTR key, unsigned int& outVal, RegistryView view)
    {
        HKEY hkey = internalOpenKey(root, path, view);
        if (!hkey)
            return false;

        // The key is opened. Query the value.
        DWORD dataType;
        DWORD dataLength = 4;
        DWORD res = ::RegQueryValueExW(hkey, key, 0, &dataType, (LPBYTE)&outVal, &dataLength);
        ::RegCloseKey(hkey);
        if ((dataType == REG_BINARY || dataType == REG_DWORD) && dataLength == 4 && res == ERROR_SUCCESS)
            return true;
        else
            return false;
    }

    bool internalSetUInt32(HKEY root, LPCWSTR path, LPCWSTR key, DWORD val, RegistryView view)
    {
        HKEY hkey;

        DWORD res = ::RegCreateKeyExW(root, path, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_SET_VALUE | internalGetRegistryViewFlag(view), NULL, &hkey, NULL);
        if (res != ERROR_SUCCESS)
            return false;

        res = ::RegSetValueExW(hkey, key, 0, REG_DWORD, (const BYTE*)&val, sizeof(val));
        ::RegCloseKey(hkey);

        return res == ERROR_SUCCESS;
    }

    bool internalSetString(HKEY root, LPCWSTR path, LPCWSTR key, LPCWSTR val, RegistryView view)
    {
        HKEY hkey;

        DWORD res = ::RegCreateKeyExW(root, path, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_SET_VALUE | internalGetRegistryViewFlag(view), NULL, &hkey, NULL);
        if (res != ERROR_SUCCESS)
            return false;

        res = ::RegSetValueExW(hkey, key, 0, REG_SZ, (const BYTE*)val, wcslen(val) * sizeof(WCHAR));
        ::RegCloseKey(hkey);
        return res == ERROR_SUCCESS;
    }

    bool internalSetBinaryValue(HKEY root, LPCWSTR path, LPCWSTR key, LPCSTR val, RegistryView view)
    {
        HKEY hkey;

        DWORD res = ::RegCreateKeyExW(root, path, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_SET_VALUE | internalGetRegistryViewFlag(view), NULL, &hkey, NULL);
        if (res != ERROR_SUCCESS)
            return false;

        res = ::RegSetValueExW(hkey, key, 0, REG_BINARY, (const BYTE*)val, (strlen(val)+1) * sizeof(CHAR));
        ::RegCloseKey(hkey);
        return res == ERROR_SUCCESS;
    }

    bool internalDeleteKey(HKEY root, LPCWSTR path, LPCWSTR key, RegistryView view)
    {
        HKEY hkey = internalOpenForDeleteKey(root, path, view);
        if (!hkey)
            return false;

        return RegDeleteValueW(hkey, key) == ERROR_SUCCESS;
    }

    template<size_t size>
    inline bool internalStripPrefix(LPCWSTR& text, const WCHAR(&prefix)[size])
    {
        if (!_wcsnicmp(text, prefix, size - 1))
        {
            text += size - 1;
            return true;
        }
        return false;
    }

    HKEY internalGetRoot(LPCWSTR& path)
    {
        if (internalStripPrefix(path, L"HKEY_CURRENT_USER\\"))
            return HKEY_CURRENT_USER;
        if (internalStripPrefix(path, L"HKEY_LOCAL_MACHINE\\"))
            return HKEY_LOCAL_MACHINE;
        if (internalStripPrefix(path, L"HKEY_CLASSES_ROOT\\"))
            return HKEY_CLASSES_ROOT;
        return NULL;
    }

    #define MAX_KEY_LENGTH 255
    #define MAX_VALUE_NAME 8191

    bool internalGetKeys(HKEY hKey, const std::string& parentPath, std::vector<std::string>& outKeys, std::vector<std::string>& outValues)
    {
        TCHAR    achKey[MAX_KEY_LENGTH];// buffer for subkey name
        DWORD    cbName;               // size of name string
        TCHAR    achClass[MAX_PATH];// buffer for class name
        DWORD    cchClassName = MAX_PATH;// size of class string
        DWORD    cSubKeys = 0;         // number of subkeys
        DWORD    cbMaxSubKey;          // longest subkey size
        DWORD    cchMaxClass;          // longest class string
        DWORD    cValues;          // number of values for key
        DWORD    cchMaxValue;      // longest value name
        DWORD    cbMaxValueData;   // longest value data
        DWORD    cbSecurityDescriptor;// size of security descriptor
        FILETIME ftLastWriteTime;  // last write time

        DWORD i, rc;


        strcpy(achClass, parentPath.c_str());
        cchClassName = strlen(achClass) + 1;

        rc = RegOpenKeyEx(hKey, achClass, 0, KEY_READ, &hKey);

        if (rc != ERROR_SUCCESS)
            return false;

        // query for count of children
        rc = RegQueryInfoKey(
                hKey,            // key handle
                achClass,        // buffer for class name
                &cchClassName,   // size of class string
                NULL,            // reserved
                &cSubKeys,       // number of subkeys
                &cbMaxSubKey,    // longest subkey size
                &cchMaxClass,    // longest class string
                &cValues,        // number of values for this key
                &cchMaxValue,    // longest value name
                &cbMaxValueData, // longest value data
                &cbSecurityDescriptor, // security descriptor
                &ftLastWriteTime); // last write time

        if (rc != ERROR_SUCCESS)
        {
            return FALSE;
        }


        char achValue[MAX_VALUE_NAME];


        //Iterate child keys
        for (i = 0;
                i < cValues; i++)
        {
            //get child key name
            //dwNSize = dim(wszName);
            //dwCSize = dim(wszClass);
            DWORD cchValue = MAX_VALUE_NAME;
            cbName = MAX_KEY_LENGTH;
            achKey[0] = 0;
            achValue[0] = 0;
            rc = RegEnumValue(hKey, i,
                    achKey,
                    &cbName,
                    NULL,
                    NULL,
                    reinterpret_cast<BYTE*>(achValue),
                    &cchValue);


            if (rc != ERROR_SUCCESS)
            {
                continue;
            }

            outKeys.push_back(achKey);
            outValues.push_back(achValue);
        }

        return true;
    }

    std::wstring getStringW(LPCWSTR path, LPCWSTR key, LPCWSTR def, RegistryView view)
    {
        std::wstring res;
        const HKEY root = internalGetRoot(path);
        if (root != NULL)
        {
            if (internalGetString(root, path, key, res, view))
                return res;
        }
        else
        {
            if (internalGetString(HKEY_CURRENT_USER, path, key, res, view))
                return res;
            if (internalGetString(HKEY_LOCAL_MACHINE, path, key, res, view))
                return res;
            if (internalGetString(HKEY_CLASSES_ROOT, path, key, res, view))
                return res;
        }
        return def == NULL ? L"" : def;
    }

    unsigned int getUInt32W(LPCWSTR path, LPCWSTR key, unsigned int def, RegistryView view)
    {
        unsigned int res;
        const HKEY root = internalGetRoot(path);
        if (root != NULL)
        {
            if (internalGetUInt32(root, path, key, res, view))
                return res;
        }
        else
        {
            if (internalGetUInt32(HKEY_CURRENT_USER, path, key, res, view))
                return res;
            if (internalGetUInt32(HKEY_LOCAL_MACHINE, path, key, res, view))
                return res;
        }
        return def;
    }

    bool setUInt32W(LPCWSTR path, LPCWSTR key, DWORD val, RegistryView view)
    {
        const HKEY root = internalGetRoot(path);
        if (root != NULL)
        {
            return internalSetUInt32(root, path, key, val, view);
        }
        else {
            return false; //(root is not specified)
        }
    }

    bool setStringW(LPCWSTR path, LPCWSTR key, LPCWSTR val, RegistryView view)
    {
        const HKEY root = internalGetRoot(path);
        if (root != NULL)
        {
            return internalSetString(root, path, key, val, view);
        } else {
            return false; // (root is not specified)
        }
    }

    bool getKeyValuesW(std::string path, std::vector<std::string>& outKeys, std::vector<std::string>& outValues)
    {
        // string conversion hell
        std::wstring widePath;
        ConvertUTF8ToWideString(path, widePath);

        LPCWSTR wideSubPath = widePath.c_str();

        const HKEY root = internalGetRoot(wideSubPath);
        std::string subPath;
        ConvertWideToUTF8String(wideSubPath, subPath);
        // string conversion hell

        if (root != NULL)
        {
            return internalGetKeys(root, subPath, outKeys, outValues);
        } else {
            return internalGetKeys(HKEY_CURRENT_USER, subPath, outKeys, outValues); // (root is not specified)
        }
    }
}
#endif
