/*
 * Copyright (c) 2011-13 Miles Sabin 
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package shapeless

import scala.collection.{ GenTraversable, GenTraversableLike }
import scala.collection.generic.{ CanBuildFrom, IsTraversableLike }

/**
 * Wrapper for a collection type witnessing that it has the statically specified length. Can be
 * applied to any type which can be viewed as a `GenTraversableLike`, ie. standard collections,
 * `Array`s, `String`s etc.
 * 
 * @author Miles Sabin
 */
final class Sized[+Repr, L <: Nat](val unsized : Repr) extends AnyVal

/**
 * Carrier for `Sized` operations.
 * 
 * These operations are implemented here as extension methods of the minimal `Sized` type to avoid issues that would
 * otherwise be caused by its covariance.
 * 
 * @author Miles Sabin
 */
class SizedOps[A, Repr, L <: Nat](r : GenTraversableLike[A, Repr]) { outer =>
  import nat._
  import ops.nat._
  import LT._
  import Sized.wrap
  
  /**
   * Returns the head of this collection. Available only if there is evidence that this collection has at least one
   * element.
   */
  def head(implicit ev : _0 < L) : A = r.head
  
  /**
   * Returns the tail of this collection. Available only if there is evidence that this collection has at least one
   * element.
   */
  def tail(implicit pred : Pred[L]) = wrap[Repr, pred.Out](r.tail)
  
  /**
   * Returns the first ''m'' elements of this collection. An explicit type argument must be provided. Available only if
   * there is evidence that this collection has at least ''m'' elements. The resulting collection will be statically
   * known to have ''m'' elements.
   */
  def take[M <: Nat](implicit diff : Diff[L, M], ev : ToInt[M]) = wrap[Repr, M](r.take(toInt[M]))
  
  /**
   * Returns the first ''m'' elements of this collection. Available only if there is evidence that this collection has
   * at least ''m'' elements. The resulting collection will be statically known to have ''m'' elements.
   */
  def take(m : Nat)(implicit diff : Diff[L, m.N], ev : ToInt[m.N]) = wrap[Repr, m.N](r.take(toInt[m.N]))

  /**
   * Returns all but the  first ''m'' elements of this collection. An explicit type argument must be provided. Available
   * only if there is evidence that this collection has at least ''m'' elements. The resulting collection will be 
   * statically known to have ''m'' less elements than this collection.
   */
  def drop[M <: Nat](implicit diff : Diff[L, M], ev : ToInt[M]) = wrap[Repr, diff.Out](r.drop(toInt[M]))
  
  /**
   * Returns all but the  first ''m'' elements of this collection. Available only if there is evidence that this
   * collection has at least ''m'' elements. The resulting collection will be statically known to have ''m'' less
   * elements than this collection.
   */
  def drop(m : Nat)(implicit diff : Diff[L, m.N], ev : ToInt[m.N]) = wrap[Repr, diff.Out](r.drop(toInt[m.N]))
  
  /**
   * Splits this collection at the ''mth'' element, returning the prefix and suffix as a pair. An explicit type argument
   * must be provided. Available only if there is evidence that this collection has at least ''m'' elements. The
   * resulting collections will be statically know to have ''m'' and ''n-m'' elements respectively.
   */
  def splitAt[M <: Nat](implicit diff : Diff[L, M], ev : ToInt[M]) = (take[M], drop[M])
  
  /**
   * Splits this collection at the ''mth'' element, returning the prefix and suffix as a pair. Available only if there
   * is evidence that this collection has at least ''m'' elements. The resulting collections will be statically know to
   * have ''m'' and ''n-m'' elements respectively.
   */
  def splitAt(m : Nat)(implicit diff : Diff[L, m.N], ev : ToInt[m.N]) = (take[m.N], drop[m.N])
  
  /**
   * Prepend the argument element to this collection. The resulting collection will be statically known to have a size
   * one greater than this collection.
   */
  def +:(elem : A)(implicit cbf : CanBuildFrom[Repr, A, Repr]) = {
    val builder = cbf.apply(r.repr)
    builder += elem
    builder ++= r.toIterator
    wrap[Repr, Succ[L]](builder.result)
  }
  
  /**
   * Append the argument element to this collection. The resulting collection will be statically known to have a size
   * one greater than this collection.
   */
   def :+(elem : A)(implicit cbf : CanBuildFrom[Repr, A, Repr]) = {
    val builder = cbf.apply(r.repr)
    builder ++= r.toIterator
    builder += elem
    wrap[Repr, Succ[L]](builder.result)
  }
  
  /**
   * Append the argument collection to this collection. The resulting collection will be statically known to have
   * ''m+n'' elements.
   */
  def ++[B >: A, That, M <: Nat](that : Sized[That, M])
    (implicit
      sum : Sum[L, M],
      cbf : CanBuildFrom[Repr, B, That],
      convThat : That => GenTraversableLike[B, That]) = wrap[That, sum.Out](r ++ that.unsized)
    
  /**
   * Map across this collection. The resulting collection will be statically known to have the same number of elements
   * as this collection.
   */
  def map[B, That](f : A => B)(implicit cbf : CanBuildFrom[Repr, B, That]) = wrap[That, L](r map f)
}

trait LowPrioritySized {
  implicit def sizedToRepr[Repr](s : Sized[Repr, _]) : Repr = s.unsized
}

object Sized extends LowPrioritySized {
  implicit def sizedOps[Repr, L <: Nat](s : Sized[Repr, L])
    (implicit itl: IsTraversableLike[Repr]): SizedOps[itl.A, Repr, L] =
      new SizedOps[itl.A, Repr, L](itl.conversion(s.unsized))
  
  def apply[CC[_]] = new SizedBuilder[CC]
  
  def apply[CC[_]]()
    (implicit cbf : CanBuildFrom[Nothing, Nothing, CC[Nothing]]) =
      new Sized[CC[Nothing], _0](cbf().result)
  
  def wrap[Repr, L <: Nat](r : Repr) = new Sized[Repr, L](r)

  def unapplySeq[Repr, L <: Nat](x : Sized[Repr, L]) = Some(x.unsized)
}