import type { StateUpdater } from "preact/hooks";

export const nextFrame = async (): Promise<number> => new Promise((resolve) => requestAnimationFrame(resolve));
export const delay = async (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

export const randInt = (min: number, max: number) => Math.round(min + (max - min - 1) * Math.random());
export const randBool = () => Math.random() < 0.5;
export const choice = <T>(array: T[]): T => array[randInt(0, array.length)];
export const weightedChoice = <T extends string | number>(options: [T, number][] | Partial<Record<T, number>>): T | null => {
    if (!Array.isArray(options)) {
        options = Object.entries(options) as [T, number][];
    }
    const sum = options.reduce((acc, o) => acc + o[1], 0);

    const rnd = Math.random() * sum;

    let weight = 0;
    for (const [item, probability] of options) {
        weight += probability;
        if (rnd < weight) {
            return item;
        }
    }

    return null;
}
export const shuffle = <T>(array: T[]): T[] => {
    const shuffledArray = [...array];

    for (let i = shuffledArray.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]];
    }

    return shuffledArray;
}
export const mapNumber = (num: number, inMin: number, inMax: number, outMin: number, outMax: number) =>
    (num - inMin) * (outMax - outMin) / (inMax - inMin) + outMin;

export function zip<T1, T2, T3>(a1: Iterable<T1>, a2: Iterable<T2>, a3: Iterable<T3>): Generator<[T1, T2, T3]>;
export function zip<T1, T2>(a1: Iterable<T1>, a2: Iterable<T2>): Generator<[T1, T2]>;
export function zip<T1>(a1: Iterable<T1>): Generator<[T1]>;
export function* zip(...args: Iterable<any>[]) {
    const iterators = args.map(i => i[Symbol.iterator]());

    while (true) {
        const nextValues = iterators.map(i => i.next());
        if (nextValues.some(v => v.done)) return;

        yield nextValues.map(v => v.value);
    }
}

export function* enumerate<T>(iterable: Iterable<T>): Generator<[number, T]> {
    let i = 0;
    for (const item of iterable) {
        yield [i++, item];
    }
}

export const range = (size: number | string) => Object.keys((new Array(+size)).fill(0)).map(k => +k);
export const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));
export const lerp = (start: number, end: number, t: number) => (start + (end - start) * t);

export const prevent = (e: Event) => (e.preventDefault(), false);

export const intHash = (seed: number, ...parts: number[]) => {
    let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
    for (let i = 0; i < parts.length; i++) {
        const ch = parts[i];
        h1 = Math.imul(h1 ^ ch, 2654435761);
        h2 = Math.imul(h2 ^ ch, 1597334677);
    }
    h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
    h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
    return h1;
};
export const sinHash = (...data: number[]) => data.reduce((hash, n) => Math.sin((hash * 123.12 + n) * 756.12), 0) / 2 + 0.5;
/** FNV-1a 32-bit hash — turns a string into a uint32 seed. */
export const stringHash = (s: string): number => {
  let h = 0x811c9dc5;
  for (let i = 0; i < s.length; i++) {
    h = Math.imul(h ^ s.charCodeAt(i), 0x01000193) >>> 0;
  }
  return h;
}

type F<T, A extends unknown[], R> = (this: T, ...args: A) => R;
export const throttle = function <T, A extends unknown[], R>(func: F<T, A, R>, ms: number, trailing = false): F<T, A, R> {
    let isThrottled = false;
    let savedResult: R;
    let savedThis: T;
    let savedArgs: A | undefined;

    const wrapper = function (this: T, ...args: A): R {
        if (isThrottled) {
            savedThis = this;
            savedArgs = args;
        } else {
            savedResult = func.apply(this, args);
            savedArgs = undefined;

            isThrottled = true;

            setTimeout(function () {
                isThrottled = false;
                if (trailing && savedArgs) {
                    savedResult = wrapper.apply(savedThis, savedArgs);
                }
            }, ms);
        }

        return savedResult;
    };

    return wrapper;
}

export const callUpdater = <T>(f: StateUpdater<T>, prev: T) =>
    typeof f === 'function' ? (f as Function)(prev) : f;

export const formatNumber = (n: number): string => {
    if (n === 0) return '0';
    if (n < 0) return `-${formatNumber(-n)}`;
    if (n >= 1_000_000_000_000) return `${(n / 1_000_000_000_000).toFixed(2)}T`;
    if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(2)}B`;
    if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`;
    if (n >= 1_000) return `${(n / 1_000).toFixed(2)}k`;
    if (Math.trunc(n) === n) return n.toString();
    if (n >= 1) return n.toFixed(2);
    const result = n.toPrecision(2);
    if (parseFloat(result) >= 1) return parseFloat(result).toFixed(2);
    return result;
};

export const formatTime = (seconds: number): string => {
    const secInMinute = 60;
    const secInHour = 60 * secInMinute;
    const secInDay = 24 * secInHour;
    const secInWeek = 7 * secInDay;
    const secInMonth = 30 * secInDay;
    const secInYear = 365 * secInDay;

    const y = Math.floor(seconds / secInYear);
    let rem = seconds % secInYear;
    const mo = Math.floor(rem / secInMonth);
    rem %= secInMonth;
    const w = Math.floor(rem / secInWeek);
    rem %= secInWeek;
    const d = Math.floor(rem / secInDay);
    rem %= secInDay;
    const h = Math.floor(rem / secInHour);
    rem %= secInHour;
    const mi = Math.floor(rem / secInMinute);
    const s = Math.floor(rem % secInMinute);

    const parts: string[] = [];
    if (y > 0) parts.push(`${y}y`);
    if (mo > 0) parts.push(`${mo}m`);
    if (w > 0) parts.push(`${w}w`);
    if (d > 0) parts.push(`${d}d`);

    const hasBigParts = parts.length > 0;
    const timeStr = hasBigParts || h > 0
        ? `${h}:${String(mi).padStart(2, '0')}:${String(s).padStart(2, '0')}`
        : `${mi}:${String(s).padStart(2, '0')}`;

    parts.push(timeStr);
    return parts.join(' ');
};

export const fuzzyMatch = (target: string, query: string): boolean => {
    const t = target.toLowerCase();
    const q = query.toLowerCase();
    let qi = 0;
    for (let ti = 0; ti < t.length && qi < q.length; ti++) {
        if (t[ti] === q[qi]) qi++;
    }
    return qi === q.length;
};

export const extractString = (e: Event | string): string => {
    if (typeof e === 'string') {
        return e;
    } else {
        const { target } = e;
        if (target && 'value' in target && typeof target.value === 'string') {
            return target.value;
        } else if (target instanceof HTMLElement) {
            return target.innerHTML;
        }
    }
    return '';
}