import { DateTime, add, cmp, timedelta, time } from '@silane/datetime';


/**
 * @template {object} T
 * @param {T} obj
 * @returns {{[P in keyof T]: T[P] extends undefined ? never : T[P]}}
 */
export function deleteUndefinedProperties(obj) {
    for(const [key, value] of Object.entries(obj)) {
        if(value === undefined) {
            delete obj[key];
        }
    }
    return obj;
}

/**
 * @template T
 * @param {T[]} array
 * @param {(item: T) => boolean} condition
 */
export function arrayRemoveIf(array, condition) {
    for(let i = 0; i < array.length; ++i) {
        if(condition(array[i])) {
            array.splice(i, 1);
            --i;
        }
    }
}

/**
 * @typedef {string | number | bigint | boolean | undefined | null} TotalOrderable
 */

/**
 * @param {TotalOrderable} a
 * @param {TotalOrderable} b
 * @returns {-1 | 0 | 1}
 */
function primitiveTotalOrdering(a, b) {
    function compare(a, b) {
        if(a < b) return -1;
        if(a > b) return +1;
        return 0;
    }

    const typeOfA = typeof a;
    const typeOfB = typeof b;

    if(['symbol', 'function'].includes(typeOfA) || ['symbol', 'function'].includes(typeOfB)) {
        throw new Error();
    }
    if(typeOfA === 'object' && a !== null || typeOfB === 'object' && b !== null) {
        throw new Error();
    }

    const typeOrderTable = {
        string: 4, number: 3, bigint: 3, boolean: 2, undefined: 0, object: 1,
    };
    let cmp = compare(typeOrderTable[typeOfA], typeOrderTable[typeOfB]);
    if(cmp) return cmp;

    const typeOrder = typeOrderTable[typeOfA];
    if(typeOrder === 0 || typeOrder === 1) return 0;
    if(typeOrder === 2 || typeOrder === 4) return compare(a, b);
    if(Number.isNaN(a) && Number.isNaN(b)) return 0;
    if(Number.isNaN(a)) return -1;
    if(Number.isNaN(b)) return +1;
    return compare(a, b);
}

/**
 * @param {TotalOrderable[]} array1
 * @param {TotalOrderable[]} array2
 * @returns {-1 | 0 | 1}
 */
function arrayCompare(array1, array2) {
    for(let i = 0; i < Math.min(array1.length, array2.length); ++i) {
        const cmp = primitiveTotalOrdering(array1[i], array2[i]);
        if(cmp) return cmp;
    }
    if(array1.length < array2.length) return -1;
    if(array1.length > array2.length) return +1;
    return 0;
}

/**
 * @param {unknown} a
 * @param {unknown} b
 * @returns {-1 | 0 | 1}
 */
function compare(a, b) {
    if(Array.isArray(a) && Array.isArray(b)) {
        for(let i = 0; i < Math.min(a.length, b.length); ++i) {
            const cmp = compare(a[i], b[i]);
            if(cmp) return cmp;
        }
        if(a.length < b.length) return -1;
        if(a.length > b.length) return +1;
        return 0;
    }
    if(a < b) return -1;
    if(a > b) return +1;
    return 0;
}

/**
 * @template T
 * @param {(item: T) => unknown} keySelector
 * @returns {(a: T, b: T) => -1 | 0 | 1}
 */
export function comparerByKey(keySelector=item => item, reverse=false) {
    if(reverse) {
        return (a, b) => -compare(keySelector(a), keySelector(b));
    } else {
        return (a, b) => compare(keySelector(a), keySelector(b));
    }
}

/**
 * @template T
 * @template K
 * @param {Iterable<T>} source
 * @param {(item: T) => K} keySelector
 * @param {(a: K, b: K) => boolean} comparer
 * @returns {{ key: K, items: T[] }[]}
 */
export function groupBy(source, keySelector, comparer=Object.is) {
    const groups = [];
    for(let item of source) {
        const key = keySelector(item);
        let group = groups.find(x => comparer(x.key, key));
        if(group === undefined) {
            group = {key, items: []};
            groups.push(group);
        }
        group.items.push(item);
    }
    return groups;
}

/**
 * @template {import('@silane/datetime').Date} T
 * @param {T} start
 * @param {T} end
 * @param {import('@silane/datetime').TimeDelta} step
 */
export function* datetimeRange(start, end, step) {
    for(let cur = start; cmp(cur, end) < 0; cur = add(cur, step)) {
        yield cur;
    }
}

/**
 * @param {import('@silane/datetime').Date} date
 * @param {import('@silane/datetime').Time} startTime
 * @param {import('@silane/datetime').Time} endTime
 * @returns {[DateTime, DateTime]}
 */
export function dateAndTimeRangeToDateTimeRange(date, startTime, endTime) {
    const endDateTime = DateTime.combine(date, endTime);
    return [
        DateTime.combine(date, startTime), cmp(
            endTime, time()
        ) ? endDateTime : add(endDateTime, timedelta({ days: 1 })),
    ];
}

/**
 * @param {File} file 
 * @returns {Promise<string>}
 */
export function readTextFile(file) {
    return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.readAsText(file);
        reader.addEventListener('load', () => {
            resolve(reader.result);
        });
        reader.addEventListener('error', () => {
            reject(reader.error);
        });
    });
}

/**
 * @param {File} file
 */
export function downloadFile(file) {
    const url = URL.createObjectURL(file);

    const anchor = document.createElement('a');
    anchor.href = url;
    anchor.download = file.name;
    document.body.appendChild(anchor);
    anchor.click();
    document.body.removeChild(anchor);

    URL.revokeObjectURL(url);
}
