require 'fastercsv'

module GoodData

  class Row < FasterCSV::Row
    def ==(other)
       len = length()
       return false if len != other.length
       result = true

       len.times do |i|
         result = false unless convert_field(field(i)) == convert_field(other.field(i))
       end
       result
    end

    private
    def convert_field(val)
      if val.is_a?(String) && val.match(/^[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?$/)
        # Is it a Number?
        val = val.scan(/[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?/).first
        val = val.include?('.') ? val.to_f.round : val.to_i
        return val
      elsif val.nil? || (val.respond_to?(:strip) && val.strip.empty?)
        #is ia a String
        return ''
      elsif val.respond_to? :round
        # No idea what that one does
        return val.round
      else
        return val
      end
    end
  end

  class DataResult

    attr_reader :data

    def initialize(data)
      @data = data
    end

    def print
      puts to_s
    end

    def to_s
      a = to_table.to_a
      a.transpose.unshift((1..a.length).to_a).each_with_index.map{|col, i|
        col.unshift(i.zero?? nil : i)   # inserts row labels #
        w = col.map{|cell| cell.to_s.length}.max   # w = "column width" #
        col.each_with_index.map{|cell, i|
          i.zero?? cell.to_s.center(w) : cell.to_s.ljust(w)}   # alligns the column #
      }.transpose.map{|row| "[#{row.join(' | ')}]"}.join("\n")
    end

    def to_table
      raise "Should be implemented in subclass"
    end

  end

  class EmptyResult < DataResult

    def initialize(data, options = {})
      super(data)
      @options = options
      assemble_table
    end

    def to_s
      "No Data"
    end

    def assemble_table
      @table = FasterCSV::Table.new([GoodData::Row.new([],[],false)])
    end

    def to_table
      @table
    end

    def without_column_headers
      @table
    end

    def == (otherDataResult)
      false
    end
    
    def diff(otherDataResult)
      ['empty']
    end
  end

  class SFDataResult < DataResult

    def initialize(data, options = {})
      super(data)
      @options = options
      assemble_table
    end

    def assemble_table
      sf_data = data[:queryResponse][:result][:records]
      sf_data = sf_data.is_a?(Hash) ? [sf_data] : sf_data
      if @options[:soql]
        # puts @options[:soql]
        fields = @options[:soql].strip.match(/SELECT (.*) FROM/i)[1]
        @headers = fields.strip.split(",").map do |item|
          item.strip.split(/\s/)
        end.map do |item|
          item.last.to_sym
        end
        # pp @headers
      elsif @options[:headers]
        @headers = @options[:headers]
      else
        @headers = sf_data.first.keys - [:type, :Id]
      end
      @table = FasterCSV::Table.new(sf_data.collect do |line|
        GoodData::Row.new([], @headers.map {|h| line[h] || ' '}, false)
      end)
    rescue
      fail "Unable to assemble the table. Either the data provided are empty or the SOQL is malformed."
    end

    def to_table
      @table
    end

    def == (otherDataResult)
      result = true
      len =  @table.length
      other_table = otherDataResult.to_table
      if len != other_table.length
        # puts "TABLES ARE OF DIFFERENT SIZES"
        return false
      end

      diff(otherDataResult).empty?() ? true : false

    end

    def diff(otherDataResult)
      other_table = otherDataResult.to_table
      differences = []

      @table.each do |row|
        differences << row unless other_table.detect {|r| r == row}
      end
      differences
    end

  end

  class ReportDataResult < DataResult

    attr_reader :row_headers, :column_headers, :table, :headers_height, :headers_width

    def initialize(data)
      super
      @row_headers      = []
      @column_headers   = []
      @table            = []

      @row_headers, @headers_width = tabularize_rows
      @column_headers, @headers_height = tabularize_columns

      assemble_table
    end

    def without_column_headers
      @table = table.transpose[headers_height, 1000000].transpose
      self
    end

    def each_line
      table.transpose.each {|line| yield line}
    end

    def to_table
      FasterCSV::Table.new(table.transpose.map {|line| GoodData::Row.new([], line.map {|item| item || ' '}, false)})
    end

    def == (otherDataResult)
      result = true
      csv_table = to_table
      len =  csv_table.length
      return false if len != otherDataResult.to_table.length


      result
    end

    def diff(otherDataResult)
      csv_table = to_table
      other_table = otherDataResult.to_table
      differences = []

      csv_table.each do |row|
        differences << row unless other_table.detect {|r| r == row}
      end
      differences
    end

    private
    def each_level(table, level, children, lookup)
      max_level = level + 1
      children.each do |kid|
        first = kid["first"]
        last = kid["last"]
        repetition = last - first + 1
        repetition.times do |i|
          table[first + i] ||= []
          if kid["type"] == 'total'
            table[first + i][level] = kid["id"]
          else
            table[first + i][level] = lookup[level][kid["id"].to_s]
          end
        end
        if (!kid["children"].empty?)
          new_level = each_level(table, level+1, kid["children"], lookup)
          max_level = [max_level, new_level].max
        end
      end
      max_level
    end

    def tabularize_rows
      rows    = data["xtab_data"]["rows"]
      kids = rows["tree"]["children"]

      if kids.empty? || (kids.size == 1 && kids.first['type'] == 'metric')
        headers, size = [[nil]], 0
      else
        headers = []
        size = each_level(headers, 0, rows["tree"]["children"], rows["lookups"])
      end
      return headers, size
    end

    def tabularize_columns
      columns = data["xtab_data"]["columns"]
      kids = columns["tree"]["children"]

      if kids.empty? || (kids.size == 1 && kids.first['type'] == 'metric')
        headers, size = [[nil]], 0
      else
        headers = []
        size = each_level(headers, 0, columns["tree"]["children"], columns["lookups"])
      end
      return headers, size
    end

    def assemble_table()
  #    puts "=== COLUMNS === #{column_headers.size}x#{headers_height}"
      (column_headers.size).times do |i|
        (headers_height).times do |j|
          table[headers_width + i] ||= []
  #        puts "[#{headers_width + i}][#{j}] #{column_headers[i][j]}"
          table[headers_width + i][j] = column_headers[i][j]
        end
      end

  #    puts "=== ROWS ==="
      (row_headers.size).times do |i|
        (headers_width).times do |j|
          table[j] ||= []
  #        puts "[#{j}][#{headers_height + i}] #{row_headers[i][j]}"
          table[j][headers_height + i] = row_headers[i][j]
        end
      end

      xtab_data = data["xtab_data"]["data"]
  #    puts "=== DATA === #{column_headers.size}x#{row_headers.size}"
      (column_headers.size).times do |i|
        (row_headers.size).times do |j|
          table[headers_width + i] ||= []
  #        puts "[#{headers_width + i}, #{headers_height + j}] [#{i}][#{j}]=#{xtab_data[j][i]}"
          table[headers_width + i][headers_height + j] = xtab_data[j][i]
        end
      end
    end
  end
end