import { safeFilter, safeGetTime, safeIndexOf, safeJoin, safeMap, safePush, safeToISOString, safeToString, Map, String, Symbol as StableSymbol, } from './globals.js';
const safeArrayFrom = Array.from;
const safeBufferIsBuffer = typeof Buffer !== 'undefined' ? Buffer.isBuffer : undefined;
const safeJsonStringify = JSON.stringify;
const safeNumberIsNaN = Number.isNaN;
const safeObjectKeys = Object.keys;
const safeObjectGetOwnPropertySymbols = Object.getOwnPropertySymbols;
const safeObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
const safeObjectGetPrototypeOf = Object.getPrototypeOf;
const safeNegativeInfinity = Number.NEGATIVE_INFINITY;
const safePositiveInfinity = Number.POSITIVE_INFINITY;
export const toStringMethod = Symbol.for('fast-check/toStringMethod');
export function hasToStringMethod(instance) {
    return (instance !== null &&
        (typeof instance === 'object' || typeof instance === 'function') &&
        toStringMethod in instance &&
        typeof instance[toStringMethod] === 'function');
}
export const asyncToStringMethod = Symbol.for('fast-check/asyncToStringMethod');
export function hasAsyncToStringMethod(instance) {
    return (instance !== null &&
        (typeof instance === 'object' || typeof instance === 'function') &&
        asyncToStringMethod in instance &&
        typeof instance[asyncToStringMethod] === 'function');
}
const findSymbolNameRegex = /^Symbol\((.*)\)$/;
function getSymbolDescription(s) {
    if (s.description !== undefined)
        return s.description;
    const m = findSymbolNameRegex.exec(String(s));
    return m && m[1].length ? m[1] : null;
}
function stringifyNumber(numValue) {
    switch (numValue) {
        case 0:
            return 1 / numValue === safeNegativeInfinity ? '-0' : '0';
        case safeNegativeInfinity:
            return 'Number.NEGATIVE_INFINITY';
        case safePositiveInfinity:
            return 'Number.POSITIVE_INFINITY';
        default:
            return numValue === numValue ? String(numValue) : 'Number.NaN';
    }
}
function isSparseArray(arr) {
    let previousNumberedIndex = -1;
    for (const index in arr) {
        const numberedIndex = Number(index);
        if (numberedIndex !== previousNumberedIndex + 1)
            return true;
        previousNumberedIndex = numberedIndex;
    }
    return previousNumberedIndex + 1 !== arr.length;
}
export function stringifyInternal(value, previousValues, getAsyncContent) {
    const currentValues = [...previousValues, value];
    if (typeof value === 'object') {
        if (safeIndexOf(previousValues, value) !== -1) {
            return '[cyclic]';
        }
    }
    if (hasAsyncToStringMethod(value)) {
        const content = getAsyncContent(value);
        if (content.state === 'fulfilled') {
            return content.value;
        }
    }
    if (hasToStringMethod(value)) {
        try {
            return value[toStringMethod]();
        }
        catch {
        }
    }
    switch (safeToString(value)) {
        case '[object Array]': {
            const arr = value;
            if (arr.length >= 50 && isSparseArray(arr)) {
                const assignments = [];
                for (const index in arr) {
                    if (!safeNumberIsNaN(Number(index)))
                        safePush(assignments, `${index}:${stringifyInternal(arr[index], currentValues, getAsyncContent)}`);
                }
                return assignments.length !== 0
                    ? `Object.assign(Array(${arr.length}),{${safeJoin(assignments, ',')}})`
                    : `Array(${arr.length})`;
            }
            const stringifiedArray = safeJoin(safeMap(arr, (v) => stringifyInternal(v, currentValues, getAsyncContent)), ',');
            return arr.length === 0 || arr.length - 1 in arr ? `[${stringifiedArray}]` : `[${stringifiedArray},]`;
        }
        case '[object BigInt]':
            return `${value}n`;
        case '[object Boolean]': {
            const unboxedToString = value == true ? 'true' : 'false';
            return typeof value === 'boolean' ? unboxedToString : `new Boolean(${unboxedToString})`;
        }
        case '[object Date]': {
            const d = value;
            return safeNumberIsNaN(safeGetTime(d)) ? `new Date(NaN)` : `new Date(${safeJsonStringify(safeToISOString(d))})`;
        }
        case '[object Map]':
            return `new Map(${stringifyInternal(Array.from(value), currentValues, getAsyncContent)})`;
        case '[object Null]':
            return `null`;
        case '[object Number]':
            return typeof value === 'number' ? stringifyNumber(value) : `new Number(${stringifyNumber(Number(value))})`;
        case '[object Object]': {
            try {
                const toStringAccessor = value.toString;
                if (typeof toStringAccessor === 'function' && toStringAccessor !== Object.prototype.toString) {
                    return value.toString();
                }
            }
            catch {
                return '[object Object]';
            }
            const mapper = (k) => `${k === '__proto__'
                ? '["__proto__"]'
                : typeof k === 'symbol'
                    ? `[${stringifyInternal(k, currentValues, getAsyncContent)}]`
                    : safeJsonStringify(k)}:${stringifyInternal(value[k], currentValues, getAsyncContent)}`;
            const stringifiedProperties = [
                ...(safeObjectGetPrototypeOf(value) === null ? ['__proto__:null'] : []),
                ...safeMap(safeObjectKeys(value), mapper),
                ...safeMap(safeFilter(safeObjectGetOwnPropertySymbols(value), (s) => {
                    const descriptor = safeObjectGetOwnPropertyDescriptor(value, s);
                    return descriptor && descriptor.enumerable;
                }), mapper),
            ];
            return '{' + safeJoin(stringifiedProperties, ',') + '}';
        }
        case '[object Set]':
            return `new Set(${stringifyInternal(Array.from(value), currentValues, getAsyncContent)})`;
        case '[object String]':
            return typeof value === 'string' ? safeJsonStringify(value) : `new String(${safeJsonStringify(value)})`;
        case '[object Symbol]': {
            const s = value;
            if (StableSymbol.keyFor(s) !== undefined) {
                return `Symbol.for(${safeJsonStringify(StableSymbol.keyFor(s))})`;
            }
            const desc = getSymbolDescription(s);
            if (desc === null) {
                return 'Symbol()';
            }
            const knownSymbol = desc.startsWith('Symbol.') && StableSymbol[desc.substring(7)];
            return s === knownSymbol ? desc : `Symbol(${safeJsonStringify(desc)})`;
        }
        case '[object Promise]': {
            const promiseContent = getAsyncContent(value);
            switch (promiseContent.state) {
                case 'fulfilled':
                    return `Promise.resolve(${stringifyInternal(promiseContent.value, currentValues, getAsyncContent)})`;
                case 'rejected':
                    return `Promise.reject(${stringifyInternal(promiseContent.value, currentValues, getAsyncContent)})`;
                case 'pending':
                    return `new Promise(() => {/*pending*/})`;
                case 'unknown':
                default:
                    return `new Promise(() => {/*unknown*/})`;
            }
        }
        case '[object Error]':
            if (value instanceof Error) {
                return `new Error(${stringifyInternal(value.message, currentValues, getAsyncContent)})`;
            }
            break;
        case '[object Undefined]':
            return `undefined`;
        case '[object Int8Array]':
        case '[object Uint8Array]':
        case '[object Uint8ClampedArray]':
        case '[object Int16Array]':
        case '[object Uint16Array]':
        case '[object Int32Array]':
        case '[object Uint32Array]':
        case '[object Float32Array]':
        case '[object Float64Array]':
        case '[object BigInt64Array]':
        case '[object BigUint64Array]': {
            if (typeof safeBufferIsBuffer === 'function' && safeBufferIsBuffer(value)) {
                return `Buffer.from(${stringifyInternal(safeArrayFrom(value.values()), currentValues, getAsyncContent)})`;
            }
            const valuePrototype = safeObjectGetPrototypeOf(value);
            const className = valuePrototype && valuePrototype.constructor && valuePrototype.constructor.name;
            if (typeof className === 'string') {
                const typedArray = value;
                const valuesFromTypedArr = typedArray.values();
                return `${className}.from(${stringifyInternal(safeArrayFrom(valuesFromTypedArr), currentValues, getAsyncContent)})`;
            }
            break;
        }
    }
    try {
        return value.toString();
    }
    catch {
        return safeToString(value);
    }
}
export function stringify(value) {
    return stringifyInternal(value, [], () => ({ state: 'unknown', value: undefined }));
}
export function possiblyAsyncStringify(value) {
    const stillPendingMarker = StableSymbol();
    const pendingPromisesForCache = [];
    const cache = new Map();
    function createDelay0() {
        let handleId = null;
        const cancel = () => {
            if (handleId !== null) {
                clearTimeout(handleId);
            }
        };
        const delay = new Promise((resolve) => {
            handleId = setTimeout(() => {
                handleId = null;
                resolve(stillPendingMarker);
            }, 0);
        });
        return { delay, cancel };
    }
    const unknownState = { state: 'unknown', value: undefined };
    const getAsyncContent = function getAsyncContent(data) {
        const cacheKey = data;
        if (cache.has(cacheKey)) {
            return cache.get(cacheKey);
        }
        const delay0 = createDelay0();
        const p = asyncToStringMethod in data
            ? Promise.resolve().then(() => data[asyncToStringMethod]())
            : data;
        p.catch(() => { });
        pendingPromisesForCache.push(Promise.race([p, delay0.delay]).then((successValue) => {
            if (successValue === stillPendingMarker)
                cache.set(cacheKey, { state: 'pending', value: undefined });
            else
                cache.set(cacheKey, { state: 'fulfilled', value: successValue });
            delay0.cancel();
        }, (errorValue) => {
            cache.set(cacheKey, { state: 'rejected', value: errorValue });
            delay0.cancel();
        }));
        cache.set(cacheKey, unknownState);
        return unknownState;
    };
    function loop() {
        const stringifiedValue = stringifyInternal(value, [], getAsyncContent);
        if (pendingPromisesForCache.length === 0) {
            return stringifiedValue;
        }
        return Promise.all(pendingPromisesForCache.splice(0)).then(loop);
    }
    return loop();
}
export async function asyncStringify(value) {
    return Promise.resolve(possiblyAsyncStringify(value));
}
