import { normalizeStringifyOptions, replaceValue, resolveStringifyMode } from './utils.js';

function encodeString(value) {
    if (/[^\x20\x21\x23-\x5B\x5D-\uD799]/.test(value)) { // [^\x20-\uD799]|[\x22\x5c]
        return JSON.stringify(value);
    }

    return '"' + value + '"';
}

export function* stringifyChunked(value, ...args) {
    const { replacer, getKeys, space, ...options } = normalizeStringifyOptions(...args);
    const highWaterMark = Number(options.highWaterMark) || 0x4000; // 16kb by default
    const roots = resolveStringifyMode(options.mode) === 'jsonl' && Array.isArray(value) ? value : [value];

    const rootCount = roots.length;
    const keyStrings = new Map();
    const stack = [];
    let rootValue = null;
    let prevState = null;
    let state = null;
    let stateValue = null;
    let stateEmpty = true;
    let stateKeys = [];
    let stateIndex = 0;
    let buffer = '';

    for (let i = 0; i < rootCount; i++) {
        if (rootValue !== null) {
            buffer += '\n';
        }

        rootValue = { '': roots[i] };
        prevState = null;
        state = () => printEntry('', roots[i]);
        stateValue = rootValue;
        stateEmpty = true;
        stateKeys = [''];
        stateIndex = 0;

        do {
            state();

            if (buffer.length >= highWaterMark || (prevState === null && i === rootCount - 1)) {
                // flush buffer
                yield buffer;
                buffer = '';
            }
        } while (prevState !== null);
    }

    function printObject() {
        if (stateIndex === 0) {
            stateKeys = getKeys(stateValue);
            buffer += '{';
        }

        // when no keys left
        if (stateIndex === stateKeys.length) {
            buffer += space && !stateEmpty
                ? `\n${space.repeat(stack.length - 1)}}`
                : '}';

            popState();
            return;
        }

        const key = stateKeys[stateIndex++];
        printEntry(key, stateValue[key]);
    }

    function printArray() {
        if (stateIndex === 0) {
            buffer += '[';
        }

        if (stateIndex === stateValue.length) {
            buffer += space && !stateEmpty
                ? `\n${space.repeat(stack.length - 1)}]`
                : ']';

            popState();
            return;
        }

        printEntry(stateIndex, stateValue[stateIndex++]);
    }

    function printEntryPrelude(key) {
        if (stateEmpty) {
            stateEmpty = false;
        } else {
            buffer += ',';
        }

        if (space && prevState !== null) {
            buffer += `\n${space.repeat(stack.length)}`;
        }

        if (state === printObject) {
            let keyString = keyStrings.get(key);

            if (keyString === undefined) {
                keyStrings.set(key, keyString = encodeString(key) + (space ? ': ' : ':'));
            }

            buffer += keyString;
        }
    }

    function printEntry(key, value) {
        value = replaceValue(stateValue, key, value, replacer);

        if (value === null || typeof value !== 'object') {
            // primitive
            if (state !== printObject || value !== undefined) {
                printEntryPrelude(key);
                pushPrimitive(value);
            }
        } else {
            // If the visited set does not change after adding a value, then it is already in the set
            if (stack.includes(value)) {
                throw new TypeError('Converting circular structure to JSON');
            }

            printEntryPrelude(key);
            stack.push(value);

            pushState();
            state = Array.isArray(value) ? printArray : printObject;
            stateValue = value;
            stateEmpty = true;
            stateIndex = 0;
        }
    }

    function pushPrimitive(value) {
        switch (typeof value) {
            case 'string':
                buffer += encodeString(value);
                break;

            case 'number':
                buffer += Number.isFinite(value) ? String(value) : 'null';
                break;

            case 'boolean':
                buffer += value ? 'true' : 'false';
                break;

            case 'undefined':
            case 'object': // typeof null === 'object'
                buffer += 'null';
                break;

            default:
                throw new TypeError(`Do not know how to serialize a ${value.constructor?.name || typeof value}`);
        }
    }

    function pushState() {
        prevState = {
            keys: stateKeys,
            index: stateIndex,
            prev: prevState
        };
    }

    function popState() {
        stack.pop();
        const value = stack.length > 0 ? stack[stack.length - 1] : rootValue;

        // restore state
        state = Array.isArray(value) ? printArray : printObject;
        stateValue = value;
        stateEmpty = false;
        stateKeys = prevState.keys;
        stateIndex = prevState.index;

        // pop state
        prevState = prevState.prev;
    }
};
