/**
 *  MatomoOptOut
 *
 *  @copyright Copyright (c) 2020, Bit&Black
 *  @author Tobias Köngeter <hello@bitandblack.com>
 *  @link https://www.bitandblack.com
 */

import httpJsonp from "http-jsonp";

/**
 * MatomoOptOut.
 */
class MatomoOptOut
{
    /**
     * The root URL to Matomo.
     * 
     * @private
     */
    private readonly matomoRoot: string;

    /**
     * The whole tracking URL.
     * 
     * @private
     */
    private readonly matomoTrackingURL: string;

    /**
     * The current tracking status.
     * 
     * @private
     */
    private isTrackingActive: boolean;

    /**
     * The HTML element for error messages.
     * 
     * @private
     */
    private errorMessage: HTMLElement = null;

    /**
     * The interval for fetching the status.
     * 
     * @private
     */
    private interval: number;

    /**
     * The HTML element when tracking is enabled.
     * 
     * @private
     */
    private readonly trackingEnabledElement: HTMLElement;

    /**
     * The HTML element when tracking is disabled.
     * 
     * @private
     */
    private readonly trackingDisabledElement: HTMLElement;

    /**
     * The callback when an error appears.
     * 
     * @private
     */
    private errorCallback: () => void = null;

    /**
     * The callback when the status has changed.
     * 
     * @private
     */
    private onStatusChangeCallback: (isTrackingActive: boolean) => void = null;

    /**
     * The CSS class to make an element visible.
     * 
     * @private
     */
    private classElementIsVisible: string = null;

    /**
     * The CSS class to make an element invisible.
     * 
     * @private
     */
    private classElementIsInvisible: string = null;

    /**
     * If classes should be used to show or hide HTML.
     * 
     * @private
     */
    private useClassesForVisibility: boolean = false;
    
    /**
     * Constructor.
     * 
     * @param matomoRoot              The root URL to your Matomo instance.
     * @param trackingEnabledElement  The html element holding text and checkbox when tracking is enabled.
     * @param trackingDisabledElement The html element holding text and checkbox when tracking is disabled.
     */
    constructor(matomoRoot: string, trackingEnabledElement: HTMLElement, trackingDisabledElement: HTMLElement) 
    {
        this.matomoRoot = matomoRoot.replace(/\/+$/, "");
        this.trackingEnabledElement = trackingEnabledElement;
        this.trackingDisabledElement = trackingDisabledElement;
        this.matomoTrackingURL = `${this.matomoRoot}/index.php?module=API&format=json&method=AjaxOptOut`;

        this.updateHTMLOutput.bind(this);
        
        const inputs: Array<HTMLInputElement> = this.getElements(
            this.trackingEnabledElement.querySelectorAll("input[type='checkbox']"),
            this.trackingDisabledElement.querySelectorAll("input[type='checkbox']")
        );

        inputs.forEach((input) => {
            input.addEventListener("change", (event) => {
                event.stopPropagation();
                event.preventDefault();
                
                const doTrack = this.isChildOf(
                    event.target as HTMLElement, 
                    this.trackingDisabledElement
                );
                
                this.setMatomoTrackingStatus(
                    doTrack,
                    () => {
                        this.getMatomoTrackingStatus(
                            (response) => this.updateHTMLOutput(response)
                        );
                    }
                );
            });
        });

        /**
         * Initial call
         */
        this.getMatomoTrackingStatus(
            (response) => this.updateHTMLOutput(response)
        );
    }

    /**
     * Returns if a element is child of another element.
     * 
     * @param child
     * @param parent
     */
    private isChildOf = (child: HTMLElement, parent: HTMLElement) => {
        let node = child.parentNode;
        
        while (node !== null) {
            if (node === parent) {
                return true;
            }
            
            node = node.parentNode;
        }
        
        return false;
    }

    /**
     * Updates the HTML output. This will be called as a callback after requesting the tracking status.
     *
     * @param response
     */
    private updateHTMLOutput = (response) => {
        const status: boolean = response.value;

        if (true === this.useClassesForVisibility) {
            this.trackingEnabledElement.classList.remove(this.classElementIsVisible);
            this.trackingDisabledElement.classList.remove(this.classElementIsVisible);
            this.trackingEnabledElement.classList.add(this.classElementIsInvisible);
            this.trackingDisabledElement.classList.add(this.classElementIsInvisible);
        } else {
            this.trackingEnabledElement.style.display = "none";
            this.trackingDisabledElement.style.display = "none";
        }
        
        const inputs: Array<HTMLInputElement> = this.getElements(
            this.trackingEnabledElement.querySelectorAll("input[type='checkbox']"),
            this.trackingDisabledElement.querySelectorAll("input[type='checkbox']")
        );
        
        let currentTrackingElement: HTMLElement = true === status
            ? this.trackingEnabledElement
            : this.trackingDisabledElement
        ;

        inputs.forEach((input) => {
            input.checked = status;
        });

        if (true === this.useClassesForVisibility) {
            currentTrackingElement.classList.remove(this.classElementIsInvisible);
            currentTrackingElement.classList.add(this.classElementIsVisible);
        } else {
            currentTrackingElement.style.display = "block";
        }
        
        this.checkStatusChange(status);
    };
    
    /**
     * Gets the matomo tracking status.
     * 
     * @param callback The callback when requesting the tracking status.
     */
    getMatomoTrackingStatus = (callback?: (response) => void) => {
        httpJsonp({
            url: `${this.matomoTrackingURL}.isTracked`,
            callbackProp: "callback",
            callback: callback || {},
        });
    };

    /**
     * Sets the tracking status.
     * 
     * @param status   The new status
     * @param callback The callback when the changing the status
     */
    private setMatomoTrackingStatus = (status: boolean, callback = null) => {
        const setter = true === status ? ".doTrack" : ".doIgnore";
        httpJsonp({
            url: this.matomoTrackingURL + setter,
            callbackProp: "callback",
            callback: callback || {},
        });
    };

    /**
     * Checks if the status changed.
     * 
     * @param statusNew
     */
    private checkStatusChange = (statusNew) => {
        if (this.isTrackingActive === statusNew) {
            if (null !== this.errorMessage) {
                if (true === this.useClassesForVisibility) {
                    this.errorMessage.classList.remove(this.classElementIsInvisible);
                    this.errorMessage.classList.add(this.classElementIsVisible);
                } else {
                    this.errorMessage.style.display = "block";
                }
            }
            
            if (null !== this.errorCallback) {
                this.errorCallback();
            }
            
            return false;
        }

        this.isTrackingActive = statusNew;
        
        if (null !== this.errorMessage) {
            if (true === this.useClassesForVisibility
                && this.errorMessage.classList.contains(this.classElementIsVisible)
            ) {
                this.errorMessage.classList.remove(this.classElementIsVisible);
                this.errorMessage.classList.add(this.classElementIsInvisible);
            } else if (this.errorMessage.style.display === "block") {
                this.errorMessage.style.display = "none";
            }
        }
        
        if (null !== this.onStatusChangeCallback) {
            this.onStatusChangeCallback(this.isTrackingActive);
        }
    }

    /**
     * Sets an HTML element with a custom error message in case the tracking status could not changed.
     * 
     * @param element
     */
    setErrorMessage = (element: HTMLElement) => {
        this.errorMessage = element;
        this.setErrorMessageVisibility();
        return this;
    };

    /**
     * Sets a custom error callback.
     * 
     * @param errorCallback
     */
    setErrorCallback = (errorCallback: () => void) => {
        this.errorCallback = errorCallback;
        return this;
    };

    /**
     * Sets a custom callback for when the tracking status has changed.
     * 
     * @param onStatusChangeCallback
     */
    setOnStatusChangeCallback = (onStatusChangeCallback: (isTrackingActive: boolean) => void) => {
        this.onStatusChangeCallback = onStatusChangeCallback;
        return this;
    }

    /**
     * Sets CSS classes to handle the visibility of the HTML elements.
     * 
     * @param classElementIsVisible   The class name for visible elements.
     * @param classElementIsInvisible The class name for invisible elements.
     */
    setCSSClasses = (classElementIsVisible: string, classElementIsInvisible: string) => {
        this.classElementIsVisible = classElementIsVisible;
        this.classElementIsInvisible = classElementIsInvisible;
        this.useClassesForVisibility = true;
        this.setErrorMessageVisibility();
        return this;
    }

    /**
     * Enables a constant check for the current tracking status.
     * 
     * @param seconds The interval in seconds.
     */
    watchStatusChange = (seconds: number) => {
        clearInterval(this.interval);

        this.interval = window.setInterval(
            this.getMatomoTrackingStatus,
            seconds * 1000
        );
        
        return this;
    }

    /**
     * Concat multiple selectors together.
     * 
     * @param elements
     */
    private getElements = (...elements) => {
        let elementsAll = [];

        elements.forEach((element) => {
            elementsAll = elementsAll.concat(Array.prototype.slice.call(element));
        });

        return elementsAll;
    }

    private setErrorMessageVisibility = () => {
        if (true === this.useClassesForVisibility) {
            this.errorMessage.style.removeProperty("display");
            this.errorMessage.classList.remove(this.classElementIsVisible, this.classElementIsInvisible);
            this.errorMessage.classList.add(this.classElementIsInvisible);
        } else {
            this.errorMessage.style.display = "none";
        }
    }
}

export { MatomoOptOut };