/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * license agreements; and to You under the Apache License, version 2.0:
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * This file is part of the Apache Pekko project, which was derived from Akka.
 */

/*
 * Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
 */

package org.apache.pekko.http.javadsl.server.directives

import org.apache.pekko
import pekko.actor.ActorSystem
import pekko.dispatch.ExecutionContexts
import pekko.event.LoggingAdapter
import pekko.japi.Util
import pekko.stream.Materializer
import pekko.stream.javadsl.Source
import pekko.util.ByteString
import pekko.util.FutureConverters._
import pekko.util.JavaDurationConverters._

import pekko.http.impl.model.JavaUri
import pekko.http.impl.util.JavaMapping
import pekko.http.impl.util.Util.convertIterable
import pekko.http.javadsl.model.{
  HttpEntity,
  HttpHeader,
  HttpRequest,
  HttpResponse,
  RequestEntity,
  ResponseEntity,
  Uri
}
import pekko.http.javadsl.server
import pekko.http.javadsl.server._
import pekko.http.javadsl.settings.{ ParserSettings, RoutingSettings }
import pekko.http.scaladsl
import pekko.http.scaladsl.server.{ Directives => D }
import pekko.http.scaladsl.util.FastFuture._

import java.lang.{ Iterable => JIterable }
import java.time.{ Duration => JDuration }
import java.util.{ List => JList }
import java.util.concurrent.CompletionStage
import java.util.function.{ Function => JFunction }
import java.util.function.Predicate
import java.util.function.Supplier

import scala.concurrent.ExecutionContextExecutor
import scala.concurrent.duration.FiniteDuration

abstract class BasicDirectives {
  import pekko.http.impl.util.JavaMapping.Implicits._
  import RoutingJavaMapping._

  def mapRequest(f: JFunction[HttpRequest, HttpRequest], inner: Supplier[Route]): Route = RouteAdapter {
    D.mapRequest(rq => f.apply(rq.asJava).asScala) { inner.get.delegate }
  }

  def mapRequestContext(f: JFunction[RequestContext, RequestContext], inner: Supplier[Route]): Route = RouteAdapter {
    D.mapRequestContext(rq => f.apply(RequestContext.toJava(rq)).asScala) { inner.get.delegate }
  }

  def mapRejections(f: JFunction[JList[Rejection], JList[Rejection]], inner: Supplier[Route]): Route = RouteAdapter {
    D.mapRejections(rejections =>
      convertIterable[Rejection, Rejection](f.apply(Util.javaArrayList(rejections.map(_.asJava)))).map(
        _.asScala)) { inner.get.delegate }
  }

  def mapResponse(f: JFunction[HttpResponse, HttpResponse], inner: Supplier[Route]): Route = RouteAdapter {
    D.mapResponse(resp => f.apply(resp.asJava).asScala) { inner.get.delegate }
  }

  def mapResponseEntity(f: JFunction[ResponseEntity, ResponseEntity], inner: Supplier[Route]): Route = RouteAdapter {
    D.mapResponseEntity(e => f.apply(e.asJava).asScala) { inner.get.delegate }
  }

  def mapResponseHeaders(f: JFunction[JList[HttpHeader], JList[HttpHeader]], inner: Supplier[Route]): Route =
    RouteAdapter {
      D.mapResponseHeaders(l =>
        convertIterable[HttpHeader, HttpHeader](f.apply(Util.javaArrayList(l))).map(_.asScala)) {
        inner.get.delegate
      } // TODO try to remove map()
    }

  def mapInnerRoute(f: JFunction[Route, Route], inner: Supplier[Route]): Route = RouteAdapter {
    D.mapInnerRoute(route => f(RouteAdapter(route)).delegate) { inner.get.delegate }
  }

  def mapRouteResult(f: JFunction[RouteResult, RouteResult], inner: Supplier[Route]): Route = RouteAdapter {
    D.mapRouteResult(route => f(route.asJava).asScala) { inner.get.delegate }
  }

  def mapRouteResultPF(f: PartialFunction[RouteResult, RouteResult], inner: Supplier[Route]): Route = RouteAdapter {
    D.mapRouteResult(route => f(route.asJava).asScala) { inner.get.delegate }
  }

  def mapRouteResultFuture(f: JFunction[CompletionStage[RouteResult], CompletionStage[RouteResult]],
      inner: Supplier[Route]): Route = RouteAdapter {
    D.mapRouteResultFuture(stage =>
      CompletionStageOps(
        f(stage.fast.map(_.asJava)(ExecutionContexts.parasitic).asJava)).asScala.fast.map(_.asScala)(
        ExecutionContexts.parasitic)) {
      inner.get.delegate
    }
  }

  def mapRouteResultWith(f: JFunction[RouteResult, CompletionStage[RouteResult]], inner: Supplier[Route]): Route =
    RouteAdapter {
      D.mapRouteResultWith(r =>
        CompletionStageOps(f(r.asJava)).asScala.fast.map(_.asScala)(ExecutionContexts.parasitic)) {
        inner.get.delegate
      }
    }

  def mapRouteResultWithPF(
      f: PartialFunction[RouteResult, CompletionStage[RouteResult]], inner: Supplier[Route]): Route = RouteAdapter {
    D.mapRouteResultWith(r =>
      CompletionStageOps(f(r.asJava)).asScala.fast.map(_.asScala)(ExecutionContexts.parasitic)) {
      inner.get.delegate
    }
  }

  /**
   * Runs the inner route with settings mapped by the given function.
   */
  def mapSettings(f: JFunction[RoutingSettings, RoutingSettings], inner: Supplier[Route]): Route = RouteAdapter {
    D.mapSettings(rs => f(rs.asJava).asScala) { inner.get.delegate }
  }

  /**
   * Always passes the request on to its inner route
   * (i.e. does nothing with the request or the response).
   */
  def pass(inner: Supplier[Route]): Route = RouteAdapter {
    D.pass { inner.get.delegate }
  }

  /**
   * Injects the given value into a directive.
   */
  def provide[T](t: T, inner: JFunction[T, Route]): Route = RouteAdapter {
    D.provide(t) { t => inner.apply(t).delegate }
  }

  /**
   * Adds a TransformationRejection cancelling all rejections equal to the given one
   * to the list of rejections potentially coming back from the inner route.
   */
  def cancelRejection(rejection: Rejection, inner: Supplier[Route]): Route = RouteAdapter {
    D.cancelRejection(rejection.asScala) { inner.get.delegate }
  }

  /**
   * Adds a TransformationRejection cancelling all rejections of one of the given classes
   * to the list of rejections potentially coming back from the inner route.
   */
  def cancelRejections(classes: JIterable[Class[_]], inner: Supplier[Route]): Route = RouteAdapter {
    D.cancelRejections(convertIterable[Class[_], Class[_]](classes): _*) { inner.get.delegate }
  }

  /**
   * Adds a TransformationRejection cancelling all rejections for which the given filter function returns true
   * to the list of rejections potentially coming back from the inner route.
   */
  def cancelRejections(filter: Predicate[Rejection], inner: Supplier[Route]): Route = RouteAdapter {
    D.cancelRejections(r => filter.test(r)) { inner.get.delegate }
  }

  def recoverRejections(f: JFunction[JIterable[Rejection], RouteResult], inner: Supplier[Route]): Route = RouteAdapter {
    D.recoverRejections(rs => f.apply(Util.javaArrayList(rs.map(_.asJava))).asScala) { inner.get.delegate }
  }

  def recoverRejectionsWith(
      f: JFunction[JIterable[Rejection], CompletionStage[RouteResult]], inner: Supplier[Route]): Route = RouteAdapter {
    D.recoverRejectionsWith(rs =>
      CompletionStageOps(f.apply(Util.javaArrayList(rs.map(_.asJava)))).asScala.fast.map(_.asScala)(
        ExecutionContexts.parasitic)) { inner.get.delegate }
  }

  /**
   * Transforms the unmatchedPath of the RequestContext using the given function.
   */
  def mapUnmatchedPath(f: JFunction[String, String], inner: Supplier[Route]): Route = RouteAdapter {
    D.mapUnmatchedPath(path => scaladsl.model.Uri.Path(f.apply(path.toString))) { inner.get.delegate }
  }

  /**
   * Extracts the yet unmatched path from the RequestContext.
   */
  def extractUnmatchedPath(inner: JFunction[String, Route]) = RouteAdapter {
    D.extractUnmatchedPath { path =>
      inner.apply(path.toString).delegate
    }
  }

  /**
   * Extracts the already matched path from the RequestContext.
   */
  def extractMatchedPath(inner: JFunction[String, Route]) = RouteAdapter {
    D.extractMatchedPath { path =>
      inner.apply(path.toString).delegate
    }
  }

  /**
   * Extracts the current [[HttpRequest]] instance.
   */
  def extractRequest(inner: JFunction[HttpRequest, Route]) = RouteAdapter {
    D.extractRequest { rq =>
      inner.apply(rq).delegate
    }
  }

  /**
   * Extracts the complete request URI.
   */
  def extractUri(inner: JFunction[Uri, Route]) = RouteAdapter {
    D.extractUri { uri =>
      inner.apply(JavaUri(uri)).delegate
    }
  }

  /**
   * Extracts the current http request entity.
   */
  @CorrespondsTo("extract")
  def extractEntity(inner: JFunction[RequestEntity, Route]): Route = RouteAdapter {
    D.extractRequest { rq =>
      inner.apply(rq.entity).delegate
    }
  }

  /**
   * Extracts the [[Materializer]] from the [[RequestContext]].
   */
  def extractMaterializer(inner: JFunction[Materializer, Route]): Route = RouteAdapter(
    D.extractMaterializer { m => inner.apply(m).delegate })

  /**
   * Extracts the [[pekko.actor.ActorSystem]] if the Materializer is available.
   * Otherwise throws an exception as it won't be able to extract the system from arbitrary materializers.
   */
  def extractActorSystem(inner: JFunction[ActorSystem, Route]): Route = RouteAdapter(
    D.extractActorSystem { system => inner.apply(system).delegate })

  /**
   * Extracts the [[ExecutionContextExecutor]] from the [[RequestContext]].
   */
  def extractExecutionContext(inner: JFunction[ExecutionContextExecutor, Route]): Route = RouteAdapter(
    D.extractExecutionContext { c => inner.apply(c).delegate })

  /**
   * Extracts a single value using the given function.
   */
  def extract[T](extract: JFunction[RequestContext, T], inner: JFunction[T, Route]): Route = RouteAdapter {
    D.extract(sc => extract.apply(JavaMapping.toJava(sc)(server.RoutingJavaMapping.RequestContext))) { c =>
      inner.apply(c).delegate
    }
  }

  /**
   * Runs its inner route with the given alternative [[LoggingAdapter]].
   */
  def withLog(log: LoggingAdapter, inner: Supplier[Route]): Route = RouteAdapter {
    D.withLog(log) { inner.get.delegate }
  }

  /**
   * Runs its inner route with the given alternative [[scala.concurrent.ExecutionContextExecutor]].
   */
  def withExecutionContext(ec: ExecutionContextExecutor, inner: Supplier[Route]): Route = RouteAdapter {
    D.withExecutionContext(ec) { inner.get.delegate }
  }

  /**
   * Runs its inner route with the given alternative [[pekko.stream.Materializer]].
   */
  def withMaterializer(mat: Materializer, inner: Supplier[Route]): Route = RouteAdapter {
    D.withMaterializer(mat) { inner.get.delegate }
  }

  /**
   * Runs its inner route with the given alternative [[RoutingSettings]].
   */
  def withSettings(s: RoutingSettings, inner: Supplier[Route]): Route = RouteAdapter {
    D.withSettings(s.asScala) { inner.get.delegate }
  }

  /**
   * Extracts the [[LoggingAdapter]]
   */
  def extractLog(inner: JFunction[LoggingAdapter, Route]): Route = RouteAdapter {
    D.extractLog { log => inner.apply(log).delegate }
  }

  /**
   * Extracts the [[pekko.http.javadsl.settings.ParserSettings]] from the [[pekko.http.javadsl.server.RequestContext]].
   */
  def extractParserSettings(inner: JFunction[ParserSettings, Route]) = RouteAdapter {
    D.extractParserSettings { settings =>
      inner.apply(settings).delegate
    }
  }

  /**
   * Extracts the [[RoutingSettings]] from the [[pekko.http.javadsl.server.RequestContext]].
   */
  def extractSettings(inner: JFunction[RoutingSettings, Route]) = RouteAdapter {
    D.extractSettings { settings =>
      inner.apply(settings).delegate
    }
  }

  /**
   * Extracts the [[pekko.http.javadsl.server.RequestContext]] itself.
   */
  def extractRequestContext(inner: JFunction[RequestContext, Route]) = RouteAdapter {
    D.extractRequestContext { ctx =>
      inner.apply(JavaMapping.toJava(ctx)(server.RoutingJavaMapping.RequestContext)).delegate
    }
  }

  /**
   * Extracts the entities `dataBytes` [[pekko.stream.javadsl.Source]] from the [[pekko.http.javadsl.server.RequestContext]].
   */
  def extractDataBytes(inner: JFunction[Source[ByteString, Any], Route]) = RouteAdapter {
    D.extractRequest { ctx => inner.apply(ctx.entity.dataBytes.asJava).delegate }
  }

  /**
   * Extracts the [[pekko.http.javadsl.model.RequestEntity]] from the [[pekko.http.javadsl.server.RequestContext]].
   */
  def extractRequestEntity(inner: JFunction[RequestEntity, Route]): Route = extractEntity(inner)

  /**
   * WARNING: This will read the entire request entity into memory and effectively disable streaming.
   *
   * To help protect against excessive memory use, the request will be aborted if the request is larger
   * than allowed by the `pekko.http.parsing.max-to-strict-bytes` configuration setting.
   *
   * Converts the HttpEntity from the [[pekko.http.javadsl.server.RequestContext]] into an
   * [[pekko.http.javadsl.model.HttpEntity.Strict]] and extracts it, or fails the route if unable to drain the
   * entire request body within the timeout.
   *
   * @param timeout The directive is failed if the stream isn't completed after the given timeout.
   * @deprecated As of 1.3.0, use the overloaded method taking a `java.time.Duration` instead.
   */
  @Deprecated
  @deprecated("use the overloaded method taking a `java.time.Duration` instead.", "1.3.0")
  def extractStrictEntity(timeout: FiniteDuration, inner: JFunction[HttpEntity.Strict, Route]): Route = RouteAdapter {
    D.extractStrictEntity(timeout) { strict => inner.apply(strict).delegate }
  }

  /**
   * WARNING: This will read the entire request entity into memory and effectively disable streaming.
   *
   * To help protect against excessive memory use, the request will be aborted if the request is larger
   * than allowed by the `pekko.http.parsing.max-to-strict-bytes` configuration setting.
   *
   * Converts the HttpEntity from the [[pekko.http.javadsl.server.RequestContext]] into an
   * [[pekko.http.javadsl.model.HttpEntity.Strict]] and extracts it, or fails the route if unable to drain the
   * entire request body within the timeout.
   *
   * @param timeout The directive is failed if the stream isn't completed after the given timeout.
   * @since 1.3.0
   */
  def extractStrictEntity(timeout: JDuration, inner: JFunction[HttpEntity.Strict, Route]): Route = RouteAdapter {
    D.extractStrictEntity(timeout.asScala) { strict => inner.apply(strict).delegate }
  }

  /**
   * WARNING: This will read the entire request entity into memory and effectively disable streaming.
   *
   * To help protect against excessive memory use, the request will be aborted if the request is larger
   * than allowed by the `pekko.http.parsing.max-to-strict-bytes` configuration setting.
   *
   * Converts the HttpEntity from the [[pekko.http.javadsl.server.RequestContext]] into an
   * [[pekko.http.javadsl.model.HttpEntity.Strict]] and extracts it, or fails the route if unable to drain the
   * entire request body within the timeout.
   *
   * @param timeout The directive is failed if the stream isn't completed after the given timeout.
   * @deprecated As of 1.3.0, use the overloaded method taking a `java.time.Duration` instead.
   */
  @Deprecated
  @deprecated("use the overloaded method taking a `java.time.Duration` instead.", "1.3.0")
  def extractStrictEntity(timeout: FiniteDuration, maxBytes: Long, inner: JFunction[HttpEntity.Strict, Route]): Route =
    RouteAdapter {
      D.extractStrictEntity(timeout, maxBytes) { strict => inner.apply(strict).delegate }
    }

  /**
   * WARNING: This will read the entire request entity into memory and effectively disable streaming.
   *
   * To help protect against excessive memory use, the request will be aborted if the request is larger
   * than allowed by the `pekko.http.parsing.max-to-strict-bytes` configuration setting.
   *
   * Converts the HttpEntity from the [[pekko.http.javadsl.server.RequestContext]] into an
   * [[pekko.http.javadsl.model.HttpEntity.Strict]] and extracts it, or fails the route if unable to drain the
   * entire request body within the timeout.
   *
   * @param timeout The directive is failed if the stream isn't completed after the given timeout.
   * @since 1.3.0
   */
  def extractStrictEntity(timeout: JDuration, maxBytes: Long, inner: JFunction[HttpEntity.Strict, Route]): Route =
    RouteAdapter {
      D.extractStrictEntity(timeout.asScala, maxBytes) { strict => inner.apply(strict).delegate }
    }

  /**
   * WARNING: This will read the entire request entity into memory and effectively disable streaming.
   *
   * To help protect against excessive memory use, the request will be aborted if the request is larger
   * than allowed by the `pekko.http.parsing.max-to-strict-bytes` configuration setting.
   *
   * Extracts the [[pekko.http.javadsl.server.RequestContext]] itself with the strict HTTP entity,
   * or fails the route if unable to drain the entire request body within the timeout.
   *
   * @param timeout The directive is failed if the stream isn't completed after the given timeout.
   * @deprecated As of 1.3.0, use the overloaded method taking a `java.time.Duration` instead.
   */
  @Deprecated
  @deprecated("use the overloaded method taking a `java.time.Duration` instead.", "1.3.0")
  def toStrictEntity(timeout: FiniteDuration, inner: Supplier[Route]): Route = RouteAdapter {
    D.toStrictEntity(timeout) { inner.get.delegate }
  }

  /**
   * WARNING: This will read the entire request entity into memory and effectively disable streaming.
   *
   * To help protect against excessive memory use, the request will be aborted if the request is larger
   * than allowed by the `pekko.http.parsing.max-to-strict-bytes` configuration setting.
   *
   * Extracts the [[pekko.http.javadsl.server.RequestContext]] itself with the strict HTTP entity,
   * or fails the route if unable to drain the entire request body within the timeout.
   *
   * @param timeout The directive is failed if the stream isn't completed after the given timeout.
   * @since 1.3.0
   */
  def toStrictEntity(timeout: JDuration, inner: Supplier[Route]): Route = RouteAdapter {
    D.toStrictEntity(timeout.asScala) { inner.get.delegate }
  }

  /**
   * WARNING: This will read the entire request entity into memory and effectively disable streaming.
   *
   * To help protect against excessive memory use, the request will be aborted if the request is larger
   * than allowed by the `pekko.http.parsing.max-to-strict-bytes` configuration setting.
   *
   * Extracts the [[pekko.http.javadsl.server.RequestContext]] itself with the strict HTTP entity,
   * or fails the route if unable to drain the entire request body within the timeout.
   *
   * @param timeout The directive is failed if the stream isn't completed after the given timeout.
   * @deprecated As of 1.3.0, use the overloaded method taking a `java.time.Duration` instead.
   */
  @Deprecated
  @deprecated("use the overloaded method taking a `java.time.Duration` instead.", "1.3.0")
  def toStrictEntity(timeout: FiniteDuration, maxBytes: Long, inner: Supplier[Route]): Route = RouteAdapter {
    D.toStrictEntity(timeout, maxBytes) { inner.get.delegate }
  }

  /**
   * WARNING: This will read the entire request entity into memory and effectively disable streaming.
   *
   * To help protect against excessive memory use, the request will be aborted if the request is larger
   * than allowed by the `pekko.http.parsing.max-to-strict-bytes` configuration setting.
   *
   * Extracts the [[pekko.http.javadsl.server.RequestContext]] itself with the strict HTTP entity,
   * or fails the route if unable to drain the entire request body within the timeout.
   *
   * @param timeout The directive is failed if the stream isn't completed after the given timeout.
   * @since 1.3.0
   */
  def toStrictEntity(timeout: JDuration, maxBytes: Long, inner: Supplier[Route]): Route = RouteAdapter {
    D.toStrictEntity(timeout.asScala, maxBytes) { inner.get.delegate }
  }
}
