var cp         = require('child_process'),
    _          = require('lodash'),
    fs         = require('fs'),
    url        = require('url'),
    net        = require('net'),
    when       = require('when'),
    path       = require('path'),
    config     = require('../../server/config');
    
function findFreePort(port) {
    var deferred = when.defer();

    if (typeof port === 'string') port = parseInt(port);
    if (typeof port !== 'number') port = 2368;
    port = port + 1;

    var server = net.createServer();
    server.on('error', function(e) {
        if (e.code === 'EADDRINUSE') {
            when.chain(findFreePort(port), deferred);
        } else {
            deferred.reject(e);
        }
    });
    server.listen(port, function() {
        var listenPort = server.address().port;
        server.close(function() {
            deferred.resolve(listenPort);
        });
    });
    
    return deferred.promise;
}

// Get a copy of current config object from file, to be modified before
// passing to forkGhost() method
function forkConfig() {
    // require caches values, and we want to read it fresh from the file
    delete require.cache[config.paths.config];
    return _.cloneDeep(require(config.paths.config)[process.env.NODE_ENV]);
}

// Creates a new fork of Ghost process with a given config
// Useful for tests that want to verify certain config options
function forkGhost(newConfig, envName) {
    var deferred = when.defer();
    envName = envName || 'forked';
    findFreePort(newConfig.server ? newConfig.server.port : undefined)
        .then(function(port) {
            newConfig.server = newConfig.server || {};
            newConfig.server.port = port;
            newConfig.url = url.format(_.extend(url.parse(newConfig.url), {port: port, host: null}));
            
            var newConfigFile = path.join(config.paths.appRoot, 'config.test' + port + '.js');
            fs.writeFile(newConfigFile, 'module.exports = {' + envName + ': ' + JSON.stringify(newConfig) + '}', function(err) {
                if (err) throw err;
                
                // setup process environment for the forked Ghost to use the new config file
                var env = _.clone(process.env);
                env['GHOST_CONFIG'] = newConfigFile;
                env['NODE_ENV'] = envName;
                var child = cp.fork(path.join(config.paths.appRoot, 'index.js'), {env: env});
                
                var pingTries = 0;
                var pingCheck;
                var pingStop = function() {
                    if (pingCheck) {
                        clearInterval(pingCheck);
                        pingCheck = undefined;
                        return true;
                    }
                    return false;
                };
                // periodic check until forked Ghost is running and is listening on the port
                pingCheck = setInterval(function() {
                    var socket = net.connect(port);
                    socket.on('connect', function() {
                        socket.end();
                        if (pingStop()) {
                            deferred.resolve(child);
                        }
                    });
                    socket.on('error', function(err) {
                        // continue checking
                        if (++pingTries >= 20 && pingStop()) {
                            deferred.reject(new Error("Timed out waiting for child process"));
                        }
                    });
                }, 200);
        
                child.on('exit', function(code, signal) {
                    child.exited = true;
                    if (pingStop()) {
                        deferred.reject(new Error("Child process exit code: " + code));
                    }
                    // cleanup the temporary config file
                    fs.unlink(newConfigFile);
                });
                
                // override kill() to have an async callback
                var baseKill = child.kill;
                child.kill = function(signal, cb) {
                    if (typeof signal === 'function') {
                        cb = signal;
                        signal = undefined;
                    }
                    
                    if (cb) {
                        child.on('exit', function() {
                            cb();
                        });
                    }

                    if (child.exited) {
                        process.nextTick(cb);
                    } else {
                        baseKill.apply(child, [signal]);
                    }
                };
            });
        })
        .otherwise(deferred.reject);

    return deferred.promise;
}

module.exports.ghost = forkGhost;
module.exports.config = forkConfig;