import {
    ICallback,
    IErrorCallback,
    IListener,
    IMarkedForUnsubscribe,
    IObserver,
    IOnceMarker,
    ISetup,
    IStream,
    ISubscribe,
    ISubscribeObject,
    ISubscriptionLike
} from "./Types";
import {negativeCallback, positiveCallback, quickDeleteFromArray, randomCallback} from "./FunctionLibs";

export class SubscribeObject<T> implements ISubscribeObject<T>, IMarkedForUnsubscribe {
    isMarkedForUnsubscribe: boolean = false;
    protected observable: IObserver<T> | undefined;
    protected listener: IListener<T> | undefined;
    protected errorHandler: IErrorCallback = (errorData: any, errorMessage: any) => {
        console.log(`(Unit of SubscribeObject).send(${errorData}) ERROR:`, errorMessage);
    };
    protected _order = 0;
    private isListenPaused = false;
    private once: IOnceMarker = {isOnce: false, isFinished: false};
    private unsubscribeByNegativeCondition: ICallback<T> = <any>null;
    private unsubscribeByPositiveCondition: ICallback<T> = <any>null;
    private emitByNegativeCondition: ICallback<T> = <any>null;
    private emitByPositiveCondition: ICallback<T> = <any>null;
    private emitMatchCondition: ICallback<T> = <any>null;
    protected isPipe = false;

    constructor(observable?: IObserver<T>, isPipe?: boolean) {
        this.observable = observable;
        this.isPipe = !!isPipe;
    }

    private static callbackSend<T>(value: T, subsObj: SubscribeObject<T>): void {
        const listener = subsObj.listener;
        if (!listener) return subsObj.unsubscribe();
        if (!subsObj.observable) return subsObj.unsubscribe();
        if (subsObj.isListenPaused) return;
        if (!subsObj.isPipe) return listener(value);
        if (subsObj.emitByPositiveCondition && subsObj.emitByPositiveCondition(value)) return listener(value);
        if (subsObj.emitByNegativeCondition && !subsObj.emitByNegativeCondition(value)) return listener(value);
        if (subsObj.once.isOnce) {
            subsObj.once.isFinished = true;
            listener(value);
            return subsObj.unsubscribe();
        }
        if (subsObj.unsubscribeByNegativeCondition) {
            if (!subsObj.unsubscribeByNegativeCondition(value)) {
                subsObj.unsubscribeByNegativeCondition = <any>null;
                return subsObj.unsubscribe();
            }
            return listener(value);
        }
        if (subsObj.unsubscribeByPositiveCondition) {
            if (subsObj.unsubscribeByPositiveCondition(value)) {
                subsObj.unsubscribeByPositiveCondition = <any>null;
                return subsObj.unsubscribe();
            }
            return listener(value);
        }
        if (subsObj.emitMatchCondition && (subsObj.emitMatchCondition(value) === value)) return listener(value);
    }

    subscribe(listener: IListener<T>, errorHandler?: IErrorCallback): ISubscriptionLike {
        this.listener = listener;
        errorHandler && (this.errorHandler = errorHandler);
        return this;
    }

    public unsubscribe(): void {
        if (!this.observable) return;
        this.observable.unSubscribe(this);
        this.observable = <any>null;
        this.listener = <any>null;
    }

    send(value: T): void {
        try {
            SubscribeObject.callbackSend(value, this);
        } catch (err) {
            this.errorHandler(value, err);
        }
    }

    setOnce(): ISubscribe<T> {
        this.once.isOnce = true;
        return this;
    }

    unsubscribeByNegative(condition: ICallback<T>): ISubscribe<T> {
        this.unsubscribeByNegativeCondition = condition ?? negativeCallback;
        return this
    }

    unsubscribeByPositive(condition: ICallback<T>): ISubscribe<T> {
        this.unsubscribeByPositiveCondition = condition ?? positiveCallback;
        return this;
    }

    emitByNegative(condition: ICallback<T>): ISubscribe<T> {
        this.emitByNegativeCondition = condition ?? positiveCallback;
        return this;
    }

    emitByPositive(condition: ICallback<T>): ISubscribe<T> {
        this.emitByPositiveCondition = condition ?? negativeCallback;
        return this;
    }

    emitMatch(condition: ICallback<T>): ISubscribe<T> {
        this.emitMatchCondition = condition ?? randomCallback;
        return this;
    }

    resume(): void {
        this.isListenPaused = false;
    }

    pause(): void {
        this.isListenPaused = true;
    }

    get order(): number {
        return this._order;
    }

    set order(value: number) {
        this._order = value;
    }
}

export class Observable<T> implements IObserver<T>, IStream<T> {
    protected listeners: ISubscribeObject<T>[] = [];
    private _isEnable: boolean = true;
    protected _isDestroyed = false;
    protected isNextProcess = false;
    protected listenersForUnsubscribe: ISubscriptionLike[] = [];

    constructor(private value: T) {
    }

    disable(): void {
        this._isEnable = false;
    }

    enable(): void {
        this._isEnable = true;
    }

    get isEnable(): boolean {
        return this._isEnable;
    }

    public next(value: T): void {
        if (this._isDestroyed) return;
        if (!this._isEnable) return;
        this.isNextProcess = true;
        this.value = value;

        for (let i = 0; i < this.listeners.length; i++) this.listeners[i].send(value);

        this.isNextProcess = false;
        this.listenersForUnsubscribe.length && this.handleListenersForUnsubscribe();
    }

    stream(values: T[]): void {
        if (this._isDestroyed) return;
        if (!this._isEnable) return;

        for (let i = 0; i < values.length; i++) this.next(values[i]);
    }

    private handleListenersForUnsubscribe(): void {
        const length = this.listenersForUnsubscribe.length;

        for (let i = 0; i < length; i++) this.unSubscribe(this.listenersForUnsubscribe[i]);

        this.listenersForUnsubscribe.length = 0;
    }

    public unSubscribe(listener: ISubscriptionLike): void {
        if (this._isDestroyed) return;
        if (this.isNextProcess && listener) {
            const marker: IMarkedForUnsubscribe = <any>listener;
            !marker.isMarkedForUnsubscribe && this.listenersForUnsubscribe.push(listener);
            marker.isMarkedForUnsubscribe = true;
            return;
        }
        this.listeners && !quickDeleteFromArray(this.listeners, listener);
    }

    public destroy(): void {
        this.value = <any>null;
        this.unsubscribeAll();
        this.listeners = <any>null;
        this._isDestroyed = true;
    }

    public unsubscribeAll(): void {
        if (this._isDestroyed) return;
        this.listeners.length = 0;
    }

    public getValue(): T | undefined {
        if (this._isDestroyed) return undefined;
        return this.value;
    }

    public size(): number {
        if (this._isDestroyed) return 0;
        return this.listeners.length;
    }

    public subscribe(listener: IListener<T>, errorHandler?: IErrorCallback): ISubscriptionLike | undefined {
        if (this._isDestroyed) return undefined;
        if (!listener) return undefined;
        const subscribeObject = new SubscribeObject(this, false);
        subscribeObject.subscribe(listener, errorHandler);
        this.listeners.push(subscribeObject);
        return subscribeObject;
    }

    pipe(): ISetup<T> | undefined {
        if (this._isDestroyed) return undefined;
        const subscribeObject = new SubscribeObject(this, true);
        this.listeners.push(subscribeObject);
        return subscribeObject;
    }

    get isDestroyed(): boolean {
        return this._isDestroyed;
    }
}
