/**
 *  This is a collection of interfaces and types for cross-application routing.
 *
 *  Author: McNielsen <knielsen@alertlogic.com>
 *  Copyright 2017 Alert Logic Inc.
 */
/**
 * @private
 *
 * Utility class for menu tree refreshes and activation checks; mostly intended to avoid repetitive string manipulation :)
 */
class AlRouteIterationState {
    constructor(rootNode) {
        this.rootNode = rootNode;
        this.depth = 0;
        this.host = rootNode.host;
        this.currentUrlNoParams = rootNode.host.currentUrl.includes("?")
            ? rootNode.host.currentUrl.substring(0, rootNode.host.currentUrl.indexOf("?"))
            : this.host.currentUrl;
    }
}
/**
 *  @public
 *
 *  An AlRoute is an instantiated route definition, attached to a routing host, and capable of actually calculating target URLs based on context
 *  and handling navigation events.
 */
class AlRoute {
    constructor(host, /* Link to the routing host, which exposes current routing context, routing parameters, and actions that influence the environment */ definition, /* The raw data of the route */ parent = undefined /* Parent menu item (if not a top level navigational slot) */) {
        this.host = host;
        this.definition = definition;
        this.parent = parent;
        /* Is the menu item visible? */
        this.visible = true;
        /* Is the menu item enabled?  Disabled menu items and their children will not be considered for activation. */
        this.enabled = true;
        /* Is the menu item locked?  This prevents refresh cycles from changing its state. */
        this.locked = false;
        /* Is the menu item currently activated/expanded?  This will allow child items to be seen. */
        this.activated = false;
        /* Child menu items */
        this.children = [];
        /* Arbitrary properties */
        this.properties = {};
        this.definition = definition;
        this.caption = definition.caption;
        if (definition.bookmarkId) {
            this.host.setBookmark(definition.bookmarkId, this);
        }
        if (definition.children) {
            for (let i = 0; i < definition.children.length; i++) {
                this.children.push(new AlRoute(host, definition.children[i], this));
            }
        }
        if (definition.properties) {
            this.properties = Object.assign(this.properties, definition.properties); //  definition properties provide the "starting point" for the route's properties, but remain immutable defaults
        }
        if (parent === undefined) {
            //  This effectively performs the initial refresh/state evaluation to occur once, after the top level item has finished populating
            this.refresh(true);
        }
    }
    static link(host, locationId, path, caption = "Link") {
        return new AlRoute(host, {
            caption,
            action: {
                type: "link",
                location: locationId,
                path: path
            },
            properties: {}
        });
    }
    static trigger(host, triggerName, caption = "Trigger") {
        return new AlRoute(host, {
            caption,
            action: {
                type: "trigger",
                trigger: triggerName
            },
            properties: {}
        });
    }
    /**
     * Sets an arbitrary property for the route
     */
    setProperty(propName, value) {
        if (value === undefined) {
            this.deleteProperty(propName);
        }
        else {
            this.properties[propName] = value;
        }
    }
    /**
     * Deletes a property.  If the immutable route definition contains the same property, it will be
     * restored.
     */
    deleteProperty(propName) {
        if (this.definition.properties && this.definition.properties.hasOwnProperty(propName)) {
            this.properties[propName] = this.definition.properties[propName];
        }
        else {
            delete this.properties[propName];
        }
    }
    /**
     * Retrieves a property.
     */
    getProperty(propName, defaultValue = null) {
        return this.properties.hasOwnProperty(propName) ? this.properties[propName] : defaultValue;
    }
    /**
     * Refreshes the state of a given route.  Internal refresh should be considered an extension of this method; the outer function
     * simply wraps the tree iteration process in exception handling.
     *
     * @param resolve - If true, forces the calculated href and visibility properties to be recalculated.
     *
     * @returns Returns true if the route (or one of its children) is activated, false otherwise.
     */
    refresh(resolve = false) {
        try {
            let state = new AlRouteIterationState(this);
            this.internalRefresh(resolve, state);
        }
        catch (e) {
            console.error(`Navigation Error: could not refresh menu: `, e);
        }
    }
    internalRefresh(resolve = false, state) {
        if (this.locked) {
            //  If this menu item has been locked, then we won't reevaluate its URL, its visibility, or its activated status.
            //  This lets outside software take "manual" control of the state of a given menu.
            return undefined;
        }
        if (this.definition.enabled) {
            this.enabled = this.evaluateCondition(this.definition.enabled);
            if (!this.enabled) {
                this.visible = false;
                //  This item and its family tree are disabled, and thus not considered for visibility or activation.
                return undefined;
            }
        }
        state.depth++;
        /* Evaluate visibility */
        this.visible = true; //  true until proven otherwise
        if (this.parent) {
            if (typeof (this.definition.isPublic) !== "boolean" && this.parent) {
                //  Selectively inherit parent's publicity specificer (null|true|false|undefined)
                this.definition.isPublic = this.parent.definition.isPublic;
            }
        }
        if ("anonymous" in this.host.routeParameters) {
            if (this.host.routeParameters.anonymous === "true" && typeof (this.definition.isPublic) === 'boolean') {
                //  If the current user is anonymous/unauthenticated and this route isn't public (undefined or null), then set visible to false
                //  Important note: `null` does not trigger this logic, and the top route of each menu always has this property set to `null`.
                this.visible = this.definition.isPublic;
            }
            else if (this.host.routeParameters.anonymous === "false" && this.definition.isPublic) {
                //  If the current user is authenticated and this route is public ONLY (boolean true), then this route should not be visible
                this.visible = false;
            }
        }
        if (this.visible) {
            if (typeof (this.definition.options) !== 'undefined') {
                this.visible = this.visible && this.evaluateRouteOptions();
            }
            else {
                this.visible = this.visible && (this.definition.hasOwnProperty('visible') ? this.evaluateCondition(this.definition.visible || false) : true);
            }
        }
        /* Evaluate children recursively, and deduce activation state from them. */
        let childActivated = this.children.reduce((activated, child) => child.internalRefresh(resolve, state) || activated, false);
        /* Evaluate fully qualified href, if visible/relevant */
        let action = this.getRouteAction();
        if (action) {
            if (this.visible && (resolve || this.href === null) && action.type === 'link') {
                if (!this.evaluateHref(action)) {
                    state.depth--;
                    return this.disable();
                }
            }
        }
        this.activated = childActivated;
        //  activation test for path match
        if (!this.activated) {
            this.evaluateActivation(state);
        }
        //  bubble route?  if so, push calculated activation/visibility/route state to parent.
        //  useful note: this property works in a nested fashion, so grandchild elements can bubble up to their
        //  grandparents if the parent/child node between them also has `bubble` set to true.
        if (this.definition.bubble && this.parent) {
            this.parent.activated = this.parent.activated || this.activated;
            this.parent.visible = this.parent.visible || this.visible;
            this.parent.href = this.href;
        }
        state.depth--;
        return this.activated;
    }
    evaluateRouteOptions() {
        if (typeof (this.definition.options) === 'undefined') {
            return false;
        }
        const activatedOption = this.definition.options.find(option => {
            if (typeof (option.visible) === 'undefined') {
                return true;
            }
            if (typeof (option.visible) === 'boolean') {
                return option.visible;
            }
            return this.evaluateCondition(option.visible || false);
        });
        if (!activatedOption) {
            return false;
        }
        this.definition.action = activatedOption.action;
        return true;
    }
    /**
     * Disables a route
     */
    disable() {
        this.activated = false;
        this.visible = false;
        return false;
    }
    /**
     * "Executes" a route.  This invokes the `dispatch` method on whatever routing host was provided to the menu at load time.
     */
    dispatch() {
        this.refresh(true);
        return this.host.dispatch(this);
    }
    /**
     * Retrieves the full URL for a route, if applicable.
     */
    toHref() {
        this.refresh(true);
        return this.href;
    }
    /**
     * Diagnostic method for logging the current hierarchy and state of a given navigational tree.  Excluded from unit test coverage because no production code should utilize it!
     */
    /* istanbul ignore next */
    summarize(showAll = true, depth = 0) {
        let response = '';
        if (showAll || this.visible) {
            response += "    ".repeat(depth) + (depth > 0 ? `- ` : '');
            response += `[${this.definition.caption}] (${this.visible ? 'visible' : 'hidden'}, ${this.activated ? 'activated' : 'inactive'})` + (this.href ? ' - ' + this.href : '');
            response += "\n";
            for (let i = 0; i < this.children.length; i++) {
                response += this.children[i].summarize(showAll, depth + 1);
            }
        }
        return response;
    }
    /**
     *---- Helper Methods ---------------------------------------------
     */
    /**
     * Evaluates the HREF for an route with action type 'link'
     */
    evaluateHref(action) {
        if (action.url) {
            this.href = action.url;
            return true;
        }
        if (!action.location) {
            console.warn(`Warning: cannot link to undefined location in menu item '${this.caption}`);
            return false;
        }
        this.baseHREF = this.host.context.resolveURL(action.location);
        let path = action.path ? action.path : '';
        let missing = false;
        //  Substitute route parameters into the path pattern; fail on missing required parameters,
        //  ignore missing optional parameters (denoted by percentage sign), and trim any trailing slashes and spaces.
        path = path.replace(/:[a-zA-Z_%]+/g, match => {
            let variableId = match.substring(1);
            let required = true;
            if (variableId[variableId.length - 1] === '%') {
                required = false;
                variableId = variableId.substring(0, variableId.length - 1);
            }
            if (this.host.routeParameters.hasOwnProperty(variableId)) {
                return this.host.routeParameters[variableId];
            }
            else if (required) {
                missing = true;
                return `:${variableId}`;
            }
            else {
                return '';
            }
        })
            /* eslint-disable-next-line */
            .replace(/[ \/]+$/g, '');
        this.href = this.baseHREF + path;
        if (this.host.decorateHref) {
            this.host.decorateHref(this);
        }
        return !missing;
    }
    /**
     * Evaluates the activation state of the route
     */
    evaluateActivation(state) {
        if (this.definition.activationRule === 'inert' || !this.href) {
            //  Not a candidate for activation
            return false;
        }
        if (this.baseHREF && this.host.currentUrl.startsWith(this.baseHREF)) {
            const noParamsHref = this.href.includes('?') ? this.href.substring(0, this.href.indexOf('?')) : this.href;
            if (state.currentUrlNoParams === noParamsHref) {
                //  If our full URL *contains* the current URL, we are activated
                if (AlRoute.debug) {
                    console.log("Navigation: activating route [%s] based on exact path match", this.definition.caption, state.currentUrlNoParams, this.definition);
                }
                this.activated = true;
            }
            else if (this.definition.matches) {
                //  If we match any other match patterns, we are activated
                let matchedPattern = this.definition.matches.find(matchPattern => {
                    if (!(matchPattern in AlRoute.reCache)) {
                        try {
                            const normalized = `^${this.baseHREF}${matchPattern}$`.replace("/", "\\/");
                            AlRoute.reCache[matchPattern] = new RegExp(normalized);
                        }
                        catch (e) {
                            console.warn(`Warning: invalid navigation match pattern '${matchPattern}' cannot be compiled as a regular expression; ignoring.`);
                            AlRoute.reCache[matchPattern] = null;
                        }
                    }
                    let regexp = AlRoute.reCache[matchPattern];
                    return regexp ? regexp.test(state.currentUrlNoParams) : false;
                });
                if (matchedPattern) {
                    if (AlRoute.debug) {
                        console.log(`Navigation: activating route [%s] based on regular expression match [%s]`, this.definition.caption, matchedPattern, this.definition);
                    }
                    this.activated = true;
                }
            }
        }
        return this.activated;
    }
    /**
     *  Evaluates an AlRouteCondition recursively.
     *
     *  Internally, the methods compiles boolean results of
     *      a) nested conditions
     *      b) route parameters or parameter expressions
     *      c) path matches
     *      d) entitlements or other externally calculated conditions
     *  And then uses a simple reducer to roll up all results based on the condition rule, which defaults to 'all'.
     */
    evaluateCondition(condition) {
        if (typeof (condition) === 'string') {
            condition = this.host.getConditionById(condition);
        }
        if (typeof (condition) === 'boolean') {
            return condition;
        }
        if (typeof (condition) !== 'object' || condition === null) {
            return false;
        }
        let evaluations = [];
        if (condition.rule && condition.conditions) {
            evaluations = evaluations.concat(condition.conditions.map(condition => this.evaluateCondition(condition)));
        }
        if (condition.parameters) {
            evaluations = evaluations.concat(condition.parameters.map(parameterExpression => this.evaluateParameterExpression(parameterExpression)));
        }
        if (condition.path_matches) {
            //  Evaluates true only if the current path matches a given regular expression
            evaluations.push(this.evaluatePathMatch(condition.path_matches));
        }
        if (condition.authentication
            || condition.accounts || condition.primaryAccounts
            || condition.entitlements || condition.primaryEntitlements
            || condition.environments
            || condition.experiences) {
            //  This condition refers to entitlement or other externally managed data -- ask the host to evaluate it.
            evaluations = evaluations.concat(this.host.evaluate(condition));
        }
        if (condition.rule === 'none') {
            return !evaluations.some(value => value); //  no items are true
        }
        else if (condition.rule === 'any') {
            return evaluations.some(value => value); //  any items are true
        }
        else /* all conditions */ {
            return evaluations.every(value => value); //  all items are true
        }
    }
    /**
     * Determine whether a route parameter test is true or not
     */
    evaluateParameterExpression(expression) {
        if (expression.includes("!=")) {
            const [parameterName, parameterValue] = expression.split("!=");
            return !this.host.routeParameters.hasOwnProperty(parameterName) || this.host.routeParameters[parameterName] !== parameterValue;
        }
        else if (expression.includes("=")) {
            const [parameterName, parameterValue] = expression.split("=");
            return this.host.routeParameters.hasOwnProperty(parameterName) && this.host.routeParameters[parameterName] === parameterValue;
        }
        else {
            return this.host.routeParameters.hasOwnProperty(expression);
        }
    }
    /**
     * Determines whether a path pattern matches the current URL
     */
    evaluatePathMatch(pathMatches) {
        /* eslint-disable-next-line */
        let pattern = "^.*" + pathMatches.replace(/[{}|[\]\\\/]/g, '\\$&') + "$";
        let comparison = new RegExp(pattern);
        return comparison.test(this.host.currentUrl);
    }
    /**
     * Retrieves the route's action, which may be a shared "named" route or embedded directly into the route's definition.
     */
    getRouteAction() {
        if (typeof (this.definition.action) === 'string') {
            if (typeof (this.host.getRouteByName) === 'function') {
                const definition = this.host.getRouteByName(this.definition.action);
                if (definition && definition.action) {
                    return definition.action;
                }
            }
            return null;
        }
        else if (typeof (this.definition.action) === 'object') {
            return this.definition.action;
        }
        return null;
    }
    /**
     * Updates the route to use a specified action
     */
    setAction(action) {
        this.definition.action = action;
    }
    /**
     * Updates the route to use a specific callback action.
     */
    setCallback(callback) {
        this.setAction({
            type: 'callback',
            callback: callback
        });
    }
    /**
     * Retrieves a nested child route by matching bookmarks, captions, IDs, or numerical indices.
     */
    findChild(idPath) {
        const path = typeof (idPath) === 'string' ? idPath.split("/") : idPath;
        const childId = path[0];
        let child = null;
        if (childId[0] === '#') {
            //  Retrieve by numerical index
            let childIndex = parseInt(childId.substring(1), 10);
            child = this.children.length > childIndex ? this.children[childIndex] : null;
        }
        else {
            if (typeof (idPath) === 'string') {
                child = this.host.getBookmark(childId);
            }
            if (!child) {
                child = this.children ? this.children.find(child => child.definition.caption === childId || child.definition.id === childId) : null;
            }
        }
        if (path.length > 1) {
            return child ? child.findChild(path.slice(1)) : null;
        }
        return child || null;
    }
    /**
     * Generic method for recursing a menu hierarchy, using a callback to match the route.
     *
     * @param callback is a method that accepts a route (and optionally its definition) and returns
     *          `true` if the route is the correct one, in which case it is the result of the search method.
     *          `false` if the route is incorrect but its children should be iterated.
     *          `null` if the route is incorrect and its children should be ignored
     */
    search(matcher, enabledOnly = true) {
        if (this.enabled !== enabledOnly) {
            return undefined;
        }
        if (matcher(this, this.definition)) {
            return this;
        }
        else {
            for (let i = 0; i < this.children.length; i++) {
                let target = this.children[i].search(matcher, enabledOnly);
                if (target) {
                    return target;
                }
            }
        }
        return undefined;
    }
    /**
     * This method will return the deepest activated, childless route within its first activated child.  If this sounds obtuse,
     * it is -- but's it's important for determining the "cursor" menu item within a menu hierarchy.
     */
    getActivationCursor() {
        let activatedChild = undefined;
        this.children.find(child => {
            if (child.activated) {
                if (child.children.length === 0) {
                    activatedChild = child;
                }
                else {
                    activatedChild = child.getActivationCursor();
                }
            }
            return activatedChild ? true : false;
        });
        if (activatedChild) {
            return activatedChild;
        }
        return this.activated ? this : undefined;
    }
    /**
     * This method uses the method above to generate a flat array of activated menu items, from top to bottom,
     * or undefined if no child of the given menu is activated.
     */
    getActivationCursorFlat() {
        let cursor = this.getActivationCursor();
        if (!cursor) {
            return undefined;
        }
        let activationPath = [];
        while (cursor) {
            activationPath.unshift(cursor);
            cursor = cursor.parent;
        }
        return activationPath;
    }
}
AlRoute.debug = false;
/* A global cache of compiled regexes for match checking */
AlRoute.reCache = {};

export { AlRoute };
//# sourceMappingURL=al-core-navigation.esm2015.js.map
