import React from 'react';
import {
    BrowserRouter,
    RouteObject,
    Params,
    useParams,
    Location,
    useLocation,
    NavigateFunction,
    useNavigate,
    Navigate,
} from 'react-router-dom'
import { Loading as LoadingComponent } from '../layout/loading';
import Localization from '../providers/localization';
import Themes, { Theme } from '../providers/theme';
import { NoData } from '../services';

export namespace Hook {

    /*************************************** PROVIDER ***************************************/

    interface ProviderProps {
        routes?: Routes,
        dialogs?: DialogComponent<any, any>[],
        children?: React.ReactNode | React.ReactNode[] | any,
    }

    export const Provider: React.FC<ProviderProps> = ({ routes, dialogs, children }) => {
        if (!dialogs) {
            dialogs = []
        }

        const ProviderContent: React.FC<any> = () => {
            const location = useLocation();
            const navigate = useNavigate();

            const setupRouteGuard = React.useCallback(() => {
                if (routes && routes.guard) {
                    const path = routes.guard(location.pathname);

                    if (typeof path === 'string') {
                        if (location.pathname !== path) {
                            navigate(path);
                        }
                    }
                    else {
                        path.then(path => {
                            if (location.pathname !== path) {
                                navigate(path);
                            }
                        })
                    }
                }
            }, [location, navigate])

            React.useEffect((setupGuard = setupRouteGuard) => {
                setupGuard();
            });

            return (
                <>
                    {children}
                    {
                        dialogs && dialogs.map((component, key) => (
                            <DialogHook key={key} component={component} />
                        ))
                    }
                </>
            )
        }

        return (
            <BrowserRouter>
                <ProviderContent />
            </BrowserRouter>
        );
    }

    /**************************************** DIALOG ****************************************/

    type DialogComponentProps<D = {}, O = {}, P extends { [K in keyof P]?: string } = {}> = P & {
        open?: boolean,
        data: D,
        onClose?: DialogCallback<O>,
    }

    export interface DialogCallbackEvent<D = {}> {
        data: D,
        rejected?: boolean
    }
    export type DialogCallback<O = {}> = (event: DialogCallbackEvent<O>) => void;

    export type DialogComponent<D = {}, O = {}> = React.ComponentType<DialogComponentProps<D, O>>;

    const DialogHook: React.FC<{ component: DialogComponent }> = ({ component: Component }) => {
        const [open, setOpen] = React.useState(false);
        const [data, setData] = React.useState<any>(null);
        const [callback, setCallback] = React.useState<{ func: DialogCallback }>({
            func: () => { }
        });

        (Component as any).open = (data: any, callback: DialogCallback) => {
            setData(data);
            setOpen(true);
            setCallback({ func: callback });
        }
        (Component as any).close = () => {
            setData(null);
            setOpen(false);
            setCallback({ func: () => { } });
        }
        const handleClose = (event: DialogCallbackEvent) => {
            setOpen(false);

            if (callback && callback.func) {
                callback.func(event);
            }
        }

        return (
            <>
                {
                    open &&
                    <Component open={!!open} data={data} onClose={handleClose} />
                }
            </>
        );
    }

    export function Dialog<D = {}, O = {}>(dialog: (props: DialogComponentProps<D, O>, context?: any) => React.ReactElement<any, any> | null): {
        (props: DialogComponentProps<D, O>, context?: any): React.ReactElement<any, any> | null;
        open: (data: D, callback?: DialogCallback<O>) => void;
        close: () => void;
    } { return dialog as any }

    /**************************************** EVENTS ****************************************/

    class EventHandler {
        private subscriptions: {
            [key: string]: (value: any) => void
        } = {}

        public register(key: string, callback: (value: any) => void) {
            this.subscriptions[key] = callback;
        }

        public unregister(key: string) {
            delete this.subscriptions[key];
        }

        public send(value: any) {
            for (const key in this.subscriptions) {
                if (Object.prototype.hasOwnProperty.call(this.subscriptions, key)) {
                    const subscription = this.subscriptions[key];

                    if (subscription) {
                        subscription(value)
                    }
                }
            }
        }
    }
    const eventHandlers: { [key: string]: EventHandler } = {}

    export interface Event<T> {
        id: T,
    }

    export function createEvent<T>(): Event<T> {
        const id = guid() as any;

        eventHandlers[id] = new EventHandler();

        const result: Event<T> = {
            id,
        }
        return result;
    }

    export function sendEvent<T>(context: Event<T>, value: T) {
        if (context) {
            const handler = eventHandlers[context.id as any];

            if (handler) {
                handler.send(value)
            }
        }
    }

    export function useEvent<T>(context: Event<T>, callback: (value: T) => void) {
        React.useEffect(() => {
            const handler = crossStateHandlers[context.id as any];

            if (handler) {
                const id = guid();

                handler.register(id, callback)

                return () => {
                    handler.unregister(id);
                }
            }
        })
    }

    /************************************* CROSS STATES *************************************/

    class CrossStateHandler {
        private subscriptions: {
            [key: string]: (value: any) => void
        } = {}
        private currentValue: any;

        constructor(defaultValue: any) {
            this.currentValue = defaultValue;
        }

        public register(key: string, callback: (value: any) => void) {
            this.subscriptions[key] = callback;
        }

        public unregister(key: string) {
            delete this.subscriptions[key];
        }

        public get value(): any {
            return this.currentValue;
        }

        public set value(val: any) {
            if (this.currentValue !== val && (!Object.is(this.currentValue, NaN) || !Object.is(val, NaN))) {
                this.currentValue = val;

                for (const key in this.subscriptions) {
                    if (Object.prototype.hasOwnProperty.call(this.subscriptions, key)) {
                        const subscription = this.subscriptions[key];

                        if (subscription) {
                            subscription(this.currentValue)
                        }
                    }
                }
            }
        }
    }
    const crossStateHandlers: { [key: string]: CrossStateHandler } = {}

    export interface CrossStateContext<T> {
        id: T
    }

    export function createCrossState<T>(defaultValue?: T): CrossStateContext<T> {
        const id = guid() as any;

        crossStateHandlers[id] = new CrossStateHandler(defaultValue);

        const result: CrossStateContext<T> = {
            id
        }
        return result;
    }

    export function crossState<T>(context: CrossStateContext<T>): { getValue: () => T, setValue: (newValue: T) => void } {
        const handler = crossStateHandlers[context.id as any];

        return handler
            ? {
                getValue: () => handler.value,
                setValue: (newValue: T) => { handler.value = newValue }
            }
            : {
                getValue: () => NoData,
                setValue: () => { }
            }
    }

    export function useCrossState<T>(context: CrossStateContext<T>): [T, (value: T) => void] {
        const handler = crossStateHandlers[context.id as any];

        const [value, setValue] = React.useState<T>(
            context && crossStateHandlers[context.id as any]
                ? crossStateHandlers[context.id as any].value
                : NoData
        );

        const handleSetValue = React.useCallback((newValue: T) => {
            if (handler) {
                handler.value = newValue;

                if (value !== newValue) {
                    setValue(handler.value);
                }
            }
        }, [value, setValue])

        React.useEffect(() => {
            if (handler) {
                const id = guid();

                if (value !== handler.value) {
                    setValue(handler.value);
                }
                handler.register(id, handleSetValue)

                return () => {
                    handler.unregister(id);
                }
            }
        })
        return [value, handleSetValue]
    }

    export function useRefresh(): [() => void] {
        const [forceUpdate, setForceUpdate] = React.useState(false);

        return [() => setForceUpdate(!forceUpdate)]
    }

    /**************************************** THEMES ****************************************/

    export function useTheme(): [Theme, (name: string) => void] {
        const { theme, setTheme } = React.useContext(Themes.Context);

        return [theme, setTheme];
    }

    /************************************* LOCALIZATION *************************************/

    export function useLanguage(): [string, (lan: string) => void] {
        const { language, setLanguage } = React.useContext(Localization.Context);

        return [language, setLanguage];
    }

    export function useLocalization<T>(texts: { [language: string]: T }): [T, string, (language: string) => void] {
        const { language, setLanguage } = React.useContext(Localization.Context);

        return [texts[language], language, setLanguage];
    }

    /**************************************** LOADING ***************************************/

    class LoadingHandler {
        private subscriptions: {
            [key: string]: (value: boolean) => void
        } = {}
        private currentValue: number = 0;

        public register(key: string, callback: (value: boolean) => void) {
            this.subscriptions[key] = callback;
        }

        public unregister(key: string) {
            delete this.subscriptions[key];
        }

        public increase() {
            this.currentValue++;

            if (this.currentValue === 1) {
                for (const key in this.subscriptions) {
                    if (Object.prototype.hasOwnProperty.call(this.subscriptions, key)) {
                        const subscription = this.subscriptions[key];

                        if (subscription) {
                            subscription(true)
                        }
                    }
                }
            }
        }

        public decrease() {
            if (this.currentValue > 0) {
                this.currentValue--;

                if (this.currentValue === 0) {
                    for (const key in this.subscriptions) {
                        if (Object.prototype.hasOwnProperty.call(this.subscriptions, key)) {
                            const subscription = this.subscriptions[key];

                            if (subscription) {
                                subscription(false)
                            }
                        }
                    }
                }
            }
        }

        public get value(): boolean {
            return !!this.currentValue;
        }
    }
    const loadingHandler = new LoadingHandler();

    export const Loading: React.FC<{}> = () => {
        const [loading, setLoading] = React.useState(loadingHandler.value);

        const handleSetValue = React.useCallback((newValue: boolean) => {
            if (loading !== newValue) {
                setLoading(newValue);
            }
        }, [loading, setLoading])

        React.useEffect(() => {
            const id = guid();

            if (loading !== loadingHandler.value) {
                setLoading(loadingHandler.value);
            }
            loadingHandler.register(id, handleSetValue)

            return () => {
                loadingHandler.unregister(id);
            }
        })
        return (
            <>
                {loading && <LoadingComponent />}
            </>
        )
    }

    export function loading(): { start: () => void, stop: () => void } {
        return {
            start: () => loadingHandler.increase(),
            stop: () => loadingHandler.decrease()
        }
    }

    /**************************************** ROUTES ****************************************/

    export interface RouteMenu {
        space?: boolean,
        icon: React.FC<any>,
        sidebar?: React.FC<any>,
    }

    export interface Route {
        menu?: RouteMenu,
        admin?: boolean,
        component: React.FC<any>,
        children?: { [path: string]: Route },
    }

    export interface Routes {
        guard?: (path: string) => Promise<string> | string,
        labels?: { [lan: string]: { [path: string]: string } },
        children?: { [path: string]: Route },
    }

    export function createRoutes(routes: Routes) {
        return routes
            ? convertToBrowserRoutes(routes.children)
            : []
    }

    function convertToBrowserRoutes(routes?: { [path: string]: Route }, nested?: boolean): RouteObject[] {
        const result: RouteObject[] = [];

        if (routes) {
            let defaultPath = '';
            let needDefault = true;

            for (const path in routes) {
                if (Object.prototype.hasOwnProperty.call(routes, path)) {
                    const route = routes[path];

                    result.push({
                        path: nested && path && path.length > 0 && path[0] === '/'
                            ? path.substring(1)
                            : path,
                        caseSensitive: false,
                        element: <route.component />,
                        children: convertToBrowserRoutes(route.children, true)
                    })
                    if (path === '/' || path === '*') {
                        needDefault = false;
                    }
                    else if (!defaultPath) {
                        defaultPath = path;
                    }
                }
            }
            if (needDefault) {
                result.push({
                    path: '*',
                    element: <Navigate to={defaultPath} replace={true} />
                })
            }
        }
        return result;
    }

    export function getVisibleRoutes(routes: Routes, isVisible: (path: string, route?: Route) => boolean): Routes {
        return routes
            ? {
                guard: routes.guard,
                labels: routes.labels,
                children: findVisibleRoutes(routes.children, '', isVisible)
            }
            : {}
    }

    function findVisibleRoutes(routes: { [path: string]: Route } | undefined, globalPath: string, isVisible: (path: string, route?: Route) => boolean): { [path: string]: Route } {
        const result: { [path: string]: Route } = {}

        if (routes) {
            for (const path in routes) {
                if (Object.prototype.hasOwnProperty.call(routes, path)) {
                    const route = routes[path];
                    const children = findVisibleRoutes(route.children, globalPath + path, isVisible);

                    if (Object.keys(children).length === 0 && !isVisible(globalPath + path, route)) {
                        continue;
                    }
                    result[path] = {
                        menu: route.menu,
                        component: route.component,
                        children
                    }
                }
            }
        }
        return result;
    }

    export function isValidPath(routes: Routes, path: string) {
        if (routes) {
            const splitPath = path.split('/').map(x => x.trim()).filter(x => x);

            let route: any = {
                children: !!routes.children && routes.children['/']
                    ? routes.children['/'].children
                    : routes.children,
            }
            for (let i = 0; !!route && i < splitPath.length; i++) {
                const splitKey = `/${splitPath[i]}`;

                if (route.children) {
                    route = route.children[splitKey];
                }
            }
            return !!route;
        }
        return false;
    }

    export function getCrumbroad(routes: Routes) {
        if (routes && routes.labels) {
            const language = (localStorage && localStorage.getItem("app-localization")) || "en";

            if (routes.labels[language]) {
                let result = '';
                const splitPath =
                    window.location.pathname
                        .split('/')
                        .map((x) => x.trim())
                        .filter((x) => x)
                        .map(x => `/${x}`);
                let currentPath = '';

                for (let i = 0; i < splitPath.length; i++) {
                    const path = splitPath[i];

                    currentPath += path;

                    if (!routes.labels[language][currentPath]) {
                        return '';
                    }
                    result = result
                        ? `${result} > ${routes.labels[language][currentPath]}`
                        : routes.labels[language][currentPath]
                }
                return result;
            }
        }
        return '';
    }

    export function useRouter(): [Location & { params: Readonly<Params<string>> }, NavigateFunction] {
        const params = useParams();
        const location = useLocation();
        const navigate = useNavigate();

        return [{ ...location, params }, navigate]
    }

    /************************************** DIMENSIONS **************************************/

    export const useDimensions = (excludePadding = false) => {
        const ref = React.useRef<HTMLDivElement>(null);
        const [dimensions, setDimensions] = React.useState<any>({
            width: 0,
            height: 0
        });
        const handleSetDimensions = React.useCallback(() => {
            if (ref?.current) {
                const style = window.getComputedStyle(ref.current, null);
                const innerSize = { height: style.height, width: style.width };
                const outterSize = ref.current.getBoundingClientRect().toJSON();
                setDimensions(excludePadding ? innerSize : outterSize);
            }
        }, [excludePadding]);

        React.useLayoutEffect(() => {
            handleSetDimensions();
        }, [handleSetDimensions]);

        React.useEffect(() => {
            const listener = () => {
                handleSetDimensions();
            };
            window.addEventListener("resize", listener);
            return () => {
                window.removeEventListener("resize", listener);
            };
        }, [handleSetDimensions]);

        return [ref, dimensions];
    }

    /*********************************** SCROLL POSITION ************************************/

    export type ScrollPosition = {
        x: number,
        y: number,
        w: number,
        h: number,
        c: { w: number, h: number }
    }
    export const DefaultScrollPosition: ScrollPosition =
        { x: 0, y: 0, w: 0, h: 0, c: { w: 0, h: 0 } };

    type ScrollPositionEffect =
        (params: { previous: ScrollPosition, current: ScrollPosition }) => void

    export const useScrollPosition = (element: any, handler?: ScrollPositionEffect, debounce: number = 100, deps?: any[]) => {
        const position = React.useRef(getScrollPosition())
        const throttleTimeout = React.useRef<any>(null);

        const callback = React.useCallback(() => {
            const currPos = getScrollPosition(element);

            if (handler) {
                handler({ previous: position.current, current: currPos });
            }
            position.current = currPos;
            throttleTimeout.current = null;
        }, [handler, element])

        React.useLayoutEffect(() => {
            const handleScroll = () => {
                if (debounce) {
                    if (throttleTimeout.current === null) {
                        throttleTimeout.current = setTimeout(callback, debounce);
                    }
                }
                else {
                    callback()
                }
            }
            if (element && element.current) {
                element.current.addEventListener('scroll', handleScroll);
            }
            else {
                window.addEventListener('scroll', handleScroll);
            }

            return () => {
                if (element && element.current) {
                    element.current.removeEventListener('scroll', handleScroll);
                }
                else {
                    window.removeEventListener('scroll', handleScroll);
                }
            }
        }, [callback, handler, element, deps, debounce])

        React.useLayoutEffect(() => {
            const handleResize = () => {
                if (debounce) {
                    if (throttleTimeout.current === null) {
                        throttleTimeout.current = setTimeout(callback, debounce);
                    }
                }
                else {
                    callback()
                }
            }
            const currPos = getScrollPosition(element);

            if (currPos.x !== position.current.x || currPos.y !== position.current.y || currPos.w !== position.current.w || currPos.h !== position.current.h) {
                handleResize()
            }
            window.addEventListener('resize', handleResize);

            return () => {
                window.removeEventListener('resize', handleResize);
            }

        }, [callback, handler, element, deps, debounce])

        if (!handler) {
            return position.current;
        }
    }

    function getScrollPosition(element?: any): ScrollPosition {
        const isBrowser = typeof window !== `undefined`;

        if (!isBrowser) return DefaultScrollPosition;

        const target = element ? element.current : document.body

        return {
            x: target.scrollLeft,
            y: target.scrollTop,
            w: target.clientWidth,
            h: target.clientHeight,
            c: {
                w: target.scrollWidth,
                h: target.scrollHeight,
            }
        }
    }

    /*************************************** DEBOUNCE ***************************************/

    export function useDebounce<T extends (...args: any[]) => void>(
        callback: T,
        delay: number,
        deps: React.DependencyList,
    ): T {
        const context = React.useRef({
            disposed: false as boolean,
            timeoutId: undefined as number | undefined,
            callbackWrapper: undefined as Function | undefined
        })
        React.useLayoutEffect(() => {
            return () => {
                context.current.disposed = true

                if (context.current.callbackWrapper && context.current.timeoutId) {
                    context.current.callbackWrapper()
                    clearTimeout(context.current.timeoutId)
                }
            }
        }, deps)

        return React.useCallback<T>(((...args) => {
            if (context.current.disposed) {
                throw new Error(
                    [
                        'Trying to call an already disposed callback.',
                        'In theory you should never call a disposed callback.',
                        'This is probably a bug.',
                    ].join(' '),
                )
            }
            clearTimeout(context.current.timeoutId)

            context.current.callbackWrapper = () => {
                context.current.timeoutId = undefined;

                return callback(...args)
            }
            context.current.timeoutId = setTimeout(context.current.callbackWrapper, delay)
        }) as T, deps)
    }

    /****************************************************************************************/

    function guid() {
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
            const r = Math.random() * 16 | 0, v = c === 'x' ? r : ((r & 0x3) | 0x8);
            return v.toString(16);
        });
    }
}
export default Hook;