module Rubinius
  class Tuple
    include Enumerable

    def self.allocate
      raise TypeError, "Tuple cannot be created via allocate()"
    end

    def self.new(cnt)
      Rubinius.primitive :tuple_allocate
      raise PrimitiveFailure, "Tuple.new primitive failed"
    end

    def self.pattern(size, obj)
      Rubinius.primitive :tuple_pattern
      raise PrimitiveFailure, "Tuple.pattern primitive failed"
    end

    def [](idx)
      Rubinius.primitive :tuple_at

      unless idx.kind_of? Fixnum
        raise TypeError, "Only Fixnums valid for Tuple indices"
      end

      raise PrimitiveFailure, "Tuple#[] primitive failed"
    end

    alias_method :at, :[]

    def []=(idx, val)
      Rubinius.primitive :tuple_put

      unless idx.kind_of? Fixnum
        raise TypeError, "Only Fixnums valid for Tuple indices"
      end
      raise PrimitiveFailure, "Tuple#[]= primitive failed"
    end

    alias_method :put, :[]=

    def fields
      Rubinius.primitive :tuple_fields
      raise PrimitiveFailure, "Tuple#fields primitive failed"
    end

    def copy_from(other, start, length, dest)
      Rubinius.primitive :tuple_copy_from

      unless other.kind_of? Rubinius::Tuple
        raise TypeError, "Tuple#copy_from was expecting a Tuple, not a #{other.class}"
      end
      start = Rubinius::Type.coerce_to start, Fixnum, :to_i
      length = Rubinius::Type.coerce_to length, Fixnum, :to_i
      dest = Rubinius::Type.coerce_to dest, Fixnum, :to_i

      if start < 0 || start > other.fields
        raise IndexError, "Start %d is out of bounds %d" % [start, other.fields]
      end

      if dest < 0 || dest > self.fields
        raise IndexError, "Destination %d is out of bounds %d" % [dest, self.fields]
      end

      if length < 0
        raise IndexError, "length %d must be positive" % [length]
      end

      if (start + length) > other.fields
        raise IndexError, "end index %d can not exceed size of source %d" % [start+length, other.fields]
      end

      if length > (self.fields - dest)
        raise IndexError, "length %d can not exceed space %d in destination" % [length, self.fields - dest]
      end

      src = start
      dst = dest
      while src < (start + length)
        put dst, other.at(src)
        src += 1
        dst += 1
      end

      self
    end

    def dup
      Rubinius.primitive :tuple_dup

      obj = Rubinius::Type.object_class(self).new(self.size)

      Rubinius.invoke_primitive :object_copy_object, obj, self

      Rubinius.privately do
        obj.initialize_copy self
      end

      obj
    end

    def delete(start, length, object)
      Rubinius.primitive :tuple_delete_inplace

      start = Rubinius::Type.coerce_to start, Fixnum, :to_i
      length = Rubinius::Type.coerce_to length, Fixnum, :to_i

      lend = start
      rend = lend + length

      return 0 if self.fields == 0
      if lend < 0 || lend >= self.fields
        raise IndexError, "Index %d is not within Tuple of length %d" % [lend, self.fields]
      end

      if rend < 0 || rend > self.fields
        raise IndexError, "Index %d is not within Tuple of length %d" % [rend, self.fields]
      end

      i = lend
      while i < rend
        if at(i) == object
          j = i
          i += 1
          while i < rend
            val = at(i)
            if val != object
              put j, val
              j += 1
            end
            i += 1
          end

          # cleanup all the remaining bins in the tuple
          i = j
          while i < rend
            put i, nil
            i += 1
          end

          return rend - j
        end
        i += 1
      end

      return 0
    end

    def reverse!(start, total)
      Rubinius.primitive :tuple_reverse

      reverse(
        Rubinius::Type.coerce_to(start, Fixnum, :to_i),
        Rubinius::Type.coerce_to(total, Fixnum, :to_i))
    end

    def delete_at_index(index)
      return if size == 1

      s = size - 1
      t = self.class.new s

      if index != 0
        t.copy_from self, 0, index, 0
      end

      if index < s
        t.copy_from self, index+1, s-index, index
      end

      t
    end

    def insert_at_index(index, value)
      t = self.class.new size + 1

      if index != 0
        t.copy_from self, 0, index, 0
      end

      if index < size
        t.copy_from self, index, size-index, index+1
      end

      t[index] = value

      t
    end

    def self.[](*args)
      args.to_tuple
    end

    def to_s
      "#<#{self.class}:0x#{object_id.to_s(16)} #{fields} elements>"
    end

    def each
      i = 0
      t = fields
      while i < t
        yield at(i)
        i += 1
      end
      self
    end

    def ==(tup)
      return super unless tup.kind_of?(Tuple)

      t = fields()

      return false unless t == tup.size

      i = 0
      while i < t
        return false unless at(i) == tup.at(i)
        i += 1
      end

      return true
    end

    def +(o)
      t = Tuple.new(size + o.size)
      t.copy_from(self, 0, size, 0)
      t.copy_from(o, 0, o.size, size)
      t
    end

    def inspect
      str = "#<#{self.class}"
      if fields == 0
        str << " empty>"
      else
        str << ": #{join(", ", :inspect)}>"
      end
      return str
    end

    def join(sep, meth=:to_s)
      join_upto(sep, fields, meth)
    end

    def join_upto(sep, count, meth=:to_s)
      str = ""
      return str if count == 0 or empty?

      count = fields if count >= fields
      count -= 1
      i = 0
      while i < count
        str.append at(i).__send__(meth)
        str.append sep.dup
        i += 1
      end

      str.append at(count).__send__(meth)
      return str
    end

    def ===(other)
      return false unless Tuple === other and fields == other.fields
      i = 0
      while i < fields
        return false unless at(i) === other.at(i)
        i += 1
      end
      true
    end

    def to_a
      array = Array.allocate

      m = Rubinius::Mirror.reflect array
      m.tuple = dup
      m.total = fields

      array
    end

    alias_method :to_ary, :to_a

    def shift
      return self unless fields > 0
      t = Tuple.new(fields-1)
      t.copy_from self, 1, fields-1, 0
      return t
    end

    # Swap elements of the two indexes.
    def swap(a, b)
      temp = at(a)
      put a, at(b)
      put b, temp
    end

    alias_method :size, :fields
    alias_method :length, :fields

    def empty?
      size == 0
    end

    def first
      at(0)
    end

    def last
      at(fields - 1)
    end

    # Marshal support - _dump / _load are deprecated so eventually we should figure
    # out a better way.
    def _dump(depth)
      # TODO use depth
      Marshal.dump to_a
    end

    def self._load(str)
      ary = Marshal.load(str)
      t = new(ary.size)
      ary.each_with_index { |obj, idx| t[idx] = obj }
      return t
    end
  end
end
