# = Indexable
#
# Indexable is a mixin that provides index based methods,
# working soley 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.
#
# == Authors
#
# * Thomas Sawyer


# = Indexable
#
# Indexable is a mixin that provides index based methods,
# working soley 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 mehtod 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.
#
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 upto,
  # 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
  # orgin (0).
  #
  def mid(offset=0)
    slice((size / 2) + offset)
  end

  # Returns the middle element(s) of an array. Even-sized arrays,
  # not having an exact middle, returns 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]
  #
  # In contrast to #mid which utilizes an offset.
  #
  def middle
    if size % 2 == 0
     slice( (size / 2) - 1, 2 )
     #[slice((size / 2) - 1, 1), slice(size / 2, 1)]
    else
      slice(size / 2)
    end
  end

  # Fetch values from a start index thru 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]
  #
  def thru(from, to)
    a = []
    i = from
    while i <= to
      a << slice(i)
      i += 1
    end
    a
  end

  # Returns first _n_ elements.
  #
  #   "Hello World".first(3)  #=> "Hel"
  #
  def first(n=1)
    slice(0, n.to_i)
  end

  # Returns last _n_ elements.
  #
  #   "Hello World".last(3)  #=> "rld"
  #
  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"
  #   p a           #=> ["x","y","z"]
  #
  def first=(x)
    splice(0,x)
  end

  # Change the last element.
  #
  #   a = [1,2,5]
  #   a.last = 3
  #   p 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..2
  #
  def range(a=nil,z=nil)
    if !a
      0..(size-1)
    else
      index(a)..index(z)
    end
  end

end

