import { extractPrefixedAttributesFromElement } from '@nimius/dom-utility';
import { onPageLoadComplete } from '@nimius/event-utility';
/* eslint-disable */
/**
 * ## Simple value reduction
 *
 * Allows the combination / reduction of multiple values into a single value which
 * will be written into the DOM Element.
 *
 * NOTE: This Script builds upon an understanding of how Array.reduce works and basically
 *       translates this functionality into HTML attributes. To learn more about `Array.reduce`
 *       check out the MDN Documentation for it:
 *       https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce
 *
 *
 * ### Subjects
 * In the remainder of this script the concept of subjects will be named a couple of times.
 * A subject is a single value in the reduction calculation that can be one of the following:
 *
 * - A simple numeric constant - For example `140`.
 * - A selector to one or more form elements.
 * - A selector to one or more HTML Elements.
 *
 *
 * ### Reduction methods
 * The following reduction methods exist:
 * - `*`: Multiplies all subjects.
 * - `+`: Adds all subjects.
 * - `-`: Subtracts all subjects.
 * - `/`: Divides all subjects.
 *
 *
 * ### Attributes
 * The following attributes may be specified:
 * - `[data-reduce]`: Contains the reduction method that should be used to combine the values.
 *                    For a list of all valid values, see the list above.
 * - `[data-reduce.subjects.{N}]`: Can be used to specify one or more subjects. Substitute `{N}`
 *                    for a running number starting with 0.
 *
 *
 * @example
 * <caption>Simple example: Multiply a constant with the value of an input</caption>
 * <input id="input_one">
 * <span data-reduce="*"
 *       data-reduce.subjects.0="140"
 *       data-reduce.subjects.1="#input_one"></span>
 *
 * @example
 * <caption>
 *     Complex example: Same as above, but multiple rows with an additional total price
 *     that is calculated by adding up all results of the single rows.
 * </caption>
 *
 * <input id="input_one">
 * <span data-row-result
 *       data-reduce="*"
 *       data-reduce.subjects.0="140"
 *       data-reduce.subjects.1="#input_one"></span>
 *
 * <input id="input_two">
 * <span data-row-result
 *       data-reduce="*"
 *       data-reduce.subjects.0="20"
 *       data-reduce.subjects.1="#input_two"></span>
 *
 * <input id="three">
 * <span data-row-result
 *       data-reduce="*"
 *       data-reduce.subjects.0="55"
 *       data-reduce.subjects.1="#three"></span>
 *
 * <strong>
 *     Total:
 *     <span data-reduce="+"
 *           data-reduce.subjects.0="[data-row-result]"></span>
 * </strong>
 */
onPageLoadComplete(() => {
    for (const element of document.querySelectorAll('[data-reduce]')) {

        /**
         * The method that should be used to combine the elements.
         * @type {string}
         */
        const method = element.getAttribute('data-reduce');

        /**
         * The options that were passed to the DOM Node.
         * @type {Object}
         */
        const options = extractPrefixedAttributesFromElement(element, 'data-reduce.');

        /**
         * Array of all elements that are currently being watched.
         * @type {Node[]}
         */
        const elements = [];

        /**
         * Array of all value getters. These methods will return the current value
         * of the subjects when called.
         * @type {function[]}
         */
        const valueGetters = [];

        /*
         * Extract subjects from the given options.
         * Subjects can be defined as:
         *
         * - Simple constants. For example `100`.
         * - Selector to a single element
         * - Selector to multiple elements
         *
         * If the target of a selector is a form element, then it's value
         * will be used for calculations. If it is not, then it's contents
         * will be used.
         */
        for (const subject of Object.values(options.subjects)) {

            // Handle simple numeric constants
            if (parseFloat(subject) === subject) {
                valueGetters.push(() => parseFloat(subject));
                continue;
            }

            try {
                for (const s of document.querySelectorAll(subject)) {
                    elements.push(s);

                    if (['input', 'select', 'textarea'].indexOf(s.nodeName.toLowerCase()) !== -1) {
                        // Handle form elements.
                        valueGetters.push(() => parseFloat(s.value));
                    } else {
                        // Handle simple HTML Elements.
                        valueGetters.push(() => parseFloat(s.innerHTML));
                    }
                }
            } catch (e) {
                // This happens, if the given string is not a valid selector.
                // In this case we assume that it is a simple numeric constant.
                valueGetters.push(() => parseFloat(subject));
            }
        }

        /**
         * Main update method.
         * This method contains the main reduction logic that
         * combines all of the values and writes them into the given
         * HTML Element.
         */
        const update = () => {
            const reducedElement = valueGetters.reduce((carry, current) => {
                if (typeof carry === 'function') {
                    // eslint-disable-next-line no-param-reassign
                    carry = carry();
                }

                switch (method) {
                case '+': return carry + current();
                case '-': return carry - current();
                case '*': return carry * current();
                case '/': return carry / current();
                default:
                    throw new Error(
                        `The given reduction method ${method} is not defined.
                        Valid reduction methods are: '+', '-', '*', '/'`
                    );
                }
            }, method === '*' ? 1 : 0);
            element.innerHTML = reducedElement;
        };

        // Attach event listeners to `change`, `input` and DOMChange events
        // in order to update the value, if one of the subjects change.
        const observer = new MutationObserver(update);
        for (const el of elements) {
            el.addEventListener('change', update);
            el.addEventListener('input', update);
            observer.observe(el, { childList: true });
        }

        // Call the update method initially to ensure a
        // consistent state.
        update();
    }
});
/* eslint-enable */
