import {
    filter,
    map,
    publishReplay,
    refCount,
    scan,
    skip,
    startWith,
    tap,
    withLatestFrom,
} from "rxjs/operators";
import {
    BehaviorSubject,
    Observable,
    Subject,
    Subscription,
} from "rxjs";

import { Spatial } from "../Geo";
import { RenderCamera, RenderMode, ISize } from "../Render";
import { IFrame } from "../State";
import SubscriptionHolder from "../utils/SubscriptionHolder";

interface IRenderCameraOperation {
    (rc: RenderCamera): RenderCamera;
}

export class RenderService {
    private _bearing$: Observable<number>;

    private _element: HTMLElement;
    private _currentFrame$: Observable<IFrame>;

    private _renderCameraOperation$: Subject<IRenderCameraOperation>;
    private _renderCameraHolder$: Observable<RenderCamera>;
    private _renderCameraFrame$: Observable<RenderCamera>;
    private _renderCamera$: Observable<RenderCamera>;

    private _resize$: Subject<void>;
    private _size$: BehaviorSubject<ISize>;

    private _spatial: Spatial;

    private _renderMode$: BehaviorSubject<RenderMode>;

    private _subscriptions: SubscriptionHolder = new SubscriptionHolder();

    constructor(element: HTMLElement, currentFrame$: Observable<IFrame>, renderMode: RenderMode, renderCamera?: RenderCamera) {
        this._element = element;
        this._currentFrame$ = currentFrame$;

        this._spatial = new Spatial();

        renderMode = renderMode != null ? renderMode : RenderMode.Fill;

        this._resize$ = new Subject<void>();
        this._renderCameraOperation$ = new Subject<IRenderCameraOperation>();

        this._size$ =
            new BehaviorSubject<ISize>(
                {
                    height: this._element.offsetHeight,
                    width: this._element.offsetWidth,
                });

        const subs = this._subscriptions;
        subs.push(this._resize$.pipe(
            map(
                (): ISize => {
                    return { height: this._element.offsetHeight, width: this._element.offsetWidth };
                }))
            .subscribe(this._size$));

        this._renderMode$ = new BehaviorSubject<RenderMode>(renderMode);

        this._renderCameraHolder$ = this._renderCameraOperation$.pipe(
            startWith(
                (rc: RenderCamera): RenderCamera => {
                    return rc;
                }),
            scan(
                (rc: RenderCamera, operation: IRenderCameraOperation): RenderCamera => {
                    return operation(rc);
                },
                !!renderCamera ? renderCamera : new RenderCamera(this._element.offsetWidth, this._element.offsetHeight, renderMode)),
            publishReplay(1),
            refCount());

        this._renderCameraFrame$ = this._currentFrame$.pipe(
            withLatestFrom(this._renderCameraHolder$),
            tap(
                ([frame, rc]: [IFrame, RenderCamera]): void => {
                    rc.setFrame(frame);
                }),
            map(
                (args: [IFrame, RenderCamera]): RenderCamera => {
                    return args[1];
                }),
            publishReplay(1),
            refCount());

        this._renderCamera$ = this._renderCameraFrame$.pipe(
            filter(
                (rc: RenderCamera): boolean => {
                    return rc.changed;
                }),
            publishReplay(1),
            refCount());

        this._bearing$ = this._renderCamera$.pipe(
            map(
                (rc: RenderCamera): number => {
                    let bearing: number =
                        this._spatial.radToDeg(
                            this._spatial.azimuthalToBearing(rc.rotation.phi));

                    return this._spatial.wrap(bearing, 0, 360);
                }),
            publishReplay(1),
            refCount());

        subs.push(this._size$.pipe(
            skip(1),
            map(
                (size: ISize) => {
                    return (rc: RenderCamera): RenderCamera => {
                        rc.setSize(size);

                        return rc;
                    };
                }))
            .subscribe(this._renderCameraOperation$));

        subs.push(this._renderMode$.pipe(
            skip(1),
            map(
                (rm: RenderMode) => {
                    return (rc: RenderCamera): RenderCamera => {
                        rc.setRenderMode(rm);

                        return rc;
                    };
                }))
            .subscribe(this._renderCameraOperation$));

        subs.push(this._bearing$.subscribe(() => { /*noop*/ }));
        subs.push(this._renderCameraHolder$.subscribe(() => { /*noop*/ }));
        subs.push(this._size$.subscribe(() => { /*noop*/ }));
        subs.push(this._renderMode$.subscribe(() => { /*noop*/ }));
        subs.push(this._renderCamera$.subscribe(() => { /*noop*/ }));
        subs.push(this._renderCameraFrame$.subscribe(() => { /*noop*/ }));
    }

    public get bearing$(): Observable<number> {
        return this._bearing$;
    }

    public get element(): HTMLElement {
        return this._element;
    }

    public get resize$(): Subject<void> {
        return this._resize$;
    }

    public get size$(): Observable<ISize> {
        return this._size$;
    }

    public get renderMode$(): Subject<RenderMode> {
        return this._renderMode$;
    }

    public get renderCameraFrame$(): Observable<RenderCamera> {
        return this._renderCameraFrame$;
    }

    public get renderCamera$(): Observable<RenderCamera> {
        return this._renderCamera$;
    }

    public dispose(): void {
        this._subscriptions.unsubscribe();
    }
}

export default RenderService;
