package httpz

import java.io.{ByteArrayOutputStream, InputStream}

import scalaz._, Free.FreeC
import argonaut._

object Core extends Core[RequestF] {
  /**
   * @see [[https://dl.dropboxusercontent.com/u/4588997/ReasonablyPriced.pdf]]
   * @see [[https://gist.github.com/runarorama/a8fab38e473fafa0921d]]
   */
  implicit def instance[F[_]](implicit I: Inject[RequestF, F]) =
    new Core[F]

  def inputStream2bytes(in: InputStream): Array[Byte] = {
    val buf = new ByteArrayOutputStream()
    val data = new Array[Byte](4096)
    @annotation.tailrec
    def loop(): Unit = {
      in.read(data, 0, data.length) match {
        case n if n > 0 =>
          buf.write(data, 0, n)
          loop()
        case _ =>
      }
    }
    loop()
    buf.toByteArray
  }
}

sealed class Core[F[_]](implicit I: Inject[RequestF, F]) {

  private[this] implicit val f = Free.freeMonad[({type l[a] = Coyoneda[F, a]})#l]

  private[this] def lift[A, B](f: RequestF[A \/ B]) =
    EitherT[({type l[a] = FreeC[F, a]})#l, A, B](Free.liftFC(I.inj(f)))

  def json[A](req: Request)(implicit A: DecodeJson[A]): EitherT[({type l[a] = FreeC[F, a]})#l, Error, A] =
    jsonResponse[A](req).map(_.body)

  def jsonResponse[A](req: Request)(implicit A: DecodeJson[A]): EitherT[({type l[a] = FreeC[F, a]})#l, Error, Response[A]] =
    lift(RequestF.one[Error \/ Response[A], Error \/ Response[Json]](
      req,
      \/.left,
      (request, response) => {
        val str = response.bodyUTF8
        Parse.parse(str) match {
          case \/-(json) =>
            \/-(response.copy(body = json))
          case -\/(e) =>
            -\/(Error.parse(response, e))
        }
      },
      (request, either) => either.flatMap{ json =>
        A.decodeJson(json.body).result match {
          case \/-(r) => \/-(json.copy(body = r))
          case -\/((msg, history)) => -\/(Error.decode(request, msg, history, json.body))
        }
      }
    ))


  def raw(req: Request): EitherT[({type l[a] = FreeC[F, a]})#l, Throwable, Response[ByteArray]] =
    lift(RequestF.one[Throwable \/ Response[ByteArray], Response[ByteArray]](
      req,
      \/.left,
      (_, response) => response,
      (_, result) => \/-(result)
    ))

  def bytes(req: Request): EitherT[({type l[a] = FreeC[F, a]})#l, Throwable, ByteArray] =
    raw(req).map(_.body)

  def string(req: Request): EitherT[({type l[a] = FreeC[F, a]})#l, Throwable, String] =
    stringResponse(req).map(_.body)

  def stringResponse(req: Request): EitherT[({type l[a] = FreeC[F, a]})#l, Throwable, Response[String]] =
    lift(RequestF.one[Throwable \/ Response[String], Response[String]](
      req,
      \/.left,
      (_, response) => response.asUTF8StringBody,
      (_, result) => \/-(result)
    ))
}