// Define a very simple JSON AST
sealed trait Json
final case class JsObject(get: Map[String, Json]) extends Json
final case class JsString(get: String) extends Json
final case class JsNumber(get: Double) extends Json
case object JsNull extends Json

////////////////// Step 1
// The "serialize to JSON" behaviour is encoded in this trait
trait JsonWriter[A] {
  def write(value: A): Json
}

final case class Person(name: String, email: String)

////////////////// Step 2
// any definitions marked implicit in Scala MUST BE placed inside an object or trait
object JsonWriterInstances {
  implicit val stringWriter: JsonWriter[String] =
    (value: String) => JsString(value)

  implicit val personWriter: JsonWriter[Person] =
    (value: Person) =>
      JsObject(
        Map("name" -> JsString(value.name), "email" -> JsString(value.email))
    )

  // etc...
}

////////////////// Step 3 - 1
object Json {
  def toJson[A](value: A)(implicit w: JsonWriter[A]): Json =
    w.write(value)
}

////////////////// Step 3 - 2
object JsonSyntax {
  implicit class JsonWriterOps[A](value: A) {
    def toJson(implicit w: JsonWriter[A]): Json =
      w.write(value)
  }
}

////////////////// Usage

object Main extends App { 

  import JsonWriterInstances._
  Json.toJson(Person("Dave", "dave@example.com"))

  import JsonWriterInstances._
  import JsonSyntax._
  Person("Dave", "dave@example.com").toJson
}

////////////////// Cats Show Usage
final case class Cat(name: String, age: Int, color: String)

object CatShowMain extends App {

  implicit val catShow = Show.show[Cat] { cat =>
    import cats.instances.int._ // for Show
    import cats.instances.string._ // for Show
    val name = cat.name.show
    val age = cat.age.show
    val color = cat.color.show
    s"$name is a $age year-old $color cat."
  }
  println(Cat("Garfield", 38, "ginger and black").show)
  println(Cat("Garfield", 38, "ginger and black").show)
}

////////////////// Cats Show Source code

// 1
trait Show[T] extends Show.ContravariantShow[T]

trait ContravariantShow[-T] extends Serializable {
    def show(t: T): String
}

// 2 (default, and need to define the customized class type instance)

trait StringInstances extends cats.kernel.instances.StringInstances {
  implicit val catsStdShowForString: Show[String] =
    Show.fromToString[String]
}


// 3

trait Ops[A] {
    def typeClassInstance: Show[A]
    def self: A
    def show: String = typeClassInstance.show(self)
  }

trait ToShowOps {
implicit def toShow[A](target: A)(implicit tc: Show[A]): Ops[A] = new Ops[A] {
  val self = target
  val typeClassInstance = tc
}
}

trait ShowSyntax extends Show.ToShowOps {
  implicit final def showInterpolator(sc: StringContext): Show.ShowInterpolator = Show.ShowInterpolator(sc)
}