/*!
 * Custom elements for Nanopore-Digital Web applications
 */
import { filter as smartFilter } from "smart-array-filter";
import { getNestedProperty } from "./table.property-utils";
const dataWorkers = new Map();
function uid() {
    return Date.now().toString(36) + Math.random().toString(36).substring(2);
}
/**
 * Creates a new worker cache / store with the given data
 * @param rows
 * @param columns
 * @returns the newly created worker store ID
 */
export async function createWorkerStore(rows, columns) {
    const id = uid();
    dataWorkers.set(id, { rows, columns: colsFromStore(columns) });
    return id;
}
export async function syncDataToWorker(workerId, rows) {
    const workerStore = dataWorkers.get(workerId);
    if (!workerStore)
        throw new Error('cannot find worker with ID ' + workerId);
    workerStore.rows = rows;
    if (!workerStore.rows && !workerStore.columns)
        destroyWorkerStore(workerId);
    return true;
}
/**
 * Sync the column config to the worker store
 * @param workerId
 * @param columns
 * @returns `true` upon success
 */
export async function syncConfigToWorker(workerId, columns) {
    const workerStore = dataWorkers.get(workerId);
    if (!workerStore)
        throw new Error('cannot find worker with ID ' + workerId);
    workerStore.columns = colsFromStore(columns);
    if (!workerStore.rows && !workerStore.columns)
        destroyWorkerStore(workerId);
    return true;
}
/** SEARCH */
/**
 * A general text search across all table columns.
 * Search is the first thing we do. Cache the result for filtering / sorting
 * @param workerId
 * @param term
 * @returns the searched for data rows - with col filters / sorts also applied
 */
export async function workerSearch(workerId, term) {
    const workerStore = dataWorkers.get(workerId);
    if (!workerStore)
        throw new Error('cannot find worker with ID ' + workerId);
    workerStore.searchTerm = term;
    if (!term || !term.length) {
        workerStore.filterRows = workerStore.rows;
        return applyFiltersAndSort(workerStore, workerStore.rows);
    }
    const availCols = workerStore.columns.filter((c) => c.searchable !== false);
    // cache result
    workerStore.filterRows = smartFilter(workerStore.rows, {
        keywords: term,
        predicate: 'OR',
        includePaths: [...availCols.map((c) => c.prop)],
        ignorePaths: ['__uuid', '__index'],
    });
    // apply filter and sort
    return applyFiltersAndSort(workerStore, workerStore.filterRows);
}
/** FILTER */
/**
 * Finds any currently applied filter conditions from the column config
 * @param workerStore
 * @returns the found column filter object array
 */
function getCurrentFilters(workerStore) {
    return workerStore.columns
        .filter((c) => c.filter !== undefined && c.filter !== null)
        .reduce((filters, col) => {
        const { prop, filter } = col;
        filters.push({ prop, filter });
        return filters;
    }, []);
}
function filter(workerStore, rows, filters = [], force = false) {
    if ((!filters || !filters.length) && !force)
        filters = getCurrentFilters(workerStore);
    if (!filters || !filters.length)
        return rows;
    filters.forEach((filter) => {
        if (typeof filter.filter === 'boolean') {
            rows = rows.filter((row) => getNestedProperty(row, filter.prop) === filter.filter);
        }
        else {
            rows = smartFilter(rows, {
                predicate: typeof filter.filter === 'string' ? 'AND' : 'OR',
                keywords: filter.filter,
                includePaths: [filter.prop],
                ignorePaths: ['__uuid', '__index'],
            });
        }
    });
    return rows;
}
/**
 * Filter the data rows based on the given filters
 * @param workerId
 * @param filters
 * @returns the filtered data rows
 */
export async function workerFilter(workerId, filters) {
    const workerStore = dataWorkers.get(workerId);
    if (!workerStore)
        throw new Error('cannot find worker with ID ' + workerId);
    // get cached results from a search
    const data = (!filters || !filters.length) && workerStore.searchTerm
        ? await workerSearch(workerId, workerStore.searchTerm)
        : workerStore.searchTerm
            ? workerStore.filterRows
            : workerStore.rows;
    workerStore.filterRows = filter(workerStore, data, filters, true);
    // apply sort
    return sort(workerStore, workerStore.filterRows);
}
/** SORT */
/**
 * Finds any currently applied sort conditions from the column config
 * @param workerStore
 * @returns the found column name and it's sort direction
 */
function getCurrentSort(workerStore) {
    const foundSort = workerStore.columns.find((c) => !!c.order);
    if (foundSort) {
        const { prop, order } = foundSort;
        return { prop, order };
    }
    return null;
}
function sort(workerStore, rows, prop, order) {
    if (!prop && !order) {
        const currentSort = getCurrentSort(workerStore);
        if (currentSort) {
            ({ prop, order } = currentSort);
        }
    }
    if (!prop || !order || !rows.length)
        return rows;
    const col = workerStore.columns.find((c) => c.prop === prop);
    // custom sort
    if (!!col?.sortCompareFn && typeof col.sortCompareFn === 'function') {
        const sorted = rows.slice().sort(col.sortCompareFn(prop, order));
        return sorted;
    }
    // text sort
    if (col?.type === 'text' &&
        typeof getNestedProperty(rows[0], prop) === 'string') {
        const sorted = rows.slice().sort((a, b) => {
            const aVal = getNestedProperty(a, prop);
            const bVal = getNestedProperty(b, prop);
            if (!aVal)
                return 1;
            if (!bVal)
                return -1;
            return order === 'asc'
                ? aVal.localeCompare(bVal)
                : bVal.localeCompare(aVal);
        });
        return sorted;
    }
    // catch-all - number / date / string sort
    const sorted = rows.slice().sort((a, b) => {
        const aVal = getNestedProperty(a, prop);
        const bVal = getNestedProperty(b, prop);
        if ([undefined, null].includes(aVal))
            return 1;
        if ([undefined, null].includes(bVal))
            return -1;
        if (aVal < bVal)
            return order === 'asc' ? -1 : 1;
        if (aVal > bVal)
            return order === 'asc' ? 1 : -1;
        return 0;
    });
    return sorted;
}
/**
 * Finds and applies the sort conditions to the data rows
 * @param workerId
 * @param prop to sort by
 * @param order order to sort by
 * @returns the sorted data rows
 */
export async function workerSort(workerId, prop, order) {
    // Sorting is the last thing we do - don't cache this, there's no point
    const workerStore = dataWorkers.get(workerId);
    if (!workerStore)
        throw new Error('cannot find worker with ID ' + workerId);
    // get cached results from a search / filter
    const data = workerStore.filterRows || workerStore.rows;
    return sort(workerStore, data, prop, order);
}
/** UTILS */
// used after a search
function applyFiltersAndSort(workerStore, rows) {
    rows = filter(workerStore, rows);
    return sort(workerStore, rows);
}
function colsFromStore(safeColumns) {
    return safeColumns.map((c) => {
        if (c?.sortCompareFn && typeof c.sortCompareFn === 'string') {
            // eslint-disable-next-line @typescript-eslint/no-implied-eval
            c.sortCompareFn = new Function('return ' + c.sortCompareFn)();
        }
        return c;
    });
}
/**
 * Clean up
 * @param workerId
 */
export async function destroyWorkerStore(workerId) {
    dataWorkers.delete(workerId);
}
