import registrydef, strutils, typetraits, winlean

type
    RegistryError* = object of ValueError

const
    FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x100
    FORMAT_MESSAGE_IGNORE_INSERTS = 0x200
    FORMAT_MESSAGE_FROM_SYSTEM = 0x1000

    ERROR_SUCCESS = 0
    ERROR_FILE_NOT_FOUND = 2
    USER_LANGUAGE = 0x0400

    MAX_KEY_LEN = 255
    MAX_VALUE_LEN = 16383

proc getErrorMessage(code: int32): string {.raises: [].} =
    var msgbuf: pointer
    when useWinUnicode:
        discard formatMessageW(FORMAT_MESSAGE_FROM_SYSTEM or FORMAT_MESSAGE_ALLOCATE_BUFFER or
            FORMAT_MESSAGE_IGNORE_INSERTS, nil, code, USER_LANGUAGE, msgbuf.addr, 0, nil)
        result = $cast[WideCString](msgbuf)
    else:
        discard formatMessageA(FORMAT_MESSAGE_FROM_SYSTEM or FORMAT_MESSAGE_ALLOCATE_BUFFER or
            FORMAT_MESSAGE_IGNORE_INSERTS, nil, code, USER_LANGUAGE, msgbuf.addr, 0, nil)
        result = $cast[CString](msgbuf)
    localFree(msgbuf)

proc raiseError(code: int32) {.inline, raises: [RegistryError].} =
    raise newException(RegistryError, $code & ": " & getErrorMessage(code))

proc close*(this: RegistryKey) {.raises: [RegistryError].} =
    ## Closes the key and flushes it to disk if its contents have been modified.

    let code = regCloseKey(this)
    if unlikely(code != ERROR_SUCCESS):
        raiseError(code)

proc createSubKey*(this: RegistryKey, subkey: string, writable: bool): RegistryKey {.raises: [RegistryError].} =
    ## Creates a new subkey or opens an existing subkey with the specified access.

    var createdHandle: RegistryKey
    when useWinUnicode:
        let code = regCreateKeyExW(this, newWideCString(subkey), 0, nil, 0,
            if writable: KEY_ALL_ACCESS else: KEY_READ, nil, createdHandle.addr, nil)
    else:
        let code = regCreateKeyExA(this, newCString(subkey), 0, nil, 0,
            if writable: KEY_ALL_ACCESS else: KEY_READ, nil, createdHandle.addr, nil)

    if unlikely(code != ERROR_SUCCESS):
        raiseError(code)

    return createdHandle

proc createSubKey*(this: RegistryKey, subkey: string): RegistryKey {.raises: [RegistryError].} =
    ## Creates a new subkey or opens an existing subkey for write access.
    return this.createSubKey(subkey, true)

proc deleteSubKey*(this: RegistryKey, subkey: string, raiseOnMissingSubKey: bool) {.raises: [RegistryError].} =
    ## Deletes the specified subkey, and specifies whether an exception is raised if the subkey is not found.

    when useWinUnicode:
        let code = regDeleteKeyW(this, newWideCString(subkey))
    else:
        let code = regDeleteKeyA(this, newCString(subkey))

    if unlikely(code != ERROR_SUCCESS) and (raiseOnMissingSubKey or code != ERROR_FILE_NOT_FOUND):
        raiseError(code)

proc deleteSubKey*(this: RegistryKey, subkey: string) {.raises: [RegistryError].} =
    ## Deletes the specified subkey.
    this.deleteSubKey(subkey, true)

proc deleteSubKeyTree*(this: RegistryKey, subkey: string, raiseOnMissingSubKey: bool)
    {.raises: [RegistryError].} =
    ## Deletes the specified subkey and any child subkeys recursively, and
    ## specifies whether an exception is raised if the subkey is not found.

    when useWinUnicode:
        let code = regDeleteTreeW(this, newWideCString(subkey))
    else:
        let code = regDeleteTreeA(this, newCString(subkey))

    if unlikely(code != ERROR_SUCCESS) and (raiseOnMissingSubKey or code != ERROR_FILE_NOT_FOUND):
        raiseError(code)

proc deleteSubKeyTree*(this: RegistryKey, subkey: string) {.raises: [RegistryError].} =
    ## Deletes a subkey and any child subkeys recursively.
    this.deleteSubKeyTree(subkey, true)

proc deleteValue*(this: RegistryKey, name: string, raiseOnMissingValue: bool) {.raises: [RegistryError].} =
    ## Deletes the specified value from this key, and specifies whether
    ## an exception is raised if the value is not found.

    when useWinUnicode:
        let code = regDeleteKeyValueW(this, nil, newWideCString(name))
    else:
        let code = regDeleteKeyValueA(this, nil, newCString(name))

    if unlikely(code != ERROR_SUCCESS) and (raiseOnMissingValue or code != ERROR_FILE_NOT_FOUND):
        raiseError(code)

proc deleteValue*(this: RegistryKey, name: string) {.raises: [RegistryError].} =
    ## Deletes the specified value from this key.
    this.deleteValue(name, true)

proc flush*(this: RegistryKey) {.raises: [RegistryError].} =
    ## Writes all the attributes of the specified open registry key into the registry.

    let code = regFlushKey(this)
    if unlikely(code != ERROR_SUCCESS):
        raiseError(code)

iterator getSubKeyNames*(this: RegistryKey): string {.raises: [RegistryError].} =
    ## Retrieves an iterator of strings that runs over all the subkey names.

    var keyCount: int32
    when useWinUnicode:
        let code = regQueryInfoKeyW(this, nil, nil, nil, keyCount.addr, nil, nil, nil, nil, nil, nil, nil)
        if unlikely(code != ERROR_SUCCESS):
            raiseError(code)

        var nameBuffer: WideCString
        unsafeNew(nameBuffer, (MAX_KEY_LEN + 1) * sizeof(Utf16Char))

        for i in 0..<keyCount:
            var nameLen: int32 = MAX_KEY_LEN
            let code = regEnumKeyExW(this, int32(i), nameBuffer, nameLen.addr, nil, nil, nil, nil)
            if unlikely(code != ERROR_SUCCESS):
                raiseError(code)

            nameBuffer[nameLen] = Utf16Char(0)
            yield $nameBuffer
    else:
        let code = regQueryInfoKeyA(this, nil, nil, nil, keyCount.addr, nil, nil, nil, nil, nil, nil, nil)
        if unlikely(code != ERROR_SUCCESS):
            raiseError(code)

        var nameBuffer: CString
        unsafeNew(nameBuffer, MAX_KEY_LEN + 1)
        
        for i in 0..<keyCount:
            var nameLen: int32 = MAX_KEY_LEN
            let code = regEnumKeyExA(this, int32(i), nameBuffer, nameLen.addr, nil, nil, nil, nil)
            if unlikely(code != ERROR_SUCCESS):
                raiseError(code)

            nameBuffer[nameLen] = 0
            yield $nameBuffer

proc tryGetValue*[T](this: RegistryKey, name: string, value: var T): bool
    {.raises: [RegistryError, ValueError, Defect].} =
    ## Retrieves the value associated with the specified name.
    ## Attempts to convert values to the correct type, if applicable.

    var kind: RegistryValueType
    var size: int32
    when useWinUnicode:
        let code = regQueryValueExW(this, newWideCString(name), nil, kind.addr, nil, size.addr)
    else:
        let code = regQueryValueExA(this, newCString(name), nil, kind.addr, nil, size.addr)

    if code == ERROR_FILE_NOT_FOUND:
        return false
    if unlikely(code != ERROR_SUCCESS):
        raiseError(code)

    case kind:
    of REG_DWORD:
        var tmp: int32
        size = int32(sizeof(tmp))

        when useWinUnicode:
            let code = regGetValueW(this, nil, newWideCString(name), 0x0000ffff, nil,
                cast[pointer](tmp.addr), size.addr)
        else:
            let code = regGetValueA(this, nil, newCString(name), 0x0000ffff, nil,
                cast[pointer](tmp.addr), size.addr)

        if code == ERROR_FILE_NOT_FOUND:
            return false
        if unlikely(code != ERROR_SUCCESS):
            raiseError(code)

        when T is SomeNumber:
            value = T(tmp)
            return true
        elif T is string:
            value = $tmp
            return true
        else:
            {.fatal: "The type " & T.name & " is not supported yet.".}
    of REG_QWORD:
        var tmp: int64
        size = int32(sizeof(tmp))

        when useWinUnicode:
            let code = regGetValueW(this, nil, newWideCString(name), 0x0000ffff, nil,
                cast[pointer](tmp.addr), size.addr)
        else:
            let code = regGetValueA(this, nil, newCString(name), 0x0000ffff, nil,
                cast[pointer](tmp.addr), size.addr)

        if code == ERROR_FILE_NOT_FOUND:
            return false
        if unlikely(code != ERROR_SUCCESS):
            raiseError(code)

        when T is SomeNumber:
            value = T(tmp)
            return true
        elif T is string:
            value = $tmp
            return true
        else:
            {.fatal: "The type " & T.name & " is not supported yet.".}
    of REG_BINARY:
        var tmp: float64
        size = int32(sizeof(tmp))

        when useWinUnicode:
            let code = regGetValueW(this, nil, newWideCString(name), 0x0000ffff, nil,
                cast[pointer](tmp.addr), size.addr)
        else:
            let code = regGetValueA(this, nil, newCString(name), 0x0000ffff, nil,
                cast[pointer](tmp.addr), size.addr)

        if code == ERROR_FILE_NOT_FOUND:
            return false
        if unlikely(code != ERROR_SUCCESS):
            raiseError(code)

        when T is SomeNumber:
            value = T(tmp)
            return true
        elif T is string:
            value = $tmp
            return true
        else:
            {.fatal: "The type " & T.name & " is not supported yet.".}
    of REG_SZ, REG_EXPAND_SZ:
        when useWinUnicode:
            var buffer: WideCString
            unsafeNew(buffer, size + sizeof(Utf16Char))
            buffer[size div sizeof(Utf16Char) - 1] = Utf16Char(0)

            let code = regGetValueW(this, nil, newWideCString(name), 0x0000ffff, nil,
                cast[pointer](buffer), size.addr)
        else:
            var buffer: CString
            unsafeNew(buffer, size + 1)
            buffer[size - 1] = 0

            let code = regGetValueA(this, nil, newCString(name), 0x0000ffff, nil,
                cast[pointer](buffer), size.addr)

        if code == ERROR_FILE_NOT_FOUND:
            return false
        if unlikely(code != ERROR_SUCCESS):
            raiseError(code)

        when T is SomeOrdinal:
            value = parseInt($buffer)
            return true
        elif T is SomeNumber:
            value = parseFloat($buffer)
            return true
        elif T is string:
            value = $buffer
            return true
        else:
            {.fatal: "The type " & T.name & " is not supported yet.".}
    else:
        raise newException(RegistryError, "The registry value is of type " & $kind & ", which is not supported")

proc getValue*[T](this: RegistryKey, name: string, default: T): T
    {.raises: [RegistryError, ValueError, Defect].} =
    ## Retrieves the value associated with the specified name. If the name is not found, returns
    ## the default value that you provide. Attempts to convert values to the correct type, if applicable.

    if not this.tryGetValue(name, result):
        result = default

proc getValueKind*(this: RegistryKey, name: string): RegistryValueType {.raises: [RegistryError].} =
    ## Retrieves the registry data type of the value associated with the specified name.

    when useWinUnicode:
        let code = regQueryValueExW(this, newWideCString(name), nil, result.addr, nil, nil)
    else:
        let code = regQueryValueExA(this, newCString(name), nil, result.addr, nil, nil)

    if unlikely(code != ERROR_SUCCESS):
        raiseError(code)

iterator getValueNames*(this: RegistryKey): string {.raises: [RegistryError].} =
    ## Retrieves an iterator of strings that runs over all the value names associated with this key.

    var valCount: int32
    when useWinUnicode:
        let code = regQueryInfoKeyW(this, nil, nil, nil, nil, nil, nil, valCount.addr, nil, nil, nil, nil)
        if unlikely(code != ERROR_SUCCESS):
            raiseError(code)

        var nameBuffer: WideCString
        unsafeNew(nameBuffer, (MAX_VALUE_LEN + 1) * sizeof(Utf16Char))

        for i in 0..<valCount:
            var nameLen: int32 = MAX_VALUE_LEN
            let code = regEnumValueW(this, int32(i), nameBuffer, nameLen.addr, nil, nil, nil, nil)
            if unlikely(code != ERROR_SUCCESS):
                raiseError(code)

            nameBuffer[nameLen] = Utf16Char(0)
            yield $nameBuffer
    else:
        let code = regQueryInfoKeyA(this, nil, nil, nil, nil, nil, nil, valCount.addr, nil, nil, nil, nil)
        if unlikely(code != ERROR_SUCCESS):
            raiseError(code)

        var nameBuffer: CString
        unsafeNew(nameBuffer, MAX_VALUE_LEN + 1)

        for i in 0..<valCount:
            var nameLen: int32 = MAX_VALUE_LEN
            let code = regEnumValueA(this, int32(i), nameBuffer, nameLen.addr, nil, nil, nil, nil)
            if unlikely(code != ERROR_SUCCESS):
                raiseError(code)

            nameBuffer[nameLen] = 0
            yield $nameBuffer

proc openSubKey*(this: RegistryKey, name: string, writable: bool): RegistryKey {.raises: [RegistryError].} =
    ## Retrieves a specified subkey, and specifies whether write access is to be applied to the key.

    when useWinUnicode:
        let code = regOpenKeyExW(this, newWideCString(name), 0, if writable: KEY_ALL_ACCESS else: KEY_READ, result.addr)
    else:
        let code = regOpenKeyExA(this, newCString(name), 0, if writable: KEY_ALL_ACCESS else: KEY_READ, result.addr)

    if unlikely(code != ERROR_SUCCESS):
        raiseError(code)

proc openSubKey*(this: RegistryKey, name: string): RegistryKey {.raises: [RegistryError].} =
    ## Retrieves a subkey as read-only.
    return this.openSubKey(name, false)

proc setValue[T](this: RegistryKey, name: string, value: T, valueKind: RegistryValueType) {.raises: [RegistryError].} =
    ## Sets the specified name/value pair in the registry key, using the specified registry data type.

    when T is string:
        when useWinUnicode:
            let wstr = newWideCString(value)
            let code = regSetKeyValueW(this, nil, newWideCString(name), valueKind,
                cast[pointer](wstr), int32(wstr.len * sizeof(Utf16Char) + sizeof(Utf16Char)))
        else:
            let cstr = newCString(value)
            let code = regSetKeyValueExA(this, nil, newCString(name), valueKind,
                cast[pointer](cstr), int32(cstr.len + 1))
    elif T is SomeNumber:
        var val = value
        when useWinUnicode:
            let code = regSetKeyValueW(this, nil, newWideCString(name), valueKind, val.addr, int32(sizeof(value)))
        else:
            let code = regSetKeyValueA(this, nil, newCString(name), valueKind, val.addr, int32(sizeof(value)))
    else:
        {.fatal: "A value of type " & T.name & " cannot be written directly to the registry.".}

    if unlikely(code != ERROR_SUCCESS):
        raiseError(code)

proc setValue*[T](this: RegistryKey, name: string, value: T) {.raises: [RegistryError].} =
    ## Sets the specified name/value pair.

    # int and uint get a special handling on 64bit because of consistency,
    # else writing the value on 64bit and reading the value on 32bit might cause problems.
    when T is int and sizeof(int) == 8:

        {.warning: "Only a REG_DWORD is written when using int in 64bit mode.".}
        this.setValue(name, int32(value), REG_DWORD)
    when T is uint and sizeof(uint) == 8:
        {.warning: "Only a REG_DWORD is written when using uint in 64bit mode.".}
        this.setValue(name, uint32(value), REG_DWORD)
    when T is int64 or T is uint64:
        this.setValue(name, value, REG_QWORD)
    elif T is SomeOrdinal:
        this.setValue(name, value, REG_DWORD)
    elif T is SomeNumber:
        this.setValue(name, float64(value), REG_BINARY)
    elif T is string:
        this.setValue(name, value, REG_SZ)
    else:
        {.fatal: "A value of type " & T.name & " cannot be written directly to the registry.".}
