/**
 * Created by km on 11/16/16.
 */
import * as urls from "../network/urls";
import * as urlUtil from "./url-util";
// endregion
// region private URLRule helpers.
/**
 * Checks whether or not a given _pathComponents component contains a parameter.
 * @param pathComponent The _pathComponents component to check.
 * @returns true if the _pathComponents component is surrounded by curly braces; false otherwise.
 */
function isPathComponentParameter(pathComponent) {
    const parameterStartIndex = pathComponent.indexOf("{");
    const parameterEndIndex = pathComponent.indexOf("}");
    return parameterStartIndex >= 0 && parameterEndIndex > parameterStartIndex + 1;
}
/**
 * Extracts the parameter contained in a _pathComponents component.
 * @param pathComponent A _pathComponents component surrounded by curly braces.
 * @returns The parameter contained in the component.
 */
function getPathComponentParameter(pathComponent) {
    const parameterStartIndex = pathComponent.indexOf("{");
    const parameterEndIndex = pathComponent.indexOf("}");
    const pathHasRequiredCurlyBraces = parameterStartIndex >= 0 && parameterEndIndex > parameterStartIndex;
    return pathHasRequiredCurlyBraces
        ? pathComponent.substring(parameterStartIndex + 1, parameterEndIndex)
        : pathComponent;
}
/**
 * Extracts the value from a path component, for a given internal key example:
 * Path Component: "id123456"
 * Internal Key: "id{id}"
 * Return Value: "123456"
 * @param pathComponent A _pathComponents component surrounded by curly braces.
 * @returns The parameter contained in the component.
 */
export function getPathComponentParameterValueUsingInternalKey(pathComponent, internalKey) {
    const valueStartIndex = internalKey.indexOf("{");
    const valueEndIndex = pathComponent.length - (internalKey.length - (internalKey.indexOf("}") + 1));
    const pathHasRequiredCurlyBraces = valueStartIndex >= 0 && valueEndIndex > valueStartIndex;
    return pathHasRequiredCurlyBraces ? pathComponent.substring(valueStartIndex, valueEndIndex) : pathComponent;
}
/**
 * Creates a mapping from key to _pathComponents component index
 * for efficiently extracting parameters from a _pathComponents.
 * @param rulePath  The _pathComponents to create a mapping for.
 * @returns A map of keys to _pathComponents component indexes.
 */
function makePathParameterMapping(rulePath) {
    const mapping = {};
    rulePath.forEach((ruleComponent, index) => {
        if (isPathComponentParameter(ruleComponent)) {
            mapping[ruleComponent] = index;
        }
    });
    return mapping;
}
/**
 * Normalizes a given protocol string for matching.
 * @param protocol  The protocol to match against.
 * @returns The `protocol` with colon added if needed.
 */
function normalizeProtocol(protocol) {
    // An empty string is falsy.
    if (protocol === null || protocol === undefined) {
        return null;
    }
    return protocol;
}
/**
 * Creates `UrlRouteQuery` objects from substring of url.
 * ? = optional
 * -caseInsensitive = case insensitive
 * @param parameters strings of form `<key>[?][-i]=<value>`.
 * @returns Array of `UrlRouteQuery` objects.
 */
function parseQuery(parameters) {
    const parsedQuery = [];
    if (!parameters) {
        return parsedQuery;
    }
    for (const param of parameters) {
        const parts = param.split("=");
        let key = parts[0];
        const optional = key.indexOf("?") !== -1;
        key = key.replace("?", "");
        const caseInsensitive = key.indexOf("-caseInsensitive") !== -1;
        key = key.replace("-caseInsensitive", "");
        let value = null;
        if (parts.length > 1) {
            value = decodeURIComponent(parts[1]);
        }
        parsedQuery.push({
            key,
            value,
            optional,
            caseInsensitive,
        });
    }
    return parsedQuery;
}
// endregion
// region Url Rule
/**
 * The `UrlRule` class extracts the pattern format from `UrlRuleDefinition`s, and encapsulates
 * the information needed to match against a candidate URL and extract parameters from it.
 *
 * The terminology here is:
 * - rule: A specific url pattern.
 * - route: A group of rules that together form a single route, i.e. UrlRule[].
 */
export class UrlRule {
    /**
     * Construct the route with all required properties.
     * @param rule      The rule to match.
     */
    constructor(rule) {
        this.identifier = rule.identifier;
        this._protocol = normalizeProtocol(rule.protocol);
        this._hostName = rule.hostName;
        if (rule.path) {
            this._pathComponents = rule.path.split("/").filter((component) => component.length > 0);
            this._pathParameterMap = makePathParameterMapping(this._pathComponents);
        }
        else {
            this._pathComponents = null;
            this._pathParameterMap = null;
        }
        this._pathExtension = rule.pathExtension;
        this._query = parseQuery(rule.query);
        this._hash = rule.hash;
        this._regex = rule.regex;
        if (rule.exclusions) {
            this._exclusions = rule.exclusions.map(function (ex) {
                return new UrlRule(ex);
            });
        }
        else {
            this._exclusions = null;
        }
    }
    /**
     * Checks whether or not the route matches a given URL.
     * @param url   The URL to check against.
     * @returns true if the route matches `urls`; false otherwise.
     */
    matches(url) {
        var _a, _b;
        if (this._regex) {
            if (!this._regex.length) {
                // If the rule specifies regex but does not supply patterns, we need to return false. Otherwise, we will
                // risk matching against everything. This is because an empty regex with no other rule parameters will
                // cause us to fallthrough to the end and match against all URLs.
                return false;
            }
            let didMatchRegex = false;
            for (const regexPattern of this._regex) {
                if (regexPattern.test(url.toString())) {
                    // If we match against any of regex patterns, then we should proceed.
                    // If no matches are found, then this rule is not matched.
                    didMatchRegex = true;
                    break;
                }
            }
            if (!didMatchRegex) {
                return false;
            }
        }
        if (this._protocol && url.protocol !== this._protocol) {
            return false;
        }
        if (this._hostName && url.host !== this._hostName) {
            return false;
        }
        if (this._pathComponents) {
            const rulePathComponents = this._pathComponents;
            const urlPathComponents = url.pathComponents();
            if (rulePathComponents.length !== urlPathComponents.length) {
                return false;
            }
            // We're iterating two arrays here, an old style for-loop is appropriate
            const length = rulePathComponents.length;
            for (let i = 0; i < length; i++) {
                const ruleComponent = rulePathComponents[i];
                if (isPathComponentParameter(ruleComponent)) {
                    // component parameters always match
                    continue;
                }
                const urlComponent = urlPathComponents[i];
                if (ruleComponent !== urlComponent) {
                    return false;
                }
            }
        }
        if (this._pathExtension) {
            if (url.pathExtension() !== this._pathExtension) {
                return false;
            }
        }
        if (this._query) {
            for (const param of this._query) {
                let value;
                if (param.caseInsensitive) {
                    for (const [queryKey, queryValue] of Object.entries((_a = url.query) !== null && _a !== void 0 ? _a : {})) {
                        if (param.key.toLocaleLowerCase() === queryKey.toLocaleLowerCase()) {
                            value = queryValue;
                        }
                    }
                }
                else {
                    value = (_b = url.query) === null || _b === void 0 ? void 0 : _b[param.key];
                }
                if (!value && !param.optional) {
                    return false;
                }
                if (param.value && param.value !== value) {
                    return false;
                }
            }
        }
        if (this._hash && url.hash !== this._hash) {
            return false;
        }
        if (this._exclusions) {
            for (const exclusionRule of this._exclusions) {
                if (exclusionRule._exclusions) {
                    throw Error("Matching exclusion rules with further exclusion rules may introduce significant code-complexity and/or reduce the ease with which developers are able to reason about your desired goals. Are there any simpler options?");
                }
                if (exclusionRule.matches(url)) {
                    return false;
                }
            }
        }
        return true;
    }
    /**
     * Extract information from a matching url.
     * @param matchingUrl    The url to extract parameters from.
     * @returns `Parameters` extracted from `matchingUrl`
     * @note This function is only valid when `this.matches(matchingUrl) === true`.
     */
    extractParameters(matchingUrl) {
        var _a, _b;
        const parameters = {};
        if (this._pathComponents !== null && this._pathParameterMap !== null) {
            const urlPathComponents = matchingUrl.pathComponents();
            for (const internalKey of Object.keys(this._pathParameterMap)) {
                const externalKey = getPathComponentParameter(internalKey);
                const index = this._pathParameterMap[internalKey];
                const parameterValue = getPathComponentParameterValueUsingInternalKey(urlPathComponents[index], internalKey);
                parameters[externalKey] = decodeURIComponent(parameterValue);
            }
        }
        if (this._query) {
            for (const param of this._query) {
                parameters[param.key] = (_b = (_a = matchingUrl.query) === null || _a === void 0 ? void 0 : _a[param.key]) !== null && _b !== void 0 ? _b : undefined;
            }
        }
        return parameters;
    }
}
/**
 * `UrlRouter` manages a set of url rule templates to allow `urls` to serve as keys for different associated objects (like Builders).
 *
 * @note This is replaces old `UrlRouter` as a synchronous way match route URLs to handlers. In contrast to the previous implementation,
 * it maps entire objects (containing related async handlers and properties) to urls.
 */
export class UrlRouter {
    /// Constructs an empty URL router object.
    constructor() {
        this._routeMappings = [];
    }
    /**
     * Register a new route defined by a set of definitions and object on the router.
     * @param routeDefinitions     The definitions of rules to register.
     * @param object   The object for the rule.
     */
    associate(routeDefinitions, object) {
        const route = [];
        for (const definition of routeDefinitions) {
            route.push(new UrlRule(definition));
        }
        this._routeMappings.push({ route: route, object: object });
    }
    /**
     * Resolve given url to associated object, if any exist.
     * @param urlOrString URL or string representation of url to resolve objects for.
     * @returns `UrlRouterResult` containing url, extracted parameters, and associated object, or `null` if no match was found.
     */
    routedObjectForUrl(urlOrString) {
        let url = typeof urlOrString === "string" ? new urls.URL(urlOrString) : urlOrString;
        url = urlUtil.normalizedAppStoreUrl(url);
        url = urlUtil.normalizedActionUrl(url);
        for (const mapping of this._routeMappings) {
            for (const rule of mapping.route) {
                if (rule.matches(url)) {
                    return {
                        normalizedUrl: url,
                        parameters: rule.extractParameters(url),
                        object: mapping.object,
                        matchedRuleIdentifier: rule.identifier,
                    };
                }
            }
        }
        // No match. Still return a result with normalized url.
        return {
            normalizedUrl: url,
            parameters: null,
            object: null,
            matchedRuleIdentifier: null,
        };
    }
}
// endregion
//# sourceMappingURL=routing-components.js.map