'use strict';

const object = {};
const hasOwnProperty = object.hasOwnProperty;
const forOwn = (object, callback) => {
    for (const key in object) {
        if (hasOwnProperty.call(object, key)) {
            callback(key, object[key]);
        }
    }
};

const extend = (destination, source) => {
    if (!source) {
        return destination;
    }
    forOwn(source, (key, value) => {
        destination[key] = value;
    });
    return destination;
};

const forEach = (array, callback) => {
    const length = array.length;
    let index = -1;
    while (++index < length) {
        callback(array[index]);
    }
};

const toString = object.toString;
const isArray = Array.isArray;
const isBuffer = Buffer.isBuffer;
const isObject = (value) => {
    // This is a very simple check, but it’s good enough for what we need.
    return toString.call(value) == '[object Object]';
};
const isString = (value) => {
    return typeof value == 'string' ||
        toString.call(value) == '[object String]';
};
const isNumber = (value) => {
    return typeof value == 'number' ||
        toString.call(value) == '[object Number]';
};
const isFunction = (value) => {
    return typeof value == 'function';
};
const isMap = (value) => {
    return toString.call(value) == '[object Map]';
};
const isSet = (value) => {
    return toString.call(value) == '[object Set]';
};

/*--------------------------------------------------------------------------*/

// https://mathiasbynens.be/notes/javascript-escapes#single
const singleEscapes = {
    '"': '\\"',
    '\'': '\\\'',
    '\\': '\\\\',
    '\b': '\\b',
    '\f': '\\f',
    '\n': '\\n',
    '\r': '\\r',
    '\t': '\\t'
    // `\v` is omitted intentionally, because in IE < 9, '\v' == 'v'.
    // '\v': '\\x0B'
};
const regexSingleEscape = /["'\\\b\f\n\r\t]/;

const regexDigit = /[0-9]/;
const regexWhitelist = /[ !#-&\(-\[\]-_a-~]/;

const jsesc = (argument, options) => {
    const increaseIndentation = () => {
        oldIndent = indent;
        ++options.indentLevel;
        indent = options.indent.repeat(options.indentLevel)
    };
    // Handle options
    const defaults = {
        'escapeEverything': false,
        'minimal': false,
        'isScriptContext': false,
        'quotes': 'single',
        'wrap': false,
        'es6': false,
        'json': false,
        'compact': true,
        'lowercaseHex': false,
        'numbers': 'decimal',
        'indent': '\t',
        'indentLevel': 0,
        '__inline1__': false,
        '__inline2__': false
    };
    const json = options && options.json;
    if (json) {
        defaults.quotes = 'double';
        defaults.wrap = true;
    }
    options = extend(defaults, options);
    if (
        options.quotes != 'single' &&
        options.quotes != 'double' &&
        options.quotes != 'backtick'
    ) {
        options.quotes = 'single';
    }
    const quote = options.quotes == 'double' ?
        '"' :
        (options.quotes == 'backtick' ?
            '`' :
            '\''
        );
    const compact = options.compact;
    const lowercaseHex = options.lowercaseHex;
    let indent = options.indent.repeat(options.indentLevel);
    let oldIndent = '';
    const inline1 = options.__inline1__;
    const inline2 = options.__inline2__;
    const newLine = compact ? '' : '\n';
    let result;
    let isEmpty = true;
    const useBinNumbers = options.numbers == 'binary';
    const useOctNumbers = options.numbers == 'octal';
    const useDecNumbers = options.numbers == 'decimal';
    const useHexNumbers = options.numbers == 'hexadecimal';

    if (json && argument && isFunction(argument.toJSON)) {
        argument = argument.toJSON();
    }

    if (!isString(argument)) {
        if (isMap(argument)) {
            if (argument.size == 0) {
                return 'new Map()';
            }
            if (!compact) {
                options.__inline1__ = true;
                options.__inline2__ = false;
            }
            return 'new Map(' + jsesc(Array.from(argument), options) + ')';
        }
        if (isSet(argument)) {
            if (argument.size == 0) {
                return 'new Set()';
            }
            return 'new Set(' + jsesc(Array.from(argument), options) + ')';
        }
        if (isBuffer(argument)) {
            if (argument.length == 0) {
                return 'Buffer.from([])';
            }
            return 'Buffer.from(' + jsesc(Array.from(argument), options) + ')';
        }
        if (isArray(argument)) {
            result = [];
            options.wrap = true;
            if (inline1) {
                options.__inline1__ = false;
                options.__inline2__ = true;
            }
            if (!inline2) {
                increaseIndentation();
            }
            forEach(argument, (value) => {
                isEmpty = false;
                if (inline2) {
                    options.__inline2__ = false;
                }
                result.push(
                    (compact || inline2 ? '' : indent) +
                    jsesc(value, options)
                );
            });
            if (isEmpty) {
                return '[]';
            }
            if (inline2) {
                return '[' + result.join(', ') + ']';
            }
            return '[' + newLine + result.join(',' + newLine) + newLine +
                (compact ? '' : oldIndent) + ']';
        } else if (isNumber(argument)) {
            if (json) {
                // Some number values (e.g. `Infinity`) cannot be represented in JSON.
                return JSON.stringify(argument);
            }
            if (useDecNumbers) {
                return String(argument);
            }
            if (useHexNumbers) {
                let hexadecimal = argument.toString(16);
                if (!lowercaseHex) {
                    hexadecimal = hexadecimal.toUpperCase();
                }
                return '0x' + hexadecimal;
            }
            if (useBinNumbers) {
                return '0b' + argument.toString(2);
            }
            if (useOctNumbers) {
                return '0o' + argument.toString(8);
            }
        } else if (!isObject(argument)) {
            if (json) {
                // For some values (e.g. `undefined`, `function` objects),
                // `JSON.stringify(value)` returns `undefined` (which isn’t valid
                // JSON) instead of `'null'`.
                return JSON.stringify(argument) || 'null';
            }
            return String(argument);
        } else { // it’s an object
            result = [];
            options.wrap = true;
            increaseIndentation();
            forOwn(argument, (key, value) => {
                isEmpty = false;
                result.push(
                    (compact ? '' : indent) +
                    jsesc(key, options) + ':' +
                    (compact ? '' : ' ') +
                    jsesc(value, options)
                );
            });
            if (isEmpty) {
                return '{}';
            }
            return '{' + newLine + result.join(',' + newLine) + newLine +
                (compact ? '' : oldIndent) + '}';
        }
    }

    const string = argument;
    // Loop over each code unit in the string and escape it
    let index = -1;
    const length = string.length;
    result = '';
    while (++index < length) {
        const character = string.charAt(index);
        if (options.es6) {
            const first = string.charCodeAt(index);
            if ( // check if it’s the start of a surrogate pair
                first >= 0xD800 && first <= 0xDBFF && // high surrogate
                length > index + 1 // there is a next code unit
            ) {
                const second = string.charCodeAt(index + 1);
                if (second >= 0xDC00 && second <= 0xDFFF) { // low surrogate
                    // https://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
                    const codePoint = (first - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;
                    let hexadecimal = codePoint.toString(16);
                    if (!lowercaseHex) {
                        hexadecimal = hexadecimal.toUpperCase();
                    }
                    result += '\\u{' + hexadecimal + '}';
                    ++index;
                    continue;
                }
            }
        }
        if (!options.escapeEverything) {
            if (regexWhitelist.test(character)) {
                // It’s a printable ASCII character that is not `"`, `'` or `\`,
                // so don’t escape it.
                result += character;
                continue;
            }
            if (character == '"') {
                result += quote == character ? '\\"' : character;
                continue;
            }
            if (character == '`') {
                result += quote == character ? '\\`' : character;
                continue;
            }
            if (character == '\'') {
                result += quote == character ? '\\\'' : character;
                continue;
            }
        }
        if (
            character == '\0' &&
            !json &&
            !regexDigit.test(string.charAt(index + 1))
        ) {
            result += '\\0';
            continue;
        }
        if (regexSingleEscape.test(character)) {
            // no need for a `hasOwnProperty` check here
            result += singleEscapes[character];
            continue;
        }
        const charCode = character.charCodeAt(0);
        if (options.minimal && charCode != 0x2028 && charCode != 0x2029) {
            result += character;
            continue;
        }
        let hexadecimal = charCode.toString(16);
        if (!lowercaseHex) {
            hexadecimal = hexadecimal.toUpperCase();
        }
        const longhand = hexadecimal.length > 2 || json;
        const escaped = '\\' + (longhand ? 'u' : 'x') +
            ('0000' + hexadecimal).slice(longhand ? -4 : -2);
        result += escaped;
        continue;
    }
    if (options.wrap) {
        result = quote + result + quote;
    }
    if (quote == '`') {
        result = result.replace(/\$\{/g, '\\\$\{');
    }
    if (options.isScriptContext) {
        // https://mathiasbynens.be/notes/etago
        return result
            .replace(/<\/(script|style)/gi, '<\\/$1')
            .replace(/<!--/g, json ? '\\u003C!--' : '\\x3C!--');
    }
    return result;
};

jsesc.version = '2.5.2';

module.exports = jsesc;