import React from 'react'
import { Location, Route, createBrowserRouter, createRoutesFromElements, useLocation, useNavigate } from "react-router-dom";
import { useEffect, useState } from "react";
import Signal, { Req } from "badmfck-signal";


export interface INavigationPoint{
    name:string,
    id:string,
    path?:string,
    sub?:INavigationPoint[],
    page?:React.ReactElement,
    selected?:boolean
    hidden?:boolean
    disabled?:boolean
    redirect?:string
    title?:string
}

export interface INavigationParams<T=any>{
    [key:string]:T
}

export interface ICurrentNav<T=any>{
    url:string,
    path:INavigationPoint[]
    current:INavigationPoint,
    params?:INavigationParams<T>
}

// Signals and requests
export const S_NAV_CHANGED=new Signal<ICurrentNav>();
const REQ_NAV_MENU=new Req<string|null,INavigationPoint[]>();
const REQ_NAV_CURRENT=new Req<void,ICurrentNav|null>()

// Handle all navigation requests, Singleton
export class Presenter {

    // Sitemap
    static title:string;
    private sitemap:INavigationPoint[]=[]
    private currentNav:ICurrentNav|null=null;
    private interceptor?:(navigationPoint:string|INavigationPoint|null,currentNav:ICurrentNav|null,params?:INavigationParams)=>Promise<boolean>;

    private static instance?:Presenter;

    constructor(){
        if(Presenter.instance)
            throw(Error("Presenter already constructed"));
        Presenter.instance = this
        
        REQ_NAV_CURRENT.listener=async(data) =>this.currentNav
        
        REQ_NAV_MENU.listener=async (menuID)=>{
            if(!menuID)
                menuID = "home"
            let menu = this.getMenuById(menuID);
            
            if(!menu)
                return [{name:"not-found",id:"not-found"}];

            if(!menu.sub)
                return [{name:"no-items",id:"no-items"}];
            
            return menu.sub;
        }    
    }

    static setupInterceptor(interceptor:(navigationPoint:string|INavigationPoint|null,currentNav:ICurrentNav|null,params?:INavigationParams)=>Promise<boolean>){
        if(!Presenter.instance){
            console.error("Presenter not initialized");
            return;
        }
        Presenter.instance.interceptor=interceptor;
    }

    setupNavigationTree(tree:INavigationPoint[]){
        this.sitemap = tree;
    }

    markUnselected(arr?:INavigationPoint[]){
        if(!arr)
            arr = this.sitemap;
        for(let i of arr){
            i.selected=false;
            if(i.sub && i.sub.length>0)
                this.markUnselected(i.sub)
        }
    }

    static async intercept(navigationPoint:string|INavigationPoint|null,params?:INavigationParams):Promise<boolean>{
        if(!Presenter.instance)
            return true;

        if(!Presenter.instance.interceptor)
            return true;

        return await Presenter.instance.interceptor(navigationPoint,Presenter.instance.currentNav,params); // await ?

    }

    static getRouter(Outfit:React.ReactElement,ErrorPage?:React.ReactElement,InnerErrorPage?:React.ReactElement){
        if(!Presenter.instance)
            return null;
        let i =0;
        const router = createBrowserRouter(
            createRoutesFromElements(
                <Route
                    path='/'
                    element={<><Navigator/>{Outfit}</>}
                    errorElement={ErrorPage}
                >
                    
                    {Presenter.instance.createRouterTree("/",InnerErrorPage ?? <></>)}
                </Route>
            )
        )
        return router;
    }
    
    createRouterTree(menuID:string,ErrorPage:React.ReactElement):any{
        let i = 0;
        const map = this.getMenuById(menuID)?.sub?.map(val=>{
            return <Route
                key={i++}
                path={val.path ?? val.id}
                element={val.page}
                errorElement={ErrorPage}
            >
                {val.sub && this.createRouterTree(val.id,ErrorPage)}
            </Route>
        })
        return map;
    }

    getLinkTree(id:string,tree?:INavigationPoint[],result?:INavigationPoint[]):INavigationPoint[]{
        if(!result)
            result = [];

        if(!tree)
            tree=this.sitemap;

        for(let i of tree){
            if(i.id === id){
                result?.push(i)
                break;
            }
            if(i.sub && i.sub.length>0){
                let ref = result.length;
                const r = this.getLinkTree(id,i.sub,result);
                if(r.length>ref)
                    result.unshift(i)
            }

        }
        return result;
    }

    static setSiteMap(sitemap:INavigationPoint[]){
        if(!Presenter.instance)
            return;
        Presenter.instance.sitemap=sitemap
    }

    static changeNavigation(navpoint:INavigationPoint,params?:INavigationParams){
        if(!Presenter.instance)
            return;

        // detect menu ID
        const linkTree=Presenter.instance.getLinkTree(navpoint.id)

        if(!linkTree || linkTree.length===0){
            console.error("Can't navigate to: "+navpoint.id+", no routes");
            return;
        }
        let url = linkTree.map(val=>val.id==="/"?undefined:(val.path ?? val.id)).join("/")//+(data.params?data.params:"")
        if(params){
            for(let i in params){
                let replacedValue="";
                let val = params[i];
                if(val!==null && val !== undefined && (typeof val === "string" || typeof val === "number" || typeof val ==="boolean"))
                    replacedValue = val+"";
                url=url.replaceAll(":"+i,replacedValue);
            }
        }
        if(url.indexOf("/:")!==-1){
            const tmp = url.split("/")
            for(let i in tmp){
                let p = tmp[i]
                if(p.startsWith(":")){
                    tmp[i]="" // remove unused param
                }
            }
            url=tmp.join("/")
        }
            
        Presenter.instance.markUnselected()
        linkTree.map(val=>val.selected=true)
        Presenter.instance.currentNav = {url:url,path:linkTree,current:linkTree[linkTree.length-1],params:params};
        if(Presenter.instance.currentNav.current.redirect){
            const redirect  = Presenter.instance.currentNav.current.redirect
            if(redirect.toLowerCase().startsWith("http")){
                window.location.href=redirect;
                return;
            }
            const newpoint=getPointOrDie(redirect)
            if(newpoint){
                this.changeNavigation(newpoint);
                return;
            }
        }

        S_NAV_CHANGED.invoke(Presenter.instance.currentNav);
    }

    static getNavigationPointByID(id:string):INavigationPoint|null{
        if(!Presenter.instance)
            return null;
        return Presenter.instance.getMenuById(id);
    }

    getMenuById(id:string,tree?:INavigationPoint[]):INavigationPoint|null{
        let result = null;
        if(!tree)
            tree=this.sitemap;

        for(let i of tree){
            if(i.id === id){
                result = i;
                break;
            }
            if(i.sub && i.sub.length>0){
                result = this.getMenuById(id,i.sub)
                if(result && result.id === id)
                    break;
            }
        }
        

        return result;
    }

    static setInitialLocation(loc:Location){
        if(!loc || !loc.pathname || loc.pathname.indexOf("/")===-1){
            if(Presenter.instance)
                this.changeNavigation(Presenter.instance.sitemap[0])
            return;
        }
        const tmp = loc.pathname.split("/");
        tmp.shift();
        if(tmp.length===0){
            if(Presenter.instance)
                this.changeNavigation(Presenter.instance.sitemap[0])
            return;
        }
        
        let found=null;
        const linkParams=[];
        if(!Presenter.instance)
            return;
        for(let i of tmp){
            const f = Presenter.instance.getMenuById(i);
            if(!f){
                linkParams.push(i)
                break;
            }
            if(linkParams.length===0)
                found=f;
        }
        
        
        if(!found){
            if(Presenter.instance)
                this.changeNavigation(Presenter.instance.sitemap[0])
            return;
        }

        let params:{[key:string]:string}|undefined=undefined;
        if(linkParams && found.path){
            let n =0;
            const tmp = found.path.split("/")
            for(let i of tmp){
                if(!i.startsWith(":"))
                    continue;
                const p = i.substring(1);
                if(!params)
                    params={}
                params[p]=linkParams[n];
                n++;
            }
        }
        
        this.changeNavigation(found,params)  
    }

}

export const useCurrentPath = ()=>{
    return  REQ_NAV_CURRENT.use(undefined,[S_NAV_CHANGED.use([])]);
}

export const useMenu=(id?:string) =>{
    return REQ_NAV_MENU.use(id ?? "/",[S_NAV_CHANGED.use([])]);
}


// Create router from sitemap
export const useCreateRouter=(sitemap:INavigationPoint[],Outfit:React.ReactElement,ErrorPage?:React.ReactElement) =>{
    const [router,sR] = useState<any|null>(null);
    useEffect(()=>{
        Presenter.setSiteMap(sitemap);
        const r = Presenter.getRouter(Outfit,ErrorPage ?? <></>);
        sR(r)
    },[])
    return router;
}



// Presenter functions
const getPointOrDie=(navigationPoint:string|INavigationPoint|null):INavigationPoint|null=>{
    if(typeof navigationPoint === "string" )
        navigationPoint = Presenter.getNavigationPointByID(navigationPoint) ?? ""
    if(!navigationPoint || typeof navigationPoint==="string")
        return null;
    return navigationPoint;
}

const navigationChange=(navigationPoint:string|INavigationPoint|null,params?:INavigationParams)=>{
    
    Presenter.intercept(navigationPoint,params).then(value=>{
        if(!value)
            return;
        const nav = getPointOrDie(navigationPoint);
        if(!nav)
            return;
        Presenter.changeNavigation(nav,params)
    
    })
    /*navigationPoint = getPointOrDie(navigationPoint);
    if(!navigationPoint)
        return null;
    Presenter.changeNavigation(navigationPoint,params);*/
}

const navigationPointDisable=(navigationPoint:string|INavigationPoint|null,disable:boolean)=>{
    navigationPoint = getPointOrDie(navigationPoint);
    if(!navigationPoint)
        return null;
    navigationPoint.disabled = disable;
    // TODO: FIRE NAV_CHANGED
}

interface IUsePresenter{
    navChange:(navigationPoint:string|INavigationPoint,params?:INavigationParams)=>void,
    navDisable:(navigationPoint:string|INavigationPoint,disable:boolean)=>void,
}

export const usePresenter=():IUsePresenter=>{
    return {
        navChange:navigationChange,
        navDisable:navigationPointDisable,
    }
}


// Navigator component to determine the start location
const Navigator = () =>{
    const navigate = useNavigate();
    S_NAV_CHANGED.use([],data=>{
        document.title =data?.current.title ?? Presenter.title+"-"+(data?.current.name ?? data?.current.id ?? "");
        navigate(data!.url)
    })
    const loc=useLocation();
    useEffect(()=>{
        Presenter.setInitialLocation(loc);
    },[])
    return <></>
}

new Presenter();