# Indexable is a mixin that provides index based methods,
# working solely with four methods: #index, #slice, #splice
# and #size.
#
# These methods work in harmony. Where #index returns a
# position of a given element, #slice returns elements
# for given positions. #splice is like #slice but replaces
# the given position with new values. This method is not
# part of ruby core, but it generally just an alias for #[]=,
# just as #slice is an alias of #[]. #size of course simply
# returns the total length of the indexable object.
#
# NOTE: To test the following methods Indexable needs to be
# included into Array and array must have #splice defined.
#
#   require 'facets/array/splice'
#
#   class ::Array
#     include Indexable
#   end
# 
# CREDIT: Thomas Sawyer

module Indexable

  # Like #first but returns the first element
  # in a new array.
  #
  #   [1,2,3].head  #=> [1]
  #
  def head
    slice(0,1)
  end

  # Returns an array from second element to last element.
  #
  #   [1,2,3].tail  #=> [2,3]
  #
  def tail
    slice(1,length-1)
  end

  # Like #last, returning the last element
  # in an array.
  #
  #   [1,2,3].foot  #=> [3]
  #
  def foot
    slice(-1,1)
  end

  # Returns an array of the first element up to,
  # but not including, the last element.
  #
  #   [1,2,3].body  #=> [1,2]
  #
  #--
  # Better name for this? (bulk, perhaps?)
  #++
  #
  def body
    slice(0,size-1)
  end

  # Returns the middle element of an array, or the element offset
  # from middle if the parameter is given. Even-sized arrays,
  # not having an exact middle, return the middle-right element.
  #
  #   [1,2,3,4,5].mid        #=> 3
  #   [1,2,3,4,5,6].mid      #=> 4
  #   [1,2,3,4,5,6].mid(-1)  #=> 3
  #   [1,2,3,4,5,6].mid(-2)  #=> 2
  #   [1,2,3,4,5,6].mid(1)   #=> 5
  #
  # In other words, If there are an even number of elements the
  # higher-indexed of the two center elements is indexed as
  # origin (0).
  #
  def mid(offset=0)
    slice((size / 2) + offset)
  end

  # Returns an Array of the middle element(s) of an array.
  # Even-sized arrays, not having an exact middle, return
  # a two-element array of the two middle elements.
  #
  #   [1,2,3,4,5].middle        #=> [3]
  #   [1,2,3,4,5,6].middle      #=> [3,4]
  #
  # A +birth+ can be give to widen the middle on either side.
  #
  #   [1,2,3,4,5].middle(1)   #=> [2,3,4]
  #   [1,2,3,4,5,6].middle(1)   #=> [2,3,4,5]
  #
  # In contrast to #mid which utilizes an offset.
  #
  def middle(birth=0)
    i = size / 2 - birth
    if size % 2 == 0
      slice(i - 1, 2 + (2 * birth))
    else
      slice(i, 1 + (2 * birth))
    end
  end

  # Fetch values from a start index through an end index.
  #
  #   [1,2,3,4,5].thru(0,2)  #=> [1,2,3]
  #   [1,2,3,4,5].thru(2,4)  #=> [3,4,5]
  #
  #   [1,2,3,4,5].thru(2)  #=> [1,2,3]
  #   [1,2,3,4,5].thru(4)  #=> [1,2,3,4,5]
  #
  def thru(from, to=nil)
    from, to = 0, from unless to
    to = size - 1 if to >= size
    a = []
    i = from
    while i <= to
      a << slice(i)
      i += 1
    end
    a
  end

  # Returns last _n_ elements.
  #
  #   %w{W o r l d}.from(3)  #=> %w{l d}
  #
  def from(i)
    return [] if i >= size
    slice(i, size - i) #slice(-n..-1)
  end

  ## DEPRECATED: use #thru instead
  ## Returns first _n_ elements.
  ##
  ##   %w{H e l l o}.upto(3)  #=> %w{H e l l}
  ##
  ##def upto(n)
  ##  slice(0, n.to_i+1)
  ##end

  # Returns first _n_ elements.
  #
  #   %w{H e l l o}.first(3)  #=> %w{H e l}
  #
  def first(n=1)
    slice(0, n.to_i)
  end

  # Returns last _n_ elements.
  #
  #   %w{H e l l o}.last(3)  #=> %w{l l o}
  #
  def last(n=1)
    n = n.to_i
    return self if n > size
    slice(-n, n) #slice(-n..-1)
  end

  # Change the first element.
  #
  #   a = ["a","y","z"]
  #   a.first = "x"
  #   a           #=> ["x","y","z"]
  #
  def first=(x)
    splice(0,x)
  end

  # Change the last element.
  #
  #   a = [1,2,5]
  #   a.last = 3
  #   a           #=> [1,2,3]

  def last=(x)
    splice(-1,x)
  end

  # Remove and return the first element.
  #
  #   a = [1,2,3]
  #   a.first!      #=> 1
  #   a             #=> [2,3]
  #
  def first!
    splice(0)
  end

  # Remove and return the last element.
  #
  #   a = [1,2,3]
  #   a.last!       #=> 3
  #   a             #=> [1,2]
  #
  def last!
    splice(-1)
  end

  # A shorting of "ends at", returns the
  # last index of the indexable object.
  # Returns nil if there are no elements.
  #
  #   [1,2,3,4,5].ends  #=> 4
  #
  # This nearly equivalent to +size - 1+.
  #
  def ends
    return nil if size == 0
    size - 1
  end

  # Returns the positive ordinal index given
  # a cardinal position, 1 to n or -n to -1.
  #
  #   [1,2,3,4,5].pos(1)   #=> 0
  #   [1,2,3,4,5].pos(-1)  #=> 4
  #
  def pos(i)
    if i > 0
      return i - 1
    else
      size + i
    end
  end

  # Returns the index of the first element equal to the
  # given object or satisfying the block condition.
  #
  #   [1,2,3,4].index{ |e| e == 3 }  #=> 2
  #   [1,2,3,4].index{ |e| e > 3 }   #=> 3
  #
  def index(obj=nil, &block)
    if block_given?
      size.times do |i|
        return i if yield(slice(i))
      end
    else
      size.times do |i|
        return i if obj == slice(i)
      end
    end
    nil
  end

  # to be deprecated
  alias_method :index_of, :index

  # TODO Maybe #range could use a little error catch code (todo).

  # Returns the index range between two elements. If no elements are
  # given, returns the range from first to last.
  #
  #   ['a','b','c','d'].range            #=> (0..3)
  #   ['a','b','c','d'].range('b','d')   #=> (1..3)
  #
  def range(a=nil,z=-1)
    if a
      index(a)..index(z)
    else
      (0..(size-1))
    end
  end

end

