"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.colIsLoading = void 0;
const React = __importStar(require("react"));
const utils_1 = require("../../utils");
const memoizee_1 = __importDefault(require("memoizee"));
const ReactContext_1 = __importDefault(require("../../ReactContext"));
const components_1 = require("../components");
const react_spinner_1 = __importDefault(require("react-spinner"));
const colIsLoading = (col) => { var _a; return ((_a = col.compareStrategy) === null || _a === void 0 ? void 0 : _a.strategy) !== "default" && !col.compare; };
exports.colIsLoading = colIsLoading;
const colsLoading = (sortCols) => sortCols.filter(exports.colIsLoading).length > 0;
class Comparer {
    constructor(options, colName, formContext) {
        this.options = options;
        this.formContext = formContext;
        this.colName = colName;
    }
}
class TaxonomicComparer extends Comparer {
    constructor(options, colName, formContext) {
        super(options, colName, formContext);
        this.compare = this.compare.bind(this);
    }
    initialize() {
        return __awaiter(this, void 0, void 0, function* () {
            this.idToIdx = (yield this.formContext.apiClient.get("/taxa", { query: Object.assign(Object.assign({}, this.options.query), { pageSize: 10000, selectedFields: "id" }) }))
                .results.reduce((idToIdx, { id }, idx) => {
                idToIdx[id] = idx;
                return idToIdx;
            }, {});
        });
    }
    compare(a, b) {
        const aValue = a[this.options.valueField || this.colName];
        const bValue = b[this.options.valueField || this.colName];
        if (this.idToIdx[aValue] === undefined && this.idToIdx[bValue] !== undefined) {
            return 1;
        }
        else if (this.idToIdx[aValue] !== undefined && this.idToIdx[bValue] === undefined) {
            return -1;
            // Fall back to default comparison if it's neither are in the taxonomic set.
        }
        else if (this.idToIdx[aValue] === undefined && this.idToIdx[bValue] === undefined) {
            return undefined;
        }
        return this.idToIdx[aValue] - this.idToIdx[bValue];
    }
}
class DefaultComparer extends Comparer {
    compare(a, b, sortCol, schema) {
        const { name } = sortCol;
        const colSchema = schema.items.properties[name];
        const aValue = getValue(a[name], colSchema);
        const bValue = getValue(b[name], colSchema);
        if (aValue === undefined && bValue !== undefined || aValue < bValue) {
            return -1;
        }
        else if (aValue !== undefined && bValue === undefined || aValue > bValue) {
            return 1;
        }
        return 0;
    }
}
const compareStrategyMap = {
    taxonomic: TaxonomicComparer,
    default: DefaultComparer
};
const comparers = {};
const ColumnOptionsUI = ({ field, compareStrategies = [], sortCol, updateSortCol, formContext }) => {
    const { MenuItem, Glyphicon, Dropdown } = React.useContext(ReactContext_1.default).theme;
    const selectedStrategy = sortCol === null || sortCol === void 0 ? void 0 : sortCol.compareStrategy;
    const selectedStrategyIdx = selectedStrategy ? compareStrategies.indexOf(selectedStrategy) : 0;
    const onClick = React.useCallback((e) => {
        e.stopPropagation();
    }, []);
    const onSelect = React.useCallback((idx) => {
        const strategy = compareStrategies[idx];
        updateSortCol(Object.assign(Object.assign({}, (sortCol || {})), { compareStrategy: strategy }));
    }, [compareStrategies, sortCol, updateSortCol]);
    if (!compareStrategies) {
        return null;
    }
    return (React.createElement(components_1.TooltipComponent, { tooltip: formContext.translations["SortStrategies"], placement: "left" },
        React.createElement(Dropdown, { id: `${field}-column-options`, onClick: onClick, className: "laji-form-col-options", pullRight: true },
            React.createElement(Dropdown.Toggle, { noCaret: true },
                React.createElement(Glyphicon, { glyph: "cog" })),
            React.createElement(Dropdown.Menu, { onSelect: onSelect }, compareStrategies.map((strategy, i) => {
                const label = formContext.translations[`SortStrategy${(0, utils_1.capitalizeFirstLetter)(strategy.strategy)}`];
                return React.createElement(MenuItem, { key: i, eventKey: i, active: selectedStrategyIdx === i }, label);
            })))));
};
const getUpdateSortCol = (0, memoizee_1.default)((col, sortCols, setSortCols) => (sortCol) => {
    const existingIdx = sortCols.findIndex(c => c.name === col);
    const _sortCols = [...sortCols];
    _sortCols.splice(existingIdx === -1 ? _sortCols.length : existingIdx, 1, Object.assign(Object.assign({}, sortCol), { name: col, descending: sortCol.descending }));
    setSortCols(_sortCols);
});
const getUI = (columns, sortCols, setSortCols, formContext) => {
    return Object.keys(columns || {}).reduce((fieldToUI, field) => {
        var _a, _b;
        fieldToUI[field] = React.createElement(React.Fragment, null,
            colsLoading(sortCols) && React.createElement(react_spinner_1.default, null),
            (((_b = (_a = columns === null || columns === void 0 ? void 0 : columns[field]) === null || _a === void 0 ? void 0 : _a.compareStrategies) === null || _b === void 0 ? void 0 : _b.length) || 0) > 1 && React.createElement(ColumnOptionsUI, Object.assign({ field: field }, columns === null || columns === void 0 ? void 0 : columns[field], { sortCol: sortCols === null || sortCols === void 0 ? void 0 : sortCols.find(c => c.name === field), updateSortCol: getUpdateSortCol(field, sortCols, setSortCols), formContext: formContext })));
        return fieldToUI;
    }, {});
};
const getValue = (formData, schema) => {
    var _a;
    if (schema.oneOf) {
        return (_a = schema.oneOf.find(({ const: v }) => v === formData)) === null || _a === void 0 ? void 0 : _a.title;
    }
    return formData;
};
const sort = (schema, { sortCols }, sortTimeIdToSortedIdx, sortTimeIdToOrigIdx) => (a, b) => {
    var _a, _b;
    // Don't sort items that weren't there when the sorting was done.
    // For example when adding a new item to the array, it should be exactly where it is added.
    // const idToOrigIdx = getIdToOrigIdx(formData);
    const aSortedIdx = sortTimeIdToOrigIdx[(0, utils_1.getUUID)(a)];
    const bSortedIdx = sortTimeIdToOrigIdx[(0, utils_1.getUUID)(b)];
    if (aSortedIdx === undefined || bSortedIdx === undefined) {
        return sortTimeIdToSortedIdx[(0, utils_1.getUUID)(a)] - sortTimeIdToSortedIdx[(0, utils_1.getUUID)(b)];
    }
    const defaultSort = DefaultComparer.prototype.compare;
    let result;
    for (const sortCol of sortCols) {
        result = sortCol.descending === undefined
            ? 0
            : ((_b = (_a = sortCol.compare) === null || _a === void 0 ? void 0 : _a.call(sortCol, a, b, sortCol, schema)) !== null && _b !== void 0 ? _b : defaultSort(a, b, sortCol, schema)) || 0;
        if (result !== 0) {
            result = sortCol.descending ? result * -1 : result;
            break;
        }
    }
    return result;
};
const getSortedData = (0, memoizee_1.default)((formData, schema, state, sortTimeIdToSortedIdx, sortTimeIdToOrigIdx) => {
    return (state.sortCols.length && !colsLoading(state.sortCols))
        ? [...formData].sort(sort(schema, state, sortTimeIdToSortedIdx, sortTimeIdToOrigIdx))
        : formData;
}, { max: 1 });
const getIdToIdx = (formData) => formData.reduce((idToIdx, item, idx) => {
    idToIdx[(0, utils_1.getUUID)(item)] = idx;
    return idToIdx;
}, {});
// These are otherwise identical, but are memoized against different input.
// Use max 2 because current props / next props might be compared.
const getIdToSortedIdx = (0, memoizee_1.default)((formData) => getIdToIdx(formData), { max: 2 });
const getIdToOrigIdx = (0, memoizee_1.default)((formData) => getIdToIdx(formData), { max: 2 });
/**
 * Sorts date according to selected columns. This component provides only the sorting logic,
 * a child component must handle the UI for selecting the sort columns.
 *
 * Can be used only if items are objects (non-objects don't have UUIDs which are used for
 * keeping the original order intact upon changes);
 */
class SortArrayField extends React.Component {
    constructor(props) {
        super(props);
        this.state = { sortCols: [] };
        this.sortTimeIdToSortedIdx = {};
        /**
         * Id-to-idx mapping when the sorting was touched. Used to sorting newly items where they are put, instead of sorting them according to the sort cols.
        */
        this.sortTimeIdToOrigIdx = {};
        // Note: if next component hard sorts the data, this won't work with that.
        // Probably not a common use case to have both hard/soft sorting.
        this.onChange = (formData) => {
            if (!this.state.sortCols.length) {
                this.props.onChange(formData);
                return;
            }
            else {
                const idToOrigIdx = getIdToOrigIdx(this.props.formData);
                this.sortTimeIdToSortedIdx = getIdToSortedIdx(formData);
                const existingItems = [];
                const newItems = [];
                let prevUUID = -1; // Start with UUID that won't match any item.
                const UUIDToPrev = {};
                formData.forEach(i => {
                    const uuid = (0, utils_1.getUUID)(i);
                    if (idToOrigIdx[uuid] !== undefined) {
                        existingItems.push(i);
                    }
                    else {
                        newItems.push(i);
                        UUIDToPrev[uuid] = prevUUID;
                    }
                    prevUUID = uuid;
                });
                const sortedToOriginal = existingItems.sort((a, b) => {
                    const aIdx = idToOrigIdx[(0, utils_1.getUUID)(a)];
                    const bIdx = idToOrigIdx[(0, utils_1.getUUID)(b)];
                    return aIdx - bIdx;
                });
                newItems.forEach(item => {
                    const uuid = (0, utils_1.getUUID)(item);
                    const precedingUUID = UUIDToPrev[uuid];
                    const precedingOriginalIdx = sortedToOriginal.findIndex((i) => (0, utils_1.getUUID)(i) === precedingUUID);
                    const precedingSortedIdx = this.sortTimeIdToSortedIdx[(0, utils_1.getUUID)(item)];
                    // Detect whether i's a new empty item. If it is, put it to the end of the original array.
                    // Otherwise (it's e.g. a copied item), put it in the original array after the preceding item of the sorted order.
                    if (precedingSortedIdx === sortedToOriginal.length && (0, utils_1.isDefaultData)(item, this.props.schema.items)) {
                        sortedToOriginal.push(item);
                    }
                    else {
                        sortedToOriginal.splice(precedingOriginalIdx + 1, 0, item);
                    }
                });
                this.props.onChange(sortedToOriginal);
            }
        };
        props.formContext.services.settings.bind(this, props);
        this.syncColumns();
    }
    componentDidUpdate() {
        this.syncColumns();
    }
    /**
     * Sort cols might be retrieved from settings JSON, which doesn't hold the 'compare' fn. We add the fn
     * asynchronously when the  component updates according to the comparison strategy name.
     */
    syncColumns() {
        const { sortCols } = this.state;
        sortCols.forEach(sortCol => {
            const { compareStrategy } = sortCol;
            if (sortCol.compare) {
                return;
            }
            const finish = (comparer) => {
                const descending = (sortCol === null || sortCol === void 0 ? void 0 : sortCol.descending) === undefined
                    ? true
                    : sortCol === null || sortCol === void 0 ? void 0 : sortCol.descending;
                this.updateSortCol(Object.assign(Object.assign({}, (sortCol || {})), { compareStrategy, compare: comparer.compare, descending }));
            };
            const comparer = comparers[compareStrategy.strategy];
            if (!comparer) {
                comparers[compareStrategy.strategy] = new compareStrategyMap[compareStrategy.strategy](compareStrategy, sortCol.name, this.props.formContext);
                const instance = comparers[compareStrategy.strategy];
                if (instance.initialize) {
                    instance.initialize().then(() => finish(instance));
                }
                else {
                    finish(instance);
                }
            }
            else {
                finish(comparer);
            }
        });
    }
    getUiShema(props, { sortCols }, sortedData) {
        var _a;
        const idToSortedIdx = getIdToSortedIdx(sortedData);
        const idToOrigIdx = getIdToSortedIdx(props.formData);
        const nextComponentUiSchema = (0, utils_1.getInnerUiSchema)(props.uiSchema);
        const uiOptions = (0, utils_1.getUiOptions)(props.uiSchema);
        return Object.assign(Object.assign({}, nextComponentUiSchema), { "ui:options": Object.assign(Object.assign({}, (0, utils_1.getUiOptions)(nextComponentUiSchema)), { onSortToggle: this.onSortToggle.bind(this), sortCols, ui: getUI(uiOptions.columns, sortCols, this.setSortCols.bind(this), props.formContext), sortableColumns: this.getSortableColumns(props), sortColTooltips: (_a = Object.keys(uiOptions.columns || {})) === null || _a === void 0 ? void 0 : _a.reduce((map, c) => {
                    var _a;
                    const { tooltip } = ((_a = uiOptions.columns) === null || _a === void 0 ? void 0 : _a[c]) || {};
                    if (tooltip) {
                        map[c] = tooltip;
                    }
                    return map;
                }, {}), idxMap: Object.keys(idToOrigIdx).reduce((map, id) => {
                    map[idToSortedIdx[id]] = idToOrigIdx[id];
                    map["_" + idToOrigIdx[id]] = idToSortedIdx[id];
                    return map;
                }, {}) }) });
    }
    getSortableColumns(props) {
        const { uiSchema, schema } = props;
        const { sortableColumns, excludeSortableColumns } = (0, utils_1.getUiOptions)(uiSchema);
        if (sortableColumns) {
            if (excludeSortableColumns) {
                return sortableColumns.filter(c => !excludeSortableColumns.includes(c));
            }
            return sortableColumns;
        }
        if (excludeSortableColumns) {
            return Object.keys(schema.items.properties).filter(c => !excludeSortableColumns.includes(c));
        }
        return undefined;
    }
    getNextComponentProps(props, state) {
        const sortedFormData = getSortedData(props.formData || [], props.schema, state, this.sortTimeIdToSortedIdx, this.sortTimeIdToOrigIdx);
        return {
            uiSchema: this.getUiShema(props, state, sortedFormData),
            formData: sortedFormData,
            onChange: this.onChange
        };
    }
    updateSortCol(sortCol) {
        const existingIdx = this.state.sortCols.findIndex(c => c.name === sortCol.name);
        const _sortCols = [...this.state.sortCols];
        _sortCols.splice(existingIdx === -1 ? _sortCols.length : existingIdx, 1, sortCol);
        this.setSortCols(_sortCols);
    }
    setSortCols(sortCols) {
        this.sortTimeIdToSortedIdx = {};
        this.sortTimeIdToOrigIdx = getIdToOrigIdx(this.props.formData);
        this.setState({ sortCols });
    }
    onSortToggle(name) {
        var _a, _b;
        const { sortCols: _sortCols = [] } = this.state || {};
        const sortCols = [..._sortCols];
        const colSortIdx = sortCols === null || sortCols === void 0 ? void 0 : sortCols.findIndex(s => s.name === name);
        const current = sortCols[colSortIdx];
        const options = (0, utils_1.getUiOptions)(this.props.uiSchema);
        const { multisort } = options;
        const colOptions = ((_a = options.columns) === null || _a === void 0 ? void 0 : _a[name]) || {};
        const compareStrategy = (current === null || current === void 0 ? void 0 : current.compareStrategy) || ((_b = colOptions.compareStrategies) === null || _b === void 0 ? void 0 : _b[0]) || { strategy: "default" };
        const { noDescending } = compareStrategy;
        const nextDescending = (current === null || current === void 0 ? void 0 : current.descending) === undefined
            ? false
            : current.descending
                ? undefined
                : noDescending ? undefined : true;
        const sortCol = Object.assign(Object.assign({}, (current || {})), { compareStrategy,
            name, descending: nextDescending });
        if (!multisort) {
            this.setSortCols([sortCol]);
            return;
        }
        if (current && current.descending === undefined) { // Push to end if the "descending" changes from undefined.
            sortCols.splice(colSortIdx, 1);
            sortCols.push(sortCol);
        }
        else { // Replace current with new
            sortCols.splice(current ? colSortIdx : sortCols.length, 1, sortCol);
        }
        this.setSortCols(sortCols);
    }
    render() {
        const SchemaField = this.props.registry.fields.SchemaField; // TODO as any
        const nextProps = this.getNextComponentProps(this.props, this.state);
        return (React.createElement(SchemaField, Object.assign({}, this.props, nextProps, { onChange: this.onChange })));
    }
}
exports.default = SortArrayField;
