import noUiSlider from 'nouislider';
import { extractPrefixedAttributesFromElement } from '@nimius/dom-utility';
import { fireEvent } from '@nimius/event-utility';

/**
 * @typedef {object} ToubizRangeSlider~options
 * @property {object} userField
 * @property {string} userField.min
 * @property {string} userField.max
 * @property {object} bounds
 * @property {number} bounds.min
 * @property {number} bounds.max
 * @property {object} dataField
 * @property {string} [dataField.min]
 * @property {string} [dataField.max]
 * @property {number} [factor]
 * @property {number} [step]
 * @property {string} [decimals]
 * @property {string} [roundFactor]
 * @property {string} [changeEvent]
 */

/**
 * @typedef {object} ToubizRangeSlider~state
 * @property {number} min
 * @property {number} max
 */

/**
 * The example below shows a range-slider with two mandatory input elements
 * defined by 'data-toubiz-range-slider-input.<min / max>' (#target-input-min, #target-input-max)
 * and some optional outputs defined by 'data-toubiz-range-slider-outputs.<min / max>'
 * (#target-output-min__1, #target-output-min__2, #target-output-max).
 *
 * Two additional options may be set:
 *  factor - This will ONLY scale the displayed value of output elements by the given amount. The factor is
 *           the number to multiply a real data value with to get a display value.
 *  step -  With step two things are done. First, the jump interval of the handles are set.
 *          Secondly, it sets the number of decimals the number has to fit the step size in.
 *
 * @example
 * <div>
 *      <div
 *          data-toubiz-range-slider
 *          data-toubiz-range-slider-user-field.min="#target-input-min"
 *          data-toubiz-range-slider-user-field.max="#target-input-max"
 *          data-toubiz-range-slider-data-field.min="#target-output-min__1, #target-output-min__2"
 *          data-toubiz-range-slider-data-field.max="#target-output-max"
 *          data-toubiz-range-slider-factor="(double) 0.1"
 *          data-toubiz-range-slider-step="(double) 0.4">
 *
 *      </div>
 *      <div>
 *          <input type="number" min="0" max="100" id="target-input-min" placeholder="Bound value of index min" />
 *          <div id="target-display-min_1"></div>
 *          <div id="target-display-min_2"></div>
 *          <input type="number" min="0" max="100" id="target-input-max" placeholder="Bound value of index max" />
 *          <div id="target-display-max"></div>
 *      </div>
 * </div>
 */
export default class ToubizRangeSlider {

    /**
     * @private
     * @type {ToubizRangeSlider~state}
     */
    state;

    /**
     * @param {HTMLElement} node
     */
    constructor(node) {
        /** @private {HTMLElement} */
        this.node = node;

        /** @private {ToubizRangeSlider~options} */
        this.options = this.extractOptions(node);

        /** @private {{ min: HTMLInputElement, max: HTMLInputElement }} */
        this.dataField = {
            min: document.querySelector(this.options.dataField.min),
            max: document.querySelector(this.options.dataField.max),
        };

        this.validateConfiguration();

        this.options.bounds.min = parseFloat(this.options.bounds.min)
            || parseFloat(this.dataField.min.min)
            || 0;
        this.options.bounds.max = parseFloat(this.options.bounds.max)
            || parseFloat(this.dataField.max.max)
            || 0;

        /** @private {ToubizRangeSlider~state} */
        this.state = {
            min: parseFloat(this.dataField.min.value) || this.options.bounds.min,
            max: parseFloat(this.dataField.max.value) || this.options.bounds.max,
        };

        this.setOptionalSettings();
        this.setupListeners();
        this.initializeRangeSlider();
        this.render();
    }

    /**
     * @param {HTMLElement} node
     * @returns {ToubizRangeSlider~options}
     * @private
     */
    extractOptions(node) {
        this.node = node;
        const options = extractPrefixedAttributesFromElement(node, 'data-toubiz-range-slider-');
        options.input = options.input || {};
        options.bounds = options.bounds || {};
        // Optional settings
        options.dataField = options.dataField || {};
        options.factor = parseFloat(options.factor) || 1;
        options.step = parseFloat(options.step) || 1;
        options.decimals = 0;
        options.roundFactor = 1;

        return options;
    }

    /**
     * Updates slider and bound elements to the current state.
     * Should the slider be on its default values the inputs are cleared.
     *
     * @param {boolean} updateSlider - Slider needs to be set to the input values.
     * @public
     */
    render(updateSlider = true) {
        if (
            this.state.min !== this.options.bounds.min
            || this.state.max !== this.options.bounds.max
        ) {
            let minValue = this.state.min !== null ? this.state.min : this.options.bounds.min;
            let maxValue = this.state.max !== null ? this.state.max : this.options.bounds.max;

            this.dataField.min.value = minValue;
            this.dataField.max.value = maxValue;
            if (this.options.changeEvent) {
                fireEvent(this.dataField.min, this.options.changeEvent);
                fireEvent(this.dataField.max, this.options.changeEvent);
            }
        } else {
            this.dataField.min.value = '';
            this.dataField.max.value = '';
        }

        for (const field of this.userField.min) {
            field.value = this.state.min !== this.options.bounds.min ? this.transformValue(this.state.min) : null;
        }

        for (const field of this.userField.max) {
            field.value = this.state.max !== this.options.bounds.max ? this.transformValue(this.state.max) : null;
        }

        if (this.slider && updateSlider) {
            this.slider.set([ this.state.min, this.state.max ]);
        }
    }

    /**
     * This.options.step is used to determine how many decimals the output should have.
     *
     * @private
     */
    setOptionalSettings() {
        this.userField = {
            min: this.options.userField.min ? [ ...document.querySelectorAll(this.options.userField.min) ] : [],
            max: this.options.userField.max ? [ ...document.querySelectorAll(this.options.userField.max) ] : [],
        };

        if(this.options.step !== null && !Number.isInteger(this.options.step)) {
            this.options.decimals = JSON.stringify(this.options.step).split('.')[1].length;
            this.options.roundFactor = Math.pow(10, this.options.decimals);
        }
    }

    /**
     * Applies scaling and rounding according to the settings.
     *
     * @param {string|number} value
     * @returns {string}
     * @private
     */
    transformValue(value) {
        if (this.options.factor) {
            value *= this.options.factor;
        }

        value = Math.round(value * this.options.roundFactor) / this.options.roundFactor;
        return value.toFixed(this.options.decimals);
    }

    updateState({ min, max }) {
        min = parseFloat(min);
        max = parseFloat(max);

        const minChanged = !isNaN(min) && this.state.min !== min;
        const maxChanged = !isNaN(max) && this.state.max !== max;

        min = Math.max(min, this.options.bounds.min);
        max = Math.min(max, this.options.bounds.max);

        if (minChanged) {
            this.state.min = min;
        }
        if (maxChanged) {
            this.state.max = max;
        }
        if (minChanged || maxChanged) {
            this.render();
        }
    }

    reset() {
        this.slider.reset();

        for (const userField of [ ...this.userField.min, ...this.userField.max ]) {
            userField.value = null;
        }
    }

    /** @private */
    setupListeners() {
        for (const field of this.userField.min) {
            field.addEventListener(
                'input',
                () => this.updateState({ min: parseFloat(field.value) / this.options.factor })
            );
        }

        for (const field of this.userField.max) {
            field.addEventListener(
                'input',
                () => this.updateState({ max: parseFloat(field.value) / this.options.factor })
            );
        }

        this.node.addEventListener('range-slider.reset', () => this.reset());

        if (this.dataField.min) {
            this.dataField.min.addEventListener('change', () => this.updateState({ min: this.dataField.min.value }));
            this.dataField.min.addEventListener('range-slider.reset', () => this.reset());
        }

        if (this.dataField.max) {
            this.dataField.max.addEventListener('change', () => this.updateState({ max: this.dataField.max.value }));
            this.dataField.max.addEventListener('range-slider.reset', () => this.reset());
        }
    }

    /** @private */
    initializeRangeSlider() {
        this.slider = noUiSlider.create(this.node, {
            start: [
                this.state.min,
                this.state.max,
            ],
            range: {
                min: [ this.options.bounds.min ],
                max: [ this.options.bounds.max ],
            },
            step: this.options.step,
            keyboardSupport: false,
            // Handle connections: only middle is connected
            // See https://refreshless.com/nouislider/slider-options/#section-connect
            connect: [ false, true, false ],
        });

        this.slider.on('update', ([ min, max ]) => {
            const state = {
                min: parseFloat(min) || this.options.bounds.min,
                max: parseFloat(max) || this.options.bounds.max,
            };

            if (state.min !== this.state.min || state.max !== this.state.max) {
                this.state = state;
                this.render(false);
            }
        });
    }

    /**
     * Check for validity of the mandatory inputs.
     *
     * @private
     */
    validateConfiguration() {
        if (!this.dataField.min || !this.dataField.max) {
            throw new Error(`
                Error initializing range slider: You must specify at least
                [data-toubiz-range-slider-data-field.min] and [data-toubiz-range-slider-data-field.max]
                pointing to existing DOM Nodes.
            `);
        } else if (!this.dataField.min.min || !this.dataField.max.max) {
            console.warn(`
                Warning initializing range slider:
                Make sure 'min' and 'max' exist in the bound data fields.
                [data-toubiz-range-slider-input]
            `);
        }
    }

}
