// https://github.com/ethereum/cpp-ethereum/wiki/PoC-6-JS-API

if (typeof(window.eth) === "undefined")
{
if (typeof(require) !== "undefined")
	require( ['ethString'], function() {} )
else if (typeof(String.prototype.pad) === "undefined")
{
	var scriptTag = document.getElementsByTagName('script');
	scriptTag = scriptTag[scriptTag.length - 1]; 
	var scriptPath = scriptTag.src; 
	var path = scriptPath.substr(0, scriptPath.lastIndexOf( '/' ));
	var start = '<script src="' + path + '/';
	var slash = '"><'+'/script>';
	document.write(start + 'BigInteger.js' + slash);
	document.write(start + 'ethString.js' + slash);
}

var spec = [
            { "method": "coinbase", "params": null, "order": [], "returns" : "" },
            { "method": "setCoinbase", "params": { "address": "" }, "order": ["address"], "returns" : true },
            { "method": "listening", "params": null, "order": [], "returns" : false },
            { "method": "setListening", "params": { "listening": false }, "order" : ["listening"], "returns" : true },
            { "method": "mining", "params": null, "order": [], "returns" : false },
            { "method": "setMining", "params": { "mining": false }, "order" : ["mining"], "returns" : true },
            { "method": "gasPrice", "params": null, "order": [], "returns" : "" },
            { "method": "key", "params": null, "order": [], "returns" : "" },
            { "method": "keys", "params": null, "order": [], "returns" : [] },
            { "method": "peerCount", "params": null, "order": [], "returns" : 0 },
            { "method": "defaultBlock", "params": null, "order": [], "returns" : 0},
            { "method": "number", "params": null, "order": [], "returns" : 0},

            { "method": "balanceAt", "params": { "address": "", "block": 0}, "order": ["address", "block"], "returns" : ""},
            { "method": "stateAt", "params": { "address": "", "storage": "", "block": 0}, "order": ["address", "storage", "block"], "returns": ""},
            { "method": "countAt", "params": { "address": "", "block": 0}, "order": ["address", "block"], "returns" : 0.0},
            { "method": "codeAt", "params": { "address": "", "block": 0}, "order": ["address", "block"], "returns": ""},

            { "method": "transact", "params": { "json": {}}, "order": ["json"], "returns": ""},
            { "method": "call", "params": { "json": {}}, "order": ["json"], "returns": ""},

            { "method": "block", "params": { "params": {}}, "order": ["params"], "returns": {}},
            { "method": "transaction", "params": { "params": {}, "i": 0}, "order": ["params", "i"], "returns": {}},
            { "method": "uncle", "params": { "params": {}, "i": 0}, "order": ["params", "i"], "returns": {}},

            { "method": "messages", "params": { "params": {}}, "order": ["params"], "returns": []},
            { "method": "watch", "params": { "params": ""}, "order": ["params"], "returns": 0},
            { "method": "check", "params": { "id": 0}, "order": [], "returns": true},
            { "method": "killWatch", "params": { "id": 0}, "order": ["params"], "returns": true},

            { "method": "secretToAddress", "params": { "s": ""}, "order": ["s"], "returns": ""},
            { "method": "lll", "params": { "s": ""}, "order": ["s"], "returns": ""},
            { "method": "sha3", "params": { "s": ""}, "order": ["s"], "returns": ""},
            { "method": "toAscii", "params": { "s": ""}, "order": ["s"], "returns": ""},
            { "method": "fromAscii", "params": { "s": "", "padding": 0}, "order": ["s", "padding"], "returns": ""}, 
            { "method": "toDecimal", "params": {"s": ""}, "order": ["s"], "returns" : ""},
            { "method": "toFixed", "params": {"s": 0.0}, "order": ["s"], "returns" : ""},
            { "method": "fromFixed", "params": {"s": ""}, "order": ["s"], "returns" : 0.0},
            { "method": "offset", "params": {"s": "", "o": 0}, "order": ["s", "o"], "returns" : ""}
];

window.eth = (function ethScope() {
	var m_reqId = 0
	var ret = {}
    function reformat(m, d) { return m == "lll" ? d.bin() : d; };
	function reqSync(m, p) {
		var req = { "jsonrpc": "2.0", "method": m, "params": p, "id": m_reqId }
		m_reqId++
		var request = new XMLHttpRequest();	
        request.open("POST", "http://localhost:8080", false)
        request.send(JSON.stringify(req))
        return reformat(m, JSON.parse(request.responseText).result)
	};
	function reqAsync(m, p, f) {
		var req = { "jsonrpc": "2.0", "method": m, "params": p, "id": m_reqId }
		m_reqId++
		var request = new XMLHttpRequest();	
        request.open("POST", "http://localhost:8080", true)
        request.send(JSON.stringify(req))
		request.onreadystatechange = function() {
			if (request.readyState === 4)
                if (f)
                    f(reformat(m, JSON.parse(request.responseText).result));
	    };
	};
    
    var getParams = function (spec, name, args) {
        var setup = spec.filter(function (s) {
            return s.method === name;
        });

        if (setup.length === 0) {
            return {};
        }

        var paramSetup = setup[0];

        var p = paramSetup.params ? {} : null;
        for (j in paramSetup.order)
            p[paramSetup.order[j]] = args[j];
        return p;
    };

    var addPrefix = function (s, prefix) {
        if (!s) {
            return s;
        }
        return prefix + s.slice(0, 1).toUpperCase() + s.slice(1);
    };

    var toGetter = function (s) {
        return addPrefix(s, "get");
    };

    var toSetter = function (s) {
        return addPrefix(s, "set");
    };

    var defaults = function (def, obj) {
        if (!def) {
            return obj;
        }
        var rewriteProperties = function (dst, p) {
            Object.keys(p).forEach(function (key) {
                if (p[key] !== undefined) {
                    dst[key] = p[key];
                }
            });
        };
        var res = {};
        rewriteProperties(res, def);
        rewriteProperties(res, obj);
        return res;
    };

    var setupProperties = function (root, spec) {
        var properties = [
        { name: "coinbase", getter: "coinbase", setter: "setCoinbase" },
        { name: "listening", getter: "listening", setter: "setListening" },
        { name: "mining", getter: "mining", setter: "setMining" },
        { name: "gasPrice", getter: "gasPrice"},
        { name: "key", getter: "key" },
        { name: "keys", getter: "keys" },
        { name: "peerCount", getter: "peerCount" },
        { name: "defaultBlock", getter: "defaultBlock" },
        { name: "number", getter: "number" }];
        
        properties.forEach(function (property) {
            var p = {};
            if (property.getter) {
                p.get = function () {
                    return reqSync(property.getter, {});
                };
                root[toGetter(property.name)] = function (f) {
                    return reqAsync(property.getter, null, f);
                };
            }
            if (property.setter) {
                p.set = function (newVal) {
                    return reqSync(property.setter, getParams(spec, property.setter, arguments));
                };
                root[toSetter(property.name)] = function (newVal, f) {
                    return reqAsync(property.setter, getParams(spec, property.setter, arguments), f);
                };
            }

            Object.defineProperty(root, property.name, p);
        });
    };

    var setupMethods = function (root, spec) {
        var methods = [
        { name: "balanceAt", async: "getBalanceAt", default: {block: 0} },
        { name: "stateAt", async: "getStateAt", default: {block: 0} },
        { name: "countAt", async: "getCountAt", default: {block: 0} },
        { name: "codeAt", async: "getCodeAt", default: {block: 0} },
        { name: "transact", async: "doTransact"},
        { name: "call", async: "doCall" },
        { name: "messages", async: "getMessages" },
        { name: "block", async: "getBlock" },
        { name: "transaction", async: "getTransaction" },
        { name: "uncle", async: "getUncle" },
        { name: "secretToAddress" },
        { name: "lll" },
        { name: "sha3" },
        { name: "toAscii" },
        { name: "fromAscii" },
        { name: "toDecimal" },
        { name: "toFixed" },
        { name: "fromFixed" },
        { name: "offset" }
        ];

        methods.forEach(function (method) {
            root[method.name] = function () {
                return reqSync(method.name, defaults(method.default, getParams(spec, method.name, arguments)));
            };
            if (method.async) {
                root[method.async] = function () {
                    return reqAsync(method.name, defaults(method.default, getParams(spec, method.name, arguments)), arguments[arguments.length - 1]);
                };
            };
        });
    };

    var setupWatch = function (root) {
        root.watch = function (val) {
            if (typeof val !== 'string') {
                val = JSON.stringify(val);
            }
    
            var id;
            reqAsync('watch', {params: val}, function (result) {
                id = result;
            }); // async send watch
            var callbacks = [];
            var exist = true;
            var w = {
                changed: function (f) {
                    callbacks.push(f);
                },
                uninstall: function (f) {
                    reqAsync('killWatch', {id: id}); 
                    exist = false;
                },
                messages: function () {
                    // TODO! 
                },
                getMessages: function (f) {
                    // TODO!
                }
            };

            var check = function () {
                if (!exist) {
                    return;
                }
                if (callbacks.length) {
                    reqAsync('check', {id: id}, function (res) { 
                        if (!res) {
                            return;
                        }
                        callbacks.forEach(function (f) {
                            f();
                        });
                    });
                }
                window.setTimeout(check, 12000);
            };

            check();
            return w;
        }
    };

    setupProperties(ret, window.spec);
    setupMethods(ret, window.spec);
    setupWatch(ret);

	return ret;
}());

}