package pl.edu.pw.mini.sg.json

import argonaut.{ DecodeJson, DecodeResult, EncodeJson, Json }
import scala.reflect.ClassTag

final case class TraitSubtypeCodec[T](
  enc: EncodeJson[T],
  dec: DecodeJson[T],
  name: String
)(
  implicit val tag : ClassTag[T]
){
  def tryEncode(obj: Any) : Option[Json] =
    obj match {
      case t: T if t.getClass == tag.runtimeClass => Some(enc(t))
      case _ => None
    }
}

final class FlatTraitJsonCodec[T](
  coders: => List[TraitSubtypeCodec[_ <: T]]
) extends EncodeJson[T] with DecodeJson[T] {
  import FlatTraitJsonCodec.TYPE_NAME_PROPERTY

  override def decode(c: argonaut.HCursor): argonaut.DecodeResult[T] = 
    c.get[String](TYPE_NAME_PROPERTY).flatMap(tn =>
      coders.find(_.name == tn).map(_.dec(c))
        .getOrElse(DecodeResult.fail(s"${TYPE_NAME_PROPERTY} value ${tn} not supported", c.history)
        ).map[T](x => x))

  override def encode(a: T): argonaut.Json =
    coders.find(_.tag.runtimeClass == a.getClass).map(c => {
      import argonaut.Argonaut._
      (TYPE_NAME_PROPERTY := c.name) ->: c.tryEncode(a).get
    }).getOrElse(throw new RuntimeException(s"No encoder for type ${a.getClass}"))
}

object FlatTraitJsonCodec {
  val TYPE_NAME_PROPERTY = "type"
}

object TraitSubtypeCodec {
  def singletonCodec[T](singleton: T, name: String)(implicit ct: ClassTag[T]) : TraitSubtypeCodec[T] =
    new TraitSubtypeCodec(EncodeJson.UnitEncodeJson.contramap(x => ()) , DecodeJson(c => DecodeResult.ok(singleton)), name)
}
