import { stripMultilineIndention } from './string-utilities';
import { parse, stringify } from 'qs';

const ATTRIBUTE = Object.freeze({
    AJAX_NAME: 'data-toubiz-ajax.name',
});

/**
 * @typedef {object} AjaxFormReload~options
 * @property {string|Node} target
 * @property {string} action
 * @property {string} method
 * @property {boolean} updateUrl
 * @property {string} updateUrlParameters
 * @property {string} updateUrlPluginNamespace
 * @property {Function} beforeLoad
 * @property {Function} afterLoad
 * @property {Function} onLoadError
 * @property {Function} ignoreAjaxReload
 * @property {Function} formDataTransform
 */

export class AjaxFormReload {

    /**
     * @param {HTMLFormElement} form
     * @param {AjaxFormReload~options} options
     */
    constructor (form, options = {}) {
        /** @private {HTMLFormElement} */
        this.form = form;

        /** @private {AjaxFormReload~options} */
        this.options = this.prepareOptions(options);

        this.initializeEventHandlers();
    }

    async reload () {
        if (this.options.beforeLoad) {
            this.options.beforeLoad();
        }

        const target = this.options.target instanceof Node ?
            this.options.target :
            document.querySelector(this.options.target);

        let requestUrl = this.options.action;
        const options = {
            method: this.options.method,
            credentials: 'same-origin',
            headers: {
                Accept: 'text/html',
            },
        };

        if (this.options.method === 'GET') {
            const hasQuery = requestUrl.indexOf('?') !== -1;
            const query = [];
            this.getFormData()
                .forEach((value, key) => this.sanitize(value, key, query));
            requestUrl += (hasQuery ? '&' : '?') + query.join('&');
            this.updateUrl(requestUrl);
        } else {
            options.body = this.getFormData();
        }

        try {
            const response = await fetch(requestUrl, options);
            target.innerHTML = await response.text();
        } catch (error) {
            if (this.options.onLoadError) {
                this.options.onLoadError(error);
            } else {
                throw error;
            }
        }


        if (this.options.afterLoad) {
            this.options.afterLoad();
        }
    }

    /**
     * @param {string} value
     * @param {string} key
     * @param {Array} query
     * @private
     */
    sanitize(value, key, query){
        if(value !== ''){
            query.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
        }
    }

    /**
     * @param {string} requestUrl
     * @private
     */
    updateUrl (requestUrl) {
        if (this.options.updateUrl) {
            const currentUrl = window.location.search.slice(1);
            const updatedUrl = `${window.location.pathname}?${this.replaceParametersInPath(currentUrl, requestUrl)}`;
            window.history.replaceState('', '', updatedUrl);
        }
    }

    /**
     * @param {string} currentPath
     * @param {string} requestPath
     * @returns {string}
     * @private
     */
    replaceParametersInPath (currentPath, requestPath) {
        const currentParameters = parse(currentPath);
        const requestParameters = parse(requestPath);

        const updatedParameters = this.replaceParametersInArray(currentParameters, requestParameters);

        return stringify(updatedParameters);
    }

    /**
     * @param {object} currentParameters
     * @param {object} requestParameters
     * @returns {object}
     * @private
     */
    replaceParametersInArray (currentParameters, requestParameters) {
        const parameters = this.options.updateUrlParameters.split(',');

        const updatedParameters = JSON.parse(JSON.stringify(currentParameters));
        let currentNamespace;
        if (this.options.updateUrlPluginNamespace) {
            updatedParameters[ this.options.updateUrlPluginNamespace ] =
                updatedParameters[ this.options.updateUrlPluginNamespace ] || {};
            currentNamespace = updatedParameters[ this.options.updateUrlPluginNamespace ];
        } else {
            currentNamespace = updatedParameters;
        }

        for (const parameter of parameters) {
            currentNamespace[ parameter ] = requestParameters[ parameter ];
        }

        return updatedParameters;
    }

    /**
     * @returns {FormData}
     * @private
     */
    getFormData () {
        const data = new FormData();
        for (const element of this.form.querySelectorAll('input, select, textarea')) {
            const name = element.getAttribute(ATTRIBUTE.AJAX_NAME) || element.name;

            if ((element.type === 'checkbox' || element.type === 'radio') && !element.checked) {
                continue;
            }

            data.append(name, element.value);
        }

        if (this.options.formDataTransform) {
            return this.options.formDataTransform(data);
        }

        return data;
    }

    /** @private */
    initializeEventHandlers () {
        this.form.addEventListener('submit', event => {
            if (this.options.ignoreAjaxReload(this.getFormData()) === true) {
                return;
            }

            event.preventDefault();
            this.reload();
        });
    }

    /**
     * @private
     * @param {AjaxFormReload~options} options
     * @returns {AjaxFormReload~options}
     */
    prepareOptions (options) {
        if (!options.target) {
            throw new Error(stripMultilineIndention(`
                The 'target' option is required for 'AjaxFormReload'.
                It defines the area of the document that should be reloaded.
            `));
        }

        options.action = options.action || this.form.action;
        options.method = (options.method || this.form.method).toUpperCase();
        options.updateUrl = options.updateUrl === true;
        options.updateUrlParameters = options.updateUrlParameters || '';
        options.updateUrlPluginNamespace = options.updateUrlPluginNamespace || '';
        options.ignoreAjaxReload = options.ignoreAjaxReload || (() => false);
        return options;
    }

}
