import {
    OnAppearEventListenerCallback,
    OnChangeEventListenerCallback,
    OnStartEventListenerCallback, OnStopEventListenerCallback,
    SelectorCallback
} from "./callbacks";
import {EventEmitter} from "events";
import {OnDisappearEventListenerCallback} from "./callbacks/OnDisappearEventListenerCallback";
import {IDOMObserverInitParameters} from "./parameters/IDOMObserverInitParameters";
import {setImmediate, sleep} from "delay-me";

export class DOMObserver<T extends Element = Element>{

    private readonly emitter: EventEmitter = new EventEmitter();
    private readonly selector: SelectorCallback<T>;
    private observer: MutationObserver = null;

    private _is_active: boolean = false;
    public get isActive(): boolean{ return this._is_active; }

    private _parameters: IDOMObserverInitParameters<T> = null;
    public get parameters(): IDOMObserverInitParameters<T>{ return Object.assign({}, this._parameters); }

    private _state: 'appeared'|'disappeared' = 'disappeared';
    private $elementRef: T = null;

    /**
     * @param {SelectorCallback<T>} selector
     * @param {IDOMObserverInitParameters<T>>} parameters
     */
    constructor(selector: SelectorCallback<T>, parameters: IDOMObserverInitParameters<T>) {
        this._parameters = parameters;
        this.selector = selector;

        this.observeProcess = this.observeProcess.bind(this);
        this.onMutationOccurred = this.onMutationOccurred.bind(this);
    }

    /**
     * Starts DOM observation
     */
    public start(): void{
        if(this._is_active)
            return;

        this._is_active = true;

        this.emit('start');
        setImmediate(this.observeProcess);
    }

    /**
     * Stops DOM observation
     */
    public stop(): void{
        if(!this._is_active)
            return;

        this._is_active = false;
        this.observer.disconnect();
        this.observer = null;

        this.emit('stop');
    }

    /**
     * @returns {Promise<void>}
     * @protected
     */
    protected async observeProcess() {
        if(!this._is_active)
            return;

        switch(this._state){
            case 'appeared': {
                if(this.$elementRef === null || !document.contains(this.$elementRef)){
                    this._state = 'disappeared';
                    if(this.observer !== null){
                        this.observer.disconnect();
                        this.observer = null;
                    }

                    const $element = this.$elementRef;
                    this.$elementRef = null;

                    this.emit('disappear', $element as T);
                }
            } break;
            case 'disappeared': {
                let $element = this.selector.apply(null, []);

                if($element !== null){
                    this._state = 'appeared';
                    this.$elementRef = $element;
                    this.emit('appear', $element as T);

                    if(this.observer !== null){
                        this.observer.disconnect();
                        this.observer = null;
                    }

                    if(!this._is_active)
                        return;

                    this.observer = new MutationObserver(this.onMutationOccurred);
                    this.observer.observe(this.$elementRef, this._parameters);
                }
            } break;
        }

        if(this._is_active) {
            await sleep(this.parameters.interval);
            setImmediate(this.observeProcess);
        }
    }

    /**
     * @param {MutationRecord[]} mutations
     * @protected
     */
    protected onMutationOccurred(mutations: MutationRecord[]): void{
        if(!this._is_active)
            return;

        this.emit('change', this.$elementRef as T, mutations);
    }

    //region Events

    protected emit(event: 'start'): void;
    protected emit(event: 'appear', $element: T): void;
    protected emit(event: 'change', $element: T, mutations: MutationRecord[]): void;
    protected emit(event: 'disappear', $element: T): void;
    protected emit(event: 'stop'): void;
    protected emit(event: string, ...args: any[]): void{
        this.emitter.emit(event, ...args);
    }

    public on(event: 'start', listener: OnStartEventListenerCallback): DOMObserver<T>;
    public on(event: 'appear', listener: OnAppearEventListenerCallback<T>): DOMObserver<T>;
    public on(event: 'change', listener: OnChangeEventListenerCallback<T>): DOMObserver<T>;
    public on(event: 'disappear', listener: OnDisappearEventListenerCallback<T>): DOMObserver<T>;
    public on(event: 'stop', listener: OnStopEventListenerCallback): DOMObserver<T>;
    public on(event: string, listener: {(...args: any[]): any;}): DOMObserver<T>{
        this.emitter.on(event, listener);
        return this;
    }

    public once(event: 'start', listener: OnStartEventListenerCallback): DOMObserver<T>;
    public once(event: 'appear', listener: OnAppearEventListenerCallback<T>): DOMObserver<T>;
    public once(event: 'change', listener: OnChangeEventListenerCallback<T>): DOMObserver<T>;
    public once(event: 'disappear', listener: OnDisappearEventListenerCallback<T>): DOMObserver<T>;
    public once(event: 'stop', listener: OnStopEventListenerCallback): DOMObserver<T>;
    public once(event: string, listener: {(...args: any[]): any;}): DOMObserver<T>{
        this.emitter.once(event, listener);
        return this;
    }

    public off(event: 'start', listener: OnStartEventListenerCallback): DOMObserver<T>;
    public off(event: 'appear', listener: OnAppearEventListenerCallback<T>): DOMObserver<T>;
    public off(event: 'change', listener: OnChangeEventListenerCallback<T>): DOMObserver<T>;
    public off(event: 'disappear', listener: OnDisappearEventListenerCallback<T>): DOMObserver<T>;
    public off(event: 'stop', listener: OnStopEventListenerCallback): DOMObserver<T>;
    public off(event: string, listener: {(...args: any[]): any;}): DOMObserver<T>{
        this.emitter.off(event, listener);
        return this;
    }

    //endregion

}
