import { BehaviorSubject, merge, Observable } from 'rxjs';
import React, { JSXElementConstructor, useContext, useReducer, useEffect, useCallback, useMemo, useState } from 'react';
import { filter, map, pairwise, share, switchMap, tap } from 'rxjs/operators';
import { Location } from 'history';
import d from 'debug';

import { HistoryEvent, RouterContext } from './Router';
import { useConstant } from './useConstant';
import { processRoute } from './processRoute';

import { Ctx, PageModule, PageResponse } from './index';

const debug = d('react-sapper:PageRouter');

export interface ResolvedPage {
    mod: PageModule;
    resolved: ResolvedPath;
}

export interface ResolvedPath {
    pathname: string;
    pageName: string;
    chunkName: string;
    id?: number;
}

enum RouterStates {
    Resolving = 'Resolving',
    Resolved = 'Resolved',
    Complete = 'Complete',
}

type RouterState = {
    value: RouterStates;
    resolved: ResolvedPath | null;
    Cmp: JSXElementConstructor<any> | null;
};

type RouterEvents =
    | {
          type: 'resolve';
      }
    | {
          type: 'resolved';
          resolved: ResolvedPath;
          Cmp: JSXElementConstructor<any>;
      }
    | {
          type: 'complete';
      };

function routerReducer(state: RouterState, action: RouterEvents): RouterState {
    debug('action ->', action);
    switch (action.type) {
        case 'resolve':
            return { ...state, value: RouterStates.Resolving };
        case 'resolved':
            return {
                ...state,
                value: RouterStates.Resolved,
                resolved: action.resolved,
                Cmp: action.Cmp,
            };
        case 'complete':
            return { ...state, value: RouterStates.Complete };
    }
}

const PageRouterContext = React.createContext<{ complete(): void; page$: BehaviorSubject<{ resolved: ResolvedPath }> }>(
    {
        page$: new BehaviorSubject<{ resolved: ResolvedPath }>({
            resolved: {
                pathname: '/',
                id: -1,
                pageName: '',
                chunkName: 'none',
            },
        }),
        complete: () => {
            console.error('PageRouterContext.complete missing, did you forget to wrap?');
        },
    },
);

export const PageRouterOutlet: React.FC = React.memo(() => {
    const { resolved, Cmp, appContext, pages, store } = useContext(Ctx);
    const { historyState$, replace } = useContext(RouterContext);
    const page$ = useConstant(
        () => new BehaviorSubject<{ resolved: ResolvedPath }>({ resolved }),
    );
    const [state, dispatch] = useReducer(routerReducer, {
        value: RouterStates.Complete,
        resolved: { ...resolved },
        Cmp,
    });
    useEffect(() => {
        const historyPairs$: Observable<[HistoryEvent, HistoryEvent]> = historyState$.pipe(pairwise(), share());
        const resolve$ = historyPairs$.pipe(
            filter(([prev, next]) => {
                // todo: not sure if this is robust enough
                if (prev.location.pathname === next.location.pathname) {
                    if (prev.location.search !== next.location.search) {
                        return false;
                    }
                    if (prev.location.hash !== next.location.hash) {
                        return false;
                    }
                }
                return (
                    prev.location.pathname !== next.location.pathname ||
                    prev.location.search !== next.location.search ||
                    prev.location.hash !== next.location.hash ||
                    prev.location.state !== next.location.state
                );
            }),
            tap(() => dispatch({ type: 'resolve' })),
            map(([, next]) => next.location),
            switchMap((location: Location) => processRoute(location.pathname, location, store, pages, appContext)),
        );

        const pages$ = resolve$.pipe(
            tap(({ response, resolved, mod }: { response: PageResponse; resolved: ResolvedPath; mod: PageModule }) => {
                switch (response.kind) {
                    case 'success':
                    case 'not-found': {
                        dispatch({ type: 'resolved', resolved, Cmp: mod.default });
                        page$.next({ resolved: { ...resolved } });
                        break;
                    }
                    case 'redirect': {
                        replace(response.location);
                        break;
                    }
                    case 'error': {
                        console.error(`Error from Page Router: ${response.message}`);
                    }
                }
            }),
        );

        const sub = merge(pages$).subscribe();
        return () => {
            sub.unsubscribe();
        };
    }, [page$, pages, appContext, historyState$, store, replace]);

    const complete = useCallback(() => {
        dispatch({ type: 'complete' });
    }, []);
    const api = useMemo(() => {
        return { complete, page$ };
    }, [complete, page$]);
    return (
        <div data-router-outlet-value={state.value} data-router-outlet-page-name={state.resolved?.pageName}>
            <PageRouterContext.Provider value={api}>{state.Cmp && <state.Cmp />}</PageRouterContext.Provider>
        </div>
    );
});

export function usePageComplete() {
    const { complete } = useContext(PageRouterContext);
    useEffect(() => {
        complete();
    }, [complete]);
}

export function usePage() {
    const { page$ } = useContext(PageRouterContext);
    const [page, setPage] = useState(page$.value);
    useEffect(() => {
        const sub = page$.subscribe((x) => setPage(x));
        return () => {
            sub.unsubscribe();
        };
    }, [page$]);
    return page;
}
