/* CandyBowl ADT Interface, Immutable Method-Chaining Functions
   CSci 555: Functional Programming, Spring 2016
   H. Conrad Cunningham, Professor
   Computer and Information Science
   University of Mississippi

1234567890123456789012345678901234567890123456789012345678901234567890

2016-04-27: Developed using Lua version and Scala Immutable CookieJar


CandyBowl() to create a new empty candy bowl

put(candyType) to add one piece of candy of type 'candyType' to 
'this' bowl

take(candyType) to remove one piece of candy of type 'candyType'
from 'this' bowl

combine(that) returns the bowl resulting from pouring 'this' bowl
and 'that' bowl together to form a "larger" bowl

isEmpty to determine whether or not 'this' bowl is empty

has(candyType) to determine whether or not 'this' bowl contains any
candy of type 'candyType'

howMany to determine how many pieces of candy are in 'this' bowl
overall

howMany(candyType) to determine how many pieces of candy of type
'candyType' are in 'this' bowl 

inventory to return a list of pairs (candyType, count) for the 
candy in 'this' bowl.  The list should be in ascending order by 
candyType. For example, if candyType is denoted by a string 
and there are two Snickers and one Hershey Kiss in the bowl, 
then the list returned would be something like 
List(("Hershey Kiss", 1), ("Snickers", 2)).

*/

class CandyBowlList[CandyType <% Ordered[CandyType]]
  (val bowl: List[CandyType]) {
  extends CandyBowl[CandyType] {

  def put(candy: CandyType): CandyBowl[CandyType] =
    new CandyBowlList(candy :: bowl)
 
  def take(candy: CandyType) : Option[CandyBowl[CandyType]] =
    bowl.span(_ != candy) match { // pair (not equal from head, rest)
      case (l,_::r) => Some(new CandyBowlList(l ++ r))
      case _        => None
    }

  private def repeat[T](c: T, n: Int): List[T] =
    if (n <= 0) Nil else c :: repeat(c,n-1)
      
  private def tuplize(xs: List[CandyType]): List[(CandyType,Int)] =
    xs match {
      case Nil     => Nil
      case (y::ys) =>
        val (pre,post) = xs span (_ == y)
        (y,pre.length) :: tuplize(post)
    }

  private def detuplize(xs: List[(CandyType,Int)]): List[CandyType] =
    xs match {
      case ((c,n)::ys) => repeat(c,n) ++ detuplize(ys)
      case _           => Nil
    }

  def combine(that: CandyBowl[CandyType]): CandyBowl[CandyType] =
    new CandyBowlList(bowl ++ detuplize(that.inventory))

  def combine(that: CandyBowl[CandyType]): CandyBowl[CandyType] =
    new CandyBowlList(bowl ++ detuplize(that.inventory))

  def isEmpty: Boolean = bowl.isEmpty

  def has(candy: CandyType): Boolean = bowl.contains(candy)

  def howMany: Int = bowl.length

  def howMany(candy: CandyType): Int = bowl.filter(_ == candy).length

  def inventory: List[(CandyType, Int)]
    = tuplize(bowl.sorted) // sort the list of candy

  def filter(p: CandyType => Boolean): CandyBowl[CandyType] =
    new CandyBowlList(bowl.filter(p))

  def map[B <% Ordered[B]](f: CandyType => B): CandyBowl[B] =
    new CandyBowlList(bowl.map(f))

  /* Redefine toString to return form "CandyBowl(e1,e2,e3,...,en)"
     with all elements of bag in arbitrary order.  */

  override def toString = "CandyBowl(" + bowl.mkString(",") + ")"

}

object Test {
  // Main method for testing
  def main(args: Array[String]) {

    val b1 = new CandyBowlList[String](Nil)
    println("b1 = " + b1)
    println("b1.isEmpty = " + b1.isEmpty)
    val b2 = b1.put("Snickers").put("Zero").put("Snickers")
    println("b2 = b1.put(Snickers).put(Zero).put(Snickers)")
    println("b2                       = " + b2)
    println("b2.howMany               = " + b2.howMany)
    println("b2.howMany(Snickers)     = " + b2.howMany("Snickers"))
    println("b2.howMany(Zero)         = " + b2.howMany("Zero"))
    println("b2.howMany(Reeses)       = " + b2.howMany("Reeses"))
    println("b2.has(Zero)             = " + b2.has("Zero"))
    println("b2.has(Reeses)           = " + b2.has("Reeses"))
    println("b2.isEmpty               = " + b2.isEmpty)
    println("b2.inventory             = " + b2.inventory)
    println("b2.combine(b1)           = " + b2.combine(b1))
    println("b1.combine(b2)           = " + b1.combine(b2))
    println("b1.combine(b2.put(Kiss)  = " + b1.combine(b2.put("Kiss")))
    println("b2.combine(b1).inventory = " + b2.combine(b1).inventory)
    println("b1.put(Kiss).combine(b2).inventory  = "
      + b1.put("Kiss").combine(b2).inventory)
    println("b2.filter(_ != Zero) = " + b2.filter(_ != "Zero"))
    println("b2.filter(_ != Reeses) = " + b2.filter(_ != "Reeses"))
    println("b2.map(x => STOP) = " + b2.map(x => "STOP"))
  }

}
