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

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

/**
 * @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.
 *  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-input.min="#target-input-min"
 *          data-toubiz-range-slider-input.max="#target-input-max"
 *          data-toubiz-range-slider-outputs.min="#target-output-min__1, #target-output-min__2"
 *          data-toubiz-range-slider-outputs.max="#target-output-max"
 *          data-toubiz-range-slider-factor="0.1"
 *          data-toubiz-range-slider-step="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.input = {
            min: document.querySelector(this.options.input.min),
            max: document.querySelector(this.options.input.max),
        };

        this.validateConfiguration();

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

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

        this.setOptionalSettings();
        this.setupInputListeners();
        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.outputs = options.outputs || {};
        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
        ) {
            this.input.min.value = this.state.min || this.options.bounds.min;
            this.input.max.value = this.state.max || this.options.bounds.max;
        } else {
            this.input.min.value = '';
            this.input.max.value = '';
        }

        for (const out of this.outputs.min) {
            out.innerText = this.transformValue(this.state.min);
        }

        for (const out of this.outputs.max) {
            out.innerText = this.transformValue(this.state.max);
        }

        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.outputs = {
            min: this.options.outputs.min ? [ ...document.querySelectorAll(this.options.outputs.min) ] : [],
            max: this.options.outputs.max ? [ ...document.querySelectorAll(this.options.outputs.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);
    }

    /** @private */
    setupInputListeners() {
        this.input.min.addEventListener('input', () => {
            this.state.min = parseFloat(this.input.min.value) || this.options.bounds.min;
            this.render();
        });
        this.input.max.addEventListener('input', () => {
            this.state.max = parseFloat(this.input.max.value) || this.options.bounds.max;
            this.render();
        });
    }

    /** @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.input.min || !this.input.max) {
            throw new Error(`
                Error initializing range slider: You must specify at least
                [data-toubiz-range-slider-input.min] and [data-toubiz-range-slider-input.max]
                pointing to existing DOM Nodes.
            `);
        } else if (!this.input.min.min || !this.input.max.max) {
            console.warn(`
                Warning initializing range slider:
                Make sure 'min' and 'max' exist in the bound inputs.
                [data-toubiz-range-slider-input]
            `);
        }
    }

}
