require 'criteria'

module Criteria
  class FileTable < Table
    attr_accessor :_filename, :_types, :_cols, :skip, :split

    def initialize(filename, *cols)
      @_filename      = filename
      @_types         = []
      @_cols          = []
      @criterion_type = FileCriterion
      @split          = /\s*,\s*/
      @skip           = 0

      cols.each {
        | x |
        if(x.is_a? Array)
          @_cols  << x[0]
          @_types << (x[1] || String)
        else
          @_cols  << x
          @_types << String
        end
      }
    end #m:initialize
  end #c:FileTable


  class FileCriterion < Criterion
    attr_accessor :limit, :offset, :order_by, :order

    def initialize(*args)
      super

      @order  = :asc
      @limit  = -1
      @offset = -1
    end #m:initialize

    def select(*args)
      lines  = 0
      result = []
      
      filter = (args.size > 0)
      extras  = []
      @order_by = [@order_by] unless @order_by.is_a?(Array)
      @order_by.each { | k | extras << k unless args.member?(k) }
      
      file   = open(@_parent._filename, "r")
      file.each_line {
        | line |
        lines += 1
        next unless(lines > @_parent.skip)

        line.chomp!
        arr  = _type_map(line.split(@_parent.split))
        hash = Hash[*@_parent._cols.zip(arr).flatten!]

        if(self._apply(hash))
          if filter
            a = []
            args.each { |k| a << hash[k] }
            extras.each { |k| a << hash[k] }
            result << a
          else
            result << arr
          end
        end
      }

      file.close
      return _post_process(result, extras)
    end #m:select

    protected

    def _apply(ob)
      chain = @chain.map { |i|
        i.is_a?(FileCriterion) ? i._apply(ob) : i
      }

      if(@field.is_a? FileCriterion)
        res = @field._apply(ob)
      else
        res = ob[@field]
      end

      return res.__send__(chain.shift, *chain) if chain[0]
      return res
    end #m:_apply

    def _type_map(arr)
      res = []

      arr.each_with_index {
        | v, i |
        if(@_parent._types[i] == Integer)
          res << v.to_i
        elsif(@_parent._types[i] == Float)
          res << v.to_f
        elsif(@_parent._types[i] == Proc)
          res << @_parent._types[i].call(v)
        else
          res << arr[i].to_s
        end
      }

      return res
    end #m:_type_map


    def _post_process(res, extras)
      res = _process_order(res)
      res = _process_limit(res)
      res = _process_extras(res, extras)

      return res
    end #m:_post_process

    def _process_order(res)
      return res unless @order_by

      unless @order_by.is_a? Array
        @order_by = [@order_by]
      end

      order_map = @order_by.map { |a| @_parent._cols.index(a) }
      res.sort! {
        |a,b|
        a,b = b,a    if(@order == :desc)
        i   = 0
        while (test = (a[order_map[i]] <=> b[order_map[i]])) == 0
          i += 1
          break if(i >= order_map.size)
        end
        test
      }

      return res
    end #m:_process_order


    def _process_limit(res)
      limit  = (@limit  >= 0 ? @limit  : res.size)
      offset = (@offset >= 0 ? @offset : 0)

      return res[offset,limit]
    end #m:_process_limit


    def _process_extras(res, extras)
      return if res.size < 1
      cut = res[0].size - extras.size
      res.map { | a | a[0...cut] }
    end #m:_process_extras
  end #c:FileCriterion
end #M:Criteria
