# encoding: utf-8
module Terminal
  class Table
    
    attr_reader :title
    attr_reader :headings
    
    ##
    # Generates a ASCII table with the given _options_.
  
    def initialize options = {}, &block
      @column_widths = []
      self.style = options.fetch :style, {}
      self.headings = options.fetch :headings, []
      self.rows = options.fetch :rows, []
      self.title = options.fetch :title, nil
      yield_or_eval(&block) if block
    end
    
    ##
    # Align column _n_ to the given _alignment_ of :center, :left, or :right.
    
    def align_column n, alignment
      r = rows
      column(n).each_with_index do |col, i|
        cell = r[i][n]
        cell.alignment = alignment unless cell.alignment?
      end
    end
    
    ##
    # Add a row. 
    
    def add_row array
      row = array == :separator ? Separator.new(self) : Row.new(self, array)
      @rows << row
      recalc_column_widths row
    end
    alias :<< :add_row

    ##
    # Add a separator.

    def add_separator
      self << :separator
    end
    
    def cell_spacing
      cell_padding + style.border_y.length
    end
    
    def cell_padding
      style.padding_left + style.padding_right
    end
    
    ##
    # Return column _n_.
    
    def column n, method = :value, array = rows
      array.map { |row| 
        cell = row[n]
        cell && method ? cell.__send__(method) : cell
      }.compact 
    end
    
    ##
    # Return _n_ column including headings.
    
    def column_with_headings n, method = :value
      column n, method, headings_with_rows
    end
    
    ##
    # Return columns.
    
    def columns
      (0...number_of_columns).map { |n| column n } 
    end
    
    ##
    # Return length of column _n_.
    
    def column_width n
      width = @column_widths[n] || 0
      width + additional_column_widths[n].to_i
    end
    alias length_of_column column_width # for legacy support
    
    ##
    # Return total number of columns available.
     
    def number_of_columns
      headings_with_rows.map { |r| r.cells.size }.max
    end

    ##
    # Set the headings
    
    def headings= array
      @headings = Row.new(self, array)
      recalc_column_widths @headings
    end

    ##
    # Render the table.
  
    def render
      adjust_widths
      separator = Separator.new(self)
      buffer = [separator]
      unless @title.nil?
        buffer << Row.new(self, [title_cell_options])
        buffer << separator
      end
      unless @headings.cells.empty?
        buffer << @headings
        buffer << separator
      end
      buffer += @rows
      buffer << separator
      buffer.map { |r| r.render }.join("\n")
    end
    alias :to_s :render
    
    ##
    # Return rows without separator rows.

    def rows
      @rows.reject { |row| row.is_a? Separator }
    end
    
    def rows= array
      @rows = []
      array.each { |arr| self << arr }
    end

    def style=(options)
      style.apply options
    end
    
    def style
      @style ||= Style.new
    end
    
    def title=(title)
      @title = title
      recalc_column_widths Row.new(self, [title_cell_options])
    end
    
    ##
    # Check if _other_ is equal to self. _other_ is considered equal
    # if it contains the same headings and rows.

    def == other
      if other.respond_to? :render and other.respond_to? :rows
        self.headings == other.headings and self.rows == other.rows
      end
    end

    private
    
    def columns_width
      @column_widths.inject(0) { |s, i| s + i + cell_spacing } + style.border_y.length
    end

    def wrap_column(index, new_width)
      @rows.each do |row|
        row.wrap_cell(index, new_width)
      end
    end

    def adjust_widths
      return if style.wrap == false or style.width.nil? or style.width > columns_width

      # Make a column index to column size mapping, then sort it
      current_index = -1
      total_column_widths = @column_widths.map { |s| current_index +=1; [current_index, s + cell_spacing] }
      total_column_widths = total_column_widths.sort_by { |a,b| b }

      packed_length = 0
      current_index = 0
      # Pack the smallest first, but make sure the remaining space is enough for
      # the rest of the columns to have at least style.wrap_minimum_width spaces
      # to wrap into.
      while (style.width - (packed_length + total_column_widths[current_index][1] + style.border_y.length)) >
            (style.wrap_minimum_width * (total_column_widths.size - current_index - 1)) do
        packed_length += total_column_widths[current_index][1]
        current_index += 1
      end

      # Calculate the remaining space and figure out how big to wrap the other columns to
      remaining_space = style.width - packed_length - style.border_y.length
      trim_to = (remaining_space / (total_column_widths.size - current_index)) - cell_spacing
      trim_to -= (1 + style.padding_left + style.padding_right)
      if trim_to < 1
        raise "Cannot fit a #{total_column_widths.size} column table in width #{style.width}."
      end

      # The remaining columns are then wrapped
      (current_index...total_column_widths.size).each do |i|
        wrap_column(total_column_widths[i][0], trim_to)
        @column_widths[total_column_widths[i][0]] = trim_to + cell_spacing
      end
    end

    def additional_column_widths
      return [] if style.width.nil?
      spacing = style.width - columns_width
      if spacing < 0
        raise "Table width exceeds wanted width of #{style.width} characters."
      else
        per_col = spacing / number_of_columns
        arr = (1...number_of_columns).to_a.map { |i| per_col }
        other_cols = arr.inject(0) { |s, i| s + i }
        arr << spacing - other_cols
        arr
      end
    end

    def recalc_column_widths row
      return if row.is_a? Separator
      i = 0
      row.cells.each do |cell|
        colspan = cell.colspan
        cell_value = cell.value_for_column_width_recalc
        colspan.downto(1) do |j|
          cell_length = cell_value.to_s.length
          if colspan > 1
            spacing_length = cell_spacing * (colspan - 1)
            length_in_columns = (cell_length - spacing_length)
            cell_length = (length_in_columns.to_f / colspan).ceil
          end
          if @column_widths[i].to_i < cell_length
            @column_widths[i] = cell_length
          end
          i = i + 1
        end
      end
    end
    
    ##
    # Return headings combined with rows.
    
    def headings_with_rows
      [@headings] + rows
    end
    
    def yield_or_eval &block
      return unless block
      if block.arity > 0
        yield self
      else
        self.instance_eval(&block)
      end
    end

    def title_cell_options
      {:value => @title, :alignment => :center, :colspan => number_of_columns}
    end
  end
end
