/*
 * Copyright (C) 2009-2015 Typesafe Inc. <http://www.typesafe.com>
 */
package play.api.libs.ws

import java.net.URI

import scala.concurrent.{ Future, ExecutionContext }

import java.io.File

import play.api.http.Writeable
import play.api.libs.iteratee._

import play.api._
import scala.xml.Elem
import play.api.libs.json.JsValue

/**
 * The WSClient holds the configuration information needed to build a request, and provides a way to get a request holder.
 */
trait WSClient {

  /**
   * The underlying implementation of the client, if any.  You must cast explicitly to the type you want.
   * @tparam T the type you are expecting (i.e. isInstanceOf)
   * @return the backing class.
   */
  def underlying[T]: T

  /**
   * Generates a request holder which can be used to build requests.
   *
   * @param url The base URL to make HTTP requests to.
   * @return a WSRequestHolder
   */
  def url(url: String): WSRequest

  /** Closes this client, and releases underlying resources. */
  def close(): Unit
}

/**
 * WSRequestMagnet magnet.  Please see the companion object for implicit definitions.
 *
 * @see <a href="http://spray.io/blog/2012-12-13-the-magnet-pattern/">The magnet pattern</a>
 */
trait WSRequestMagnet {
  def apply(): WSRequest
}

/**
 * Asynchronous API to to query web services, as an http client.
 *
 * Usage example:
 * {{{
 * WS.url("http://example.com/feed").get()
 * WS.url("http://example.com/item").post("content")
 * }}}
 *
 * When greater flexibility is needed, you can also create clients explicitly and pass them into WS:
 *
 * {{{
 * implicit val client = new NingWSClient(builder.build())
 * WS.url("http://example.com/feed").get()
 * }}}
 *
 * Or call the client directly:
 *
 * {{{
 * import com.typesafe.config.ConfigFactory
 * import play.api.libs.ws._
 * import play.api.libs.ws.ning._
 *
 * val configuration = play.api.Configuration(ConfigFactory.parseString(
 * """
 *   |ws.ssl.trustManager = ...
 * """.stripMargin))
 * val parser = new DefaultWSConfigParser(configuration, Play.current.classloader)
 * val builder = new NingAsyncHttpClientConfigBuilder(parser.parse())
 * val secureClient: WSClient = new NingWSClient(builder.build())
 * val response = secureClient.url("https://secure.com").get()
 * }}}
 *
 * Note that the resolution of URL is done through the magnet pattern defined in
 * `WSRequestMagnet`.
 *
 * The value returned is a {@code Future[WSResponse]}, and you should use Play's asynchronous mechanisms to
 * use this response.
 */
object WS {

  private val wsapiCache = Application.instanceCache[WSAPI]
  protected[play] def wsapi(implicit app: Application): WSAPI = wsapiCache(app)

  import scala.language.implicitConversions

  /**
   * Retrieves or creates underlying HTTP client.  Note that due to the Plugin architecture, an
   * implicit application must be in scope.  Most of the time you will want the current app:
   *
   * {{{
   * import play.api.Play.current
   * val client = WS.client
   * }}}
   */
  def client(implicit app: Application): WSClient = wsapi.client

  /**
   * Prepares a new request using an implicit application.  This creates a default client, which you can then
   * use to construct a request.
   *
   * {{{
   *   import play.api.Play.current
   *   WS.url("http://localhost/").get()
   * }}}
   *
   * @param url the URL to request
   * @param app the implicit application to use.
   */
  def url(url: String)(implicit app: Application): play.api.libs.ws.WSRequest = wsapi(app).url(url)

  /**
   * Prepares a new request using a provided magnet.  This method gives you the ability to create your own
   * URL implementations at the cost of some complexity.
   *
   * {{{
   * object PairMagnet {
   *   implicit def fromPair(pair: Pair[WSClient, java.net.URL]) =
   *     new WSRequestHolderMagnet {
   *       def apply(): WSRequestHolder = {
   *         val (client, netUrl) = pair
   *         client.url(netUrl.toString)
   *       }
   *    }
   * }
   * import scala.language.implicitConversions
   * val client = WS.client
   * val exampleURL = new java.net.URL("http://example.com")
   * WS.url(client -> exampleURL).get()
   * }}}
   *
   * @param magnet a magnet pattern.
   * @see <a href="http://spray.io/blog/2012-12-13-the-magnet-pattern/">The magnet pattern</a>
   */
  def url(magnet: WSRequestMagnet): play.api.libs.ws.WSRequest = magnet()

  /**
   * Prepares a new request using an implicit client.  The client must be in scope and configured, i.e.
   *
   * {{{
   * implicit val sslClient = new play.api.libs.ws.ning.NingWSClient(sslBuilder.build())
   * WS.clientUrl("http://example.com/feed")
   * }}}
   *
   * @param url the URL to request
   * @param client the client to use to make the request.
   */
  def clientUrl(url: String)(implicit client: WSClient): play.api.libs.ws.WSRequest = client.url(url)
}

/**
 * The base WS API trait.  Plugins should extend this.
 */
trait WSAPI {

  def client: WSClient

  def url(url: String): WSRequest
}

/**
 *
 */
trait WSResponse {

  /**
   * Return the current headers of the request being constructed
   */
  def allHeaders: Map[String, Seq[String]]

  /**
   * Get the underlying response object.
   */
  def underlying[T]: T

  /**
   * The response status code.
   */
  def status: Int

  /**
   * The response status message.
   */
  def statusText: String

  /**
   * Get a response header.
   */
  def header(key: String): Option[String]

  /**
   * Get all the cookies.
   */
  def cookies: Seq[WSCookie]

  /**
   * Get only one cookie, using the cookie name.
   */
  def cookie(name: String): Option[WSCookie]

  /**
   * The response body as String.
   */
  def body: String

  /**
   * The response body as Xml.
   */
  def xml: Elem

  /**
   * The response body as Json.
   */
  def json: JsValue

  /**
   * The response body as a byte array.
   */
  def bodyAsBytes: Array[Byte]
}

/**
 * A body for the request
 */
sealed trait WSBody

/**
 * An in memory body
 *
 * @param bytes The bytes of the body
 */
case class InMemoryBody(bytes: Array[Byte]) extends WSBody

/**
 * A streamed body
 *
 * @param bytes An enumerator of the bytes of the body
 */
case class StreamedBody(bytes: Enumerator[Array[Byte]]) extends WSBody {
  throw new NotImplementedError("A streaming request body is not yet implemented")
}

/**
 * A file body
 */
case class FileBody(file: File) extends WSBody

/**
 * An empty body
 */
case object EmptyBody extends WSBody

/**
 * A WS Request builder.
 */
trait WSRequest {

  /**
   * The base URL for this request
   */
  val url: String

  /**
   * The URI for this request
   */
  lazy val uri: URI = {
    val enc = (p: String) => java.net.URLEncoder.encode(p, "utf-8")
    new java.net.URI(if (queryString.isEmpty) url else {
      val qs = (for {
        (n, vs) <- queryString
        v <- vs
      } yield s"${enc(n)}=${enc(v)}").mkString("&")
      s"$url?$qs"
    })
  }

  /**
   * The method for this request
   */
  val method: String

  /**
   * The body of this request
   */
  val body: WSBody

  /**
   * The headers for this request
   */
  val headers: Map[String, Seq[String]]

  /**
   * The query string for this request
   */
  val queryString: Map[String, Seq[String]]

  /**
   * A calculator of the signature for this request
   */
  val calc: Option[WSSignatureCalculator]

  /**
   * The authentication this request should use
   */
  val auth: Option[(String, String, WSAuthScheme)]

  /**
   * Whether this request should follow redirects
   */
  val followRedirects: Option[Boolean]

  /**
   * The timeout for the request
   */
  val requestTimeout: Option[Int]

  /**
   * The virtual host this request will use
   */
  val virtualHost: Option[String]

  /**
   * The proxy server this request will use
   */
  val proxyServer: Option[WSProxyServer]

  /**
   * sets the signature calculator for the request
   * @param calc
   */
  def sign(calc: WSSignatureCalculator): WSRequest

  /**
   * sets the authentication realm
   */
  def withAuth(username: String, password: String, scheme: WSAuthScheme): WSRequest

  /**
   * adds any number of HTTP headers
   * @param hdrs
   */
  def withHeaders(hdrs: (String, String)*): WSRequest

  /**
   * adds any number of query string parameters to the
   */
  def withQueryString(parameters: (String, String)*): WSRequest

  /**
   * Sets whether redirects (301, 302) should be followed automatically
   */
  def withFollowRedirects(follow: Boolean): WSRequest

  /**
   * Sets the maximum time in milliseconds you expect the request to take.
   * Warning: a stream consumption will be interrupted when this time is reached.
   */
  def withRequestTimeout(timeout: Long): WSRequest

  /**
   * Sets the virtual host to use in this request
   */
  def withVirtualHost(vh: String): WSRequest

  /**
   * Sets the proxy server to use in this request
   */
  def withProxyServer(proxyServer: WSProxyServer): WSRequest

  /**
   * Sets the body for this request
   */
  def withBody(body: WSBody): WSRequest

  /**
   * Sets the body for this request
   */
  def withBody[T](body: T)(implicit wrt: Writeable[T]): WSRequest = {
    val wsBody = InMemoryBody(wrt.transform(body))
    if (headers.contains("Content-Type")) {
      withBody(wsBody)
    } else {
      wrt.contentType.fold(withBody(wsBody)) { contentType =>
        withBody(wsBody).withHeaders("Content-Type" -> contentType)
      }
    }
  }

  /**
   * Sets the method for this request
   */
  def withMethod(method: String): WSRequest

  /**
   * performs a get
   */
  def get() = withMethod("GET").execute()

  /**
   * performs a get
   * @param consumer that's handling the response
   */
  def get[A](consumer: WSResponseHeaders => Iteratee[Array[Byte], A])(implicit ec: ExecutionContext): Future[Iteratee[Array[Byte], A]] = {
    getStream().flatMap {
      case (response, enumerator) =>
        enumerator(consumer(response))
    }
  }

  /**
   * performs a get
   */
  def getStream(): Future[(WSResponseHeaders, Enumerator[Array[Byte]])] = {
    withMethod("GET").stream()
  }

  /**
   * Perform a PATCH on the request asynchronously.
   */
  def patch[T](body: T)(implicit wrt: Writeable[T]) =
    withMethod("PATCH").withBody(body).execute()

  /**
   * Perform a PATCH on the request asynchronously.
   * Request body won't be chunked
   */
  def patch(body: File) = withMethod("PATCH").withBody(FileBody(body)).execute()

  /**
   * performs a POST with supplied body
   * @param consumer that's handling the response
   */
  def patchAndRetrieveStream[A, T](body: T)(consumer: WSResponseHeaders => Iteratee[Array[Byte], A])(implicit wrt: Writeable[T], ec: ExecutionContext): Future[Iteratee[Array[Byte], A]] = {
    withMethod("PATCH").withBody(body).stream().flatMap {
      case (response, enumerator) =>
        enumerator(consumer(response))
    }
  }

  /**
   * Perform a POST on the request asynchronously.
   */
  def post[T](body: T)(implicit wrt: Writeable[T]) =
    withMethod("POST").withBody(body).execute()

  /**
   * Perform a POST on the request asynchronously.
   * Request body won't be chunked
   */
  def post(body: File) = withMethod("POST").withBody(FileBody(body)).execute()

  /**
   * performs a POST with supplied body
   * @param consumer that's handling the response
   */
  def postAndRetrieveStream[A, T](body: T)(consumer: WSResponseHeaders => Iteratee[Array[Byte], A])(implicit wrt: Writeable[T], ec: ExecutionContext): Future[Iteratee[Array[Byte], A]] = {
    withMethod("POST").withBody(body).stream().flatMap {
      case (response, enumerator) =>
        enumerator(consumer(response))
    }
  }

  /**
   * Perform a PUT on the request asynchronously.
   */
  def put[T](body: T)(implicit wrt: Writeable[T]) =
    withMethod("PUT").withBody(body).execute()

  /**
   * Perform a PUT on the request asynchronously.
   * Request body won't be chunked
   */
  def put(body: File) = withMethod("PUT").withBody(FileBody(body)).execute()

  /**
   * performs a PUT with supplied body
   * @param consumer that's handling the response
   */
  def putAndRetrieveStream[A, T](body: T)(consumer: WSResponseHeaders => Iteratee[Array[Byte], A])(implicit wrt: Writeable[T], ec: ExecutionContext): Future[Iteratee[Array[Byte], A]] = {
    withMethod("PUT").withBody(body).stream().flatMap {
      case (response, enumerator) =>
        enumerator(consumer(response))
    }
  }

  /**
   * Perform a DELETE on the request asynchronously.
   */
  def delete() = withMethod("DELETE").execute()

  /**
   * Perform a HEAD on the request asynchronously.
   */
  def head() = withMethod("HEAD").execute()

  /**
   * Perform a OPTIONS on the request asynchronously.
   */
  def options() = withMethod("OPTIONS").execute()

  def execute(method: String): Future[WSResponse] = withMethod(method).execute()

  /**
   * Execute this request
   */
  def execute(): Future[WSResponse]

  /**
   * Execute this request and stream the response body in an enumerator
   */
  def stream(): Future[(WSResponseHeaders, Enumerator[Array[Byte]])]
}

/**
 *
 */
trait WSAuthScheme {
  // Purposely not sealed in case clients want to add their own auth schemes.
}

object WSAuthScheme {

  case object DIGEST extends WSAuthScheme

  case object BASIC extends WSAuthScheme

  case object NTLM extends WSAuthScheme

  case object SPNEGO extends WSAuthScheme

  case object KERBEROS extends WSAuthScheme

  case object NONE extends WSAuthScheme

}

/**
 * A WS Cookie.  This is a trait so that we are not tied to a specific client.
 */
trait WSCookie {

  /**
   * The underlying "native" cookie object for the client.
   */
  def underlying[T]: T

  /**
   * The domain.
   */
  def domain: String

  /**
   * The cookie name.
   */
  def name: Option[String]

  /**
   * The cookie value.
   */
  def value: Option[String]

  /**
   * The path.
   */
  def path: String

  /**
   * The expiry date.
   */
  def expires: Option[Long]

  /**
   * The maximum age.
   */
  def maxAge: Option[Int]

  /**
   * If the cookie is secure.
   */
  def secure: Boolean
}

/**
 * A WS proxy.
 */
trait WSProxyServer {
  /** The hostname of the proxy server. */
  def host: String

  /** The port of the proxy server. */
  def port: Int

  /** The protocol of the proxy server.  Use "http" or "https".  Defaults to "http" if not specified. */
  def protocol: Option[String]

  /** The principal (aka username) of the credentials for the proxy server. */
  def principal: Option[String]

  /** The password for the credentials for the proxy server. */
  def password: Option[String]

  def ntlmDomain: Option[String]

  def encoding: Option[String]

  def nonProxyHosts: Option[Seq[String]]
}

/**
 * A WS proxy.
 */
case class DefaultWSProxyServer(
  /** The hostname of the proxy server. */
  host: String,

  /** The port of the proxy server. */
  port: Int,

  /** The protocol of the proxy server.  Use "http" or "https".  Defaults to "http" if not specified. */
  protocol: Option[String] = None,

  /** The principal (aka username) of the credentials for the proxy server. */
  principal: Option[String] = None,

  /** The password for the credentials for the proxy server. */
  password: Option[String] = None,

  ntlmDomain: Option[String] = None,

  encoding: Option[String] = None,

  nonProxyHosts: Option[Seq[String]] = None) extends WSProxyServer

/**
 * An HTTP response header (the body has not been retrieved yet)
 */
trait WSResponseHeaders {

  def status: Int

  def headers: Map[String, Seq[String]]
}

case class DefaultWSResponseHeaders(status: Int, headers: Map[String, Seq[String]]) extends WSResponseHeaders

/**
 * Sign a WS call with OAuth.
 */
trait WSSignatureCalculator