/**
 * This file is part of the Meeds project (https://meeds.io/).
 *
 * Copyright (C) 2020 - 2025 Meeds Association contact@meeds.io
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package org.exoplatform.web.controller.router;

//import javanet.staxutils.IndentingXMLStreamWriter;

import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;

import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;

import org.exoplatform.web.controller.QualifiedName;
import org.exoplatform.web.controller.metadata.PathParamDescriptor;
import org.exoplatform.web.controller.metadata.RequestParamDescriptor;
import org.exoplatform.web.controller.metadata.RouteDescriptor;
import org.exoplatform.web.controller.metadata.RouteParamDescriptor;
import org.gatein.common.util.Tools;

/**
 * The implementation of the routing algorithm.
 *
 */
class Route {

    void writeTo(XMLStreamWriter writer) throws XMLStreamException {
        if (this instanceof SegmentRoute) {
            writer.writeStartElement("segment");
            writer.writeAttribute("path", "/" + ((SegmentRoute) this).name);
            writer.writeAttribute("terminal", "" + terminal);
        } else if (this instanceof PatternRoute) {
            PatternRoute pr = (PatternRoute) this;
            StringBuilder path = new StringBuilder("/");
            for (int i = 0; i < pr.params.length; i++) {
                path.append(pr.chunks[i]).append("{").append(pr.params[i].name.getValue()).append("}");
            }
            path.append(pr.chunks[pr.chunks.length - 1]);
            writer.writeStartElement("pattern");
            writer.writeAttribute("path", path.toString());
            writer.writeAttribute("terminal", Boolean.toString(terminal));
            for (PathParam param : pr.params) {
                writer.writeStartElement("path-param");
                writer.writeAttribute("qname", param.name.getValue());
                writer.writeAttribute("encodingMode", param.encodingMode.toString());
                writer.writeAttribute("pattern", param.matchingRegex.toString());
                writer.writeEndElement();
            }
        } else {
            writer.writeStartElement("route");
        }

        //
        for (RouteParam routeParam : routeParamArray) {
            writer.writeStartElement("route-param");
            writer.writeAttribute("qname", routeParam.name.getValue());
            writer.writeAttribute("value", routeParam.value);
            writer.writeEndElement();
        }

        //
        for (RequestParam requestParam : requestParamArray) {
            writer.writeStartElement("request-param");
            writer.writeAttribute("qname", requestParam.name.getValue());
            writer.writeAttribute("name", requestParam.matchName);
            if (requestParam.matchPattern != null) {
                writer.writeAttribute("value", requestParam.matchPattern.getPattern());
            }
            writer.writeEndElement();
        }

        //
        /*
         * for (Map.Entry<String, SegmentRoute[]> entry : segments.entrySet()) { writer.writeStartElement("segment");
         * writer.writeAttribute("name", entry.getKey()); for (SegmentRoute segment : entry.getValue()) {
         * segment.writeTo(writer); } writer.writeEndElement(); }
         *
         * // for (PatternRoute pattern : patterns) { pattern.writeTo(writer); }
         */

        //
        writer.writeEndElement();
    }

    @Override
    public String toString() {
        try {
            XMLOutputFactory factory = XMLOutputFactory.newInstance();
            StringWriter sw = new StringWriter();
            XMLStreamWriter xmlWriter = factory.createXMLStreamWriter(sw);
            // xmlWriter = new IndentingXMLStreamWriter(xmlWriter);
            writeTo(xmlWriter);
            return sw.toString();
        } catch (XMLStreamException e) {
            throw new AssertionError(e);
        }
    }

    /** . */
    private static final Route[] EMPTY_ROUTE_ARRAY = new Route[0];

    /** . */
    private static final RouteParam[] EMPTY_ROUTE_PARAM_ARRAY = new RouteParam[0];

    /** . */
    private static final RequestParam[] EMPTY_REQUEST_PARAM_ARRAY = new RequestParam[0];

    /** . */
    private final Router router;

    /** . */
    private Route parent;

    /** . */
    private boolean terminal;

    /** . */
    private Route[] children;

    /** . */
    private Map<QualifiedName, RouteParam> routeParamMap;

    /** . */
    private RouteParam[] routeParamArray;

    /** . */
    private Map<String, RequestParam> requestParamMap;

    /** . */
    private RequestParam[] requestParamArray;

    Route(Router router) {
        this.router = router;
        this.parent = null;
        this.terminal = true;
        this.children = EMPTY_ROUTE_ARRAY;
        this.routeParamMap = Collections.emptyMap();
        this.routeParamArray = EMPTY_ROUTE_PARAM_ARRAY;
        this.requestParamMap = Collections.emptyMap();
        this.requestParamArray = EMPTY_REQUEST_PARAM_ARRAY;
    }

    final boolean isTerminal() {
        return terminal;
    }

    /*
     * Ok, so this is not the fastest way to do it, but for now it's OK, it's what is needed, we'll find a way to optimize it
     * later with some precompilation.
     */
    final void render(RenderContext context, URIWriter writer) throws IOException {
        RouteMatch r = find(context);

        // We found a route we need to render it now
        if (r != null) {
            r.render(writer);
        }
    }

    static class RouteMatch {

        /** The matched route. */
        final Route route;

        /** The matched parameters. */
        final Map<QualifiedName, String> matches;

        /** . */
        final RenderContext context;

        RouteMatch(RenderContext context, Route route, Map<QualifiedName, String> matches) {
            this.context = context;
            this.route = route;
            this.matches = matches;
        }

        private void render(URIWriter writer) throws IOException {
            // Append path first
            renderPath(route, writer, false);

            // Append query parameters after
            renderQueryString(route, writer);
        }

        private boolean renderPath(Route route, URIWriter writer, boolean hasChildren) throws IOException {
            boolean endWithSlash;
            if (route.parent != null) {
                endWithSlash = renderPath(route.parent, writer, true);
            } else {
                endWithSlash = false;
            }

            //
            if (route instanceof SegmentRoute) {
                SegmentRoute sr = (SegmentRoute) route;
                if (!endWithSlash) {
                    writer.append('/');
                    endWithSlash = true;
                }
                String name = sr.encodedName;
                writer.append(name);
                if (name.length() > 0) {
                    endWithSlash = false;
                }
            } else if (route instanceof PatternRoute) {
                PatternRoute pr = (PatternRoute) route;
                if (!endWithSlash) {
                    writer.append('/');
                    endWithSlash = true;
                }
                int i = 0;
                int count = 0;
                while (i < pr.params.length) {
                    writer.append(pr.encodedChunks[i]);
                    count += pr.chunks[i].length();

                    //
                    PathParam def = pr.params[i];
                    String value = matches.get(def.name);
                    count += value.length();

                    // Write value
                    for (int len = value.length(), j = 0; j < len; j++) {
                        char c = value.charAt(j);
                        if (c == route.router.separatorEscape) {
                            if (def.encodingMode == EncodingMode.PRESERVE_PATH) {
                                writer.append('_');
                            } else {
                                writer.append('%');
                                writer.append(route.router.separatorEscapeNible1);
                                writer.append(route.router.separatorEscapeNible2);
                            }
                        } else if (c == '/') {
                            writer.append(def.encodingMode == EncodingMode.PRESERVE_PATH ? '/' : route.router.separatorEscape);
                        } else {
                            writer.appendSegment(c);
                        }
                    }

                    //
                    i++;
                }
                writer.append(pr.encodedChunks[i]);
                count += pr.chunks[i].length();
                if (count > 0) {
                    endWithSlash = false;
                }
            } else {
                if (!hasChildren) {
                    writer.append('/');
                    endWithSlash = true;
                }
            }

            //
            return endWithSlash;
        }

        private void renderQueryString(Route route, URIWriter writer) throws IOException {
            if (route.parent != null) {
                renderQueryString(route.parent, writer);
            }

            //
            for (RequestParam requestParamDef : route.requestParamArray) {
                String s = matches.get(requestParamDef.name);
                switch (requestParamDef.valueMapping) {
                    case CANONICAL:
                        break;
                    case NEVER_EMPTY:
                        if (s != null && s.length() == 0) {
                            s = null;
                        }
                        break;
                    case NEVER_NULL:
                        if (s == null) {
                            s = "";
                        }
                        break;
                }
                if (s != null) {
                    writer.appendQueryParameter(requestParamDef.matchName, s);
                }
            }
        }
    }

    final RouteMatch find(RenderContext context) {
        context.enter();
        RouteMatch route = _find(context);
        context.leave();
        return route;
    }

    private RouteMatch _find(RenderContext context) {
        // Match first the static parameteters
        for (RouteParam param : routeParamArray) {
            RenderContext.Parameter entry = context.getParameter(param.name);
            if (entry != null && !entry.isMatched() && param.value.equals(entry.getValue())) {
                entry.remove(entry.getValue());
            } else {
                return null;
            }
        }

        // Match any request parameter
        for (RequestParam requestParamDef : requestParamArray) {
            RenderContext.Parameter entry = context.getParameter(requestParamDef.name);
            boolean matched = false;
            if (entry != null && !entry.isMatched()) {
                if (requestParamDef.matchPattern == null
                        || context.matcher(requestParamDef.matchPattern).matches(entry.getValue())) {
                    matched = true;
                }
            }
            if (matched) {
                entry.remove(entry.getValue());
            } else {
                switch (requestParamDef.controlMode) {
                    case OPTIONAL:
                        // Do nothing
                        break;
                    case REQUIRED:
                        return null;
                    default:
                        throw new AssertionError();
                }
            }
        }

        // Match any pattern parameter
        if (this instanceof PatternRoute) {
            PatternRoute prt = (PatternRoute) this;
            for (int i = 0; i < prt.params.length; i++) {
                PathParam param = prt.params[i];
                RenderContext.Parameter s = context.getParameter(param.name);
                String matched = null;
                if (s != null && !s.isMatched()) {
                    switch (param.encodingMode) {
                        case FORM:
                        case PRESERVE_PATH:
                            for (int j = 0; j < param.matchingRegex.length; j++) {
                                Regex renderingRegex = param.matchingRegex[j];
                                if (context.matcher(renderingRegex).matches(s.getValue())) {
                                    matched = param.templatePrefixes[j] + s.getValue() + param.templateSuffixes[j];
                                    break;
                                }
                            }
                            break;
                        default:
                            throw new AssertionError();
                    }
                }
                if (matched != null) {
                    s.remove(matched);
                } else {
                    return null;
                }
            }
        }

        //
        if (context.isEmpty() && terminal) {
            Map<QualifiedName, String> matches = Collections.emptyMap();
            for (QualifiedName name : context.getNames()) {
                RenderContext.Parameter parameter = context.getParameter(name);
                if (matches.isEmpty()) {
                    matches = new HashMap<QualifiedName, String>();
                }
                String match = parameter.getMatch();
                matches.put(name, match);
            }
            return new RouteMatch(context, this, matches);
        }

        //
        for (Route route : children) {
            RouteMatch a = route.find(context);
            if (a != null) {
                return a;
            }
        }

        //
        return null;
    }

    /**
     * Create a route matcher for the a request.
     *
     * @param path the path
     * @param requestParams the query parameters
     * @return the route matcher
     */
    final RouteMatcher route(String path, Map<String, String[]> requestParams) {
        return new RouteMatcher(this, Path.parse(path), requestParams);
    }

    static class RouteFrame {

        /**
         * Defines the status of a frame.
         */
        static enum Status {
            BEGIN,

            MATCHED_PARAMS,

            PROCESS_CHILDREN,

            MATCHED,

            END

        }

        /** . */
        private final RouteFrame parent;

        /** . */
        private final Route route;

        /** . */
        private final Path path;

        /** . */
        private Status status;

        /** The matches. */
        private Map<QualifiedName, String> matches;

        /**
         * The index when iterating child in
         * {@link org.exoplatform.web.controller.router.Route.RouteFrame.Status#PROCESS_CHILDREN} status.
         */
        private int childIndex;

        private RouteFrame(RouteFrame parent, Route route, Path path) {
            this.parent = parent;
            this.route = route;
            this.path = path;
            this.status = Status.BEGIN;
            this.childIndex = 0;
        }

        private RouteFrame(Route route, Path path) {
            this(null, route, path);
        }

        Map<QualifiedName, String> getParameters() {
            Map<QualifiedName, String> parameters = null;
            for (RouteFrame frame = this; frame != null; frame = frame.parent) {
                if (frame.matches != null) {
                    if (parameters == null) {
                        parameters = new HashMap<QualifiedName, String>();
                    }
                    parameters.putAll(frame.matches);
                }
                for (RouteParam param : frame.route.routeParamArray) {
                    if (parameters == null) {
                        parameters = new HashMap<QualifiedName, String>();
                    }
                    parameters.put(param.name, param.value);
                }
            }
            return parameters != null ? parameters : Collections.<QualifiedName, String> emptyMap();
        }
    }

    static class RouteMatcher implements Iterator<Map<QualifiedName, String>> {

        /** . */
        private final Map<String, String[]> requestParams;

        /** . */
        private RouteFrame frame;

        /** . */
        private RouteFrame next;

        RouteMatcher(Route route, Path path, Map<String, String[]> requestParams) {
            this.frame = new RouteFrame(route, path);
            this.requestParams = requestParams;
        }

        public boolean hasNext() {
            if (next == null) {
                if (frame != null) {
                    frame = route(frame, requestParams);
                }
                if (frame != null && frame.status == RouteFrame.Status.MATCHED) {
                    next = frame;
                }
            }
            return next != null;
        }

        public Map<QualifiedName, String> next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            Map<QualifiedName, String> parameters = next.getParameters();
            next = null;
            return parameters;
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }
    }

    private static RouteFrame route(RouteFrame root, Map<String, String[]> requestParams) {
        RouteFrame current = root;

        //
        if (root.status == RouteFrame.Status.MATCHED) {
            if (root.parent != null) {
                current = root.parent;
            } else {
                return null;
            }
        } else if (root.status != RouteFrame.Status.BEGIN) {
            throw new AssertionError("Unexpected status " + root.status);
        }

        //
        while (true) {
            if (current.status == RouteFrame.Status.BEGIN) {
                boolean matched = true;

                // We enter a frame
                for (RequestParam requestParamDef : current.route.requestParamArray) {
                    String value = null;
                    String[] values = requestParams.get(requestParamDef.matchName);
                    if (values != null && values.length > 0 && values[0] != null) {
                        value = values[0];
                    }
                    if (value == null) {
                        switch (requestParamDef.controlMode) {
                            case OPTIONAL:
                                // Do nothing
                                break;
                            case REQUIRED:
                                matched = false;
                                break;
                        }
                    } else if (!requestParamDef.matchValue(value)) {
                        matched = false;
                        break;
                    }
                    switch (requestParamDef.valueMapping) {
                        case CANONICAL:
                            break;
                        case NEVER_EMPTY:
                            if (value != null && value.length() == 0) {
                                value = null;
                            }
                            break;
                        case NEVER_NULL:
                            if (value == null) {
                                value = "";
                            }
                            break;
                    }
                    if (value != null) {
                        if (current.matches == null) {
                            current.matches = new HashMap<QualifiedName, String>();
                        }
                        current.matches.put(requestParamDef.name, value);
                    }
                }

                //
                if (matched) {
                    // We enter next state
                    current.status = RouteFrame.Status.MATCHED_PARAMS;
                } else {
                    current.status = RouteFrame.Status.END;
                }
            } else if (current.status == RouteFrame.Status.MATCHED_PARAMS) {
                RouteFrame.Status next;

                // Anything that does not begin with '/' returns null
                if (current.path.length() > 0 && current.path.charAt(0) == '/') {
                    // The '/' means the current controller if any, otherwise it may be processed by the pattern matching
                    if (current.path.length() == 1 && current.route.terminal) {
                        next = RouteFrame.Status.MATCHED;
                    } else {
                        next = RouteFrame.Status.PROCESS_CHILDREN;
                    }
                } else {
                    next = RouteFrame.Status.END;
                }

                //
                current.status = next;
            } else if (current.status == RouteFrame.Status.PROCESS_CHILDREN) {
                if (current.childIndex < current.route.children.length) {
                    Route child = current.route.children[current.childIndex++];

                    // The next frame
                    RouteFrame next;

                    //
                    if (child instanceof SegmentRoute) {
                        SegmentRoute segmentRoute = (SegmentRoute) child;

                        //
                        if (segmentRoute.name.length() == 0) {
                            // Delegate the process to the next route
                            next = new RouteFrame(current, segmentRoute, current.path);
                        } else {
                            // Find the next '/' for determining the segment and next path
                            // JULIEN : this can be computed multiple times
                            int pos = current.path.indexOf('/', 1);
                            if (pos == -1) {
                                pos = current.path.length();
                            }
                            String segment = current.path.getValue().substring(1, pos);

                            // Determine next path
                            if (segmentRoute.name.equals(segment)) {
                                // Lazy create next segment path
                                // JULIEN : this can be computed multiple times
                                Path nextSegmentPath;
                                if (pos == current.path.length()) {
                                    // todo make a constant
                                    nextSegmentPath = Path.SLASH;
                                } else {
                                    nextSegmentPath = current.path.subPath(pos);
                                }

                                // Delegate the process to the next route
                                next = new RouteFrame(current, segmentRoute, nextSegmentPath);
                            } else {
                                next = null;
                            }
                        }
                    } else if (child instanceof PatternRoute) {
                        PatternRoute patternRoute = (PatternRoute) child;

                        //
                        Regex.Match[] matches = patternRoute.pattern.matcher().find(current.path.getValue());

                        // We match
                        if (matches.length > 0) {
                            // Build next controller context
                            int nextPos = matches[0].getEnd();
                            Path nextPath;
                            if (current.path.length() == nextPos) {
                                nextPath = Path.SLASH;
                            } else {
                                if (nextPos > 0 && current.path.charAt(nextPos - 1) == '/') {
                                    nextPos--;
                                }

                                //
                                nextPath = current.path.subPath(nextPos);
                            }

                            // Delegate to next patternRoute
                            next = new RouteFrame(current, patternRoute, nextPath);

                            // JULIEN : this can be done lazily
                            // Append parameters
                            int index = 1;
                            for (int i = 0; i < patternRoute.params.length; i++) {
                                PathParam param = patternRoute.params[i];
                                for (int j = 0; j < param.matchingRegex.length; j++) {
                                    Regex.Match match = matches[index + j];
                                    if (match.getEnd() != -1) {
                                        String value;
                                        if (param.encodingMode == EncodingMode.FORM) {
                                            StringBuilder sb = new StringBuilder();
                                            for (int from = match.getStart(); from < match.getEnd(); from++) {
                                                char c = current.path.charAt(from);
                                                if (c == child.router.separatorEscape && current.path.getRawLength(from) == 1) {
                                                    c = '/';
                                                }
                                                sb.append(c);
                                            }
                                            value = sb.toString();
                                        } else {
                                            value = match.getValue();
                                        }
                                        if (next.matches == null) {
                                            next.matches = new HashMap<QualifiedName, String>();
                                        }
                                        next.matches.put(param.name, value);
                                        break;
                                    } else {
                                        // It can be the match of a particular disjunction
                                        // or an optional parameter
                                    }
                                }
                                index += param.matchingRegex.length;
                            }
                        } else {
                            next = null;
                        }
                    } else {
                        throw new AssertionError();
                    }

                    //
                    if (next != null) {
                        current = next;
                    }
                } else {
                    current.status = RouteFrame.Status.END;
                }
            } else if (current.status == RouteFrame.Status.MATCHED) {
                // We found a solution
                break;
            } else if (current.status == RouteFrame.Status.END) {
                if (current.parent != null) {
                    current = current.parent;
                } else {
                    // The end of the search
                    break;
                }
            } else {
                throw new AssertionError();
            }
        }

        //
        return current;
    }

    final <R extends Route> R add(R route) throws MalformedRouteException {
        if (route == null) {
            throw new NullPointerException("No null route accepted");
        }
        if (((Route) route).parent != null) {
            throw new IllegalArgumentException("No route with an existing parent can be accepted");
        }

        //
        LinkedList<Param> ancestorParams = new LinkedList<Param>();
        findAncestorOrSelfParams(ancestorParams);
        LinkedList<Param> descendantParams = new LinkedList<Param>();
        for (Param param : ancestorParams) {
            ((Route) route).findDescendantOrSelfParams(param.name, descendantParams);
            if (descendantParams.size() > 0) {
                throw new MalformedRouteException("Duplicate parameter " + param.name);
            }
        }

        //
        if (route instanceof PatternRoute || route instanceof SegmentRoute) {
            children = Tools.appendTo(children, route);
            terminal = false;
            ((Route) route).parent = this;
        } else {
            throw new IllegalArgumentException("Only accept segment or pattern routes");
        }

        //
        return route;
    }

    final Set<String> getSegmentNames() {
        Set<String> names = new HashSet<String>();
        for (Route child : children) {
            if (child instanceof SegmentRoute) {
                SegmentRoute childSegment = (SegmentRoute) child;
                names.add(childSegment.name);
            }
        }
        return names;
    }

    final int getSegmentSize(String segmentName) {
        int size = 0;
        for (Route child : children) {
            if (child instanceof SegmentRoute) {
                SegmentRoute childSegment = (SegmentRoute) child;
                if (segmentName.equals(childSegment.name)) {
                    size++;
                }
            }
        }
        return size;
    }

    final SegmentRoute getSegment(String segmentName, int index) {
        for (Route child : children) {
            if (child instanceof SegmentRoute) {
                SegmentRoute childSegment = (SegmentRoute) child;
                if (segmentName.equals(childSegment.name)) {
                    if (index == 0) {
                        return childSegment;
                    } else {
                        index--;
                    }
                }
            }
        }
        return null;
    }

    final int getPatternSize() {
        int size = 0;
        for (Route route : children) {
            if (route instanceof PatternRoute) {
                size++;
            }
        }
        return size;
    }

    final PatternRoute getPattern(int index) {
        for (Route route : children) {
            if (route instanceof PatternRoute) {
                if (index == 0) {
                    return (PatternRoute) route;
                } else {
                    index--;
                }
            }
        }
        return null;
    }

    final Route append(RouteDescriptor descriptor) throws MalformedRouteException {
        Route route = append(descriptor.getPathParams(), descriptor.getPath());

        //
        for (RouteParamDescriptor routeParamDesc : descriptor.getRouteParams()) {
            route.add(RouteParam.create(routeParamDesc));
        }

        //
        for (RequestParamDescriptor requestParamDesc : descriptor.getRequestParams()) {
            route.add(RequestParam.create(requestParamDesc, router));
        }

        //
        for (RouteDescriptor childDescriptor : descriptor.getChildren()) {
            route.append(childDescriptor);
        }

        //
        return route;
    }

    final Route add(RouteParam param) throws MalformedRouteException {
        Param existing = findParam(param.name);
        if (existing != null) {
            throw new MalformedRouteException("Duplicate parameter " + param.name);
        }
        if (routeParamArray.length == 0) {
            routeParamMap = new HashMap<QualifiedName, RouteParam>();
        }
        routeParamMap.put(param.name, param);
        routeParamArray = Tools.appendTo(routeParamArray, param);
        return this;
    }

    final Route add(RequestParam param) throws MalformedRouteException {
        Param existing = findParam(param.name);
        if (existing != null) {
            throw new MalformedRouteException("Duplicate parameter " + param.name);
        }
        if (requestParamArray.length == 0) {
            requestParamMap = new HashMap<String, RequestParam>();
        }
        requestParamMap.put(param.matchName, param);
        requestParamArray = Tools.appendTo(requestParamArray, param);
        return this;
    }

    /**
     * Append a path, creates the necessary routes and returns the last route added.
     *
     * @param pathParamDescriptors the path param descriptors
     * @param path the path to append
     * @return the last route added
     */
    private Route append(Map<QualifiedName, PathParamDescriptor> pathParamDescriptors, String path)
            throws MalformedRouteException {
        if (path.length() == 0 || path.charAt(0) != '/') {
            throw new MalformedRouteException();
        }

        //
        int pos = path.length();
        int level = 0;
        List<Integer> start = new ArrayList<Integer>();
        List<Integer> end = new ArrayList<Integer>();
        for (int i = 1; i < path.length(); i++) {
            char c = path.charAt(i);
            if (c == '{') {
                if (level++ == 0) {
                    start.add(i);
                }
            } else if (c == '}') {
                if (--level == 0) {
                    end.add(i);
                }
            } else if (c == '/') {
                if (level == 0) {
                    pos = i;
                    break;
                }
            }
        }

        //
        Route next;
        if (start.isEmpty()) {
            String segment = path.substring(1, pos);
            SegmentRoute route = new SegmentRoute(router, segment);
            add(route);
            next = route;
        } else {
            if (start.size() == end.size()) {
                PatternBuilder builder = new PatternBuilder();
                builder.expr("^").expr('/');
                List<String> chunks = new ArrayList<String>();
                List<PathParam> parameterPatterns = new ArrayList<PathParam>();

                //
                int previous = 1;
                for (int i = 0; i < start.size(); i++) {
                    builder.litteral(path, previous, start.get(i));
                    chunks.add(path.substring(previous, start.get(i)));
                    String parameterName = path.substring(start.get(i) + 1, end.get(i));

                    //
                    QualifiedName parameterQName = QualifiedName.parse(parameterName);

                    // Now get path param metadata
                    PathParamDescriptor parameterDescriptor = pathParamDescriptors.get(parameterQName);

                    //
                    PathParam param;
                    if (parameterDescriptor != null) {
                        param = PathParam.create(parameterDescriptor, router);
                    } else {
                        param = PathParam.create(parameterQName, router);
                    }

                    // Append routing regex to the route regex surrounded by a non capturing regex
                    // to isolate routingRegex like a|b or a(.)b
                    builder.expr("(?:").expr(param.routingRegex).expr(")");

                    // Add the path param with the rendering regex
                    parameterPatterns.add(param);
                    previous = end.get(i) + 1;
                }

                //
                builder.litteral(path, previous, pos);

                // We want to satisfy one of the following conditions
                // - the next char after the matched expression is '/'
                // - the expression matched until the end
                // - the match expression is the '/' expression
                builder.expr("(?:(?<=^/)|(?=/)|$)");

                //
                chunks.add(path.substring(previous, pos));
                PatternRoute route = new PatternRoute(router, router.compile(builder.build()), parameterPatterns, chunks);

                // Wire
                add(route);

                //
                next = route;
            } else {
                throw new UnsupportedOperationException("Report error");
            }
        }

        //
        if (pos < path.length()) {
            return next.append(pathParamDescriptors, path.substring(pos));
        } else {
            return next;
        }
    }

    private Param getParam(QualifiedName name) {
        Param param = routeParamMap.get(name);
        if (param == null) {
            for (RequestParam requestParam : requestParamArray) {
                if (requestParam.name.equals(name)) {
                    param = requestParam;
                    break;
                }
            }
            if (param == null && this instanceof PatternRoute) {
                for (PathParam pathParam : ((PatternRoute) this).params) {
                    if (pathParam.name.equals(name)) {
                        param = pathParam;
                        break;
                    }
                }
            }
        }
        return param;
    }

    private Param findParam(QualifiedName name) {
        Param param = getParam(name);
        if (param == null && parent != null) {
            param = parent.findParam(name);
        }
        return param;
    }

    private void findParams(List<Param> params) {
        Collections.addAll(params, routeParamArray);
        for (RequestParam param : requestParamArray) {
            params.add(param);
        }
        if (this instanceof PatternRoute) {
            Collections.addAll(params, ((PatternRoute) this).params);
        }
    }

    private void findAncestorOrSelfParams(List<Param> params) {
        findParams(params);
        if (parent != null) {
            parent.findAncestorOrSelfParams(params);
        }
    }

    /**
     * Find the params having the specified <code>name</code> among this route or its descendants.
     *
     * @param name the name
     * @param params the list collecting the found params
     */
    private void findDescendantOrSelfParams(QualifiedName name, List<Param> params) {
        Param param = getParam(name);
        if (param != null) {
            params.add(param);
        }
    }
}
