module Restruct
  class Array < Structure

    include Enumerable
    extend Forwardable

    def_delegators :to_a, :uniq, :join, :reverse, :+, :-, :&, :|

    def at(index)
      deserialize connection.call('LINDEX', id, index)
    end

    def values_at(*args)
      args.each_with_object([]) do |arg, array|
        elements = self[arg]
        array.push *(elements ? Array(elements) : [nil])
      end
    end

    def fetch(index, default=nil, &block)
      validate_index_type! index

      if index < size
        at index
      else
        validate_index_bounds! index if default.nil? && block.nil?
        default || block.call(index)
      end
    end

    def [](*args)
      if args.count == 1
        if args[0].is_a? Integer
          at args[0].to_i
        elsif args[0].is_a? Range
          range args[0].first, args[0].last
        else
          validate_index_type! args.first
        end
      elsif args.count == 2
        range args[0], args[0] + args[1] - 1
      else
        raise ArgumentError, "wrong number of arguments (#{args.count} for 1..2)"
      end
    end

    def []=(index, element)
      validate_index_type! index
      validate_index_bounds! index

      connection.lazy 'LSET', id, index, serialize(element)
    end

    def push(*elements)
      connection.lazy 'RPUSH', id, *elements.map { |e| serialize e }
      self
    end
    alias_method :<<, :push

    def insert(index, *elements)
      validate_index_type! index
      tail_size = index >= 0 ? size - index : size - (size + index) - 1
      tail = Array(pop(tail_size))
      push *(elements + tail)
    end

    def concat(array)
      push *array
    end

    def pop(count=1)
      if count == 1
        deserialize connection.lazy('RPOP', id)
      else
        [count, size].min.times.map { pop }.reverse
      end
    end

    def shift(count=1)
      if count == 1
        deserialize connection.lazy('LPOP', id)
      else
        [count, size].min.times.map { shift }
      end
    end

    def delete(element)
      removed_count = connection.lazy 'LREM', id, 0, serialize(element)
      removed_count && removed_count > 0 ? element : nil
    end

    def delete_at(index)
      validate_index_type! index
      return nil if out_of_bounds? index
      
      element = at index
      tail_size = index >= 0 ? size - index : size - (size + index)
      tail = Array(pop(tail_size))
      push *tail[1..-1]
      element
    end

    def delete_if
      each { |e| delete e if yield e }
      self
    end

    def keep_if
      each { |e| delete e unless yield e }
      self
    end
    alias_method :select!, :keep_if

    def clear
      destroy
      self
    end

    def size
      connection.call 'LLEN', id
    end
    alias_method :count, :size
    alias_method :length, :size

    def empty?
      size == 0
    end

    def include?(element)
      each { |e| return true if e == element }
      false
    end

    def each
      index = 0
      while index < size
        yield at(index), index
        index += 1
      end
    end

    def each_index
      each { |_,i| yield i }
    end

    def first
      at 0
    end

    def last
      at -1
    end

    def to_a
      range 0, -1
    end
    alias_method :to_ary, :to_a
    alias_method :to_primitive, :to_a

    private

    def range(start, stop)
      return nil if start > size
      connection.call('LRANGE', id, start, stop).map { |e| deserialize e }
    end

    def validate_index_type!(index)
      raise TypeError, "no implicit conversion from #{index.nil? ? 'nil' : index.class.name.downcase} to integer" unless index.is_a? Integer
    end

    def validate_index_bounds!(index)
      raise IndexError, "index #{index} outside of array bounds: -#{size}...#{size}" if out_of_bounds? index
    end

    def out_of_bounds?(index)
      !(-size..size).include?(index)
    end

    def serialize(string)
      string
    end

    def deserialize(string)
      string
    end

  end
end