import { BehaviorSubject, merge, Observable, Subject } from 'rxjs';
import React, { useContext, useEffect, useCallback, useMemo } from 'react';
import { Action, History, Location, createBrowserHistory, LocationState, Path } from 'history';
import { tap } from 'rxjs/operators';

import { useConstant } from './useConstant';

import { Ctx } from './index';

interface HistoryMethodCall {
    method: 'push' | 'replace';
    args: [Path, LocationState?];
}

/**
 * This is the global router context/state
 */
export const RouterContext = React.createContext<{
    historyState$: BehaviorSubject<HistoryEvent>;
    push: (path: Path, state?: LocationState) => void;
    replace: (path: Path, state?: LocationState) => void;
}>({
    historyState$: new BehaviorSubject({
        action: 'POP',
        location: { pathname: '', search: '', hash: '', state: {} },
    } as HistoryEvent),
    push: (..._args: any[]) => {
        console.error('Not implemented - have you wrapped?');
    },
    replace: (..._args: any[]) => {
        console.error('Not implemented - have you wrapped?');
    },
});

export const RouterProvider: React.FC = React.memo((props) => {
    /**
     * This is the initial location/action param given in both SSR and browser environments
     */
    const { location, action } = useContext(Ctx);
    /**
     * This is a stream that allows child components to invoke history methods
     * (such as the <Link /> component)
     */
    const historyMethodInvocations$ = useConstant(() => new Subject<HistoryMethodCall>());
    /**
     * This tracks location/actions and can be subscribed to to receive the current value
     */
    const historyState$ = useConstant(
        () => new BehaviorSubject<HistoryEvent>({ location, action }),
    );
    /**
     * Expose a 'push' method on the context
     */
    const push = useCallback(
        (path: Path, state?: LocationState) => {
            historyMethodInvocations$.next({ method: 'push', args: [path, state] });
        },
        [historyMethodInvocations$],
    );
    /**
     * Expose a 'replace' method on the context
     */
    const replace = useCallback(
        (path: Path, state?: LocationState) => {
            historyMethodInvocations$.next({ method: 'replace', args: [path, state] });
        },
        [historyMethodInvocations$],
    );
    useEffect(() => {
        /**
         * Create a browser history. Done here to prevent errors in SSR
         */
        const history = createBrowserHistory();
        /**
         * Apply the history invocations
         */
        const appliedHistoryMethod$ = historyMethodInvocations$.pipe(
            tap((x) => history[x.method].apply(null, x.args as any)),
        );
        /**
         * A listener for raw history events (such as back button etc)
         */
        const historyEvents$ = historyAsObs(history).pipe(tap((x) => historyState$.next(x)));
        /**
         * Begin all subscriptions
         */
        const sub = merge(appliedHistoryMethod$, historyEvents$).subscribe();
        /**
         * Tear down
         */
        return () => sub.unsubscribe();
    }, [historyMethodInvocations$, historyState$]);

    /**
     * This is the public API of the context this component provides
     */
    const api = useMemo(() => {
        return {
            push,
            replace,
            historyState$,
        };
    }, [push, historyState$, replace]);
    return <RouterContext.Provider value={api}>{props.children}</RouterContext.Provider>;
});

export interface HistoryEvent {
    location: Location;
    action: Action;
}

/**
 * Wrap raw events from the history obj as an observable
 */
export function historyAsObs(history: History): Observable<HistoryEvent> {
    return new Observable((obs) => {
        const handler = (location: Location, action: Action) => {
            obs.next({
                location,
                action,
            });
        };
        const unlisten = history.listen(handler);
        return () => {
            if (typeof unlisten === 'function') {
                // debug('history listener teardown');
                unlisten();
            } else {
                console.error('Could not tear down the history component');
            }
        };
    });
}

/**
 * Re-exports for convenience
 */
export { Link } from './Link';
export { useLocation } from './useLocation';
