"use strict";
/**
 * Adapted from https://github.com/mysqljs/sqlstring/blob/cd528556b4b6bcf300c3db515026935dedf7cfa1/lib/SqlString.js
 * MIT LICENSE: https://github.com/mysqljs/sqlstring/blob/cd528556b4b6bcf300c3db515026935dedf7cfa1/LICENSE
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.raw = exports.format = exports.escape = exports.arrayToList = exports.bufferToString = exports.objectToValues = exports.escapeId = exports.temporalToString = exports.dateToString = void 0;
const node_buffer_1 = require("node:buffer");
const CONTEXT_TRIGGER = new Uint8Array(128);
const SET_CLAUSE_TERMINATORS_BY_FIRST = {};
const SET_CLAUSE_TERMINATORS = [
    'where',
    'order',
    'group',
    'having',
    'limit',
    'union',
    'returning',
    'into',
    'for',
    'lock',
    'offset',
    'window',
    'procedure',
    'on',
];
const regex = {
    backtick: /`/g,
    dot: /\./g,
    timezone: /([+\-\s])(\d\d):?(\d\d)?/,
    escapeChars: /[\0\b\t\n\r\x1a"'\\]/g,
};
const charCode = {
    singleQuote: 39,
    backtick: 96,
    backslash: 92,
    dash: 45,
    slash: 47,
    asterisk: 42,
    exclamation: 33,
    plus: 43,
    questionMark: 63,
    comma: 44,
    openParen: 40,
    closeParen: 41,
    semicolon: 59,
    newline: 10,
    space: 32,
    tab: 9,
    carriageReturn: 13,
};
// Chars that open a string, identifier, or comment
CONTEXT_TRIGGER[charCode.singleQuote] = 1;
CONTEXT_TRIGGER[charCode.backtick] = 1;
CONTEXT_TRIGGER[charCode.dash] = 1;
CONTEXT_TRIGGER[charCode.slash] = 1;
// Bucket terminators by their first character
for (const word of SET_CLAUSE_TERMINATORS) {
    const first = word.charCodeAt(0);
    const bucket = SET_CLAUSE_TERMINATORS_BY_FIRST[first];
    if (bucket)
        bucket.push(word);
    else
        SET_CLAUSE_TERMINATORS_BY_FIRST[first] = [word];
}
const hasOwnProperty = Object.prototype.hasOwnProperty;
const isRecord = (value) => typeof value === 'object' &&
    value !== null &&
    !Array.isArray(value) &&
    !(value instanceof Set) &&
    !(value instanceof Map);
const isWordChar = (code) => (code >= 65 && code <= 90) ||
    (code >= 97 && code <= 122) ||
    (code >= 48 && code <= 57) ||
    code === 95;
const isWhitespace = (code) => code === charCode.space ||
    code === charCode.tab ||
    code === charCode.newline ||
    code === charCode.carriageReturn;
const toLower = (code) => code | 32;
const matchesWord = (sql, position, word, length) => {
    const wordLength = word.length;
    for (let offset = 0; offset < wordLength; offset++)
        if (toLower(sql.charCodeAt(position + offset)) !== word.charCodeAt(offset))
            return false;
    return ((position === 0 || !isWordChar(sql.charCodeAt(position - 1))) &&
        (position + wordLength >= length ||
            !isWordChar(sql.charCodeAt(position + wordLength))));
};
const skipSqlContext = (sql, position) => {
    const currentChar = sql.charCodeAt(position);
    const nextChar = sql.charCodeAt(position + 1);
    if (currentChar === charCode.singleQuote) {
        for (let cursor = position + 1; cursor < sql.length; cursor++) {
            if (sql.charCodeAt(cursor) === charCode.backslash)
                cursor++;
            else if (sql.charCodeAt(cursor) === charCode.singleQuote)
                return cursor + 1;
        }
        return sql.length;
    }
    if (currentChar === charCode.backtick) {
        const length = sql.length;
        for (let cursor = position + 1; cursor < length; cursor++) {
            if (sql.charCodeAt(cursor) !== charCode.backtick)
                continue;
            if (sql.charCodeAt(cursor + 1) === charCode.backtick) {
                cursor++;
                continue;
            }
            return cursor + 1;
        }
        return length;
    }
    if (currentChar === charCode.dash && nextChar === charCode.dash) {
        const afterDash = sql.charCodeAt(position + 2);
        if (Number.isNaN(afterDash) || afterDash <= charCode.space) {
            const lineBreak = sql.indexOf('\n', position + 2);
            return lineBreak === -1 ? sql.length : lineBreak + 1;
        }
        return -1;
    }
    if (currentChar === charCode.slash && nextChar === charCode.asterisk) {
        const markerChar = sql.charCodeAt(position + 2);
        if (markerChar === charCode.exclamation || markerChar === charCode.plus)
            return -1;
        const commentEnd = sql.indexOf('*/', position + 2);
        return commentEnd === -1 ? sql.length : commentEnd + 2;
    }
    return -1;
};
const findNextPlaceholder = (sql, start) => {
    const sqlLength = sql.length;
    for (let position = start; position < sqlLength; position++) {
        const code = sql.charCodeAt(position);
        if (code === charCode.questionMark)
            return position;
        if (code < 128 && CONTEXT_TRIGGER[code]) {
            const contextEnd = skipSqlContext(sql, position);
            if (contextEnd !== -1)
                position = contextEnd - 1;
        }
    }
    return -1;
};
const isInSetAssignmentList = (sql, setEnd, placeholderPosition) => {
    const length = sql.length;
    let depth = 0;
    let sawContent = false;
    let lastWasComma = false;
    for (let i = setEnd; i < placeholderPosition;) {
        const code = sql.charCodeAt(i);
        if (code < 128 && CONTEXT_TRIGGER[code]) {
            const contextEnd = skipSqlContext(sql, i);
            if (contextEnd !== -1) {
                i = contextEnd;
                sawContent = true;
                lastWasComma = false;
                continue;
            }
        }
        if (isWhitespace(code)) {
            i++;
            continue;
        }
        if (code === charCode.openParen) {
            depth++;
            sawContent = true;
            lastWasComma = false;
            i++;
            continue;
        }
        if (code === charCode.closeParen) {
            if (--depth < 0)
                return false;
            sawContent = true;
            lastWasComma = false;
            i++;
            continue;
        }
        if (isWordChar(code)) {
            if (depth === 0 && !(code >= 48 && code <= 57)) {
                const bucket = SET_CLAUSE_TERMINATORS_BY_FIRST[code | 32];
                if (bucket)
                    for (let t = 0; t < bucket.length; t++)
                        if (matchesWord(sql, i, bucket[t], length))
                            return false;
            }
            do {
                i++;
            } while (i < placeholderPosition && isWordChar(sql.charCodeAt(i)));
            sawContent = true;
            lastWasComma = false;
            continue;
        }
        if (depth === 0) {
            if (code === charCode.semicolon)
                return false;
            if (code === charCode.comma) {
                lastWasComma = true;
                sawContent = true;
                i++;
                continue;
            }
        }
        sawContent = true;
        lastWasComma = false;
        i++;
    }
    return depth === 0 && (!sawContent || lastWasComma);
};
const findSetKeyword = (sql, startFrom = 0) => {
    const length = sql.length;
    for (let position = startFrom; position < length; position++) {
        const code = sql.charCodeAt(position);
        if (code < 128 && CONTEXT_TRIGGER[code]) {
            const contextEnd = skipSqlContext(sql, position);
            if (contextEnd !== -1) {
                position = contextEnd - 1;
                continue;
            }
        }
        const lower = code | 32;
        if (lower === 115 && matchesWord(sql, position, 'set', length))
            return position + 3;
        if (lower === 107 && matchesWord(sql, position, 'key', length)) {
            let cursor = position + 3;
            while (cursor < length && isWhitespace(sql.charCodeAt(cursor)))
                cursor++;
            if (matchesWord(sql, cursor, 'update', length))
                return cursor + 6;
        }
    }
    return -1;
};
const isDate = (value) => Object.prototype.toString.call(value) === '[object Date]';
const isTemporal = (value) => Object.prototype.toString.call(value).startsWith('[object Temporal.');
const hasSqlString = (value) => typeof value === 'object' &&
    value !== null &&
    'toSqlString' in value &&
    typeof value.toSqlString === 'function';
const escapeString = (value) => {
    const escapeChars = regex.escapeChars;
    escapeChars.lastIndex = 0;
    const first = escapeChars.exec(value);
    if (first === null)
        return `'${value}'`;
    const length = value.length;
    let result = "'" + value.slice(0, first.index);
    let chunkStart = first.index;
    for (let i = first.index; i < length; i++) {
        let escaped;
        switch (value.charCodeAt(i)) {
            case 0:
                escaped = '\\0';
                break;
            case 8:
                escaped = '\\b';
                break;
            case 9:
                escaped = '\\t';
                break;
            case 10:
                escaped = '\\n';
                break;
            case 13:
                escaped = '\\r';
                break;
            case 26:
                escaped = '\\Z';
                break;
            case 34:
                escaped = '\\"';
                break;
            case 39:
                escaped = "\\'";
                break;
            case 92:
                escaped = '\\\\';
                break;
            default:
                continue;
        }
        result += value.slice(chunkStart, i) + escaped;
        chunkStart = i + 1;
    }
    return result + value.slice(chunkStart) + "'";
};
const pad2 = (value) => (value < 10 ? '0' + value : '' + value);
const pad3 = (value) => value < 10 ? '00' + value : value < 100 ? '0' + value : '' + value;
const pad4 = (value) => value < 10
    ? '000' + value
    : value < 100
        ? '00' + value
        : value < 1000
            ? '0' + value
            : '' + value;
const convertTimezone = (tz) => {
    if (tz === 'Z')
        return 0;
    const timezoneMatch = tz.match(regex.timezone);
    if (timezoneMatch)
        return ((timezoneMatch[1] === '-' ? -1 : 1) *
            (Number.parseInt(timezoneMatch[2], 10) +
                (timezoneMatch[3] ? Number.parseInt(timezoneMatch[3], 10) : 0) / 60) *
            60);
    return false;
};
const dateToString = (date, timezone) => {
    if (Number.isNaN(date.getTime()))
        return 'NULL';
    let year;
    let month;
    let day;
    let hour;
    let minute;
    let second;
    let millisecond;
    if (timezone === 'local') {
        year = date.getFullYear();
        month = date.getMonth() + 1;
        day = date.getDate();
        hour = date.getHours();
        minute = date.getMinutes();
        second = date.getSeconds();
        millisecond = date.getMilliseconds();
    }
    else {
        const timezoneOffsetMinutes = convertTimezone(timezone);
        let time = date.getTime();
        if (timezoneOffsetMinutes !== false && timezoneOffsetMinutes !== 0)
            time += timezoneOffsetMinutes * 60000;
        const adjustedDate = new Date(time);
        year = adjustedDate.getUTCFullYear();
        month = adjustedDate.getUTCMonth() + 1;
        day = adjustedDate.getUTCDate();
        hour = adjustedDate.getUTCHours();
        minute = adjustedDate.getUTCMinutes();
        second = adjustedDate.getUTCSeconds();
        millisecond = adjustedDate.getUTCMilliseconds();
    }
    // YYYY-MM-DD HH:mm:ss.mmm
    return escapeString(pad4(year) +
        '-' +
        pad2(month) +
        '-' +
        pad2(day) +
        ' ' +
        pad2(hour) +
        ':' +
        pad2(minute) +
        ':' +
        pad2(second) +
        '.' +
        pad3(millisecond));
};
exports.dateToString = dateToString;
const temporalToString = (value, timezone) => {
    if (typeof value.epochMilliseconds === 'number')
        return (0, exports.dateToString)(new Date(value.epochMilliseconds), timezone || 'local');
    if (value[Symbol.toStringTag] === 'Temporal.PlainDateTime')
        return escapeString(value.toString().replace('T', ' '));
    return escapeString(value.toString());
};
exports.temporalToString = temporalToString;
const escapeId = (value, forbidQualified) => {
    if (Array.isArray(value)) {
        const length = value.length;
        let sql = '';
        for (let i = 0; i < length; i++) {
            if (i > 0)
                sql += ', ';
            sql += (0, exports.escapeId)(value[i], forbidQualified);
        }
        return sql;
    }
    const identifier = String(value);
    const hasJsonOperator = !forbidQualified && identifier.indexOf('->') !== -1;
    if (forbidQualified || hasJsonOperator) {
        if (identifier.indexOf('`') === -1)
            return `\`${identifier}\``;
        return `\`${identifier.replace(regex.backtick, '``')}\``;
    }
    if (identifier.indexOf('`') === -1 && identifier.indexOf('.') === -1)
        return `\`${identifier}\``;
    return `\`${identifier
        .replace(regex.backtick, '``')
        .replace(regex.dot, '`.`')}\``;
};
exports.escapeId = escapeId;
const objectToValues = (object, timezone) => {
    let sql = '';
    if (object instanceof Map) {
        for (const [key, value] of object) {
            if (typeof value === 'function')
                continue;
            if (sql.length > 0)
                sql += ', ';
            sql += (0, exports.escapeId)(String(key));
            sql += ' = ';
            sql += (0, exports.escape)(value, true, timezone);
        }
        return sql;
    }
    for (const key in object) {
        if (!hasOwnProperty.call(object, key))
            continue;
        const value = object[key];
        if (typeof value === 'function')
            continue;
        if (sql.length > 0)
            sql += ', ';
        sql += (0, exports.escapeId)(key);
        sql += ' = ';
        sql += (0, exports.escape)(value, true, timezone);
    }
    return sql;
};
exports.objectToValues = objectToValues;
const bufferToString = (buffer) => `X${escapeString(buffer.toString('hex'))}`;
exports.bufferToString = bufferToString;
const arrayToList = (array, timezone) => {
    const length = array.length;
    let sql = '';
    for (let i = 0; i < length; i++) {
        if (i > 0)
            sql += ', ';
        const value = array[i];
        if (Array.isArray(value))
            sql += `(${(0, exports.arrayToList)(value, timezone)})`;
        else if (value instanceof Set)
            sql += `(${(0, exports.arrayToList)(Array.from(value), timezone)})`;
        else
            sql += (0, exports.escape)(value, true, timezone);
    }
    return sql;
};
exports.arrayToList = arrayToList;
const escape = (value, stringifyObjects, timezone) => {
    if (value === undefined || value === null)
        return 'NULL';
    switch (typeof value) {
        case 'boolean':
            return value ? 'true' : 'false';
        case 'number':
        case 'bigint':
            return value + '';
        case 'object': {
            if (isDate(value))
                return (0, exports.dateToString)(value, timezone || 'local');
            if (isTemporal(value))
                return (0, exports.temporalToString)(value, timezone);
            if (Array.isArray(value))
                return (0, exports.arrayToList)(value, timezone);
            if (value instanceof Set)
                return (0, exports.arrayToList)(Array.from(value), timezone);
            if (node_buffer_1.Buffer.isBuffer(value))
                return (0, exports.bufferToString)(value);
            if (value instanceof Uint8Array)
                return (0, exports.bufferToString)(node_buffer_1.Buffer.from(value));
            if (hasSqlString(value))
                return String(value.toSqlString());
            if (!(stringifyObjects === undefined || stringifyObjects === null))
                return escapeString(String(value));
            if (isRecord(value) || value instanceof Map)
                return (0, exports.objectToValues)(value, timezone);
            return escapeString(String(value));
        }
        case 'string':
            return escapeString(value);
        default:
            return escapeString(String(value));
    }
};
exports.escape = escape;
const format = (sql, values, stringifyObjects, timezone) => {
    if (values === undefined || values === null)
        return sql;
    const valuesArray = Array.isArray(values) ? values : [values];
    const length = valuesArray.length;
    let setIndex = -2; // -2 = not yet computed, -1 = no SET found
    let nextSetIndex = -1; // -1 = no SET after setIndex
    let result = '';
    let chunkIndex = 0;
    let valuesIndex = 0;
    let placeholderPosition = findNextPlaceholder(sql, 0);
    while (valuesIndex < length && placeholderPosition !== -1) {
        // Count consecutive question marks to detect ? vs ?? vs ???+
        let placeholderEnd = placeholderPosition + 1;
        let escapedValue;
        while (sql.charCodeAt(placeholderEnd) === 63)
            placeholderEnd++;
        const placeholderLength = placeholderEnd - placeholderPosition;
        const currentValue = valuesArray[valuesIndex];
        if (placeholderLength > 2) {
            placeholderPosition = findNextPlaceholder(sql, placeholderEnd);
            continue;
        }
        if (placeholderLength === 2)
            escapedValue = (0, exports.escapeId)(currentValue);
        else if (typeof currentValue === 'number' ||
            typeof currentValue === 'bigint')
            escapedValue = `${currentValue}`;
        else if (typeof currentValue === 'object' &&
            currentValue !== null &&
            !stringifyObjects) {
            const expandable = !(Array.isArray(currentValue) ||
                currentValue instanceof Uint8Array ||
                currentValue instanceof Date ||
                hasSqlString(currentValue) ||
                isDate(currentValue)) &&
                (isRecord(currentValue) || currentValue instanceof Map);
            if (expandable) {
                // A SET assignment follows the keyword (a letter) or a comma continuing the list
                let previous = placeholderPosition - 1;
                while (previous >= chunkIndex && isWhitespace(sql.charCodeAt(previous)))
                    previous--;
                const previousChar = previous >= chunkIndex ? toLower(sql.charCodeAt(previous)) : 0;
                if ((previousChar < 97 || previousChar > 122) &&
                    previousChar !== charCode.comma)
                    escapedValue = (0, exports.escape)(currentValue, true, timezone);
                else {
                    // Lazy: resolve the first SET and its successor once
                    if (setIndex === -2) {
                        setIndex = findSetKeyword(sql);
                        nextSetIndex = setIndex === -1 ? -1 : findSetKeyword(sql, setIndex);
                    }
                    // Nearest: advance to the SET closest before this placeholder
                    while (nextSetIndex !== -1 && nextSetIndex <= placeholderPosition) {
                        setIndex = nextSetIndex;
                        nextSetIndex = findSetKeyword(sql, nextSetIndex);
                    }
                    if (setIndex !== -1 &&
                        setIndex <= placeholderPosition &&
                        isInSetAssignmentList(sql, setIndex, placeholderPosition))
                        escapedValue = (0, exports.objectToValues)(currentValue, timezone);
                    else
                        escapedValue = (0, exports.escape)(currentValue, true, timezone);
                }
            }
            else
                escapedValue = (0, exports.escape)(currentValue, true, timezone);
        }
        else
            escapedValue = (0, exports.escape)(currentValue, stringifyObjects, timezone);
        result += sql.slice(chunkIndex, placeholderPosition);
        result += escapedValue;
        chunkIndex = placeholderEnd;
        valuesIndex++;
        placeholderPosition = findNextPlaceholder(sql, placeholderEnd);
    }
    if (chunkIndex === 0)
        return sql;
    if (chunkIndex < sql.length)
        return result + sql.slice(chunkIndex);
    return result;
};
exports.format = format;
const raw = (sql) => {
    if (typeof sql !== 'string')
        throw new TypeError('argument sql must be a string');
    return {
        toSqlString: () => sql,
    };
};
exports.raw = raw;
