// Load modules

var Dgram = require('dgram'); var Dns = require('dns'); var Hoek = require('hoek');

// Declare internals

var internals = {};

exports.time = function (options, callback) {

if (arguments.length !== 2) {
    callback = arguments[0];
    options = {};
}

var settings = Hoek.clone(options);
settings.host = settings.host || 'pool.ntp.org';
settings.port = settings.port || 123;
settings.resolveReference = settings.resolveReference || false;

// Declare variables used by callback

var timeoutId = 0;
var sent = 0;

// Ensure callback is only called once

var finish = function (err, result) {

    if (timeoutId) {
        clearTimeout(timeoutId);
        timeoutId = 0;
    }

    socket.removeAllListeners();
    socket.once('error', internals.ignore);
    socket.close();
    return callback(err, result);
};

finish = Hoek.once(finish);

// Create UDP socket

var socket = Dgram.createSocket('udp4');

socket.once('error', function (err) {

    return finish(err);
});

// Listen to incoming messages

socket.on('message', function (buffer, rinfo) {

    var received = Date.now();

    var message = new internals.NtpMessage(buffer);
    if (!message.isValid) {
        return finish(new Error('Invalid server response'), message);
    }

    if (message.originateTimestamp !== sent) {
        return finish(new Error('Wrong originate timestamp'), message);
    }

    // Timestamp Name          ID   When Generated
    // ------------------------------------------------------------
    // Originate Timestamp     T1   time request sent by client
    // Receive Timestamp       T2   time request received by server
    // Transmit Timestamp      T3   time reply sent by server
    // Destination Timestamp   T4   time reply received by client
    //
    // The roundtrip delay d and system clock offset t are defined as:
    //
    // d = (T4 - T1) - (T3 - T2)     t = ((T2 - T1) + (T3 - T4)) / 2

    var T1 = message.originateTimestamp;
    var T2 = message.receiveTimestamp;
    var T3 = message.transmitTimestamp;
    var T4 = received;

    message.d = (T4 - T1) - (T3 - T2);
    message.t = ((T2 - T1) + (T3 - T4)) / 2;
    message.receivedLocally = received;

    if (!settings.resolveReference ||
        message.stratum !== 'secondary') {

        return finish(null, message);
    }

    // Resolve reference IP address

    Dns.reverse(message.referenceId, function (err, domains) {

        if (/* $lab:coverage:off$ */ !err /* $lab:coverage:on$ */) {
            message.referenceHost = domains[0];
        }

        return finish(null, message);
    });
});

// Set timeout

if (settings.timeout) {
    timeoutId = setTimeout(function () {

        timeoutId = 0;
        return finish(new Error('Timeout'));
    }, settings.timeout);
}

// Construct NTP message

var message = new Buffer(48);
for (var i = 0; i < 48; i++) {                      // Zero message
    message[i] = 0;
}

message[0] = (0 << 6) + (4 << 3) + (3 << 0)         // Set version number to 4 and Mode to 3 (client)
sent = Date.now();
internals.fromMsecs(sent, message, 40);               // Set transmit timestamp (returns as originate)

// Send NTP request

socket.send(message, 0, message.length, settings.port, settings.host, function (err, bytes) {

    if (err ||
        bytes !== 48) {

        return finish(err || new Error('Could not send entire message'));
    }
});

};

internals.NtpMessage = function (buffer) {

this.isValid = false;

// Validate

if (buffer.length !== 48) {
    return;
}

// Leap indicator

var li = (buffer[0] >> 6);
switch (li) {
    case 0: this.leapIndicator = 'no-warning'; break;
    case 1: this.leapIndicator = 'last-minute-61'; break;
    case 2: this.leapIndicator = 'last-minute-59'; break;
    case 3: this.leapIndicator = 'alarm'; break;
}

// Version

var vn = ((buffer[0] & 0x38) >> 3);
this.version = vn;

// Mode

var mode = (buffer[0] & 0x7);
switch (mode) {
    case 1: this.mode = 'symmetric-active'; break;
    case 2: this.mode = 'symmetric-passive'; break;
    case 3: this.mode = 'client'; break;
    case 4: this.mode = 'server'; break;
    case 5: this.mode = 'broadcast'; break;
    case 0:
    case 6:
    case 7: this.mode = 'reserved'; break;
}

// Stratum

var stratum = buffer[1];
if (stratum === 0) {
    this.stratum = 'death';
}
else if (stratum === 1) {
    this.stratum = 'primary';
}
else if (stratum <= 15) {
    this.stratum = 'secondary';
}
else {
    this.stratum = 'reserved';
}

// Poll interval (msec)

this.pollInterval = Math.round(Math.pow(2, buffer[2])) * 1000;

// Precision (msecs)

this.precision = Math.pow(2, buffer[3]) * 1000;

// Root delay (msecs)

var rootDelay = 256 * (256 * (256 * buffer[4] + buffer[5]) + buffer[6]) + buffer[7];
this.rootDelay = 1000 * (rootDelay / 0x10000);

// Root dispersion (msecs)

this.rootDispersion = ((buffer[8] << 8) + buffer[9] + ((buffer[10] << 8) + buffer[11]) / Math.pow(2, 16)) * 1000;

// Reference identifier

this.referenceId = '';
switch (this.stratum) {
    case 'death':
    case 'primary':
        this.referenceId = String.fromCharCode(buffer[12]) + String.fromCharCode(buffer[13]) + String.fromCharCode(buffer[14]) + String.fromCharCode(buffer[15]);
        break;
    case 'secondary':
        this.referenceId = '' + buffer[12] + '.' + buffer[13] + '.' + buffer[14] + '.' + buffer[15];
        break;
}

// Reference timestamp

this.referenceTimestamp = internals.toMsecs(buffer, 16);

// Originate timestamp

this.originateTimestamp = internals.toMsecs(buffer, 24);

// Receive timestamp

this.receiveTimestamp = internals.toMsecs(buffer, 32);

// Transmit timestamp

this.transmitTimestamp = internals.toMsecs(buffer, 40);

// Validate

if (this.version === 4 &&
    this.stratum !== 'reserved' &&
    this.mode === 'server' &&
    this.originateTimestamp &&
    this.receiveTimestamp &&
    this.transmitTimestamp) {

    this.isValid = true;
}

return this;

};

internals.toMsecs = function (buffer, offset) {

var seconds = 0;
var fraction = 0;

for (var i = 0; i < 4; ++i) {
    seconds = (seconds * 256) + buffer[offset + i];
}

for (i = 4; i < 8; ++i) {
    fraction = (fraction * 256) + buffer[offset + i];
}

return ((seconds - 2208988800 + (fraction / Math.pow(2, 32))) * 1000);

};

internals.fromMsecs = function (ts, buffer, offset) {

var seconds = Math.floor(ts / 1000) + 2208988800;
var fraction = Math.round((ts % 1000) / 1000 * Math.pow(2, 32));

buffer[offset + 0] = (seconds & 0xFF000000) >> 24;
buffer[offset + 1] = (seconds & 0x00FF0000) >> 16;
buffer[offset + 2] = (seconds & 0x0000FF00) >> 8;
buffer[offset + 3] = (seconds & 0x000000FF);

buffer[offset + 4] = (fraction & 0xFF000000) >> 24;
buffer[offset + 5] = (fraction & 0x00FF0000) >> 16;
buffer[offset + 6] = (fraction & 0x0000FF00) >> 8;
buffer[offset + 7] = (fraction & 0x000000FF);

};

// Offset singleton

internals.last = {

offset: 0,
expires: 0,
host: '',
port: 0

};

exports.offset = function (options, callback) {

if (arguments.length !== 2) {
    callback = arguments[0];
    options = {};
}

var now = Date.now();
var clockSyncRefresh = options.clockSyncRefresh || 24 * 60 * 60 * 1000;                    // Daily

if (internals.last.offset &&
    internals.last.host === options.host &&
    internals.last.port === options.port &&
    now < internals.last.expires) {

    process.nextTick(function () {

        callback(null, internals.last.offset);
    });

    return;
}

exports.time(options, function (err, time) {

    if (err) {
        return callback(err, 0);
    }

    internals.last = {
        offset: Math.round(time.t),
        expires: now + clockSyncRefresh,
        host: options.host,
        port: options.port
    };

    return callback(null, internals.last.offset);
});

};

// Now singleton

internals.now = {

intervalId: 0

};

exports.start = function (options, callback) {

if (arguments.length !== 2) {
    callback = arguments[0];
    options = {};
}

if (internals.now.intervalId) {
    process.nextTick(function () {

        callback();
    });

    return;
}

exports.offset(options, function (err, offset) {

    internals.now.intervalId = setInterval(function () {

        exports.offset(options, function () { });
    }, options.clockSyncRefresh || 24 * 60 * 60 * 1000);                                // Daily

    return callback();
});

};

exports.stop = function () {

if (!internals.now.intervalId) {
    return;
}

clearInterval(internals.now.intervalId);
internals.now.intervalId = 0;

};

exports.isLive = function () {

return !!internals.now.intervalId;

};

exports.now = function () {

var now = Date.now();
if (!exports.isLive() ||
    now >= internals.last.expires) {

    return now;
}

return now + internals.last.offset;

};

internals.ignore = function () {

};