import React from "react";
import { isObjectType } from "../schema";
import { CollapsibleItemTemplate } from "./array-field-collapsible-item";
import { SimpleItemTemplate } from "./array-field-simple-item";
import { renderDescription, renderTitle } from "./common";
import Sortable from "sortablejs";
const DRAG_HANDLE_SELECTOR = '[data-drag-handle]';
const DRAGGABLE_ITEM_SELECTOR = '.array-item[data-reorderable="true"]';
const DEFAULT_CONTAINER_CLASS = 'has-an-item-which-is-being-dragged';
const DEFAULT_DROP_ELEVATION_CLASS = 'is-elevated';
const DROP_ELEVATION_DURATION = 1000;
const TOUCH_DRAG_DELAY_MS = 200; // Adds a short hold on touch (long-press) so scroll gestures do not reorder items.
export class ArrayFieldTemplate extends React.Component {
    constructor(props) {
        super(props);
        this.itemByIndex = new Map();
        this.handleAddClick = (event) => {
            event.stopPropagation();
            this.props.onAddClick(event);
        };
        this.setContainer = (element) => {
            if (this.container === element) {
                return;
            }
            this.teardownDragController();
            this.container = element !== null && element !== void 0 ? element : undefined;
            this.setupDragController();
        };
        this.handleSortStart = (event) => {
            if (!this.canReorderItems()) {
                return;
            }
            this.dragSnapshot = [...this.state.order];
            this.draggedItemIndex = this.getReorderId(event.item);
            if (this.container) {
                this.container.classList.add(DEFAULT_CONTAINER_CLASS);
            }
            if (event.item instanceof HTMLElement) {
                this.applyDropElevation(event.item);
            }
        };
        this.handleSortEnd = (event) => {
            var _a;
            if (!this.canReorderItems()) {
                return;
            }
            if (this.container) {
                this.container.classList.remove(DEFAULT_CONTAINER_CLASS);
            }
            const snapshot = this.dragSnapshot;
            const draggedItemIndex = this.draggedItemIndex;
            this.dragSnapshot = undefined;
            this.draggedItemIndex = undefined;
            if (event.item instanceof HTMLElement) {
                this.dropElevationTarget = event.item;
            }
            const finalOrder = this.readOrderFromDom();
            if (!this.arraysEqual(this.state.order, finalOrder)) {
                this.setState({ order: finalOrder });
            }
            if (snapshot === undefined ||
                draggedItemIndex === undefined ||
                !(event.item instanceof HTMLElement)) {
                this.scheduleDropElevationRemoval();
                return;
            }
            const fromPosition = snapshot.indexOf(draggedItemIndex);
            const toPosition = finalOrder.indexOf(draggedItemIndex);
            if (fromPosition === -1 ||
                toPosition === -1 ||
                fromPosition === toPosition) {
                this.scheduleDropElevationRemoval();
                return;
            }
            const targetItem = ((_a = this.props.items) !== null && _a !== void 0 ? _a : []).find((entry) => {
                var _a;
                return ((_a = entry.index) !== null && _a !== void 0 ? _a : -1) === draggedItemIndex;
            });
            if (!targetItem || typeof targetItem.onReorderClick !== 'function') {
                this.scheduleDropElevationRemoval();
                return;
            }
            requestAnimationFrame(() => {
                const reorder = targetItem.onReorderClick(draggedItemIndex, toPosition);
                if (typeof reorder === 'function') {
                    reorder();
                }
            });
            this.scheduleDropElevationRemoval();
        };
        this.state = {
            order: this.extractIndices(props.items),
        };
    }
    componentWillUnmount() {
        this.teardownDragController();
    }
    componentDidMount() {
        this.setupDragController();
    }
    componentDidUpdate(previousProps) {
        if (previousProps.items !== this.props.items) {
            const nextOrder = this.extractIndices(this.props.items);
            if (!this.arraysEqual(this.state.order, nextOrder)) {
                this.setState({
                    order: nextOrder,
                }, () => {
                    this.setupDragController();
                });
                return;
            }
        }
        if (previousProps.items !== this.props.items ||
            previousProps.schema !== this.props.schema ||
            previousProps.disabled !== this.props.disabled ||
            previousProps.readonly !== this.props.readonly) {
            this.setupDragController();
        }
    }
    render() {
        const controls = this.getItemControls();
        const { ordered: orderedItems, byIndex } = this.getOrderedItems();
        this.itemByIndex = byIndex;
        return React.createElement('div', {}, renderTitle(this.props.title), renderDescription(this.props.schema.description), React.createElement('div', {
            className: 'array-items',
            ref: this.setContainer,
        }, orderedItems.map((item, index) => this.renderItem(item, index, controls))), this.renderAddButton());
    }
    renderAddButton() {
        if (!this.props.canAdd) {
            return;
        }
        return React.createElement('limel-button', {
            label: this.props.title || 'Add',
            onClick: this.handleAddClick,
            icon: 'plus_math',
            class: 'button-add-new',
        });
    }
    getItemControls() {
        return {
            allowItemRemoval: this.canRemoveItems(),
        };
    }
    renderItem(item, index, controls) {
        var _a;
        const { schema, formData, formContext } = this.props;
        const itemIndex = (_a = item.index) !== null && _a !== void 0 ? _a : index;
        const allowItemReorder = this.isItemReorderable(item);
        if (isObjectType(schema.items)) {
            return React.createElement(CollapsibleItemTemplate, {
                key: item.key,
                item: item,
                data: Array.isArray(formData) ? formData[itemIndex] : undefined,
                schema: schema,
                formSchema: formContext.schema,
                index: itemIndex,
                allowItemRemoval: controls.allowItemRemoval,
                allowItemReorder: allowItemReorder,
            });
        }
        return React.createElement(SimpleItemTemplate, {
            key: item.key,
            item: item,
            index: itemIndex,
            dataIndex: itemIndex,
            allowItemRemoval: controls.allowItemRemoval,
            allowItemReorder: allowItemReorder,
        });
    }
    getOrderedItems() {
        var _a, _b;
        const items = (_a = this.props.items) !== null && _a !== void 0 ? _a : [];
        const byIndex = new Map();
        let entryIndex = 0;
        for (const entry of items) {
            byIndex.set((_b = entry.index) !== null && _b !== void 0 ? _b : entryIndex, entry);
            entryIndex += 1;
        }
        const ordered = [];
        const used = new Set();
        for (const index of this.state.order) {
            const entry = byIndex.get(index);
            if (!entry) {
                continue;
            }
            ordered.push(entry);
            used.add(index);
        }
        for (const [index, entry] of byIndex.entries()) {
            if (!used.has(index)) {
                ordered.push(entry);
            }
        }
        return {
            ordered,
            byIndex,
        };
    }
    setupDragController() {
        if (!this.container || !this.canReorderItems()) {
            this.teardownDragController();
            return;
        }
        const reorderableCount = this.getReorderableOrder().length;
        if (reorderableCount < 2) {
            this.teardownDragController();
            return;
        }
        if (this.sortable) {
            this.sortable.option('handle', DRAG_HANDLE_SELECTOR);
            this.sortable.option('draggable', DRAGGABLE_ITEM_SELECTOR);
            this.sortable.option('disabled', false);
            this.sortable.option('delay', TOUCH_DRAG_DELAY_MS);
            this.sortable.option('delayOnTouchOnly', true);
            return;
        }
        this.sortable = Sortable.create(this.container, {
            animation: 150,
            handle: DRAG_HANDLE_SELECTOR,
            draggable: DRAGGABLE_ITEM_SELECTOR,
            delay: TOUCH_DRAG_DELAY_MS,
            delayOnTouchOnly: true,
            onStart: this.handleSortStart,
            onEnd: this.handleSortEnd,
        });
    }
    teardownDragController() {
        if (this.sortable) {
            this.sortable.destroy();
            this.sortable = undefined;
        }
        this.clearDropElevationTimer();
        if (this.dropElevationTarget) {
            this.dropElevationTarget.classList.remove(DEFAULT_DROP_ELEVATION_CLASS);
            this.dropElevationTarget = undefined;
        }
        if (this.container) {
            this.container.classList.remove(DEFAULT_CONTAINER_CLASS);
        }
        this.dragSnapshot = undefined;
        this.draggedItemIndex = undefined;
    }
    canReorderItems() {
        if (this.props.disabled || this.props.readonly) {
            return false;
        }
        const schema = this.props.schema;
        const limeOptions = (schema === null || schema === void 0 ? void 0 : schema.lime) || {};
        return limeOptions.allowItemReorder !== false;
    }
    canRemoveItems() {
        const schema = this.props.schema;
        const limeOptions = (schema === null || schema === void 0 ? void 0 : schema.lime) || {};
        return limeOptions.allowItemRemoval !== false;
    }
    isItemReorderable(item) {
        return (this.canReorderItems() &&
            Boolean((item === null || item === void 0 ? void 0 : item.hasMoveDown) || (item === null || item === void 0 ? void 0 : item.hasMoveUp)));
    }
    isIndexReorderable(index) {
        const item = this.itemByIndex.get(index);
        if (!item) {
            return false;
        }
        return this.isItemReorderable(item);
    }
    getReorderableOrder(order = this.state.order) {
        const result = [];
        for (const index of order) {
            if (this.isIndexReorderable(index)) {
                result.push(index);
            }
        }
        return result;
    }
    readOrderFromDom() {
        if (!this.container) {
            return [];
        }
        // Only read the order of the direct children of this array field.
        // Nested array fields may render `.array-item` elements inside items,
        // and those must not affect the parent order.
        const items = [...this.container.children].filter((element) => {
            var _a, _b;
            return Boolean((_b = (_a = element === null || element === void 0 ? void 0 : element.classList) === null || _a === void 0 ? void 0 : _a.contains) === null || _b === void 0 ? void 0 : _b.call(_a, 'array-item'));
        });
        const order = [];
        for (const element of items) {
            const index = this.getReorderId(element);
            if (index !== undefined) {
                order.push(index);
            }
        }
        return order;
    }
    getReorderId(element) {
        var _a;
        if (!element) {
            return undefined;
        }
        const value = (_a = element === null || element === void 0 ? void 0 : element.dataset) === null || _a === void 0 ? void 0 : _a.reorderId;
        if (value === undefined) {
            return undefined;
        }
        const parsed = Number.parseInt(value, 10);
        return Number.isNaN(parsed) ? undefined : parsed;
    }
    applyDropElevation(item) {
        if (this.dropElevationTarget && this.dropElevationTarget !== item) {
            this.dropElevationTarget.classList.remove(DEFAULT_DROP_ELEVATION_CLASS);
        }
        this.clearDropElevationTimer();
        item.classList.add(DEFAULT_DROP_ELEVATION_CLASS);
        this.dropElevationTarget = item;
    }
    scheduleDropElevationRemoval() {
        if (!this.dropElevationTarget) {
            return;
        }
        const target = this.dropElevationTarget;
        this.clearDropElevationTimer();
        this.dropElevationTimeout = globalThis.setTimeout(() => {
            target.classList.remove(DEFAULT_DROP_ELEVATION_CLASS);
            if (this.dropElevationTarget === target) {
                this.dropElevationTarget = undefined;
            }
            this.dropElevationTimeout = undefined;
        }, DROP_ELEVATION_DURATION);
    }
    clearDropElevationTimer() {
        if (this.dropElevationTimeout !== undefined) {
            clearTimeout(this.dropElevationTimeout);
            this.dropElevationTimeout = undefined;
        }
    }
    arraysEqual(a, b) {
        if (a.length !== b.length) {
            return false;
        }
        let index = 0;
        for (const value of a) {
            if (value !== b[index]) {
                return false;
            }
            index += 1;
        }
        return true;
    }
    extractIndices(items = []) {
        return (items !== null && items !== void 0 ? items : []).map((item, index) => { var _a; return (_a = item.index) !== null && _a !== void 0 ? _a : index; });
    }
}
