/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2014 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package io.undertow.server;

import io.undertow.predicate.Predicate;
import io.undertow.server.handlers.ResponseCodeHandler;
import io.undertow.util.CopyOnWriteMap;
import io.undertow.util.HttpString;
import io.undertow.util.Methods;
import io.undertow.util.PathTemplate;
import io.undertow.util.PathTemplateMatch;
import io.undertow.util.PathTemplateMatcher;

import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * A Handler that handles the common case of routing via path template and method name.
 *
 * @author Stuart Douglas
 */
public class RoutingHandler implements HttpHandler {

    // Matcher objects grouped by http methods.
    private final Map<HttpString, PathTemplateMatcher<RoutingMatch>> matches = new CopyOnWriteMap<>();
    // Matcher used to find if this instance contains matches for any http method for a path.
    // This matcher is used to report if this instance can match a path for one of the http methods.
    private final PathTemplateMatcher<RoutingMatch> allMethodsMatcher = new PathTemplateMatcher<>();

    // Handler called when no match was found and invalid method handler can't be invoked.
    private volatile HttpHandler fallbackHandler = ResponseCodeHandler.HANDLE_404;
    // Handler called when this instance can not match the http method but can match another http method.
    // For example: For an exchange the POST method is not matched by this instance but at least one http method is
    // matched for the same exchange.
    // If this handler is null the fallbackHandler will be used.
    private volatile HttpHandler invalidMethodHandler = ResponseCodeHandler.HANDLE_405;

    // If this is true then path matches will be added to the query parameters for easy access by later handlers.
    private final boolean rewriteQueryParameters;

    public RoutingHandler(boolean rewriteQueryParameters) {
        this.rewriteQueryParameters = rewriteQueryParameters;
    }

    public RoutingHandler() {
        this.rewriteQueryParameters = true;
    }

    @Override
    public void handleRequest(HttpServerExchange exchange) throws Exception {

        PathTemplateMatcher<RoutingMatch> matcher = matches.get(exchange.getRequestMethod());
        if (matcher == null) {
            handleNoMatch(exchange);
            return;
        }
        PathTemplateMatcher.PathMatchResult<RoutingMatch> match = matcher.match(exchange.getRelativePath());
        if (match == null) {
            handleNoMatch(exchange);
            return;
        }
        exchange.putAttachment(PathTemplateMatch.ATTACHMENT_KEY, match);
        if (rewriteQueryParameters) {
            for (Map.Entry<String, String> entry : match.getParameters().entrySet()) {
                exchange.addQueryParam(entry.getKey(), entry.getValue());
            }
        }
        for (HandlerHolder handler : match.getValue().predicatedHandlers) {
            if (handler.predicate.resolve(exchange)) {
                handler.handler.handleRequest(exchange);
                return;
            }
        }
        if (match.getValue().defaultHandler != null) {
            match.getValue().defaultHandler.handleRequest(exchange);
        } else {
            fallbackHandler.handleRequest(exchange);
        }
    }

    /**
     * Handles the case in with a match was not found for the http method but might exist for another http method.
     * For example: POST not matched for a path but at least one match exists for same path.
     *
     * @param exchange The object for which its handled the "no match" case.
     * @throws Exception
     */
    private void handleNoMatch(final HttpServerExchange exchange) throws Exception {
        // if invalidMethodHandler is null we fail fast without matching with allMethodsMatcher
        if (invalidMethodHandler != null && allMethodsMatcher.match(exchange.getRelativePath()) != null) {
            invalidMethodHandler.handleRequest(exchange);
            return;
        }
        fallbackHandler.handleRequest(exchange);
    }

    public synchronized RoutingHandler add(final String method, final String template, HttpHandler handler) {
        return add(new HttpString(method), template, handler);
    }

    public synchronized RoutingHandler add(HttpString method, String template, HttpHandler handler) {
        PathTemplateMatcher<RoutingMatch> matcher = matches.get(method);
        if (matcher == null) {
            matches.put(method, matcher = new PathTemplateMatcher<>());
        }
        RoutingMatch res = matcher.get(template);
        if (res == null) {
            matcher.add(template, res = new RoutingMatch());
        }
        if (allMethodsMatcher.get(template) == null) {
            allMethodsMatcher.add(template, res);
        }
        res.defaultHandler = handler;
        return this;
    }

    public synchronized RoutingHandler get(final String template, HttpHandler handler) {
        return add(Methods.GET, template, handler);
    }

    public synchronized RoutingHandler post(final String template, HttpHandler handler) {
        return add(Methods.POST, template, handler);
    }

    public synchronized RoutingHandler put(final String template, HttpHandler handler) {
        return add(Methods.PUT, template, handler);
    }

    public synchronized RoutingHandler delete(final String template, HttpHandler handler) {
        return add(Methods.DELETE, template, handler);
    }

    public synchronized RoutingHandler add(final String method, final String template, Predicate predicate, HttpHandler handler) {
        return add(new HttpString(method), template, predicate, handler);
    }

    public synchronized RoutingHandler add(HttpString method, String template, Predicate predicate, HttpHandler handler) {
        PathTemplateMatcher<RoutingMatch> matcher = matches.get(method);
        if (matcher == null) {
            matches.put(method, matcher = new PathTemplateMatcher<>());
        }
        RoutingMatch res = matcher.get(template);
        if (res == null) {
            matcher.add(template, res = new RoutingMatch());
        }
        if (allMethodsMatcher.get(template) == null) {
            allMethodsMatcher.add(template, res);
        }
        res.predicatedHandlers.add(new HandlerHolder(predicate, handler));
        return this;
    }

    public synchronized RoutingHandler get(final String template, Predicate predicate, HttpHandler handler) {
        return add(Methods.GET, template, predicate, handler);
    }

    public synchronized RoutingHandler post(final String template, Predicate predicate, HttpHandler handler) {
        return add(Methods.POST, template, predicate, handler);
    }

    public synchronized RoutingHandler put(final String template, Predicate predicate, HttpHandler handler) {
        return add(Methods.PUT, template, predicate, handler);
    }

    public synchronized RoutingHandler delete(final String template, Predicate predicate, HttpHandler handler) {
        return add(Methods.DELETE, template, predicate, handler);
    }

    public synchronized RoutingHandler addAll(RoutingHandler routingHandler) {
        for (Entry<HttpString, PathTemplateMatcher<RoutingMatch>> entry : routingHandler.getMatches().entrySet()) {
            HttpString method = entry.getKey();
            PathTemplateMatcher<RoutingMatch> matcher = matches.get(method);
            if (matcher == null) {
                matches.put(method, matcher = new PathTemplateMatcher<>());
            }
            matcher.addAll(entry.getValue());
            // If we use allMethodsMatcher.addAll() we can have duplicate
            // PathTemplates which we want to ignore here so it does not crash.
            for (PathTemplate template : entry.getValue().getPathTemplates()) {
                if (allMethodsMatcher.get(template.getTemplateString()) == null) {
                    allMethodsMatcher.add(template, new RoutingMatch());
                }
            }
        }
        return this;
    }

    /**
     *
     * Removes the specified route from the handler
     *
     * @param method The method to remove
     * @param path the path tempate to remove
     * @return this handler
     */
    public RoutingHandler remove(HttpString method, String path) {
        PathTemplateMatcher<RoutingMatch> handler = matches.get(method);
        if(handler != null) {
            handler.remove(path);
        }
        return this;
    }


    /**
     *
     * Removes the specified route from the handler
     *
     * @param path the path tempate to remove
     * @return this handler
     */
    public RoutingHandler remove(String path) {
        allMethodsMatcher.remove(path);
        return this;
    }

    Map<HttpString, PathTemplateMatcher<RoutingMatch>> getMatches() {
        return matches;
    }

    /**
     * @return Handler called when no match was found and invalid method handler can't be invoked.
     */
    public HttpHandler getFallbackHandler() {
        return fallbackHandler;
    }

    /**
     * @param fallbackHandler Handler that will be called when no match was found and invalid method handler can't be
     * invoked.
     * @return This instance.
     */
    public RoutingHandler setFallbackHandler(HttpHandler fallbackHandler) {
        this.fallbackHandler = fallbackHandler;
        return this;
    }

    /**
     * @return Handler called when this instance can not match the http method but can match another http method.
     */
    public HttpHandler getInvalidMethodHandler() {
        return invalidMethodHandler;
    }

    /**
     * Sets the handler called when this instance can not match the http method but can match another http method.
     * For example: For an exchange the POST method is not matched by this instance but at least one http method matched
     * for the exchange.
     * If this handler is null the fallbackHandler will be used.
     *
     * @param invalidMethodHandler Handler that will be called when this instance can not match the http method but can
     * match another http method.
     * @return This instance.
     */
    public RoutingHandler setInvalidMethodHandler(HttpHandler invalidMethodHandler) {
        this.invalidMethodHandler = invalidMethodHandler;
        return this;
    }

    private static class RoutingMatch {

        final List<HandlerHolder> predicatedHandlers = new CopyOnWriteArrayList<>();
        volatile HttpHandler defaultHandler;

    }

    private static class HandlerHolder {
        final Predicate predicate;
        final HttpHandler handler;

        private HandlerHolder(Predicate predicate, HttpHandler handler) {
            this.predicate = predicate;
            this.handler = handler;
        }
    }

}
