import {DOM} from '../../util/dom.ts';

import {warnOnce} from '../../util/util.ts';

import {Event, Evented} from '../../util/evented.ts';
import type {Map} from '../map.ts';
import type {IControl} from './control.ts';

/**
 * The {@link FullscreenControl} options object
 */
export type FullscreenControlOptions = {
    /**
     * `container` is the [compatible DOM element](https://developer.mozilla.org/en-US/docs/Web/API/Element/requestFullScreen#Compatible_elements) which should be made full screen. By default, the map container element will be made full screen.
     */
    container?: HTMLElement;
    /**
     * If `true`, the fullscreen control will always use pseudo fullscreen mode (CSS-based, expanding to browser viewport) instead of native fullscreen API.
     * This can be useful for faster transitions and to allow multiple maps to be "fullscreen" simultaneously in different browser windows.
     * @defaultValue false
     */
    pseudo?: boolean;
};

/**
 * The event class for fullscreen control events (`fullscreenstart` and `fullscreenend`).
 *
 * @group Event Related
 */
export class FullscreenEvent extends Event {
    type: 'fullscreenstart' | 'fullscreenend';
    /**
     * The `FullscreenControl` object that fired the event.
     */
    target: FullscreenControl;
}

/**
 * `FullscreenControlEventType` - a mapping between the fullscreen control event name and the event value.
 * These events are used with the {@link FullscreenControl.on} method.
 *
 * @group Event Related
 */
export type FullscreenControlEventType = {
    /**
     * Fired when fullscreen mode has started.
     */
    fullscreenstart: FullscreenEvent;
    /**
     * Fired when fullscreen mode has ended.
     */
    fullscreenend: FullscreenEvent;
};

/**
 * A `FullscreenControl` control contains a button for toggling the map in and out of fullscreen mode.
 * When [requestFullscreen](https://developer.mozilla.org/en-US/docs/Web/API/Element/requestFullscreen) is not supported, fullscreen is handled via CSS properties.
 * The map's `cooperativeGestures` option is temporarily disabled while the map
 * is in fullscreen mode, and is restored when the map exist fullscreen mode.
 *
 * @group Markers and Controls
 * @param options - the full screen control options
 *
 * @example
 * ```ts
 * map.addControl(new FullscreenControl({container: document.querySelector('body')}));
 * ```
 * @see [View a fullscreen map](https://maplibre.org/maplibre-gl-js/docs/examples/view-a-fullscreen-map/)
 *
 * ## Events
 *
 * **Event** `fullscreenstart` of type {@link FullscreenEvent} will be fired when fullscreen mode has started.
 *
 * **Event** `fullscreenend` of type {@link FullscreenEvent} will be fired when fullscreen mode has ended.
 */
export class FullscreenControl extends Evented<FullscreenControlEventType> implements IControl {
    _map: Map;
    _controlContainer: HTMLElement;
    _fullscreen: boolean;
    _fullscreenchange: string;
    _fullscreenButton: HTMLButtonElement;
    _container: HTMLElement;
    _prevCooperativeGesturesEnabled: boolean;
    _pseudo: boolean;

    /**
     * @param options - the control's options
     */
    constructor(options: FullscreenControlOptions = {}) {
        super();
        this._fullscreen = false;
        this._pseudo = options.pseudo ?? false;

        if (options?.container) {
            if (options.container instanceof HTMLElement) {
                this._container = options.container;
            } else {
                warnOnce('Full screen control \'container\' must be a DOM element.');
            }
        }

        if ('onfullscreenchange' in document) {
            this._fullscreenchange = 'fullscreenchange';
        } else if ('onmozfullscreenchange' in document) {
            this._fullscreenchange = 'mozfullscreenchange';
        } else if ('onwebkitfullscreenchange' in document) {
            this._fullscreenchange = 'webkitfullscreenchange';
        } else if ('onmsfullscreenchange' in document) {
            this._fullscreenchange = 'MSFullscreenChange';
        }
    }

    /** {@inheritDoc IControl.onAdd} */
    onAdd(map: Map): HTMLElement {
        this._map = map;
        this._container ||= this._map.getContainer();
        this._controlContainer = DOM.create('div', 'maplibregl-ctrl maplibregl-ctrl-group');
        this._setupUI();
        return this._controlContainer;
    }

    /** {@inheritDoc IControl.onRemove} */
    onRemove(): void {
        this._controlContainer.remove();
        this._map = null;
        window.document.removeEventListener(this._fullscreenchange, this._onFullscreenChange);
    }

    _setupUI(): void {
        const button = this._fullscreenButton = DOM.create('button', (('maplibregl-ctrl-fullscreen')), this._controlContainer);
        DOM.create('span', 'maplibregl-ctrl-icon', button).setAttribute('aria-hidden', 'true');
        button.type = 'button';
        this._updateTitle();
        this._fullscreenButton.addEventListener('click', this._onClickFullscreen);
        window.document.addEventListener(this._fullscreenchange, this._onFullscreenChange);
    }

    _updateTitle(): void {
        const title = this._getTitle();
        this._fullscreenButton.setAttribute('aria-label', title);
        this._fullscreenButton.title = title;
    }

    _getTitle(): string {
        return this._map._getUIString(this._isFullscreen() ? 'FullscreenControl.Exit' : 'FullscreenControl.Enter');
    }

    _isFullscreen(): boolean {
        return this._fullscreen;
    }

    _onFullscreenChange = (): void => {
        // WebKit due to https://caniuse.com/mdn-api_document_fullscreenelement
        let fullscreenElement =
            window.document.fullscreenElement ||
            (window.document as any).webkitFullscreenElement;

        while (fullscreenElement?.shadowRoot?.fullscreenElement) {
            fullscreenElement = fullscreenElement.shadowRoot.fullscreenElement;
        }

        if ((fullscreenElement === this._container) !== this._fullscreen) {
            this._handleFullscreenChange();
        }
    };

    _handleFullscreenChange(): void {
        this._fullscreen = !this._fullscreen;
        this._fullscreenButton.classList.toggle('maplibregl-ctrl-shrink');
        this._fullscreenButton.classList.toggle('maplibregl-ctrl-fullscreen');
        this._updateTitle();

        if (this._fullscreen) {
            this.fire(new FullscreenEvent('fullscreenstart'));
            this._prevCooperativeGesturesEnabled = this._map.cooperativeGestures.isEnabled();
            this._map.cooperativeGestures.disable();
        } else {
            this.fire(new FullscreenEvent('fullscreenend'));
            if (this._prevCooperativeGesturesEnabled) {
                this._map.cooperativeGestures.enable();
            }
        }
    }

    _onClickFullscreen = (): void => {
        if (this._isFullscreen()) {
            this._exitFullscreen();
        } else {
            this._requestFullscreen();
        }
    };

    _exitFullscreen(): void {
        if (this._pseudo) {
            this._togglePseudoFullScreen();
        } else if (window.document.exitFullscreen) {
            (window.document as any).exitFullscreen();
        } else if ((window.document as any).webkitCancelFullScreen) {
            // due to https://caniuse.com/mdn-api_document_exitfullscreen
            (window.document as any).webkitCancelFullScreen();
        } else {
            this._togglePseudoFullScreen();
        }
    }

    _requestFullscreen(): void {
        if (this._pseudo) {
            this._togglePseudoFullScreen();
        } else if (this._container.requestFullscreen) {
            this._container.requestFullscreen();
        } else if ((this._container as any).webkitRequestFullscreen) {
            // due to https://caniuse.com/mdn-api_element_requestfullscreen
            (this._container as any).webkitRequestFullscreen();
        } else {
            this._togglePseudoFullScreen();
        }
    }

    _togglePseudoFullScreen(): void {
        this._container.classList.toggle('maplibregl-pseudo-fullscreen');
        this._handleFullscreenChange();
        this._map.resize();
    }
}
