/**
 * <strong>Central entry point to Spring's functional web framework.</strong> Exposes routing
 * functionality, such as to {@linkplain #route(RequestPredicate, HandlerFunction) create} a {@code
 * RouterFunction} given a {@code RequestPredicate} and {@code HandlerFunction}, and to do further
 * {@linkplain #subroute(RequestPredicate, RouterFunction) subrouting} on an existing routing
 * function.
 *
 * <p>Additionally, this class can {@linkplain #toHttpHandler(RouterFunction) transform} a {@code
 * RouterFunction} into an {@code HttpHandler}, which can be run in Servlet 3.1+, Reactor, RxNetty,
 * or Undertow. And it can {@linkplain #toHandlerMapping(RouterFunction, HandlerStrategies)
 * transform} a {@code RouterFunction} into an {@code HandlerMapping}, which can be run in a {@code
 * DispatcherHandler}.
 *
 * @author Arjen Poutsma
 * @since 5.0
 */
public abstract class RouterFunctions {

  /** Name of the {@link ServerWebExchange} attribute that contains the {@link ServerRequest}. */
  public static final String REQUEST_ATTRIBUTE = RouterFunctions.class.getName() + ".request";

  /**
   * Name of the {@link ServerWebExchange} attribute that contains the URI templates map, mapping
   * variable names to values.
   */
  public static final String URI_TEMPLATE_VARIABLES_ATTRIBUTE =
      RouterFunctions.class.getName() + ".uriTemplateVariables";

  private static final HandlerFunction<Void> NOT_FOUND_HANDLER =
      request -> ServerResponse.notFound().build();

  /**
   * Route to the given handler function if the given request predicate applies.
   *
   * @param predicate the predicate to test
   * @param handlerFunction the handler function to route to
   * @param <T> the type of the handler function
   * @return a routing function that routes to {@code handlerFunction} if {@code predicate}
   *     evaluates to {@code true}
   * @see RequestPredicates
   */
  public static <T> RouterFunction<T> route(
      RequestPredicate predicate, HandlerFunction<T> handlerFunction) {
    Assert.notNull(predicate, "'predicate' must not be null");
    Assert.notNull(handlerFunction, "'handlerFunction' must not be null");

    return request -> predicate.test(request) ? Optional.of(handlerFunction) : Optional.empty();
  }

  /**
   * Route to the given routing function if the given request predicate applies.
   *
   * @param predicate the predicate to test
   * @param routerFunction the routing function to route to
   * @param <T> the type of the handler function
   * @return a routing function that routes to {@code routerFunction} if {@code predicate} evaluates
   *     to {@code true}
   * @see RequestPredicates
   */
  public static <T> RouterFunction<T> subroute(
      RequestPredicate predicate, RouterFunction<T> routerFunction) {
    Assert.notNull(predicate, "'predicate' must not be null");
    Assert.notNull(routerFunction, "'routerFunction' must not be null");

    return request -> {
      if (predicate.test(request)) {
        ServerRequest subRequest = predicate.subRequest(request);
        return routerFunction.route(subRequest);
      } else {
        return Optional.empty();
      }
    };
  }

  /**
   * Convert the given {@linkplain RouterFunction routing function} into a {@link HttpHandler}. This
   * conversion uses {@linkplain HandlerStrategies#builder() default strategies}.
   *
   * <p>The returned {@code HttpHandler} can be adapted to run in
   *
   * <ul>
   *   <li>Servlet 3.1+ using the {@link
   *       org.springframework.http.server.reactive.ServletHttpHandlerAdapter},
   *   <li>Reactor using the {@link
   *       org.springframework.http.server.reactive.ReactorHttpHandlerAdapter},
   *   <li>RxNetty using the {@link
   *       org.springframework.http.server.reactive.RxNettyHttpHandlerAdapter}, or
   *   <li>Undertow using the {@link
   *       org.springframework.http.server.reactive.UndertowHttpHandlerAdapter}.
   * </ul>
   *
   * @param routerFunction the routing function to convert
   * @return an http handler that handles HTTP request using the given routing function
   */
  public static HttpHandler toHttpHandler(RouterFunction<?> routerFunction) {
    return toHttpHandler(routerFunction, HandlerStrategies.withDefaults());
  }

  /**
   * Convert the given {@linkplain RouterFunction routing function} into a {@link HttpHandler},
   * using the given strategies.
   *
   * <p>The returned {@code HttpHandler} can be adapted to run in
   *
   * <ul>
   *   <li>Servlet 3.1+ using the {@link
   *       org.springframework.http.server.reactive.ServletHttpHandlerAdapter},
   *   <li>Reactor using the {@link
   *       org.springframework.http.server.reactive.ReactorHttpHandlerAdapter},
   *   <li>RxNetty using the {@link
   *       org.springframework.http.server.reactive.RxNettyHttpHandlerAdapter}, or
   *   <li>Undertow using the {@link
   *       org.springframework.http.server.reactive.UndertowHttpHandlerAdapter}.
   * </ul>
   *
   * @param routerFunction the routing function to convert
   * @param strategies the strategies to use
   * @return an http handler that handles HTTP request using the given routing function
   */
  public static HttpHandler toHttpHandler(
      RouterFunction<?> routerFunction, HandlerStrategies strategies) {
    Assert.notNull(routerFunction, "RouterFunction must not be null");
    Assert.notNull(strategies, "HandlerStrategies must not be null");

    return new HttpWebHandlerAdapter(
        exchange -> {
          ServerRequest request = new DefaultServerRequest(exchange, strategies);
          addAttributes(exchange, request);
          HandlerFunction<?> handlerFunction = routerFunction.route(request).orElse(notFound());
          ServerResponse<?> response = handlerFunction.handle(request);
          return response.writeTo(exchange, strategies);
        });
  }

  /**
   * Convert the given {@code RouterFunction} into a {@code HandlerMapping}. This conversion uses
   * {@linkplain HandlerStrategies#builder() default strategies}.
   *
   * <p>The returned {@code HandlerMapping} can be run in a {@link
   * org.springframework.web.reactive.DispatcherHandler}.
   *
   * @param routerFunction the routing function to convert
   * @return an handler mapping that maps HTTP request to a handler using the given routing function
   * @see org.springframework.web.reactive.function.support.HandlerFunctionAdapter
   * @see org.springframework.web.reactive.function.support.ServerResponseResultHandler
   */
  public static HandlerMapping toHandlerMapping(RouterFunction<?> routerFunction) {
    return toHandlerMapping(routerFunction, HandlerStrategies.withDefaults());
  }

  /**
   * Convert the given {@linkplain RouterFunction routing function} into a {@link HandlerMapping},
   * using the given strategies.
   *
   * <p>The returned {@code HandlerMapping} can be run in a {@link
   * org.springframework.web.reactive.DispatcherHandler}.
   *
   * @param routerFunction the routing function to convert
   * @param strategies the strategies to use
   * @return an handler mapping that maps HTTP request to a handler using the given routing function
   * @see org.springframework.web.reactive.function.support.HandlerFunctionAdapter
   * @see org.springframework.web.reactive.function.support.ServerResponseResultHandler
   */
  public static HandlerMapping toHandlerMapping(
      RouterFunction<?> routerFunction, HandlerStrategies strategies) {
    Assert.notNull(routerFunction, "RouterFunction must not be null");
    Assert.notNull(strategies, "HandlerStrategies must not be null");

    return exchange -> {
      ServerRequest request = new DefaultServerRequest(exchange, strategies);
      addAttributes(exchange, request);
      Optional<? extends HandlerFunction<?>> route = routerFunction.route(request);
      return Mono.justOrEmpty(route);
    };
  }

  private static void addAttributes(ServerWebExchange exchange, ServerRequest request) {
    Map<String, Object> attributes = exchange.getAttributes();
    attributes.put(REQUEST_ATTRIBUTE, request);
  }

  @SuppressWarnings("unchecked")
  private static <T> HandlerFunction<T> notFound() {
    return (HandlerFunction<T>) NOT_FOUND_HANDLER;
  }

  @SuppressWarnings("unchecked")
  static <T> HandlerFunction<T> cast(HandlerFunction<?> handlerFunction) {
    return (HandlerFunction<T>) handlerFunction;
  }
}