package io.github.hamsters

import scala.util.{Left, Right}

@ValidationMacro
object Validation {

  val Valid = Right
  val Invalid = Left

  /**
    * Return Either from code block
    * @param body
    * @tparam R
    * @return Either from code block
    */
  def fromCatchable[R](body: => R): Either[String, R] = {
    val throwableToErrorMessage = (t: Throwable) => Option(t.getMessage).getOrElse("Error: an exception has been thrown")
    fromCatchable(body, throwableToErrorMessage)
  }

  /**
    * Return Either from code block with custom error management
    * @param body
    * @param errorMgt : error management function
    * @tparam L
    * @tparam R
    * @return Either from code block
    */
  def fromCatchable[L, R](body: => R, errorMgt: Throwable => L): Either[L, R] =
    try {
      Right(body)
    } catch {
      case t: Throwable => Left(errorMgt(t))
    }

  /** Return successes (right) for several Either values
    * Validation.run should be used instead in most cases as it is a type-safer method (it does not return a List[Any])
    * @param eithers
    * @return successes
    */
  def successes(eithers : Either[_, _]*) : List[Any] = eithers.toList.collect { case r : Right[_, _] => r.right.get }

  /**
    * Tells if eithers contain successes (right)
    * @param eithers
    * @tparam R
    * @return boolean
    */
  def hasSuccesses[R](eithers: Either[_, R]*): Boolean = successes(eithers: _*).nonEmpty

  /**
    * Run the validation
    * This validation accumulates all errors
    * Arity 2 to 22 methods are available and generated from macro
    * @param e1 : first either
    * @param e2 : second either
    * @tparam L
    * @tparam R1
    * @tparam R2
    * @return a tuple (R1, R2) of results in a Right if validation is successful or a list of errors L if it fails
    */
  def run[L, R1, R2](e1: Either[L, R1], e2: Either[L, R2]): Either[List[L], (R1, R2)] =
    failures(e1, e2) match {
      case Nil => Right(e1.get, e2.get)
      case f: List[L] => Left(f)
    }

  /**
    * Run the validation
    * This validation accumulates all errors
    * Arity 2 to 22 methods are available and generated from macro
    * @param e1 : first either
    * @param e2 : second either
    * @param e3 : third either
    * @tparam L
    * @tparam R1
    * @tparam R2
    * @tparam R3
    * @return a tuple (R1, R2, R3) of results in a Right if validation is successful or a list of errors L if it fails
    */
  def run[L, R1, R2, R3](e1: Either[L, R1], e2: Either[L, R2], e3: Either[L, R3]): Either[List[L], (R1, R2, R3)] =
    failures(e1, e2, e3) match {
      case Nil => Right(e1.get, e2.get, e3.get)
      case f: List[L] => Left(f)
    }

  /**
    * Retrieve failures (left) for several Either values
    * @param eithers
    * @tparam L
    * @return failures
    */
  def failures[L](eithers: Either[L, _]*): List[L] = eithers.toList.collect { case l: Left[L, _] => l.left.get }

  /**
    * Tells if eithers contain failures (left)
    * @param eithers
    * @tparam L
    * @return boolean
    */
  def hasFailures[L](eithers: Either[L, _]*): Boolean = failures(eithers: _*).nonEmpty

  implicit class RightBiasedEither[L, R](e: Either[L, R]) {
    def map[R2](f: R => R2): Either[L, R2] = e.right.map(f)

    def flatMap[R2](f: R => Either[L, R2]): Either[L, R2] = e.right.flatMap(f)

    def filter(p: R => Boolean): Option[Either[Nothing, R]] = filterWith(p)

    def filterWith(p: R => Boolean): Option[Either[Nothing, R]] = e.right.filter(p)

    def get: R = e.right.get

    def getOrElse[R2 >: R](or: => R2): R2 = e.right.getOrElse(or)
  }

}
