// Load modules

var Crypto = require('crypto'); var Path = require('path'); var Util = require('util'); var Escape = require('./escape');

// Declare internals

var internals = {};

// Clone object or array

exports.clone = function (obj, seen) {

if (typeof obj !== 'object' ||
    obj === null) {

    return obj;
}

seen = seen || { orig: [], copy: [] };

var lookup = seen.orig.indexOf(obj);
if (lookup !== -1) {
    return seen.copy[lookup];
}

var newObj;
var cloneDeep = false;

if (!Array.isArray(obj)) {
    if (Buffer.isBuffer(obj)) {
        newObj = new Buffer(obj);
    }
    else if (obj instanceof Date) {
        newObj = new Date(obj.getTime());
    }
    else if (obj instanceof RegExp) {
        newObj = new RegExp(obj);
    }
    else {
        var proto = Object.getPrototypeOf(obj);
        if (proto &&
            proto.isImmutable) {

            newObj = obj;
        }
        else {
            newObj = Object.create(proto);
            cloneDeep = true;
        }
    }
}
else {
    newObj = [];
    cloneDeep = true;
}

seen.orig.push(obj);
seen.copy.push(newObj);

if (cloneDeep) {
    var keys = Object.getOwnPropertyNames(obj);
    for (var i = 0, il = keys.length; i < il; ++i) {
        var key = keys[i];
        var descriptor = Object.getOwnPropertyDescriptor(obj, key);
        if (descriptor &&
            (descriptor.get ||
             descriptor.set)) {

            Object.defineProperty(newObj, key, descriptor);
        }
        else {
            newObj[key] = exports.clone(obj[key], seen);
        }
    }
}

return newObj;

};

// Merge all the properties of source into target, source wins in conflict, and by default null and undefined from source are applied /*eslint-disable */ exports.merge = function (target, source, isNullOverride /* = true */, isMergeArrays /* = true */) { /*eslint-enable */

exports.assert(target && typeof target === 'object', 'Invalid target value: must be an object');
exports.assert(source === null || source === undefined || typeof source === 'object', 'Invalid source value: must be null, undefined, or an object');

if (!source) {
    return target;
}

if (Array.isArray(source)) {
    exports.assert(Array.isArray(target), 'Cannot merge array onto an object');
    if (isMergeArrays === false) {                                                  // isMergeArrays defaults to true
        target.length = 0;                                                          // Must not change target assignment
    }

    for (var i = 0, il = source.length; i < il; ++i) {
        target.push(exports.clone(source[i]));
    }

    return target;
}

var keys = Object.keys(source);
for (var k = 0, kl = keys.length; k < kl; ++k) {
    var key = keys[k];
    var value = source[key];
    if (value &&
        typeof value === 'object') {

        if (!target[key] ||
            typeof target[key] !== 'object' ||
            (Array.isArray(target[key]) ^ Array.isArray(value)) ||
            value instanceof Date ||
            Buffer.isBuffer(value) ||
            value instanceof RegExp) {

            target[key] = exports.clone(value);
        }
        else {
            exports.merge(target[key], value, isNullOverride, isMergeArrays);
        }
    }
    else {
        if (value !== null &&
            value !== undefined) {                              // Explicit to preserve empty strings

            target[key] = value;
        }
        else if (isNullOverride !== false) {                    // Defaults to true
            target[key] = value;
        }
    }
}

return target;

};

// Apply options to a copy of the defaults

exports.applyToDefaults = function (defaults, options, isNullOverride) {

exports.assert(defaults && typeof defaults === 'object', 'Invalid defaults value: must be an object');
exports.assert(!options || options === true || typeof options === 'object', 'Invalid options value: must be true, falsy or an object');

if (!options) {                                                 // If no options, return null
    return null;
}

var copy = exports.clone(defaults);

if (options === true) {                                         // If options is set to true, use defaults
    return copy;
}

return exports.merge(copy, options, isNullOverride === true, false);

};

// Clone an object except for the listed keys which are shallow copied

exports.cloneWithShallow = function (source, keys) {

if (!source ||
    typeof source !== 'object') {

    return source;
}

var storage = internals.store(source, keys);    // Move shallow copy items to storage
var copy = exports.clone(source);               // Deep copy the rest
internals.restore(copy, source, storage);       // Shallow copy the stored items and restore
return copy;

};

internals.store = function (source, keys) {

var storage = {};
for (var i = 0, il = keys.length; i < il; ++i) {
    var key = keys[i];
    var value = exports.reach(source, key);
    if (value !== undefined) {
        storage[key] = value;
        internals.reachSet(source, key, undefined);
    }
}

return storage;

};

internals.restore = function (copy, source, storage) {

var keys = Object.keys(storage);
for (var i = 0, il = keys.length; i < il; ++i) {
    var key = keys[i];
    internals.reachSet(copy, key, storage[key]);
    internals.reachSet(source, key, storage[key]);
}

};

internals.reachSet = function (obj, key, value) {

var path = key.split('.');
var ref = obj;
for (var i = 0, il = path.length; i < il; ++i) {
    var segment = path[i];
    if (i + 1 === il) {
        ref[segment] = value;
    }

    ref = ref[segment];
}

};

// Apply options to defaults except for the listed keys which are shallow copied from option without merging

exports.applyToDefaultsWithShallow = function (defaults, options, keys) {

exports.assert(defaults && typeof defaults === 'object', 'Invalid defaults value: must be an object');
exports.assert(!options || options === true || typeof options === 'object', 'Invalid options value: must be true, falsy or an object');
exports.assert(keys && Array.isArray(keys), 'Invalid keys');

if (!options) {                                                 // If no options, return null
    return null;
}

var copy = exports.cloneWithShallow(defaults, keys);

if (options === true) {                                         // If options is set to true, use defaults
    return copy;
}

var storage = internals.store(options, keys);   // Move shallow copy items to storage
exports.merge(copy, options, false, false);     // Deep copy the rest
internals.restore(copy, options, storage);      // Shallow copy the stored items and restore
return copy;

};

// Deep object or array comparison

exports.deepEqual = function (obj, ref, options, seen) {

options = options || { prototype: true };

var type = typeof obj;

if (type !== typeof ref) {
    return false;
}

if (type !== 'object' ||
    obj === null ||
    ref === null) {

    if (obj === ref) {                                                      // Copied from Deep-eql, copyright(c) 2013 Jake Luer, jake@alogicalparadox.com, MIT Licensed, https://github.com/chaijs/deep-eql
        return obj !== 0 || 1 / obj === 1 / ref;        // -0 / +0
    }

    return obj !== obj && ref !== ref;                  // NaN
}

seen = seen || [];
if (seen.indexOf(obj) !== -1) {
    return true;                            // If previous comparison failed, it would have stopped execution
}

seen.push(obj);

if (Array.isArray(obj)) {
    if (!Array.isArray(ref)) {
        return false;
    }

    if (!options.part && obj.length !== ref.length) {
        return false;
    }

    for (var i = 0, il = obj.length; i < il; ++i) {
        if (options.part) {
            var found = false;
            for (var r = 0, rl = ref.length; r < rl; ++r) {
                if (exports.deepEqual(obj[i], ref[r], options, seen)) {
                    found = true;
                    break;
                }
            }

            return found;
        }

        if (!exports.deepEqual(obj[i], ref[i], options, seen)) {
            return false;
        }
    }

    return true;
}

if (Buffer.isBuffer(obj)) {
    if (!Buffer.isBuffer(ref)) {
        return false;
    }

    if (obj.length !== ref.length) {
        return false;
    }

    for (var j = 0, jl = obj.length; j < jl; ++j) {
        if (obj[j] !== ref[j]) {
            return false;
        }
    }

    return true;
}

if (obj instanceof Date) {
    return (ref instanceof Date && obj.getTime() === ref.getTime());
}

if (obj instanceof RegExp) {
    return (ref instanceof RegExp && obj.toString() === ref.toString());
}

if (options.prototype) {
    if (Object.getPrototypeOf(obj) !== Object.getPrototypeOf(ref)) {
        return false;
    }
}

var keys = Object.getOwnPropertyNames(obj);

if (!options.part && keys.length !== Object.getOwnPropertyNames(ref).length) {
    return false;
}

for (var k = 0, kl = keys.length; k < kl; ++k) {
    var key = keys[k];
    var descriptor = Object.getOwnPropertyDescriptor(obj, key);
    if (descriptor.get) {
        if (!exports.deepEqual(descriptor, Object.getOwnPropertyDescriptor(ref, key), options, seen)) {
            return false;
        }
    }
    else if (!exports.deepEqual(obj[key], ref[key], options, seen)) {
        return false;
    }
}

return true;

};

// Remove duplicate items from array

exports.unique = function (array, key) {

var index = {};
var result = [];

for (var i = 0, il = array.length; i < il; ++i) {
    var id = (key ? array[i][key] : array[i]);
    if (index[id] !== true) {

        result.push(array[i]);
        index[id] = true;
    }
}

return result;

};

// Convert array into object

exports.mapToObject = function (array, key) {

if (!array) {
    return null;
}

var obj = {};
for (var i = 0, il = array.length; i < il; ++i) {
    if (key) {
        if (array[i][key]) {
            obj[array[i][key]] = true;
        }
    }
    else {
        obj[array[i]] = true;
    }
}

return obj;

};

// Find the common unique items in two arrays

exports.intersect = function (array1, array2, justFirst) {

if (!array1 || !array2) {
    return [];
}

var common = [];
var hash = (Array.isArray(array1) ? exports.mapToObject(array1) : array1);
var found = {};
for (var i = 0, il = array2.length; i < il; ++i) {
    if (hash[array2[i]] && !found[array2[i]]) {
        if (justFirst) {
            return array2[i];
        }

        common.push(array2[i]);
        found[array2[i]] = true;
    }
}

return (justFirst ? null : common);

};

// Test if the reference contains the values

exports.contain = function (ref, values, options) {

/*
    string -> string(s)
    array -> item(s)
    object -> key(s)
    object -> object (key:value)
*/

var valuePairs = null;
if (typeof ref === 'object' &&
    typeof values === 'object' &&
    !Array.isArray(ref) &&
    !Array.isArray(values)) {

    valuePairs = values;
    values = Object.keys(values);
}
else {
    values = [].concat(values);
}

options = options || {};            // deep, once, only, part

exports.assert(arguments.length >= 2, 'Insufficient arguments');
exports.assert(typeof ref === 'string' || typeof ref === 'object', 'Reference must be string or an object');
exports.assert(values.length, 'Values array cannot be empty');

var compare, compareFlags;
if (options.deep) {
    compare = exports.deepEqual;

    var hasOnly = options.hasOwnProperty('only'), hasPart = options.hasOwnProperty('part');

    compareFlags = {
        prototype: hasOnly ? options.only : hasPart ? !options.part : false,
        part: hasOnly ? !options.only : hasPart ? options.part : true
    };
}
else {
    compare = function (a, b) {

        return a === b;
    };
}

var misses = false;
var matches = new Array(values.length);
for (var i = 0, il = matches.length; i < il; ++i) {
    matches[i] = 0;
}

if (typeof ref === 'string') {
    var pattern = '(';
    for (i = 0, il = values.length; i < il; ++i) {
        var value = values[i];
        exports.assert(typeof value === 'string', 'Cannot compare string reference to non-string value');
        pattern += (i ? '|' : '') + exports.escapeRegex(value);
    }

    var regex = new RegExp(pattern + ')', 'g');
    var leftovers = ref.replace(regex, function ($0, $1) {

        var index = values.indexOf($1);
        ++matches[index];
        return '';          // Remove from string
    });

    misses = !!leftovers;
}
else if (Array.isArray(ref)) {
    for (i = 0, il = ref.length; i < il; ++i) {
        for (var j = 0, jl = values.length, matched = false; j < jl && matched === false; ++j) {
            matched = compare(values[j], ref[i], compareFlags) && j;
        }

        if (matched !== false) {
            ++matches[matched];
        }
        else {
            misses = true;
        }
    }
}
else {
    var keys = Object.keys(ref);
    for (i = 0, il = keys.length; i < il; ++i) {
        var key = keys[i];
        var pos = values.indexOf(key);
        if (pos !== -1) {
            if (valuePairs &&
                !compare(valuePairs[key], ref[key], compareFlags)) {

                return false;
            }

            ++matches[pos];
        }
        else {
            misses = true;
        }
    }
}

var result = false;
for (i = 0, il = matches.length; i < il; ++i) {
    result = result || !!matches[i];
    if ((options.once && matches[i] > 1) ||
        (!options.part && !matches[i])) {

        return false;
    }
}

if (options.only &&
    misses) {

    return false;
}

return result;

};

// Flatten array

exports.flatten = function (array, target) {

var result = target || [];

for (var i = 0, il = array.length; i < il; ++i) {
    if (Array.isArray(array[i])) {
        exports.flatten(array[i], result);
    }
    else {
        result.push(array[i]);
    }
}

return result;

};

// Convert an object key chain string ('a.b.c') to reference (object[b])

exports.reach = function (obj, chain, options) {

if (chain === false ||
    chain === null ||
    typeof chain === 'undefined') {

    return obj;
}

options = options || {};
if (typeof options === 'string') {
    options = { separator: options };
}

var path = chain.split(options.separator || '.');
var ref = obj;
for (var i = 0, il = path.length; i < il; ++i) {
    var key = path[i];
    if (key[0] === '-' && Array.isArray(ref)) {
        key = key.slice(1, key.length);
        key = ref.length - key;
    }

    if (!ref ||
        !ref.hasOwnProperty(key) ||
        (typeof ref !== 'object' && options.functions === false)) {         // Only object and function can have properties

        exports.assert(!options.strict || i + 1 === il, 'Missing segment', key, 'in reach path ', chain);
        exports.assert(typeof ref === 'object' || options.functions === true || typeof ref !== 'function', 'Invalid segment', key, 'in reach path ', chain);
        ref = options.default;
        break;
    }

    ref = ref[key];
}

return ref;

};

exports.reachTemplate = function (obj, template, options) {

return template.replace(/{([^}]+)}/g, function ($0, chain) {

    var value = exports.reach(obj, chain, options);
    return (value === undefined || value === null ? '' : value);
});

};

exports.formatStack = function (stack) {

var trace = [];
for (var i = 0, il = stack.length; i < il; ++i) {
    var item = stack[i];
    trace.push([item.getFileName(), item.getLineNumber(), item.getColumnNumber(), item.getFunctionName(), item.isConstructor()]);
}

return trace;

};

exports.formatTrace = function (trace) {

var display = [];

for (var i = 0, il = trace.length; i < il; ++i) {
    var row = trace[i];
    display.push((row[4] ? 'new ' : '') + row[3] + ' (' + row[0] + ':' + row[1] + ':' + row[2] + ')');
}

return display;

};

exports.callStack = function (slice) {

// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi

var v8 = Error.prepareStackTrace;
Error.prepareStackTrace = function (err, stack) {

    return stack;
};

var capture = {};
Error.captureStackTrace(capture, arguments.callee);     /*eslint no-caller:0 */
var stack = capture.stack;

Error.prepareStackTrace = v8;

var trace = exports.formatStack(stack);

if (slice) {
    return trace.slice(slice);
}

return trace;

};

exports.displayStack = function (slice) {

var trace = exports.callStack(slice === undefined ? 1 : slice + 1);

return exports.formatTrace(trace);

};

exports.abortThrow = false;

exports.abort = function (message, hideStack) {

if (process.env.NODE_ENV === 'test' || exports.abortThrow === true) {
    throw new Error(message || 'Unknown error');
}

var stack = '';
if (!hideStack) {
    stack = exports.displayStack(1).join('\n\t');
}
console.log('ABORT: ' + message + '\n\t' + stack);
process.exit(1);

};

exports.assert = function (condition /*, msg1, msg2, msg3 */) {

if (condition) {
    return;
}

if (arguments.length === 2 && arguments[1] instanceof Error) {
    throw arguments[1];
}

var msgs = [];
for (var i = 1, il = arguments.length; i < il; ++i) {
    if (arguments[i] !== '') {
        msgs.push(arguments[i]);            // Avoids Array.slice arguments leak, allowing for V8 optimizations
    }
}

msgs = msgs.map(function (msg) {

    return typeof msg === 'string' ? msg : msg instanceof Error ? msg.message : exports.stringify(msg);
});
throw new Error(msgs.join(' ') || 'Unknown error');

};

exports.Timer = function () {

this.ts = 0;
this.reset();

};

exports.Timer.prototype.reset = function () {

this.ts = Date.now();

};

exports.Timer.prototype.elapsed = function () {

return Date.now() - this.ts;

};

exports.Bench = function () {

this.ts = 0;
this.reset();

};

exports.Bench.prototype.reset = function () {

this.ts = exports.Bench.now();

};

exports.Bench.prototype.elapsed = function () {

return exports.Bench.now() - this.ts;

};

exports.Bench.now = function () {

var ts = process.hrtime();
return (ts[0] * 1e3) + (ts[1] / 1e6);

};

// Escape string for Regex construction

exports.escapeRegex = function (string) {

// Escape ^$.*+-?=!:|\/()[]{},
return string.replace(/[\^\$\.\*\+\-\?\=\!\:\|\\\/\(\)\[\]\{\}\,]/g, '\\$&');

};

// Base64url (RFC 4648) encode

exports.base64urlEncode = function (value, encoding) {

var buf = (Buffer.isBuffer(value) ? value : new Buffer(value, encoding || 'binary'));
return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/\=/g, '');

};

// Base64url (RFC 4648) decode

exports.base64urlDecode = function (value, encoding) {

if (value &&
    !/^[\w\-]*$/.test(value)) {

    return new Error('Invalid character');
}

try {
    var buf = new Buffer(value, 'base64');
    return (encoding === 'buffer' ? buf : buf.toString(encoding || 'binary'));
}
catch (err) {
    return err;
}

};

// Escape attribute value for use in HTTP header

exports.escapeHeaderAttribute = function (attribute) {

// Allowed value characters: !#$%&'()*+,-./:;<=>?@[]^_`{|}~ and space, a-z, A-Z, 0-9, \, "

exports.assert(/^[ \w\!#\$%&'\(\)\*\+,\-\.\/\:;<\=>\?@\[\]\^`\{\|\}~\"\\]*$/.test(attribute), 'Bad attribute value (' + attribute + ')');

return attribute.replace(/\\/g, '\\\\').replace(/\"/g, '\\"');                             // Escape quotes and slash

};

exports.escapeHtml = function (string) {

return Escape.escapeHtml(string);

};

exports.escapeJavaScript = function (string) {

return Escape.escapeJavaScript(string);

};

exports.nextTick = function (callback) {

return function () {

    var args = arguments;
    process.nextTick(function () {

        callback.apply(null, args);
    });
};

};

exports.once = function (method) {

if (method._hoekOnce) {
    return method;
}

var once = false;
var wrapped = function () {

    if (!once) {
        once = true;
        method.apply(null, arguments);
    }
};

wrapped._hoekOnce = true;

return wrapped;

};

exports.isAbsolutePath = function (path, platform) {

if (!path) {
    return false;
}

if (Path.isAbsolute) {                      // node >= 0.11
    return Path.isAbsolute(path);
}

platform = platform || process.platform;

// Unix

if (platform !== 'win32') {
    return path[0] === '/';
}

// Windows

return !!/^(?:[a-zA-Z]:[\\\/])|(?:[\\\/]{2}[^\\\/]+[\\\/]+[^\\\/])/.test(path);        // C:\ or \\something\something

};

exports.isInteger = function (value) {

return (typeof value === 'number' &&
        parseFloat(value) === parseInt(value, 10) &&
        !isNaN(value));

};

exports.ignore = function () { };

exports.inherits = Util.inherits;

exports.format = Util.format;

exports.transform = function (source, transform, options) {

exports.assert(source === null || source === undefined || typeof source === 'object' || Array.isArray(source), 'Invalid source object: must be null, undefined, an object, or an array');

if (Array.isArray(source)) {
    var results = [];
    for (var i = 0, il = source.length; i < il; ++i) {
        results.push(exports.transform(source[i], transform, options));
    }
    return results;
}

var result = {};
var keys = Object.keys(transform);

for (var k = 0, kl = keys.length; k < kl; ++k) {
    var key = keys[k];
    var path = key.split('.');
    var sourcePath = transform[key];

    exports.assert(typeof sourcePath === 'string', 'All mappings must be "." delineated strings');

    var segment;
    var res = result;

    while (path.length > 1) {
        segment = path.shift();
        if (!res[segment]) {
            res[segment] = {};
        }
        res = res[segment];
    }
    segment = path.shift();
    res[segment] = exports.reach(source, sourcePath, options);
}

return result;

};

exports.uniqueFilename = function (path, extension) {

if (extension) {
    extension = extension[0] !== '.' ? '.' + extension : extension;
}
else {
    extension = '';
}

path = Path.resolve(path);
var name = [Date.now(), process.pid, Crypto.randomBytes(8).toString('hex')].join('-') + extension;
return Path.join(path, name);

};

exports.stringify = function () {

try {
    return JSON.stringify.apply(null, arguments);
}
catch (err) {
    return '[Cannot display object: ' + err.message + ']';
}

};

exports.shallow = function (source) {

var target = {};
var keys = Object.keys(source);
for (var i = 0, il = keys.length; i < il; ++i) {
    var key = keys[i];
    target[key] = source[key];
}

return target;

};