/**
 * Copyright schukai GmbH and contributors 2022. All Rights Reserved.
 * Node module: @schukai/monster
 * This file is licensed under the AGPLv3 License.
 * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html
 */

import {extend} from "../data/extend.mjs";
import {BaseWithOptions} from "../types/basewithoptions.mjs";
import {getGlobalObject} from "../types/global.mjs";
import {isArray} from "../types/is.mjs";
import {Stack} from "../types/stack.mjs";
import {validateInstance, validateString} from "../types/validate.mjs";
import {instanceSymbol} from '../constants.mjs';
export {FocusManager}

/**
 * @private
 * @type {string}
 */
const KEY_DOCUMENT = 'document';

/**
 * @private
 * @type {string}
 */
const KEY_CONTEXT = 'context';


/**
 * @private
 * @type {Symbol}
 */
const stackSymbol = Symbol('stack');


/**
 * With the focusmanager the focus can be stored in a document, recalled and moved.
 *
 * @license AGPLv3
 * @since 1.25.0
 * @copyright schukai GmbH
 * @memberOf Monster.DOM
 * @throws {Error} unsupported locale
 * @summary Handle the focus
 */
 class FocusManager extends BaseWithOptions {

    /**
     *
     * @param {Object|undefined} options
     */
    constructor(options) {
        super(options);
        validateInstance(this.getOption(KEY_DOCUMENT), HTMLDocument);

        this[stackSymbol] = new Stack();
    }

    /**
     * This method is called by the `instanceof` operator.
     * @returns {symbol}
     * @since 2.1.0
     */
    static get [instanceSymbol]() {
        return Symbol.for("@schukai/monster/dom/focusmanager");
    }


    /**
     * @property {HTMLDocument} document the document object into which the node is to be appended
     */
    get defaults() {
        return extend({}, super.defaults, {
            [KEY_DOCUMENT]: getGlobalObject('document'),
            [KEY_CONTEXT]: undefined,
        })
    }

    /**
     * Remembers the current focus on a stack.
     * Several focus can be stored.
     *
     * @return {Monster.DOM.FocusManager}
     */
    storeFocus() {

        const active = this.getActive();
        if (active instanceof Node) {
            this[stackSymbol].push(active)
        }
        return this;
    }

    /**
     * The last focus on the stack is set again
     *
     * @return {Monster.DOM.FocusManager}
     */
    restoreFocus() {

        const last = this[stackSymbol].pop();
        if (last instanceof Node) {
            this.focus(last);
        }

        return this;
    }

    /**
     *
     * @param {Node} element
     * @param {boolean} preventScroll
     * @throws {TypeError} value is not an instance of
     * @return {Monster.DOM.FocusManager}
     */
    focus(element, preventScroll) {

        validateInstance(element, Node)

        element.focus({
            preventScroll: preventScroll ?? false
        })

        return this;
    }

    /**
     *
     * @return {Element}
     */
    getActive() {
        return this.getOption(KEY_DOCUMENT).activeElement;
    }

    /**
     * Select all elements that can be focused
     *
     * @param {string|undefined} query
     * @return {array}
     * @throws {TypeError} value is not an instance of
     */
    getFocusable(query) {

        let contextElement = this.getOption(KEY_CONTEXT);
        if (contextElement === undefined) {
            contextElement = this.getOption(KEY_DOCUMENT);
        }

        validateInstance(contextElement, Node)

        if (query !== undefined) {
            validateString(query);
        }

        return [...contextElement.querySelectorAll(
            'details, button, input, [tabindex]:not([tabindex="-1"]), select, textarea, a[href], body'
        )].filter((element) => {

            if (query !== undefined && !element.matches(query)) {
                return false;
            }

            if (element.hasAttribute('disabled')) return false;
            if (element.getAttribute("aria-hidden") === 'true') return false;

            const rect = element.getBoundingClientRect();
            if(rect.width===0) return false;
            if(rect.height===0) return false;

            return true;
        });
    }

    /**
     * @param {string} query
     * @return {Monster.DOM.FocusManager}
     */
    focusNext(query) {

        const current = this.getActive();
        const focusable = this.getFocusable(query);

        if (!isArray(focusable) || focusable.length === 0) {
            return this;
        }

        if (current instanceof Node) {
            let index = focusable.indexOf(current);

            if (index > -1) {
                this.focus(focusable[index + 1] || focusable[0]);
            } else {
                this.focus(focusable[0]);
            }
        } else {
            this.focus(focusable[0])
        }

        return this;
    }

    /**
     * @param {string} query
     * @return {Monster.DOM.FocusManager}
     */
    focusPrev(query) {

        const current = this.getActive();
        const focusable = this.getFocusable(query);

        if (!isArray(focusable) || focusable.length === 0) {
            return this;
        }

        if (current instanceof Node) {
            let index = focusable.indexOf(current);

            if (index > -1) {
                this.focus(focusable[index - 1] || focusable[focusable.length - 1]);
            } else {
                this.focus(focusable[focusable.length - 1]);
            }
        } else {
            this.focus(focusable[focusable.length - 1])
        }

        return this;
    }


}