Source: router/utils/routing-info/routing_info_utils.js

import RouteParamsUtil from "../route-params/route_params";

const URL_SEPARATOR = "/";

class RoutingInfoUtils {

    /**
     * Builds the main routing info
     * 
     * Does type checks and converts a null nestedChildFragments value to an 
     * empty array for easier manipulation later
     * 
     * @param {RoutingInfo[]} routingInfos 
     * @param {MainNavigationInfo[]} mainNavInfos The main navigational information for the app (optional)
     * @returns {RoutingInfo[]}
     */
    static buildMainRoutingInfo(routingInfos, mainNavInfos){

        let standardizedRoutingInfos = [];
        let targetNavInfo = null;
        routingInfos.forEach((routingInfo) => {

            //Check for errors
            routeIsValid(routingInfo.route);
            if(!routingInfo.target){

                throw new Error("Routing info for route " + routingInfo.route + " must have a target");
            }

            //Remove last / in route
            routingInfo.route = this.removeLastForwardSlashInUrl(routingInfo.route);

            //Standardize nestedChildFragments to empty array if null or undefined
            if (routingInfo.nestedChildFragments === null || routingInfo.nestedChildFragments === undefined){

                routingInfo.nestedChildFragments = [];
            }

            if(mainNavInfos){

                //Bind baseNavBtn property
                //special consideration for home "/" baseRoutes. Must MATCH
                // Therefore, / can catch-all, but overriden by explicitly defined extensions
                //try a direct match
                targetNavInfo = mainNavInfos.find((info) => routingInfo.route === info.baseActiveRoute);
                //if no direct match found, search for all. Longest string of qualified qualifies - to avoid shorthanding e.g "/" capturing all, even for /myRoute
                if(!targetNavInfo){

                    /**
                     * @type {MainNavigationInfo[]}
                     */
                    let matchingInfos = [];
                    mainNavInfos.forEach((info) => {
    
                        if(routingInfo.route.startsWith(info.baseActiveRoute)){
    
                            matchingInfos.push(info)
                        }
                    });
                    /**
                     * @type {MainNavigationInfo}
                     */
                    let bestQualified = null;
                    matchingInfos.forEach((info) => {
    
                        if(!bestQualified){
    
                            bestQualified = info;
                        } else {
    
                            if(bestQualified.baseActiveRoute.length < info.baseActiveRoute.length){
    
                                bestQualified = info;
                            }
                        }
                    });
                    if(bestQualified){
    
                        targetNavInfo = bestQualified;
                    }
                }
                routingInfo.baseNavBtn = targetNavInfo ? targetNavInfo.selector : null;
            }

            standardizedRoutingInfos.push(routingInfo);
        });
        
        return standardizedRoutingInfos;
    }

    /**
     * Builds the local routing info for fragments, triggered by a navigational control button
     * Use only if using buttons for automatic binding
     * 
     * @param {LocalFragmentRoutingInfo[]} localRoutingInfos 
     * @returns {LocalFragmentRoutingInfo[]} Array of standardized local fragment routing infos
     */
    static buildLocalRoutingInfo(localRoutingInfos) {

        let standardizeLocalRoutingInfos = [];
        let parsedNavBtnIds = [];
        localRoutingInfos.forEach((routingInfo) => {

            if(!parsedNavBtnIds.includes(routingInfo.navBtnID)){

                parsedNavBtnIds.push(routingInfo.navBtnID);
                //Route must exist
                routeIsValid(routingInfo.route);
                //Remove last forward slash
                routingInfo.route = this.removeLastForwardSlashInUrl(routingInfo.route);
                standardizeLocalRoutingInfos.push(routingInfo);
            } else {

                throw new Error("A navigation button can only be used once, for one valid route in your main router");
            }
        });

        return standardizeLocalRoutingInfos;
    }

    /**
     * Remove the last forward slash in url. Routing in o-js ignores it
     * @param {string} url 
     * @returns {string}
     */
    static removeLastForwardSlashInUrl(url){

        if(url.length > 1 && url.charAt(url.length - 1) === URL_SEPARATOR){

            url = url.substring(0, url.length - 1);
        }

        return url;
    }

    /**
     * 
     * @param {RoutingInfo[]} routingInfos 
     * @param {string} targetUrl 
     * @returns 
     */
    static findMatchingRoutingInfoForUrl(routingInfos, targetUrl){

        //Do a direct search first
        let targetInfo = routingInfos.find((routingEntry) => routingEntry.route === targetUrl);

        if(!targetInfo){

            //Search for congruent urls
            for(let i = 0; i < routingInfos.length; i++){

                if(RouteParamsUtil.testUrlsCongruent(routingInfos[i].route, targetUrl)){

                    targetInfo = routingInfos[i];
                    break;
                }
            }
        }
        
        return targetInfo;
    }

    /**
     * 
     * @param {RoutingInfo[]} routingInfos 
     * @param {string} targetUrl 
     * @returns {string}
     */
    static findActiveNavIDForUrl(routingInfos, targetUrl){

        const routingInfo = RoutingInfoUtils.findMatchingRoutingInfoForUrl(routingInfos, targetUrl);
        if(routingInfo){

            return routingInfo.baseNavBtn;
        } else {

            throw new Error("No active navigation ID found for url " + targetUrl);
        }
    }

    /**
     * 
     * @param {RoutingInfo[]} routingInfos 
     * @param {string} targetUrl 
     * @param {string} baseNavBtnID 
     */
    static findMatchingRoutingInfoForUrlAndBaseNavBtn(routingInfos, targetUrl, baseNavBtnID){

        //Find matching route info to targetUrl first. Then, MUST match baseNavBtnID
        //Call findMatchingRoutingInfo for url
        let targetInfo = RoutingInfoUtils.findMatchingRoutingInfoForUrl(routingInfos, targetUrl);
        return targetInfo ? targetInfo.baseNavBtn === baseNavBtnID ? targetInfo : undefined : undefined;
    }
}

/**
 * 
 * @param {string} route 
 */
function routeIsValid(route){

    if(!route){

        throw new Error("Please provide a valid route for the routing info");
    }

    if(route.charAt(0) !== URL_SEPARATOR){

        throw new Error("All routes must start with " + URL_SEPARATOR);
    }
}

export default RoutingInfoUtils;