/****************************************************************/
/**** An Ordered Bag Data-type, with Implicit Views *************/ 
/****************************************************************/

sealed abstract class Bag[A] (implicit c: A => Ord[A]) {
  def size : Int = 
    this match {
      case Empty()       => 0
      case Plus(_, rest) => 1 + rest.size
    }

  def contains(x: A) : Boolean = {
    this match {
      case Empty()                 => false
      case Plus(e, _) if (x === e) => true  
      case Plus(e, _) if (x  <  e) => false 
      case Plus(_, rest)           => rest.contains(x) 
    }
  }

  def add(x: A) : Bag[A] = {
    if (this.contains(x)) this else { Plus(x, this) }
  }

  // Should remind you of a particular ML function?
  def union(that: Bag[A]) : Bag[A] = {
    that match {
      case Empty()       => this
      case Plus(x, rest) => this.add(x).union(rest) 
    }
  }

  def gimme: Option[A] = 
    this match {
      case Plus(x, rest) => Some(x)
      case _             => None
    }
  
  def remove(x: A) : Bag[A] = {
    this match {
      case Empty()                   => this
      case Plus(e, rest) if (x == e) => rest // requires no duplicates!
      case Plus(e, rest)             => Plus(e, rest.remove(x))
    }
  }

  def toList : List[A] = 
    this match {
      case Empty()       => List()
      case Plus(x, rest) => x :: (rest.toList)
    }

  override def toString : String = this.toList.mkString(",")
}

case class Empty[A]()(implicit c: A => Ord[A]) extends Bag[A]
case class Plus[A](elt: A, rest: Bag[A])(implicit c: A => Ord[A]) extends Bag[A]

/* A Companion Object that has various key methods */
object Bag {
  def apply[A](xs: A*)(implicit c: A => Ord[A]) : Bag[A] = { 
    val s0 : Bag[A] = Empty()
    xs.toList.foldLeft(s0)(_ add _)  
  }
}


object BagTest {

  import OrdInstances._
  
  val b0   = Bag(1,2,3,4)
  val b0a  = Bag(1, 6, 12, 5)
  val tmp0 = b0 union b0a
  val tmp1 = b0 contains 2
  val tmp2 = b0 contains 20
  val b1   = Bag("cat", "dog", "monkey")
  val tmp3 = b1 contains "cat"
  val tmp4 = b1 union b1
  val b1a  = b1 add "hippo"
  val b2   = Bag(("cat", 3), ("dog", 4), ("monkey", 5))
}
