import { BindingEventService } from '@slickgrid-universal/binding';
import { createDomElement, setDeepValue, toSentenceCase } from '@slickgrid-universal/utils';
import { Constants } from '../constants.js';
import { SlickEventData } from '../core/index.js';
import { sliderValidator } from '../editorValidators/sliderValidator.js';
import { getDescendantProperty } from '../services/utilities.js';
/*
 * An example of a 'detached' editor.
 * KeyDown events are also handled to provide handling for Tab, Shift-Tab, Esc and Ctrl-Enter.
 */
export class SliderEditor {
    constructor(args) {
        this.args = args;
        this._defaultValue = 0;
        this._isValueTouched = false;
        this._sliderNumberElm = null;
        /** is the Editor disabled? */
        this.disabled = false;
        this.grid = args.grid;
        this.gridOptions = (this.grid.getOptions() || {});
        this._bindEventService = new BindingEventService();
        this.init();
    }
    /** Get Column Definition object */
    get columnDef() {
        return this.args.column;
    }
    /** Get Column Editor object */
    get columnEditor() {
        return this.columnDef?.editor ?? {};
    }
    /** Getter for the item data context object */
    get dataContext() {
        return this.args.item;
    }
    /** Getter for the Editor DOM Element */
    get editorDomElement() {
        return this._editorElm;
    }
    /** Getter for the Editor Input DOM Element */
    get editorInputDomElement() {
        return this._inputElm;
    }
    get editorOptions() {
        return { ...this.gridOptions.defaultEditorOptions?.slider, ...this.columnEditor?.options };
    }
    get hasAutoCommitEdit() {
        return this.gridOptions.autoCommitEdit ?? false;
    }
    /** Getter for the current Slider Options */
    get sliderOptions() {
        return this._sliderOptions;
    }
    /** Get the Validator function, can be passed in Editor property or Column Definition */
    get validator() {
        return this.columnEditor.validator ?? this.columnDef?.validator;
    }
    init() {
        this._cellContainerElm = this.args?.container;
        if (this._cellContainerElm && this.columnDef) {
            // define the input & slider number IDs
            const compositeEditorOptions = this.args.compositeEditorOptions;
            // create HTML string template
            this._editorElm = this.buildDomElement();
            // watch on change event
            this._cellContainerElm.appendChild(this._editorElm);
            this._bindEventService.bind(this._sliderTrackElm, ['click', 'mouseup'], this.sliderTrackClicked.bind(this));
            this._bindEventService.bind(this._inputElm, ['change', 'mouseup', 'touchend'], this.handleChangeEvent.bind(this));
            this._bindEventService.bind(this._inputElm, ['keydown'], this.handleKeyDown.bind(this));
            // if user chose to display the slider number on the right side, then update it every time it changes
            // we need to use both "input" and "change" event to be all cross-browser
            this._bindEventService.bind(this._inputElm, ['input', 'change'], this.handleChangeSliderNumber.bind(this));
            if (!compositeEditorOptions) {
                this.focus();
            }
        }
    }
    cancel() {
        if (this._inputElm) {
            this._inputElm.value = `${this._originalValue}`;
        }
        this.args.cancelChanges();
    }
    destroy() {
        this._bindEventService.unbindAll();
        this._inputElm?.remove();
        this._editorElm?.remove();
        this._sliderTrackElm?.remove();
    }
    disable(isDisabled = true) {
        const prevIsDisabled = this.disabled;
        this.disabled = isDisabled;
        if (this._inputElm) {
            if (isDisabled) {
                this._inputElm.disabled = true;
                // clear value when it's newly disabled and not empty
                const currentValue = this.getValue();
                if (prevIsDisabled !== isDisabled && this.args?.compositeEditorOptions && currentValue !== '') {
                    this.reset(0, true, true);
                }
            }
            else {
                this._inputElm.disabled = false;
            }
        }
    }
    focus() {
        // always set focus on grid first so that plugin to copy range (SlickCellExternalCopyManager) would still be able to paste at that position
        this.grid.focus();
        this._inputElm?.focus();
    }
    show() {
        if (this.args.isCompositeEditor) {
            // when it's a Composite Editor, we'll check if the Editor is editable (by checking onBeforeEditCell) and if not Editable we'll disable the Editor
            this.applyInputUsabilityState();
        }
    }
    getValue() {
        return this._inputElm?.value ?? '';
    }
    setValue(value, isApplyingValue = false, triggerOnCompositeEditorChange = true) {
        if (this._inputElm) {
            this._inputElm.value = `${value}`;
        }
        if (this._sliderNumberElm) {
            this._sliderNumberElm.textContent = `${value}`;
        }
        if (isApplyingValue) {
            this.applyValue(this.args.item, this.serializeValue());
            // if it's set by a Composite Editor, then also trigger a change for it
            const compositeEditorOptions = this.args.compositeEditorOptions;
            if (compositeEditorOptions && triggerOnCompositeEditorChange) {
                this.handleChangeOnCompositeEditor(null, compositeEditorOptions, 'system');
            }
        }
    }
    applyValue(item, state) {
        const fieldName = this.columnDef?.field ?? '';
        if (fieldName !== undefined) {
            const isComplexObject = fieldName?.indexOf('.') > 0; // is the field a complex object, "address.streetNumber"
            const validation = this.validate(undefined, state);
            const newValue = validation && validation.valid ? state : '';
            // set the new value to the item datacontext
            if (isComplexObject) {
                // when it's a complex object, user could override the object path (where the editable object is located)
                // else we use the path provided in the Field Column Definition
                const objectPath = this.columnEditor.complexObjectPath ?? fieldName ?? '';
                setDeepValue(item, objectPath, newValue);
            }
            else if (item) {
                item[fieldName] = newValue;
            }
        }
    }
    /**
     * Dynamically change an Editor option, this is especially useful with Composite Editor
     * since this is the only way to change option after the Editor is created (for example dynamically change "minDate" or another Editor)
     * @param {string} optionName - Slider editor option name
     * @param {newValue} newValue - Slider editor new option value
     */
    changeEditorOption(optionName, newValue) {
        if (this.columnEditor) {
            this.columnEditor.options ??= {};
            this.columnEditor.options[optionName] = newValue;
            this._sliderOptions[optionName] = newValue;
            switch (optionName) {
                case 'hideSliderNumber':
                    this.renderSliderNumber(this._editorElm, 0);
                    break;
                case 'sliderStartValue':
                    this._inputElm.value = `${newValue}`;
                    this._inputElm.defaultValue = `${newValue}`;
                    break;
                case 'maxValue':
                case 'minValue':
                case 'step':
                    this._inputElm[optionName.replace('Value', '')] = `${newValue}`;
                    break;
            }
        }
    }
    isValueChanged() {
        const elmValue = this._inputElm?.value ?? '';
        return !(elmValue === '' && this._originalValue === undefined) && +elmValue !== this._originalValue;
    }
    isValueTouched() {
        return this._isValueTouched;
    }
    loadValue(item) {
        const fieldName = this.columnDef?.field ?? '';
        if (item && fieldName !== undefined) {
            // is the field a complex object, "address.streetNumber"
            const isComplexObject = fieldName?.indexOf('.') > 0;
            // prettier-ignore
            let value = (isComplexObject) ? getDescendantProperty(item, fieldName) : (item.hasOwnProperty(fieldName) ? item[fieldName] : this._defaultValue);
            if (value === '' || value === null || value === undefined) {
                value = this._defaultValue; // load default value when item doesn't have any value
            }
            this._originalValue = +value;
            if (this._inputElm) {
                this._inputElm.value = `${value}`;
                this._inputElm.title = `${value}`;
            }
            if (this._sliderNumberElm) {
                this._sliderNumberElm.textContent = `${value}`;
            }
        }
        this.updateTrackFilledColorWhenEnabled();
    }
    /**
     * You can reset or clear the input value,
     * when no value is provided it will use the original value to reset (could be useful with Composite Editor Modal with edit/clone)
     */
    reset(value, triggerCompositeEventWhenExist = true, clearByDisableCommand = false) {
        const inputValue = value ?? this._originalValue ?? 0;
        if (this._inputElm) {
            this._inputElm.value = `${inputValue}`;
        }
        if (this._sliderNumberElm) {
            this._sliderNumberElm.textContent = `${inputValue}`;
        }
        this._isValueTouched = false;
        const compositeEditorOptions = this.args.compositeEditorOptions;
        if (compositeEditorOptions && triggerCompositeEventWhenExist) {
            const shouldDeleteFormValue = !clearByDisableCommand;
            this.handleChangeOnCompositeEditor(null, compositeEditorOptions, 'user', shouldDeleteFormValue);
        }
    }
    save() {
        if (this.hasAutoCommitEdit && this.validate()?.valid) {
            // do not use args.commitChanges() as this sets the focus to the next row.
            // also the select list will stay shown when clicking off the grid
            this.grid.getEditorLock().commitCurrentEdit();
        }
        else {
            this.args.commitChanges();
        }
    }
    serializeValue() {
        const elmValue = this._inputElm?.value ?? '';
        return elmValue !== '' ? parseInt(elmValue, 10) : this._originalValue;
    }
    validate(_targetElm, options) {
        // when using Composite Editor, we also want to recheck if the field if disabled/enabled since it might change depending on other inputs on the composite form
        if (this.args.isCompositeEditor) {
            this.applyInputUsabilityState();
        }
        // when field is disabled, we can assume it's valid
        if (this.disabled) {
            return { valid: true, msg: '' };
        }
        const elmValue = options?.inputValue ?? this._inputElm?.value;
        return sliderValidator(elmValue, {
            editorArgs: this.args,
            errorMessage: this.columnEditor.errorMessage,
            minValue: this.columnEditor.minValue,
            maxValue: this.columnEditor.maxValue,
            required: this.args.isCompositeEditor ? false : this.columnEditor.required,
            validator: this.validator,
        });
    }
    //
    // protected functions
    // ------------------
    /**
     * Create the HTML template as a string
     */
    buildDomElement() {
        const columnId = this.columnDef?.id ?? '';
        const title = this.columnEditor.title ?? '';
        // merge options with optional user's custom options
        this._sliderOptions = this.getSliderConfigs();
        const defaultValue = this.editorOptions.sliderStartValue ?? this._sliderOptions.minValue;
        this._defaultValue = +defaultValue;
        this._sliderTrackElm = createDomElement('div', { className: 'slider-track' });
        this._inputElm = createDomElement('input', {
            type: 'range',
            title,
            defaultValue: `${defaultValue}`,
            value: `${defaultValue}`,
            min: `${this._sliderOptions.minValue}`,
            max: `${this._sliderOptions.maxValue}`,
            step: `${this.columnEditor.valueStep ?? Constants.SLIDER_DEFAULT_STEP}`,
            ariaLabel: this.columnEditor.ariaLabel ?? `${toSentenceCase(`${columnId}`)} Slider Editor`,
            className: `slider-editor-input editor-${columnId}`,
        });
        const divContainerElm = createDomElement('div', { className: 'slider-container slider-editor' });
        const sliderInputContainerElm = createDomElement('div', { className: 'slider-input-container slider-editor' });
        sliderInputContainerElm.appendChild(this._sliderTrackElm);
        sliderInputContainerElm.appendChild(this._inputElm);
        divContainerElm.appendChild(sliderInputContainerElm);
        this.renderSliderNumber(divContainerElm, defaultValue);
        return divContainerElm;
    }
    /** get default slider defaults */
    getSliderConfigs() {
        return {
            minValue: +(this.columnEditor.minValue ?? Constants.SLIDER_DEFAULT_MIN_VALUE),
            maxValue: +(this.columnEditor.maxValue ?? Constants.SLIDER_DEFAULT_MAX_VALUE),
            step: +(this.columnEditor.valueStep ?? Constants.SLIDER_DEFAULT_STEP),
        };
    }
    renderSliderNumber(divContainerElm, defaultValue) {
        if (!this.editorOptions.hideSliderNumber) {
            divContainerElm.classList.add('input-group');
            const divGroupAddonElm = createDomElement('div', { className: 'input-group-addon input-group-append slider-value' });
            this._sliderNumberElm = createDomElement('span', { className: `input-group-text`, textContent: `${defaultValue}` });
            divGroupAddonElm.appendChild(this._sliderNumberElm);
            divContainerElm.appendChild(divGroupAddonElm);
        }
        else {
            divContainerElm.querySelector('.slider-value')?.remove();
        }
    }
    /** when it's a Composite Editor, we'll check if the Editor is editable (by checking onBeforeEditCell) and if not Editable we'll disable the Editor */
    applyInputUsabilityState() {
        const activeCell = this.grid.getActiveCell();
        const isCellEditable = this.grid.onBeforeEditCell
            .notify({
            ...activeCell,
            item: this.dataContext,
            column: this.args.column,
            grid: this.grid,
            target: 'composite',
            compositeEditorOptions: this.args.compositeEditorOptions,
        })
            .getReturnValue();
        this.disable(isCellEditable === false);
    }
    handleChangeEvent(event) {
        this._isValueTouched = true;
        const compositeEditorOptions = this.args.compositeEditorOptions;
        if (compositeEditorOptions) {
            this.handleChangeOnCompositeEditor(event, compositeEditorOptions);
        }
        else {
            this.save();
        }
    }
    handleChangeSliderNumber(event) {
        const value = event.target?.value ?? '';
        if (value !== '') {
            if (!this.editorOptions.hideSliderNumber && this._sliderNumberElm) {
                this._sliderNumberElm.textContent = value;
            }
            this._inputElm.title = value;
            // trigger mouse enter event on the editor for optionally hooked SlickCustomTooltip
            if (!this.args?.compositeEditorOptions) {
                this.grid.onMouseEnter.notify({ column: this.columnDef, grid: this.grid }, new SlickEventData(event));
            }
        }
        this.updateTrackFilledColorWhenEnabled();
    }
    handleChangeOnCompositeEditor(event, compositeEditorOptions, triggeredBy = 'user', isCalledByClearValue = false) {
        const activeCell = this.grid.getActiveCell();
        const column = this.args.column;
        const columnId = this.columnDef?.id ?? '';
        const item = this.dataContext;
        const grid = this.grid;
        const newValue = this.serializeValue();
        // when valid, we'll also apply the new value to the dataContext item object
        if (this.validate().valid) {
            this.applyValue(this.dataContext, newValue);
        }
        this.applyValue(compositeEditorOptions.formValues, newValue);
        const isExcludeDisabledFieldFormValues = this.gridOptions?.compositeEditorOptions?.excludeDisabledFieldFormValues ?? false;
        if (isCalledByClearValue ||
            (this.disabled && isExcludeDisabledFieldFormValues && compositeEditorOptions.formValues.hasOwnProperty(columnId))) {
            delete compositeEditorOptions.formValues[columnId]; // when the input is disabled we won't include it in the form result object
        }
        grid.onCompositeEditorChange.notify({
            ...activeCell,
            item,
            grid,
            column,
            formValues: compositeEditorOptions.formValues,
            editors: compositeEditorOptions.editors,
            triggeredBy,
        }, new SlickEventData(event));
    }
    /** use keydown event to increase/decrease slider value */
    handleKeyDown(e) {
        if (this.editorOptions.useArrowToSlide !== false && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) {
            const { minValue, maxValue, step } = this.getSliderConfigs();
            const currentValue = Number(this._inputElm.value);
            let newValue = e.key === 'ArrowLeft' ? currentValue - step : currentValue + step;
            newValue = newValue < minValue ? minValue : newValue > maxValue ? maxValue : newValue; // make we are within limits
            // Update input value & trigger an event to update tooltip as well
            this._inputElm.value = `${newValue}`;
            this.handleChangeSliderNumber(e);
            // Prevent default arrow key behavior
            e.stopPropagation();
            e.preventDefault();
        }
    }
    sliderTrackClicked(e) {
        e.preventDefault();
        const sliderTrackX = e.offsetX;
        const sliderTrackWidth = this._sliderTrackElm.offsetWidth;
        const trackPercentPosition = ((sliderTrackX + 0) * 100) / sliderTrackWidth;
        if (this._inputElm) {
            // automatically move to calculated clicked percentage
            // dispatch a change event to update its value & number when shown
            this._inputElm.value = `${trackPercentPosition}`;
            this._inputElm.dispatchEvent(new Event('change'));
        }
    }
    updateTrackFilledColorWhenEnabled() {
        if (this.editorOptions.enableSliderTrackColoring && this._inputElm) {
            const percent1 = 0;
            const percent2 = ((+this.getValue() - +this._inputElm.min) / (this.sliderOptions?.maxValue ?? 0 - +this._inputElm.min)) * 100;
            const bg = 'linear-gradient(to right, %b %p1, %c %p1, %c %p2, %b %p2)'
                .replace(/%b/g, '#eee')
                .replace(/%c/g, (this.editorOptions?.sliderTrackFilledColor ?? 'var(--slick-slider-filter-thumb-color, #86bff8)'))
                .replace(/%p1/g, `${percent1}%`)
                .replace(/%p2/g, `${percent2}%`);
            this._sliderTrackElm.style.background = bg;
            this._sliderOptions.sliderTrackBackground = bg;
        }
    }
}
//# sourceMappingURL=sliderEditor.js.map