
#include <napi.h>
#include <sstream>

#ifdef _WIN32
#include "registry.h"
#include "win-unicode.h"

inline size_t djb2_hash(const std::string& name)
{
    unsigned long hash = 5381;
    char c;
    const char* s = name.c_str();
    while ((c = *s++))
        hash = ((hash << 5) + hash) ^ c;
    return hash;
}
#endif

// Exposed functions

Napi::Value getString(const Napi::CallbackInfo& info)
{
    Napi::Env env = info.Env();
#ifdef _WIN32
    if (info.Length() == 3)
    {
        throw Napi::TypeError::New(env, "Expected 3 arguments: path, key, default");
    }
    if (!info[0].IsString() || !info[1].IsString() || !info[2].IsString())
    {
        throw Napi::TypeError::New(env, "Expected arguments: string (path), string (key), string (default)");
    }

    std::string path = info[0].As<Napi::String>().Utf8Value();
    std::string key = info[1].As<Napi::String>().Utf8Value();
    std::string def = info[2].As<Napi::String>().Utf8Value();

	std::wstring widePath, wideKey, wideDef;
    ConvertUTF8ToWideString(path, widePath);
    ConvertUTF8ToWideString(key, wideKey);
    ConvertUTF8ToWideString(def, wideDef);
    const std::wstring wideRes = registry::getStringW(widePath.c_str(), wideKey.c_str(), wideDef.c_str(), registry::RegistryView::kRegistryViewDefault);
    std::string res;
    ConvertWideToUTF8String(wideRes, res);

	return Napi::String::New(env, res);
#else
    throw Napi::Error::New(env, "getString is not supported on this platform");
#endif
}

Napi::Value getKeyValues(const Napi::CallbackInfo& info)
{
    Napi::Env env = info.Env();
#ifdef _WIN32
    if (info.Length() != 1)
    {
        throw Napi::TypeError::New(env, "Expected 1 argument: key");
    }
    if (!info[0].IsString())
    {
        throw Napi::TypeError::New(env, "Expected argument: string (key)");
    }
    
    std::string key = info[0].As<Napi::String>().Utf8Value();

	std::vector<std::string> outKeys;
    std::vector<std::string> outValues;

    registry::getKeyValuesW(key, outKeys, outValues);

	Napi::Array v8Keys = Napi::Array::New(env, outKeys.size());
	for (int i = 0; i < outKeys.size(); i++)
	{
		v8Keys.Set(i, Napi::String::New(env, outKeys[i]));
	}

	Napi::Array v8values = Napi::Array::New(env, outValues.size());
	for (int i = 0; i < outValues.size(); i++)
	{
		v8values.Set(i, Napi::String::New(env, outValues[i]));
	}

	Napi::Array v8ret = Napi::Array::New(env, 2);
	v8ret.Set(0u, v8Keys);
	v8ret.Set(1, v8values);

	return v8ret;
#else
    throw Napi::Error::New(env, "getKeyValues is not supported on this platform");
#endif
}

Napi::Value getUInt32(const Napi::CallbackInfo& info)
{
    Napi::Env env = info.Env();
#ifdef _WIN32
    if (info.Length() != 3)
    {
        throw Napi::TypeError::New(env, "Expected 3 arguments: path, key, default");
    }
    if (!info[0].IsString() || !info[1].IsString() || !info[2].IsNumber())
    {
        throw Napi::TypeError::New(env, "Expected arguments: string (path), string (key), number (default)");
    }
    if (info[2].As<Napi::Number>().Uint32Value() > UINT_MAX || info[2].As<Napi::Number>().Uint32Value() < 0)
    {
        throw Napi::RangeError::New(env, "Default value is out of range");
    }

    std::string path = info[0].As<Napi::String>().Utf8Value();
    std::string key = info[1].As<Napi::String>().Utf8Value();
    unsigned int def = info[2].As<Napi::Number>().Uint32Value();

	std::wstring widePath, wideKey;
    ConvertUTF8ToWideString(path, widePath);
    ConvertUTF8ToWideString(key, wideKey);
    unsigned int ret = getUInt32W(widePath.c_str(), wideKey.c_str(), def, registry::RegistryView::kRegistryViewDefault);

	return Napi::Number::New(env, ret);
#else
    throw Napi::Error::New(env, "getUInt32 is not supported on this platform");
#endif
}

Napi::Value setUInt32(const Napi::CallbackInfo& info)
{
    Napi::Env env = info.Env();
#ifdef _WIN32
    if (info.Length() != 3)
    {
        throw Napi::TypeError::New(env, "Expected 3 arguments: path, key, value");
    }
    if (!info[0].IsString() || !info[1].IsString() || !info[2].IsNumber())
    {
        throw Napi::TypeError::New(env, "Expected arguments: string (path), string (key), number (value)");
    }
    if (info[2].As<Napi::Number>().Uint32Value() > UINT_MAX || info[2].As<Napi::Number>().Uint32Value() < 0)
    {
        throw Napi::RangeError::New(env, "Value is out of range");
    }

    std::string path = info[0].As<Napi::String>().Utf8Value();
    std::string key = info[1].As<Napi::String>().Utf8Value();
    unsigned int val = info[2].As<Napi::Number>().Uint32Value();

	std::wstring widePath, wideKey;
	ConvertUTF8ToWideString(path, widePath);
	ConvertUTF8ToWideString(key, wideKey);
	bool res = setUInt32W(widePath.c_str(), wideKey.c_str(), (DWORD)val, registry::RegistryView::kRegistryViewDefault);

	return Napi::Boolean::New(env, res);
#else
    throw Napi::Error::New(env, "setUInt32 is not supported on this platform");
#endif
}

Napi::Value setString(const Napi::CallbackInfo& info)
{
    Napi::Env env = info.Env();
#ifdef _WIN32
    if (info.Length() != 3)
    {
        throw Napi::TypeError::New(env, "Expected 3 arguments: path, key, value");
    }
    if (!info[0].IsString() || !info[1].IsString() || !info[2].IsString())
    {
        throw Napi::TypeError::New(env, "Expected arguments: string (path), string (key), string (value)");
    }
    if (info[0].As<Napi::String>().Utf8Value().empty() || info[1].As<Napi::String>().Utf8Value().empty())
    {
        throw Napi::TypeError::New(env, "Path and key cannot be empty");
    }
    if (info[2].As<Napi::String>().Utf8Value().empty())
    {
        throw Napi::TypeError::New(env, "Value cannot be empty");
    }

    std::string path = info[0].As<Napi::String>().Utf8Value();
    std::string key = info[1].As<Napi::String>().Utf8Value();
    std::string val = info[2].As<Napi::String>().Utf8Value();

	std::wstring widePath, wideKey, wideVal;
	ConvertUTF8ToWideString(path, widePath);
	ConvertUTF8ToWideString(key, wideKey);
	ConvertUTF8ToWideString(val, wideVal);
	bool res = setStringW(widePath.c_str(), wideKey.c_str(), wideVal.c_str(), registry::RegistryView::kRegistryViewDefault);

	return Napi::Boolean::New(env, res);
#else
    throw Napi::Error::New(env, "setString is not supported on this platform");
#endif
}

Napi::Value getRegistryKeyName(const Napi::CallbackInfo& info)
{
    Napi::Env env = info.Env();
#ifdef _WIN32
    if (info.Length() != 1)
    {
        throw Napi::TypeError::New(env, "Expected 1 argument: name");
    }
    if (!info[0].IsString())
    {
        throw Napi::TypeError::New(env, "Expected argument: string (name)");
    }

    std::string name = info[0].As<Napi::String>().Utf8Value();
    const size_t hash = djb2_hash(name);

    std::stringstream str;
    str << name << "_h" << hash;

	std::string res = str.str().c_str();

	return Napi::String::New(env, res);
#else
    throw Napi::Error::New(env, "getRegistryKeyName is not supported on this platform");
#endif
}

Napi::Value setBinaryValue(const Napi::CallbackInfo& info)
{
    Napi::Env env = info.Env();
#ifdef _WIN32
    if (info.Length() != 3)
    {
        throw Napi::TypeError::New(env, "Expected 3 arguments: path, key, value");
    }
    if (!info[0].IsString() || !info[1].IsString() || !info[2].IsString())
    {
        throw Napi::TypeError::New(env, "Expected arguments: string (path), string (key), string (value)");
    }
    if (info[0].As<Napi::String>().Utf8Value().empty() || info[1].As<Napi::String>().Utf8Value().empty())
    {
        throw Napi::TypeError::New(env, "Path and key cannot be empty");
    }
    if (info[2].As<Napi::String>().Utf8Value().empty())
    {
        throw Napi::TypeError::New(env, "Value cannot be empty");
    }

    std::string path = info[0].As<Napi::String>().Utf8Value();
    std::string key = info[1].As<Napi::String>().Utf8Value();
    std::string val = info[2].As<Napi::String>().Utf8Value();

    std::wstring widePath, wideKey, wideVal;
    ConvertUTF8ToWideString(path, widePath);
    ConvertUTF8ToWideString(key, wideKey);

    LPCWSTR lpwPath = widePath.c_str();
    const HKEY root = registry::internalGetRoot(lpwPath);
    bool res = registry::internalSetBinaryValue(root, lpwPath, wideKey.c_str(), val.c_str(), registry::RegistryView::kRegistryViewDefault);

	return Napi::Boolean::New(env, res);
#else
    throw Napi::Error::New(env, "setBinaryValue is not supported on this platform");
#endif
}

Napi::Value deleteKey(const Napi::CallbackInfo& info)
{
    Napi::Env env = info.Env();
#ifdef _WIN32
    if (info.Length() != 2)
    {
        throw Napi::TypeError::New(env, "Expected 2 arguments: path, key");
    }
    if (!info[0].IsString() || !info[1].IsString())
    {
        throw Napi::TypeError::New(env, "Expected arguments: string (path), string (key)");
    }
    if (info[0].As<Napi::String>().Utf8Value().empty() || info[1].As<Napi::String>().Utf8Value().empty())
    {
        throw Napi::TypeError::New(env, "Path and key cannot be empty");
    }

    std::string path = info[0].As<Napi::String>().Utf8Value();
    std::string key = info[1].As<Napi::String>().Utf8Value();

    bool res = false;

    std::wstring widePath, wideKey;
    ConvertUTF8ToWideString(path, widePath);
    ConvertUTF8ToWideString(key, wideKey);
    LPCWSTR lpwPath = widePath.c_str();
    const HKEY root = registry::internalGetRoot(lpwPath);
	if (root != NULL)
    {
	    registry::internalDeleteKey(root, lpwPath, wideKey.c_str(), registry::RegistryView::kRegistryViewDefault);
	    res = true;
	}

	return Napi::Boolean::New(env, res);
#else
    throw Napi::Error::New(env, "deleteKey is not supported on this platform");
#endif
}

Napi::Object init(Napi::Env env, Napi::Object exports)
{
    exports.Set(Napi::String::New(env, "getString"), Napi::Function::New(env, getString));
    exports.Set(Napi::String::New(env, "getKeyValues"), Napi::Function::New(env, getKeyValues));
    exports.Set(Napi::String::New(env, "getUInt32"), Napi::Function::New(env, getUInt32));
    exports.Set(Napi::String::New(env, "setUInt32"), Napi::Function::New(env, setUInt32));
    exports.Set(Napi::String::New(env, "setString"), Napi::Function::New(env, setString));
    exports.Set(Napi::String::New(env, "getRegistryKeyName"), Napi::Function::New(env, getRegistryKeyName));
    exports.Set(Napi::String::New(env, "setBinaryValue"), Napi::Function::New(env, setBinaryValue));
    exports.Set(Napi::String::New(env, "deleteKey"), Napi::Function::New(env, deleteKey));
	return exports;
}

NODE_API_MODULE(registry, init)
