module PivotTable
  class Grid

    attr_accessor :source_data, :row_name, :column_name, :value_name, :field_name
    attr_reader :columns, :rows, :data_grid, :configuration

    DEFAULT_OPTIONS = {
      :sort => true
    }

    def initialize(opts = {}, &block)
      yield(self) if block_given?
      @configuration = Configuration.new(DEFAULT_OPTIONS.merge(opts))
    end

    def build
      populate_grid
      build_rows
      build_columns
      self
    end

    def build_rows
      @rows = []
      data_grid.each_with_index do |data, index|
        rows << Row.new(
          header: row_headers[index],
          data: data,
          value_name: value_name,
          orthogonal_headers: column_headers
        )
      end
    end

    def build_columns
      @columns = []
      data_grid.transpose.each_with_index do |data, index|
        columns << Column.new(
          header: column_headers[index],
          data: data,
          value_name: value_name,
          orthogonal_headers: row_headers
        )
      end
    end

    def column_headers
      @column_headers ||= headers column_name
    end

    def row_headers
      @row_headers ||= headers row_name
    end

    def column_totals
      columns.map { |c| c.total }
    end

    def row_totals
      rows.map { |r| r.total }
    end

    def grand_total
      column_totals.inject(0) { |t, x| t + x }
    end

    def prepare_grid
      @data_grid = []
      row_headers.count.times do
        data_grid << column_headers.count.times.inject([]) { |col| col << nil }
      end
      data_grid
    end

    def populate_grid
      prepare_grid
      row_headers.each_with_index do |row, row_index|
        data_grid[row_index] = build_data_row(row)
      end
      data_grid
    end

    private

    def headers(method)
      hdrs = source_data.collect { |c| c.send method }.uniq
      configuration.sort ? hdrs.sort : hdrs
    end

    def build_data_row(row)
      current_row = []
      column_headers.each_with_index do |col, col_index|
        current_row[col_index] = derive_row_value(row, col)
      end
      current_row
    end

    def find_data_item(row, col)
      source_data.find do |item|
        item.send(row_name) == row && item.send(column_name) == col
      end
    end

    def derive_row_value(row, col)
      data_item = find_data_item(row, col)
      if has_field_name?(data_item)
        data_item.send(field_name)
      else
        data_item
      end
    end

    def has_field_name?(data_item)
      !!(field_name && data_item.respond_to?(field_name))
    end
  end
end
