export function assert<T>(value: T | undefined | null, message?: string): asserts value is NonNullable<T> {
    if (value == null || value === undefined) {
        throw new Error(message ?? "Expected value to be defined");
    }
}

export function isNothing<T>(value: T): value is Extract<T, null | undefined> {
    return value === null || value === undefined;
}

export function sortRecord<T extends Record<string, any>>(
    obj: T,
    sortFn?: (a: [keyof T, T[keyof T]?], b: [keyof T, T[keyof T]?]) => number
): Record<string, T[keyof T]> {
    const entries = Object.entries(obj);
    const sortedEntries = entries.sort(sortFn ? sortFn : ([ a ]: string[], [ b ]: string[]) => a.localeCompare(b));
    return Object.fromEntries(sortedEntries);
}

export function sortMap<K, V>(map: Map<K, V>, sortFn?: (a: [K, V], b: [K, V]) => number): Map<K, V> {
    const entries = [ ...map.entries() ];
    const sortedEntries = entries.sort(sortFn ? sortFn : ([ a ], [ b ]) => String(a).localeCompare(String(b)));
    return new Map(sortedEntries);
}

/**
 * Check if a value is an object that can be merged
 * @param value The input value to check for being a mergeable object
 * @returns A boolean value indicating whether the input value is a mergeable object (true) or not (false), based on whether it is a non-null object that is not an array
 */
export function isRecord<T extends Record<string, unknown>>(value: unknown): value is T {
    if (isNothing(value)) return false;

    let maybeRecord: string;

    try {
        maybeRecord = JSON.stringify(value, undefined, 2) as string;
    }
    catch {
        return false;
    }

    const hasBrackets = maybeRecord.startsWith("{") && maybeRecord.endsWith("}");
    const isObjectNotArray = value instanceof Object && !Array.isArray(value);
    return hasBrackets && isObjectNotArray;
}

export function isMap<K = unknown, V = unknown>(value: unknown): value is Map<K, V> {
    return value instanceof Map;
}

RegExp.isRegexPattern = function isRegexPattern(str: string): str is `/${string}/` {
    // Checks if the string starts and ends with '/'
    if (!/^\/.*\/$/.test(str)) return false;

    // `/<str>/<flags>` => <str>
    const endPattern = str.lastIndexOf("/");
    const pattern = str.slice(1, endPattern);

    try {
        // Attempting to create a new RegExp instance will throw a SyntaxError
        // at runtime if the pattern is invalid.
        new RegExp(pattern);
    }
    catch {
        return false;
    }

    return true;
};
RegExp.escape = function (value: string): string {
    return value.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&");
};

export function injectGlobals(): void {
    (globalThis as typeof globalThis & { assert: typeof assert; }).assert = assert;
    (globalThis as typeof globalThis & { isNothing: typeof isNothing; }).isNothing = isNothing;
    (globalThis as typeof globalThis & { sortRecord: typeof sortRecord; }).sortRecord = sortRecord;
    (globalThis as typeof globalThis & { sortMap: typeof sortMap; }).sortMap = sortMap;
    (globalThis as typeof globalThis & { isRecord: typeof isRecord; }).isRecord = isRecord;
    (globalThis as typeof globalThis & { isMap: typeof isMap; }).isMap = isMap;
}
