/*!
Windows Registry Settings Handler

Copyright 2012 Raising the Floor - International
Copyright 2012 Antranig Basman
Copyright 2012 Astea Solutions AD

Licensed under the New BSD license. You may not use this file except in
compliance with this License.

You may obtain a copy of the License at
https://github.com/gpii/universal/LICENSE.txt
*/

"use strict"; 

var ffi = require("node-ffi");
var fluid = require("universal");
var $ = fluid.jQueryStandalone;

var gpii = fluid.registerNamespace("gpii");
var windows = fluid.registerNamespace("gpii.windows");

var NULL = new ffi.Pointer(0);

var user32 = new ffi.Library('user32', {
   'MessageBoxW': [
      'int32', [ 'int32', "pointer", 'pointer', 'int32' ]
   ]
});

// Guide to node_ffi types and conversions:
// https://github.com/rbranson/node-ffi/wiki/Node-FFI-Tutorial

var advapi32 = new ffi.Library("advapi32", {
    // http://msdn.microsoft.com/en-us/library/windows/desktop/ms724897(v=vs.85).aspx
    // HKEY, LPCWSTR, DWORD, REGSAM, PHKEY
    // Note abuse of type of argument 1 - node_ffi doesn't allow us to forge pointers from integers directly,
    // as required by base keys 
    RegOpenKeyExW: [
         "int32", ["uint32", "pointer", "uint32", "uint32", "pointer"]
    ],
    // http://msdn.microsoft.com/en-us/library/windows/desktop/ms724837(v=vs.85).aspx
    // HKEY
    RegCloseKey: [
         "long", ["pointer"]
    ],
    // http://msdn.microsoft.com/en-us/library/windows/desktop/ms724911(v=vs.85).aspx
    // HKEY, LPCWSTR, LPDWORD, LPDWORD, LPBYTE, LPDWORD
    RegQueryValueExW: [
        "int32", ["pointer", "pointer", "pointer", "pointer", "pointer", "pointer"]
    ],
    // http://msdn.microsoft.com/en-us/library/windows/desktop/ms724844(v=vs.85).aspx
    // HKEY, LPCWSTR, DWORD, LPWSTR, DWORD, REGSAM, LPSECURITY_ATTRIBUTES, PHKEY, LPDWORD
    // Similar abuse to RegOpenKeyExW
    RegCreateKeyExW: [
        "long", ["uint32", "pointer", "uint32", "pointer", "uint32", "uint32", "pointer", "pointer", "pointer"]
    ],
    // http://msdn.microsoft.com/en-us/library/windows/desktop/ms724923(v=vs.85).aspx
    // HKEY, LPCWSTR, DWORD, DWORD, const BYTE*, DWORD
    RegSetValueExW: [
        "long", ["pointer", "pointer", "uint32", "uint32", "pointer", "uint32"]
    ],
    // http://msdn.microsoft.com/en-us/library/windows/desktop/ms724845(v=vs.85).aspx
    // HKEY, LPCWSTR
    // Similar abuse to RegOpenKeyExW - for convenience, only accepts base keys
    RegDeleteKeyW: [
        "long", ["uint32", "pointer"]
    ]
    // http://msdn.microsoft.com/en-us/library/windows/desktop/ms724847(v=vs.85).aspx
    // This API is disused since it was introduced only on Windows Server 2003 (XP support required)
    // HKEY, LPCWSTR, REGSAM, DWORD
    //RegDeleteKeyExW: [
    //    "long", ["uint32", "pointer", "uint32", "uint32"]
    //]
});

var kernel32 = new ffi.Library("kernel32", {
     // http://msdn.microsoft.com/en-us/library/windows/desktop/dd319072(v=vs.85).aspx
     // UINT, DWORD, LPCSTR, INT, LPWSTR, INT
    "MultiByteToWideChar": [
        "int", ["uint", "uint32", "string", "int", "pointer", "int"]
     ],
     // http://msdn.microsoft.com/en-us/library/windows/desktop/dd374130(v=vs.85).aspx
     // UINT, DWORD, LPCWSTR, int, LPSTR, int, LPCSTR, LPBOOL
     "WideCharToMultiByte": [
        "int", ["uint", "uint32", "pointer", "int", "pointer", "int", "pointer", "pointer"]
     ]
});

// Windows API constants delved from the unfathomable deeps of windows.h
windows.API_constants = {
    HKEY_CLASSES_ROOT:   0x80000000,
    HKEY_CURRENT_USER:   0x80000001,
    HKEY_LOCAL_MACHINE:  0x80000002,
    HKEY_USERS:          0x80000003,
    HKEY_CURRENT_CONFIG: 0x80000005,
    CP_UTF8: 65001,
    KEY_QUERY_VALUE: 1,
    KEY_SET_VALUE: 2,
    returnCodes: {
        0: "ERROR_SUCCESS",
        1: "ERROR_INVALID_FUNCTION",
        2: "FILE_NOT_FOUND",
        3: "PATH_NOT_FOUND",
        6: "ERROR_INVALID_HANDLE"
    }
};

windows.registryTypes = {};

windows.registryTypes.REG_SZ = {
    code: 1,
    readConvert: function (pointer) {
        return windows.fromWideChar(pointer);
    },
    writeConvert: function (value) {
        return windows.toWideChar(value);
    }
};

windows.registryTypes.REG_DWORD = {
    code: 4, 
    readConvert: function (pointer) {
        return pointer.getUInt32();
    },
    writeConvert: function (value) {
        var pointer = new ffi.Pointer(4);
        pointer.putUInt32(value);
        return {
            pointer: pointer,
            length: 4
        };
    }
};

windows.registryTypes.REG_BINARY = {
    code: 3,
    readConvert: function (pointer, count) {
        return gpii.pointerToHex(pointer, count);
    },
    writeConvert: function (value) {
        return gpii.hexToPointer(value);
    }
};

gpii.pointerToHex = function (pointer, count) {
    var togo = "";
    for (var i = 0; i < count; ++ i) {
        var nbyte = pointer.getUInt8(true);
        togo += (nbyte < 16? "0" : "") + nbyte.toString(16);
        if (i !== count - 1) {
            togo += " ";
        }
    }
    return togo;
};

// Slow but short method to parse very forgiving hex streams - any non-hex characters will be ignored. Digits
// forming a single byte must be adjacent
gpii.hexToPointer = function (hexString) {
    var buffer = [];
    for (var i = 0; i < hexString.length; ++ i) {
        var c = hexString.charAt(i);
        var digit = parseInt(c, 16);
        if (!isNaN(digit)) {
            buffer.push(digit);
        }
    }
    if ((buffer.length % 2) === 1) {
        fluid.fail("Odd number of hex digits in buffer " + hexString);
    }
    var togo = new ffi.Pointer(buffer.length / 2);
    var writer = togo.seek(0);
    for (var i = 0; i < buffer.length; i += 2) {
        writer.putUInt8(buffer[i]*16 + buffer[i + 1], true);
    }
    return {
        pointer: togo,
        length: buffer.length / 2
    };
};

var c = windows.API_constants; 

windows.convertReturnCode = function (code) {
    return "Return code " + code + ": " + c.returnCodes[code];
};

windows.checkReturnCode = function (code) {
    if (code !== 0) {
        fluid.fail(windows.convertReturnCode(code));
    }
};

windows.getBaseKey = function (baseKey) {
    var key = c[baseKey];
    if (key === undefined) {
        fluid.fail("Unknown registry base key " + baseKey);
    }
    return key;
};

var cr = windows.checkReturnCode;

windows.toWideChar = function (string) {
    var chars = kernel32.MultiByteToWideChar(c.CP_UTF8, 0, string, -1, NULL, 0);
    
    var buffer = new ffi.Pointer(chars * 2);
    var chars2 = kernel32.MultiByteToWideChar(c.CP_UTF8, 0, string, -1, buffer, chars);
    if (chars2 != chars) {
        fluid.fail("Expected to write " + chars + " characters, only wrote " + chars2); 
    }

    return {
        pointer: buffer,
        length: chars * 2
    };
};

windows.fromWideChar = function (buffer) {
    var bytes = kernel32.WideCharToMultiByte(c.CP_UTF8, 0, buffer, -1, NULL, 0, NULL, NULL);
    var convertBuffer = new ffi.Pointer(bytes);
    kernel32.WideCharToMultiByte(c.CP_UTF8, 0, buffer, -1, convertBuffer, bytes, NULL, NULL);
    return convertBuffer.getCString();
};

windows.readRegistryKey = function (baseKey, path, subKey, type) {
    var converter = windows.registryTypes[type];
    if (!converter) {
        fluid.fail("Unsupported registry type " + type);
    }
    var togo = {
        statusCode: 200
    };
  
    var pathW = windows.toWideChar(path).pointer;
    var keyHolder = new ffi.Pointer(4);
    var base = windows.getBaseKey(baseKey);
    var code = advapi32.RegOpenKeyExW(base, pathW, 0, c.KEY_QUERY_VALUE, keyHolder);
    if (code === 2) { // FILE_NOT_FOUND
        togo.statusCode = 404;
        return togo;
    }
    cr(code);
    try {
        var subKeyW = windows.toWideChar(subKey).pointer;
        var dataLength = new ffi.Pointer(4);
        var code2 = advapi32.RegQueryValueExW(keyHolder.getPointer(), subKeyW, NULL, NULL, NULL, dataLength);
        if (code2 === 2) { // FILE_NOT_FOUND
            togo.statusCode = 404;
            return togo;
        }
        cr(code2);
        togo.bytes = dataLength.getInt32();
        
        var valueHolder = new ffi.Pointer(togo.bytes);
        var code3 = advapi32.RegQueryValueExW(keyHolder.getPointer(), subKeyW, NULL, NULL, valueHolder, dataLength);
        cr(code3);
        
        togo.value = converter.readConvert(valueHolder, togo.bytes);
    }
    finally {
        var code4 = advapi32.RegCloseKey(keyHolder.getPointer());
        cr(code4);
    }

    return togo;
};

windows.writeRegistryKey = function (baseKey, path, subKey, value, type) {
    var converter = windows.registryTypes[type];
    if (!converter) {
        fluid.fail("Unsupported registry type " + type);
    }
    var togo = {
        statusCode: 200
    };
    var pathW = windows.toWideChar(path).pointer;
    var keyHolder = new ffi.Pointer(4);
    var base = windows.getBaseKey(baseKey);
    // "int32", ["pointer", "pointer", "uint32", "pointer", "uint32", "uint32", "pointer", "pointer", "pointer"]
    var code = advapi32.RegCreateKeyExW(base, pathW, 0, NULL, 0, c.KEY_SET_VALUE, NULL, keyHolder, NULL);
    cr(code);
    try {
        var subKeyW = windows.toWideChar(subKey).pointer;
        var converted = converter.writeConvert(value);
        var code2 = advapi32.RegSetValueExW(keyHolder.getPointer(), subKeyW, 0, converter.code, converted.pointer, converted.length);
        // TODO: consider plausible errors which might result from this process
        cr(code2);
    }
    finally {
        advapi32.RegCloseKey(keyHolder.getPointer());
    }
    return togo;
};

// Note that this function cannot delete keys recursively. RegDeleteKeyExW is not available
// as a result of requirement for Windows XP level API support. This functionality can be
// implemented manually on top of this existing API if required.
windows.deleteRegistryKey = function (baseKey, path) {
    var pathW = windows.toWideChar(path).pointer;
    var base = windows.getBaseKey(baseKey);
    var code = advapi32.RegDeleteKeyW(base, pathW);
    cr(code);
};

windows.registrySettingsHandlerOne = function (payload) {
    var baseKey = payload.options.hKey;
    var path = payload.options.path;
    var results = fluid.transform(payload.settings, function(value, key) {
        var oldValue = windows.readRegistryKey(baseKey, path, key, value.dataType);
        var writeStatus = windows.writeRegistryKey(baseKey, path, key, value.value, value.dataType);
        var newValue = windows.readRegistryKey(baseKey, path, key, value.dataType);
        return {
            oldValue: oldValue,
            statusCode: writeStatus.statusCode,
            newValue: newValue
        };
    });
    console.log("Settings handler returning results ", results);
    return results;
};

gpii.invokeSettingsHandler = function (payload, handler) {
    // TODO: use model transformation
    return fluid.transform(payload, function (oneSolution) {
        return fluid.transform(oneSolution, function (element) {
              // TODO: more efficient algorithm
            var directLoad = fluid.filterKeys(fluid.copy(element), ["settings", "options"]);
            var others = fluid.censorKeys(fluid.copy(element), ["settings", "options"]);
            var results = {
                settings: handler(directLoad)
            };
            return $.extend(results, others);
        });
    });
};

windows.registrySettingsHandler = function (payload) {
    return gpii.invokeSettingsHandler(payload, windows.registrySettingsHandlerOne);
};

function expect(expected, actual) {
    if (actual !== expected) {
        console.log("FAIL: retrieved value " + actual + " where " + expected + " was required");
    }
}

function testSelf() {
    // A self-test driver that reads and writes some values from the live registry
    // Don't worry, this is perfectly safe!

    var value = windows.readRegistryKey("HKEY_LOCAL_MACHINE", "HARDWARE\\DEVICEMAP\\SERIALCOMM", "\\Device\\Serial0", "REG_SZ"); 
    console.log("Retrieved value " + value.value);
    
    var value2 = windows.readRegistryKey("HKEY_LOCAL_MACHINE", "HARDWARE\\DEVICEMAP\\VIDEO", "MaxObjectNumber", "REG_DWORD"); 
    console.log("Retrieved value " + value2.value);
    
    var value3 = windows.readRegistryKey("HKEY_LOCAL_MACHINE", "HARDWARE\\DEVICEMAP\\VIDEO", "ObjectNumberList", "REG_BINARY"); 
    console.log("Retrieved value " + value3.value);
    
    var dwordValue = 100;
    windows.writeRegistryKey("HKEY_CURRENT_USER", "Software\\GPIIMagnifier", "Magnification", dwordValue, "REG_DWORD");
    var value4 = windows.readRegistryKey("HKEY_CURRENT_USER", "Software\\GPIIMagnifier", "Magnification", "REG_DWORD");
    console.log("Retrieved value " + value4.value);
    expect(dwordValue, value4.value);
    
    var unicodeString = "I am Agent Γ";
    windows.writeRegistryKey("HKEY_CURRENT_USER", "Software\\GPIIMagnifier", "MagnificationAnnotation", unicodeString, "REG_SZ");
    var value5 = windows.readRegistryKey("HKEY_CURRENT_USER", "Software\\GPIIMagnifier", "MagnificationAnnotation", "REG_SZ");
    console.log("Retrieved value " + value5.value); // note that this may well display in the console incorrectly
    expect(unicodeString, value5.value);
    
    windows.writeRegistryKey("HKEY_CURRENT_USER", "Software\\GPIIMagnifier", "MagnificationBinary", "0a, b0, cAfebAbe", "REG_BINARY");
    var value6 = windows.readRegistryKey("HKEY_CURRENT_USER", "Software\\GPIIMagnifier", "MagnificationBinary", "REG_BINARY");
    console.log("Retrieved value " + value6.value);
    expect("0a b0 ca fe ba be", value6.value);
    
    // clean up after ourselves
    windows.deleteRegistryKey("HKEY_CURRENT_USER", "Software\\GPIIMagnifier");
}

if (process.argv[1].indexOf("egistrySettingsHandler") !== -1) {
    if (process.argv.length === 2) {
        testSelf();
    }
    else {
        var filename = __dirname + "/" + process.argv[2];
        // Note that this is now possible!
        // http://blog.gvm-it.eu/post/8175813806/node-js-0-5-2-load-json-files-with-require
        var testFile = require(filename);
        console.log("Executing payload: ", testFile);
        var results = windows.registrySettingsHandlerOne(testFile);
        console.log("Returned results: ", results);
        windows.deleteRegistryKey(testFile.options.hKey, testFile.options.path);
    }
}
    
//var OK_or_Cancel = user32.MessageBoxW(
//   0, windows.toWideChar(value), windows.toWideChar(path + "|" + subKey), 1
//);