/**
 * Copyright 2023 schukai GmbH
 * SPDX-License-Identifier: AGPL-3.0
 */

import {addAttributeToken} from "../../../dom/attributes.mjs";
import {ATTRIBUTE_ERRORMESSAGE} from "../../../dom/constants.mjs";
import {Datasource, dataSourceSymbol} from "../datasource.mjs";
import {DatasourceStyleSheet} from "../stylesheet/datasource.mjs";
import {instanceSymbol} from "../../../constants.mjs";
import {
    assembleMethodSymbol,
    registerCustomElement,
} from "../../../dom/customelement.mjs";
import {RestAPI} from "../../../data/datasource/server/restapi.mjs";
import {Formatter} from "../../../text/formatter.mjs";
import {clone} from "../../../util/clone.mjs";
import {validateBoolean} from "../../../types/validate.mjs";
import {findElementWithIdUpwards} from "../../../dom/util.mjs";
import {Observer} from "../../../types/observer.mjs";
import {Pathfinder} from "../../../data/pathfinder.mjs";
import {fireCustomEvent} from "../../../dom/events.mjs";

export {Rest};

/**
 * @private
 * @type {symbol}
 */
const intersectionObserverHandlerSymbol = Symbol("intersectionObserverHandler");

/**
 * @private
 * Original at source/components/datatable/datasource/rest.mjs
 * @type {symbol}
 */
const rawDataSymbol = Symbol.for(
    "@schukai/monster/data/datasource/server/restapi/rawdata",
);

/**
 * @private
 * @type {symbol}
 */
const intersectionObserverObserverSymbol = Symbol(
    "intersectionObserverObserver",
);

/**
 * @private
 * @type {symbol}
 */
const filterObserverSymbol = Symbol("filterObserver");

/**
 * The Datasource component is a basic class for the datatable component.
 *
 * <img src="./images/rest.png">
 *
 * Dependencies: the system uses functions of the [monsterjs](https://monsterjs.org/) library
 *
 * @startuml rest.png
 * skinparam monochrome true
 * skinparam shadowing false
 * HTMLElement <|-- CustomElement
 * CustomElement <|-- Datasource
 * Datasource <|-- Rest
 * @enduml
 *
 * @copyright schukai GmbH
 * @memberOf Monster.Components.Datatable.Datasource
 * @summary A rest api datasource
 */
class Rest extends Datasource {
    /**
     * the constructor of the class
     */
    constructor() {
        super();
        this[dataSourceSymbol] = new RestAPI();
    }

    /**
     * This method is called by the `instanceof` operator.
     * @returns {symbol}
     */
    static get [instanceSymbol]() {
        return Symbol.for("@schukai/monster/components/datasource/rest@@instance");
    }

    /**
     * To set the options via the html tag the attribute `data-monster-options` must be used.
     * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
     *
     * The individual configuration values can be found in the table.
     *
     * @property {Object} templates Template definitions
     * @property {string} templates.main Main template
     * @property {Object} features Feature definitions
     * @property {boolean} features.autoInit If true, the component is initialized automatically
     * @property {boolean} features.filter If true, the component is initialized automatically
     * @property {Object} autoInit Auto init definitions
     * @property {boolean} autoInit.intersectionObserver If true, the intersection observer is initialized automatically
     * @property {boolean} autoInit.oneTime If true, the intersection observer is initialized only once
     * @property {Object} filter Filter definitions
     * @property {string} filter.id The id of the filter control
     * @property {Object} datatable Datatable definitions
     * @property {string} datatable.id The id of the datatable control
     * @property {Object} response Response definitions
     * @property {Object} response.path Path definitions (changed in 3.56.0)
     * @property {string} response.path.message Path to the message (changed in 3.56.0)
     * @property {Object} read Read configuration
     * @property {string} read.url The url of the rest api
     * @property {string} read.method The method of the rest api
     * @property {Object} read.parameters The parameters of the rest api
     * @property {Object} read.parameters.filter The filter of the rest api
     * @property {Object} read.parameters.orderBy The order by of the rest api
     * @property {Object} read.parameters.page The page of the rest api
     * @property {Object} write Write configuration

     */
    get defaults() {
        const restOptions = new RestAPI().defaults;

        restOptions.read.parameters = {
            filter: undefined,
            oderBy: undefined,
            page: "1",
        };

        return Object.assign({}, super.defaults, restOptions, {
            templates: {
                main: getTemplate(),
            },

            features: {
                autoInit: false,
                filter: false,
            },

            autoInit: {
                intersectionObserver: false,
                oneTime: true,
            },

            filter: {
                id: undefined,
            },

            datatable: {
                id: undefined,
            },

            response: {
                path: {
                    message: "sys.message",
                    code: "sys.code",
                }
            },
        });
    }

    /**
     *
     * @param {string} page
     * @param {string} query
     * @param {string} orderBy
     * @returns {Monster.Components.Datatable.Datasource.Rest}
     */
    setParameters({page, query, orderBy}) {
        const parameters = this.getOption("read.parameters");
        if (query !== undefined) {
            parameters.query = `${query}`;
            parameters.page = "1";
        }

        // after a query the page is set to 1, so if the page is not set, it is set to 1
        if (page !== undefined) parameters.page = `${page}`;
        if (orderBy !== undefined) parameters.order = `${orderBy}`;
        this.setOption("read.parameters", parameters);
        return this;
    }

    /**
     *
     * @return {Monster.Components.Form.Form}
     */
    [assembleMethodSymbol]() {
        super[assembleMethodSymbol]();

        initEventHandler.call(this);
        initAutoInit.call(this);
    }

    /**
     * @deprecated 2023-06-25
     * @returns {Promise<never>|*}
     */
    reload() {
        return this.fetch();
    }

    /**
     * Fetches the data from the rest api
     * @returns {Promise<never>|*}
     */
    fetch() {
        const opt = clone(this.getOption("read"));
        this[dataSourceSymbol].setOption("read", opt);

        let url = this.getOption("read.url");
        const formatter = new Formatter(this.getOption("read.parameters"));

        if (!url) {
            return Promise.reject(new Error("No url defined"));
        }

        url = formatter.format(url);

        this[dataSourceSymbol].setOption("read.url", url);

        return new Promise((resolve, reject) => {
            fireCustomEvent(this, "monster-datasource-fetch", {
                datasource: this,
            });

            setTimeout(() => {
                this[dataSourceSymbol]
                    .read()
                    .then((response) => {
                        fireCustomEvent(this, "monster-datasource-fetched", {
                            datasource: this,
                        });

                        resolve(response);
                    })
                    .catch((error) => {
                        fireCustomEvent(this, "monster-datasource-error", {
                            error: error,
                        });

                        addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, error.toString());
                        reject(error);
                    });
            }, 0);
        });
    }

    /**
     *
     * @return {CSSStyleSheet[]}
     */
    static getCSSStyleSheet() {
        return [DatasourceStyleSheet];
    }

    /**
     * @private
     * @return {string}
     */
    static getTag() {
        return "monster-datasource-rest";
    }

    /**
     * This method activates the intersection observer manually.
     * For this purpose, the option `autoInit.intersectionObserver` must be set to `false`.
     *
     * @returns {Monster.Components.Datatable.Datasource.Rest}
     */
    initIntersectionObserver() {
        initIntersectionObserver.call(this);
        return this;
    }

    /**
     * @private
     */
    connectedCallback() {
        super.connectedCallback();

        setTimeout(() => {
            if (this.getOption("features.filter", false) === true) {
                initFilter.call(this);
            }
        }, 0);
    }

    /**
     * @private
     */
    disconnectedCallback() {
        super.disconnectedCallback();
        removeFilter.call(this);
    }


    read() {
        return this.fetch();
    }


    /**
     * Fetches the data from the rest api
     * @returns {Promise<never>|*}
     */
    write() {
        const opt = clone(this.getOption("write"));
        this[dataSourceSymbol].setOption("write", opt);

        let url = this.getOption("write.url");
        const formatter = new Formatter(this.getOption("write.parameters"));

        if (!url) {
            return Promise.reject(new Error("No url defined"));
        }

        url = formatter.format(url);

        this[dataSourceSymbol].setOption("write.url", url);

        return new Promise((resolve, reject) => {
            fireCustomEvent(this, "monster-datasource-fetch", {
                datasource: this,
            });

            setTimeout(() => {
                this[dataSourceSymbol]
                    .write()
                    .then((response) => {
                        fireCustomEvent(this, "monster-datasource-fetched", {
                            datasource: this,
                        });

                        resolve(response);
                    })
                    .catch((error) => {
                        fireCustomEvent(this, "monster-datasource-error", {
                            error: error,
                        });

                        addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, error.toString());
                        reject(error);
                    });
            }, 0);
        });
    }

}

/**
 * @private
 */
function removeFilter() {
    const filterID = this.getOption("filter.id", undefined);
    if (!filterID) return;

    const filterControl = findElementWithIdUpwards(this, filterID);

    if (filterControl && this[filterObserverSymbol]) {
        filterControl?.detachObserver(this[filterObserverSymbol]);
    }
}

/**
 * @private
 */
function initFilter() {
    const filterID = this.getOption("filter.id", undefined);

    if (!filterID)
        throw new Error("filter feature is enabled but no filter id is defined");

    const filterControl = findElementWithIdUpwards(this, filterID);
    if (!filterControl)
        throw new Error(
            "filter feature is enabled but no filter control with id " +
            filterID +
            " is found",
        );

    this[filterObserverSymbol] = new Observer(() => {
        const query = filterControl.getOption("query", undefined);
        this.setParameters({query: query});
        this.fetch()
            .then((response) => {
                if (!(response instanceof Response)) {
                    throw new Error("Response is not an instance of Response");
                }

                if (response?.ok === true) {
                    this.dispatchEvent(new CustomEvent("reload", {bubbles: true}));
                    filterControl?.showSuccess();
                }

                if (response.bodyUsed === true) {
                    return handleIntersectionObserver.call(this, response[rawDataSymbol], response, filterControl);
                }

                response
                    .text()
                    .then((jsonAsText) => {
                        let json;
                        try {
                            json = JSON.parse(jsonAsText);
                        } catch (e) {
                            let message = e instanceof Error ? e.message : `${e}`;
                            filterControl?.showFailureMessage(message);
                            return Promise.reject(e);
                        }

                        return handleIntersectionObserver.call(this, json, response, filterControl);


                    })
                    .catch((e) => {
                        filterControl?.showFailureMessage(e.message);
                    });
            })
            .catch((e) => {
                this.dispatchEvent(
                    new CustomEvent("error", {bubbles: true, detail: e}),
                );

                if (!(e instanceof Error)) {
                    e = new Error(e);
                }

                filterControl?.showFailureMessage(e.message);
                return Promise.reject(e);
            });
    });

    filterControl.attachObserver(this[filterObserverSymbol]);
}

function handleIntersectionObserver(json, response, filterControl) {

    const path = new Pathfinder(json);

    const codePath = this.getOption("response.path.code");

    if (path.exists(codePath)) {
        const code = `${path.getVia(codePath)}`;
        if (code && code === "200") {
            filterControl?.showSuccess();
            return Promise.resolve(response);
        }

        const messagePath = this.getOption("response.path.message");
        if (path.exists(messagePath)) {
            const message = path.getVia(messagePath);
            filterControl?.showFailureMessage(message);
            return Promise.reject(new Error(message));
        }

        return Promise.reject(new Error("Response code is not 200"));
    }
}

/**
 * @private
 */
function initAutoInit() {
    const autoInit = this.getOption("features.autoInit");
    validateBoolean(autoInit);

    if (autoInit !== true) return;

    if (this.getOption("autoInit.intersectionObserver") === true) {
        initIntersectionObserver.call(this);
        return;
    }

    setTimeout(() => {
        this.fetch().catch(() => {
        });
    }, 0);
}

function initEventHandler() {

    this[intersectionObserverHandlerSymbol] = (entries) => {
        entries.forEach((entry) => {
            if (entry.isIntersecting) {
                if (entry.intersectionRatio > 0) {
                    this.fetch();
                }

                // only load once
                if (
                    this.getOption("autoInit.oneTime") === true &&
                    this[intersectionObserverObserverSymbol] !== undefined
                ) {
                    this[intersectionObserverObserverSymbol].unobserve(this);
                }
            }
        });
    };
}

function initIntersectionObserver() {
    this.classList.add("intersection-observer");

    const options = {
        root: null,
        rootMargin: "0px",
        threshold: 0.1,
    };

    this[intersectionObserverObserverSymbol] = new IntersectionObserver(
        this[intersectionObserverHandlerSymbol],
        options,
    );
    this[intersectionObserverObserverSymbol].observe(this);
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
    // language=HTML
    return `
        <slot></slot>`;
}

registerCustomElement(Rest);