/**************************************************************************************************
 *
 * ADOBE SYSTEMS INCORPORATED
 * Copyright 2015 Adobe Systems Incorporated
 * All Rights Reserved.
 *
 * NOTICE:  Adobe permits you to use, modify, and distribute this file in accordance with the
 * terms of the Adobe license agreement accompanying it.  If you have received this file from a
 * source other than Adobe, then your use, modification, or distribution of it requires the prior
 * written permission of Adobe.
 *
 **************************************************************************************************/
/*jslint node: true, vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50, unparam: true, regexp: true */
/*global define, console */

module.exports.init = function () {
    'use strict';
    var exports = {};
    var child_process = require('child_process');
    var ffi = require('ffi-napi');
    var http = require('http');
    var https = require('https');
    var URL = require('url');
    var fs = require('fs');
    var utils = require('./utils');
    var responseCache = {};

    var nativeCallback = ffi.Function('void', []);
    var nativeParameterizedCallback = ffi.Function('void', ['CString']);
    var libProxy = ffi.Library(__dirname + '/../build/Release/ProxyResolverMac', {
        'ProxiesForURL': ['CString', ['CString']],
        'ProxiesForURLUsingScriptData': ['CString', ['CString', 'CString']],
        'ProxiesForURLUsingScriptURL': ['CString', ['CString', 'CString']],
        'StartListeningForProxySettingsChanges': ['bool', [nativeCallback]],
        'StopListeningForProxySettingsChanges': ['void', []],
        'FreeProxyReturnString': ['void', []],
        'SystemRootCA': ['CString', []],
        'FreeSystemRootCA': ['void', []],
        'GenerateNegotiateToken': ['void', ['CString', 'CString', 'CString', 'CString', nativeParameterizedCallback]],
        'LocalhostProxied': ['CString', []],
        'GetOSLocale': ['CString', []],
        'FreeOSLocaleReturnString': ['void', []]
    });

    libProxy.ProxiesForURL = utils.wrapWithPostCall(libProxy.ProxiesForURL, libProxy.FreeProxyReturnString);
    libProxy.ProxiesForURLUsingScriptData = utils.wrapWithPostCall(libProxy.ProxiesForURLUsingScriptData, libProxy.FreeProxyReturnString);
    libProxy.ProxiesForURLUsingScriptURL = utils.wrapWithPostCall(libProxy.ProxiesForURLUsingScriptURL, libProxy.FreeProxyReturnString);
    libProxy.StartListeningForProxySettingsChanges = utils.wrapNativeCallback(libProxy.StartListeningForProxySettingsChanges);
    libProxy.SystemRootCA = utils.wrapWithPostCall(libProxy.SystemRootCA, libProxy.FreeSystemRootCA);
    libProxy.GenerateNegotiateToken = utils.wrapNativeCallback(libProxy.GenerateNegotiateToken);
    libProxy.GetOSLocale = utils.wrapWithPostCall(libProxy.GetOSLocale, libProxy.FreeOSLocaleReturnString);

    var handleProxyViaURL;

    function parseProxy(proxy, type) {
        var p = utils.stripNonExistant({
            protocol: type,
            hostname: proxy.kCFProxyHostNameKey,
            port: proxy.kCFProxyPortNumberKey,
            username: proxy.kCFProxyUsernameKey,
            password: proxy.kCFProxyPasswordKey,
            config: proxy.kCFProxyAutoConfigurationURLKey,
            raw: proxy
        });
        if (p.username) {
            p.auth = p.username + ':' + p.password;
        }
        return p;
    }

    function parseProxyResponse(url, proxies, callback) {
        var data;
        try {
            data = JSON.parse(proxies);
        } catch (e) {
            return callback('Unable to parse native response: ' + proxies);
        }
        if (!Array.isArray(data)) {
            return callback('Native response not an array: ' + proxies);
        }
        if (data.length === 0) {
            return callback(null, [{
                protocol: 'none'
            }]);
        }

        var responseCollector = new utils.ResponseCollector(data.length, callback);
        data.forEach(function (proxy, i) {
            if (!proxy) {
                responseCollector.collect(i, 'No Proxy Response');
                return;
            }
            switch (proxy.kCFProxyTypeKey) {
            case 'kCFProxyTypeNone':
                responseCollector.collect(i, null, {
                    protocol: 'none',
                    raw: proxy
                });
                break;
            case 'kCFProxyTypeAutoConfigurationURL':
                //Get the URL
                var configURL = proxy.kCFProxyAutoConfigurationURLKey;
                //Handle Proxy via URL
                (function (i) {
                    handleProxyViaURL(url, configURL, function (errors, responses) {
                        responseCollector.collect(i, null, {
                            protocol: 'script',
                            errors: errors,
                            responses: responses,
                            raw: proxy
                        });
                    });
                }(i));
                break;
            case 'kCFProxyTypeHTTP':
                responseCollector.collect(i, null, parseProxy(proxy, 'http'));
                break;
            case 'kCFProxyTypeHTTPS':
                responseCollector.collect(i, null, parseProxy(proxy, 'https'));
                break;
            case 'kCFProxyTypeSOCKS':
                responseCollector.collect(i, null, parseProxy(proxy, 'socks'));
                break;
            case 'kCFProxyTypeFTP':
                responseCollector.collect(i, null, parseProxy(proxy, 'ftp'));
                break;
            case 'kCFProxyTypeAutoConfigurationJavaScript':
                (function (i) {
                    parseProxyResponse(url, libProxy.ProxiesForURLUsingScriptData(url, proxy.kCFProxyAutoConfigurationJavaScriptKey), function (errors, responses) {
                        responseCollector.collect(i, null, {
                            protocol: 'script',
                            errors: errors,
                            responses: responses,
                            raw: proxy
                        });
                    });
                }(i));
                break;
            default: //Can be kCFProxyAutoConfigurationHTTPResponseKey or something introduced new
                responseCollector.collect(i, null, parseProxy(proxy, 'unknown'));
                break;
            }
        });

    }

    function clearResponseCache() {
        responseCache = {};
    }

    handleProxyViaURL = function (url, scriptURL, callback) {
        if (responseCache[scriptURL]) {
            parseProxyResponse(url, libProxy.ProxiesForURLUsingScriptData(url, responseCache[scriptURL]), callback);
            return;
        }

        var scriptURLDetail = URL.parse(scriptURL);
        var method;
        if (scriptURLDetail.protocol === 'http:') {
            method = http;
        } else if (scriptURLDetail.protocol === 'https:') {
            method = https;
        } else {
            // Not an external URL to download
            if (scriptURLDetail.protocol === 'file:') {
                fs.readFile(scriptURLDetail.path, 'utf8', function (err, data) {
                    if (err) {
                        callback('Could not read pac file ' + scriptURL);
                    } else {
                        responseCache[scriptURL] = data;
                        setTimeout(clearResponseCache, exports.cacheInterval);
                        parseProxyResponse(url, libProxy.ProxiesForURLUsingScriptData(url, data), callback);
                    }
                });
            } else {
                utils.log(utils.logLevel.WARNING, 'Protocol not supported :' + scriptURLDetail.protocol + ', Full URL: ' + scriptURL);
                parseProxyResponse(url, libProxy.ProxiesForURLUsingScriptURL(url, scriptURL), callback);
            }
            return;
        }

        //Fetch the config
        var request = method.request({
            host: scriptURLDetail.hostname,
            port: scriptURLDetail.port,
            path: scriptURLDetail.path,
            method: 'GET'
        }, function (response) {
            var body = '';
            response.on('data', function (d) {
                body += d;
            });
            response.on('end', function () {
                var code = 4;
                try {
                    code = Math.floor(parseInt((response.statusCode), 10) / 100);
                } catch (e) {
                    utils.log(utils.logLevel.WARNING, 'Error fetching PAC file from url ' + scriptURL + ' with message ' + e);
                    parseProxyResponse(url, libProxy.ProxiesForURLUsingScriptData(url, body), callback);
                    return;
                }
                if (code !== 2) {
                    parseProxyResponse(url, libProxy.ProxiesForURLUsingScriptURL(url, scriptURL), callback);
                } else {
                    responseCache[scriptURL] = body;
                    setTimeout(clearResponseCache, exports.cacheInterval);
                    parseProxyResponse(url, libProxy.ProxiesForURLUsingScriptData(url, body), callback);
                }
            });
        });
        var handled = false;
        request.on('error', function () {
            if (handled) {
                return;
            }
            handled = true;
            parseProxyResponse(url, libProxy.ProxiesForURLUsingScriptURL(url, scriptURL), callback);
        });
        request.setTimeout(exports.socketTimeout, function () {
            if (handled) {
                return;
            }
            handled = true;
            utils.log(utils.logLevel.WARNING, 'Socket Timed out');
            request.abort();
            parseProxyResponse(url, libProxy.ProxiesForURLUsingScriptURL(url, scriptURL), callback);

        });
        request.end();
    };

    //Try to find the best way out. There might be errors. Just ignore them
    //We are returning the debug data anyways.
    function parseOptimistic(data, d) {

        if (!d) {
            d = [];
        }
        if (!data) {
            data = [];
        }
        if (!Array.isArray(d)) {
            utils.log(utils.logLevel.WARNING, 'Unexpected object to parse ' + d);
            d = [];
        }
        if (!Array.isArray(data)) {
            utils.log(utils.logLevel.WARNING, 'Unexpected object to parse ' + data);
            return [];
        }
        data.forEach(function (currentData) {
            if (!currentData) { return; }
            if (currentData.protocol === 'script') {
                d = parseOptimistic(currentData.responses, d);
            } else if (currentData.protocol === 'none') {
                d.push({});
            } else if (currentData.protocol !== 'unknown') {
                d.push(currentData);
            }
        });
        return d;
    }

    function isPacFilePresent() {
        var proxies = libProxy.ProxiesForURL("http://127.0.0.1");
        var data;
        try {
            data = JSON.parse(proxies);
        } catch (e) {
            return false;
        }
        if (!Array.isArray(data) || data.length < 1) {
            return false;
        }
        if (data[0].kCFProxyTypeKey === 'kCFProxyTypeAutoConfigurationURL' || data[0].kCFProxyTypeKey  === 'kCFProxyTypeAutoConfigurationJavaScript') {
            return true;
        }
        return false;
    }

    exports.getLocalhostPacProxyStatus = function (callback) {
        if (!isPacFilePresent()) { callback('noPacFile'); return; }
        exports.resolve('http://127.0.0.1', function (err, data) {
            if (err) {
                return callback('pacFileNoProxy');
            }
            if (!Array.isArray(data) || data.length < 1 || !data[0].hostname) {
                return callback('pacFileNoProxy');
            }
            return callback('proxied');
        });
    };


    exports.resolve = function (url, callback) {

        parseProxyResponse(url, libProxy.ProxiesForURL(url), function (errors, data) {
            if (!Array.isArray(data)) {
                callback(errors);
                return;
            }
            callback(null, parseOptimistic(data), data);
        });
    };

    exports.getRootCAUnsafe = function () {
        var nativeCerts = libProxy.SystemRootCA();
        var returnArray = [];
        try {
            nativeCerts = JSON.parse(nativeCerts);
            if (Array.isArray(nativeCerts)) {
                returnArray = nativeCerts;
            } else {
                utils.log(utils.logLevel.ERROR, 'Unknown response from native call Ca cert call ' + nativeCerts);
            }

        } catch (e) {
            utils.log(utils.logLevel.ERROR, 'Error parsing native certificate response:' + e + '\nResponse:\n' + nativeCerts);
        }
        return returnArray;
    };

    exports.getNegotiateToken = function (spn, user, password, domain, callback) {
        libProxy.GenerateNegotiateToken(spn, user, password, domain, function (data) {
            var d;
            try {
                d = JSON.parse(data);
            } catch (e) {
                callback('Error parsing native data for negotiate token: ' + data);
                return;
            }

            callback(d.error, d.data, d.more);
        });
    };

    exports.isLocalhostProxied = libProxy.LocalhostProxied;


    exports.getRootCA = function (callback) {
        // To be safe, we fetch the certificates in a separate process
        // execArgv to allow debugging parent process in VSCode
        // https://stackoverflow.com/questions/50885128
        var child = child_process.fork(__dirname + '/osxCerts.js', { execArgv: []});

        var killChild = function () {
            // Need to force-kill because the bug is that it gets in an infinite loop in native Mac code.
            child.kill('SIGKILL');
        };

        // Make sure the child process gets killed when we exit
        process.on('exit', killChild);

        var timeout;
        var onFinished = function (certs) {
            clearTimeout(timeout);
            killChild();
            process.removeListener('exit', killChild);

            if (certs) {
                callback(null, certs);
            } else {
                utils.log(utils.logLevel.ERROR, 'Timed out getting response from MacOS');
                callback('timeout');
            }
        };

        // Set a timeout of 10s - if it takes longer, we'll kill the process
        timeout = setTimeout(onFinished, 10000);

        // Listen to events from the child process - it will send us the certificates when it has them
        // (and also any messages it wants to log)
        child.on('message', function (message) {
            if (message.certs) {
                utils.log(utils.logLevel.INFO, 'Got ' + message.certs.length + ' certificates');
                onFinished(message.certs);

            } else if (message.log && message.log.level && message.log.message) {
                utils.log(message.log.level, message.log.message);
            }
        });
    };

    exports.getOSLocale = libProxy.GetOSLocale;
    exports.setSettingsChangedCallback = libProxy.StartListeningForProxySettingsChanges;
    exports.unsetSettingsChangedCallback = libProxy.StopListeningForProxySettingsChanges;

    exports.socketTimeout = 10 * 1000;
    exports.cacheInterval = 60 * 1000; //Most repeated requests should complete within a minute.

    return exports;

};
