package scalaz

sealed trait IndexedContsT[W[+_], M[+_], +R, -O, +A] {
  def run(wamo: W[A => M[O]]): M[R]

  def apply(wamo: W[A => M[O]]): M[R] = run(wamo)

  def run_(implicit W: Applicative[W], M: Applicative[M], ev: A <:< O): M[R] = run(W.point(x => M.point(x)))

  def map[B](f: A => B)(implicit W: Functor[W]): IndexedContsT[W, M, R, O, B] = IndexedContsT { wbmo =>
    run(W.map(wbmo)(f andThen _))
  }

  def flatten[E, B](implicit ev: A <:< IndexedContsT[W, M, O, E, B], W: Cobind[W]): IndexedContsT[W, M, R, E, B] = flatMap(ev)

  def flatMap[E, B](f: A => IndexedContsT[W, M, O, E, B])(implicit W: Cobind[W]): IndexedContsT[W, M, R, E, B] = IndexedContsT { wbme =>
    run(W.cobind(wbme) { wk => { a =>
      f(a).run(wk)
    } })
  }
  
  def contramap[I](f: I => O)(implicit M: Functor[M], W: Functor[W]): IndexedContsT[W, M, R, I, A] = IndexedContsT { wami =>
    run(W.map(wami) { ami => { a => M.map(ami(a))(f) } })
  }

  def imap[E](f: R => E)(implicit M: Functor[M]): IndexedContsT[W, M, E, O, A] = IndexedContsT { wamo =>
    M.map(run(wamo))(f)
  }

  def bimap[E, B](f: R => E, g: A => B)(implicit M: Functor[M], W: Functor[W]): IndexedContsT[W, M, E, O, B] = IndexedContsT { wbmo =>
    M.map(run(W.map(wbmo)(g andThen _)))(f)
  }

  def xmap[E, I](f: R => E, g: I => O)(implicit M: Functor[M], W: Functor[W]): IndexedContsT[W, M, E, I, A] = IndexedContsT { wami =>
    M.map(run(W.map(wami) { ami => { a => M.map(ami(a))(g) } }))(f)
  }

  import BijectionT._

  def bmap[X >: R <: O, Z](f: Bijection[X, Z])(implicit M: Functor[M], W: Functor[W]): ContsT[W, M, Z, A] = IndexedContsT { wami =>
    M.map(run(W.map(wami) { ami => { a => M.map(ami(a))(f from _) } }))(f to _)
  }
}

object IndexedContsT extends IndexedContsTFunctions with IndexedContsTInstances {
  def apply[W[+_], M[+_], R, O, A](f: W[A => M[O]] => M[R]): IndexedContsT[W, M, R, O, A] = new IndexedContsT[W, M, R, O, A] {
    def run(wamo: W[A => M[O]]): M[R] = f(wamo)
  }
}

trait IndexedContsTFunctions {
  def point[W[+_], M[+_], R, A](a: => A)(implicit W: Comonad[W]): ContsT[W, M, R, A] = ContsT { k => W.copoint(k)(a) }

  def liftM[W[+_], M[+_], R, O, A](a: => M[A])(implicit W: Comonad[W], M: Bind[M]): ContsT[W, M, R, A] = ContsT { k => M.bind(a)(W.copoint(k)) }

  def shift[W[+_], M[+_], I, R, J, O, A](f: (A => IndexedContsT[W, M, I, I, O]) => IndexedContsT[W, M, R, J, J])(implicit W: Comonad[W], WA: Applicative[W], M: Monad[M]): IndexedContsT[W, M, R, O, A] =
    IndexedContsT { k0 =>
      (f { a =>
        IndexedContsT { k1 =>
          M.bind(W.copoint(k0)(a))(W.copoint(k1))
        }
      }).run_
    }

  def reset[W[+_], M[+_], R, O, A](v: IndexedContsT[W, M, A, O, O])(implicit W: Comonad[W], WA: Applicative[W], M: Monad[M]): IndexedContsT[W, M, R, R, A] =
    IndexedContsT { k =>
      M.bind(v.run_)(W.copoint(k))
    }

  def callCC[W[+_], M[+_], R, O, A](f: (A => IndexedContsT[W, M, Any, O, Nothing]) => IndexedContsT[W, M, R, O, A])(implicit W: Comonad[W]): IndexedContsT[W, M, R, O, A] =
    IndexedContsT { k =>
      (f { a =>
        IndexedContsT { u =>
          W.copoint(k)(a)
        }
      }).run(k)
    }
}

trait IndexedContsTInstances0 {
  implicit def IndexedContsTFunctorRight[W[+_], M[+_], R, O](implicit W0: Functor[W]): Functor[({type f[+a]=IndexedContsT[W, M, R, O, a]})#f] = new IndexedContsTFunctorRight[W, M, R, O] {
    implicit val W: Functor[W] = W0
  }
}

trait IndexedContsTInstances extends IndexedContsTInstances0 {
  implicit def IndexedContsTFunctorLeft[W[+_], M[+_], O, A](implicit M0: Functor[M]): Functor[({type f[+r]=IndexedContsT[W, M, r, O, A]})#f] = new IndexedContsTFunctorLeft[W, M, O, A] {
    implicit val M: Functor[M] = M0
  }

  implicit def IndexedContsTContravariant[W[+_], M[+_], R, A](implicit W0: Functor[W], M0: Functor[M]): Contravariant[({type f[-o]=IndexedContsT[W, M, R, o, A]})#f] = new IndexedContsTContravariant[W, M, R, A] {
    implicit val W: Functor[W] = W0
    implicit val M: Functor[M] = M0
  }

  implicit def IndexedContsTBifunctor[W[+_], M[+_], O](implicit W0: Functor[W], M0: Functor[M]): Bifunctor[({type f[+r, +a]=IndexedContsT[W, M, r, O, a]})#f] = new IndexedContsTBifunctor[W, M, O] {
    implicit val W: Functor[W] = W0
    implicit val M: Functor[M] = M0
  }

  implicit def ContsTMonad[W[+_], M[+_], R](implicit W0: Comonad[W]): Monad[({type f[+a]=ContsT[W, M, R, a]})#f] = new ContsTMonad[W, M, R] {
    implicit val W: Comonad[W] = W0
  }
}

private[scalaz] sealed trait IndexedContsTFunctorLeft[W[+_], M[+_], O, A0] extends Functor[({type f[+r]=IndexedContsT[W, M, r, O, A0]})#f] {
  implicit val M: Functor[M]

  def map[A, B](fa: IndexedContsT[W, M, A, O, A0])(f: A => B): IndexedContsT[W, M, B, O, A0] = fa.imap(f)
}

private[scalaz] sealed trait IndexedContsTFunctorRight[W[+_], M[+_], R, O] extends Functor[({type f[+a]=IndexedContsT[W, M, R, O, a]})#f] {
  implicit val W: Functor[W]

  override def map[A, B](fa: IndexedContsT[W, M, R, O, A])(f: A => B): IndexedContsT[W, M, R, O, B] = fa.map(f)
}

private[scalaz] sealed trait IndexedContsTContravariant[W[+_], M[+_], R, A0] extends Contravariant[({type f[-o]=IndexedContsT[W, M, R, o, A0]})#f] {
  implicit val W: Functor[W]
  implicit val M: Functor[M]

  def contramap[A, B](fa: IndexedContsT[W, M, R, A, A0])(f: B => A): IndexedContsT[W, M, R, B, A0] = fa.contramap(f)
}

private[scalaz] sealed trait IndexedContsTBifunctor[W[+_], M[+_], O] extends Bifunctor[({type f[+r, +a]=IndexedContsT[W, M, r, O, a]})#f] {
  implicit val W: Functor[W]
  implicit val M: Functor[M]

  def bimap[A, B, C, D](fab: IndexedContsT[W, M, A, O, B])(f: A => C, g: B => D): IndexedContsT[W, M, C, O, D] = fab.bimap(f, g)

  override def leftFunctor[X]: Functor[({type f[+r]=IndexedContsT[W, M, r, O, X]})#f] = IndexedContsT.IndexedContsTFunctorLeft

  override def leftMap[A, B, C](fa: IndexedContsT[W, M, A, O, B])(f: A => C): IndexedContsT[W, M, C, O, B] = fa.imap(f)

  override def rightFunctor[X]: Functor[({type f[+a]=IndexedContsT[W, M, X, O, a]})#f] = IndexedContsT.IndexedContsTFunctorRight

  override def rightMap[A, B, D](fa: IndexedContsT[W, M, A, O, B])(f: B => D): IndexedContsT[W, M, A, O, D] = fa.map(f)
}

private[scalaz] sealed trait ContsTMonad[W[+_], M[+_], R] extends Monad[({type f[+a]=ContsT[W, M, R, a]})#f] with IndexedContsTFunctorRight[W, M, R, R] {
  implicit val W: Comonad[W]

  def point[A](a: => A): ContsT[W, M, R, A] = IndexedContsT.point(a)

  def bind[A, B](fa: ContsT[W, M, R, A])(f: A => ContsT[W, M, R, B]): ContsT[W, M, R, B] = fa.flatMap(f)

  override def join[A](ffa: ContsT[W, M, R, ContsT[W, M, R, A]]): ContsT[W, M, R, A] = ffa.flatten
}