# -*- coding: binary -*-
require 'ipaddr'

module Rex
module Text

###
#
# Prints text in a tablized format.  Pretty lame at the moment, but
# whatever.
#
###
class Table

  # Temporary forking logic for using the prototype `WrappedTable` implementation.
  #
  # This method replaces the default `Table.new` with the ability to call the `WrappedTable` class instead,
  # to allow users to safely toggle between wrapped/unwrapped tables at a global level without changing
  # their existing codebases. This approach will reduce the risk of enabling wrapped table behavior by default.
  #
  # To enforce all tables to be wrapped to the terminal's current width, call `Table.wrap_tables!`
  # before invoking `Table.new` as normal.
  def self.new(*args, &block)
    return super(*args, &block) unless wrap_table?(args)

    table_options = args.first
    ::Rex::Text::WrappedTable.new(table_options)
  end

  def self.wrap_table?(args)
    wrapped_tables?
  end

  def self.wrap_tables!
    @@wrapped_tables_enabled = true
  end

  def self.unwrap_tables!
    @@wrapped_tables_enabled = false
  end

  def self.wrapped_tables?
    @@wrapped_tables_enabled ||= false
  end

  #
  # Initializes a text table instance using the supplied properties.  The
  # Table class supports the following hash attributes:
  #
  # Header
  #
  #	The string to display as a heading above the table.  If none is
  #	specified, no header will be displayed.
  #
  # HeaderIndent
  #
  # 	The amount of space to indent the header.  The default is zero.
  #
  # Columns
  #
  # 	The array of columns that will exist within the table.
  #
  # Rows
  #
  # 	The array of rows that will exist.
  #
  # Width
  #
  # 	The maximum width of the table in characters.
  #
  # Indent
  #
  # 	The number of characters to indent the table.
  #
  # CellPad
  #
  # 	The number of characters to put between each horizontal cell.
  #
  # Prefix
  #
  # 	The text to prefix before the table.
  #
  # Postfix
  #
  # 	The text to affix to the end of the table.
  #
  # Sortindex
  #
  #	The column to sort the table on, -1 disables sorting.
  #
  # ColProps
  #
  # A hash specifying column MaxWidth, Stylers, and Formatters.
  #
  def initialize(opts = {})
    self.header   = opts['Header']
    self.headeri  = opts['HeaderIndent'] || 0
    self.columns  = opts['Columns'] || []
    # updated below if we got a "Rows" option
    self.rows     = []

    self.width    = opts['Width']   || 80
    self.indent   = opts['Indent']  || 0
    self.cellpad  = opts['CellPad'] || 2
    self.prefix   = opts['Prefix']  || ''
    self.postfix  = opts['Postfix'] || ''
    self.colprops = []
    self.scterm   = /#{opts['SearchTerm']}/mi if opts['SearchTerm']

    self.sort_index  = opts['SortIndex'] || 0
    self.sort_order  = opts['SortOrder'] || :forward

    # Default column properties
    self.columns.length.times { |idx|
      self.colprops[idx] = {}
      self.colprops[idx]['MaxWidth'] = self.columns[idx].length
      self.colprops[idx]['Stylers'] = []
      self.colprops[idx]['Formatters'] = []
    }

    # ensure all our internal state gets updated with the given rows by
    # using add_row instead of just adding them to self.rows.  See #3825.
    opts['Rows'].each { |row| add_row(row) } if opts['Rows']

    # Merge in options
    if (opts['ColProps'])
      opts['ColProps'].each_key { |col|
        idx = self.columns.index(col)

        if (idx)
          self.colprops[idx].merge!(opts['ColProps'][col])
        end
      }
    end

  end

  #
  # Converts table contents to a string.
  #
  def to_s
    str  = prefix.dup
    str << header_to_s || ''
    str << columns_to_s || ''
    str << hr_to_s || ''

    sort_rows
    rows.each { |row|
      if (is_hr(row))
        str << hr_to_s
      else
        str << row_to_s(row) if row_visible(row)
      end
    }

    str << postfix

    return str
  end

  #
  # Converts table contents to a csv
  #
  def to_csv
    str = ''
    str << ( columns.join(",") + "\n" )
    rows.each { |row|
      next if is_hr(row) || !row_visible(row)
      str << ( row.map{|x|
        x = x.to_s
        x.gsub(/[\r\n]/, ' ').gsub(/\s+/, ' ').gsub('"', '""')
      }.map{|x| "\"#{x}\"" }.join(",") + "\n" )
    }
    str
  end

  #
  #
  # Returns the header string.
  #
  def header_to_s # :nodoc:
    if (header)
      pad = " " * headeri

      return pad + header + "\n" + pad + "=" * header.length + "\n\n"
    end

    return ''
  end

  #
  # Prints the contents of the table.
  #
  def print
    puts to_s
  end

  #
  # Adds a row using the supplied fields.
  #
  def <<(fields)
    add_row(fields)
  end

  #
  # Adds a row with the supplied fields.
  #
  def add_row(fields = [])
    if fields.length != self.columns.length
      raise RuntimeError, 'Invalid number of columns!'
    end
    formatted_fields = fields.map.with_index { |field, idx|
      # Remove whitespace and ensure String format
      field = format_table_field(field.to_s.strip, idx)

      if (colprops[idx]['MaxWidth'] < field.to_s.length)
        old = colprops[idx]['MaxWidth']
        colprops[idx]['MaxWidth'] = field.to_s.length
      end

      field
    }

    rows << formatted_fields
  end

  def ip_cmp(a, b)
    begin
      a = IPAddr.new(a.to_s)
      b = IPAddr.new(b.to_s)
      return 1  if a.ipv6? && b.ipv4?
      return -1 if a.ipv4? && b.ipv6?
      a <=> b
    rescue IPAddr::Error
      nil
    end
  end

  #
  # Sorts the rows based on the supplied index of sub-arrays
  # If the supplied index is an IPv4 address, handle it differently, but
  # avoid actually resolving domain names.
  #
  def sort_rows(index = sort_index, order = sort_order)
    return if index == -1
    return unless rows
    rows.sort! do |a,b|
      if a[index].nil?
        cmp = -1
      elsif b[index].nil?
        cmp = 1
      elsif a[index] =~ /^[0-9]+$/ and b[index] =~ /^[0-9]+$/
        cmp = a[index].to_i <=> b[index].to_i
      elsif (cmp = ip_cmp(a[index], b[index])) != nil
      else
        cmp = a[index] <=> b[index] # assumes otherwise comparable.
      end
      cmp ||= 0
      order == :forward ? cmp : -cmp
    end
  end

  #
  # Adds a horizontal line.
  #
  def add_hr
    rows << '__hr__'
  end

  #
  # Returns new sub-table with headers and rows maching column names submitted
  #
  #
  # Flips table 90 degrees left
  #
  def drop_left
    tbl = self.class.new(
      'Columns' => Array.new(self.rows.count+1,'  '),
      'Header' => self.header,
      'Indent' => self.indent)
    (self.columns.count+1).times do |ti|
      row = self.rows.map {|r| r[ti]}.unshift(self.columns[ti]).flatten
      # insert our col|row break. kind of hackish
      row[1] = "| #{row[1]}" unless row.all? {|e| e.nil? || e.empty?}
      tbl << row
    end
    return tbl
  end

  def valid_ip?(value)
    begin
      IPAddr.new value
      true
    rescue IPAddr::Error
      false
    end
  end

  #
  # Build table from CSV dump
  #
  def self.new_from_csv(csv)
    # Read in or keep data, get CSV or die
    if csv.is_a?(String)
      csv = File.file?(csv) ? CSV.read(csv) : CSV.parse(csv)
    end
    # Adjust for skew
    if csv.first == ["Keys", "Values"]
      csv.shift # drop marker
      cols = []
      rows = []
      csv.each do |row|
        cols << row.shift
        rows << row
      end
      tbl = self.new('Columns' => cols)
      rows.in_groups_of(cols.count) {|r| tbl << r.flatten}
    else
      tbl = self.new('Columns' => csv.shift)
      while !csv.empty? do
        tbl << csv.shift
      end
    end
    return tbl
  end

  def [](*col_names)
    tbl = self.class.new('Indent' => self.indent,
                         'Header' => self.header,
                         'Columns' => col_names)
    indexes = []

    col_names.each do |col_name|
      index = self.columns.index(col_name)
      raise RuntimeError, "Invalid column name #{col_name}" if index.nil?
      indexes << index
    end

    self.rows.each do |old_row|
      new_row = []
      indexes.map {|i| new_row << old_row[i]}
      tbl << new_row
    end

    return tbl
  end


  alias p print

  attr_accessor :header, :headeri # :nodoc:
  attr_accessor :columns, :rows, :colprops # :nodoc:
  attr_accessor :width, :indent, :cellpad # :nodoc:
  attr_accessor :prefix, :postfix # :nodoc:
  attr_accessor :sort_index, :sort_order, :scterm # :nodoc:

protected

  #
  # Returns if a row should be visible or not
  #
  def row_visible(row)
    return true if self.scterm.nil?
    row_to_s(row).match(self.scterm)
  end

  #
  # Defaults cell widths and alignments.
  #
  def defaults # :nodoc:
    self.columns.length.times { |idx|
    }
  end

  #
  # Checks to see if the row is an hr.
  #
  def is_hr(row) # :nodoc:
    return ((row.kind_of?(String)) && (row == '__hr__'))
  end

  #
  # Converts the columns to a string.
  #
  def columns_to_s # :nodoc:
    nameline = ' ' * indent
    barline  = nameline.dup
    last_col = nil
    last_idx = nil
    columns.each_with_index { |col,idx|
      if (last_col)
        # This produces clean to_s output without truncation
        # Preserves full string in cells for to_csv output
        padding = pad(' ', last_col, last_idx)
        nameline << padding
        remainder = padding.length - cellpad
        remainder = 0 if remainder < 0
        barline << (' ' * (cellpad + remainder))
      end

      nameline << col
      barline << ('-' * col.length)

      last_col = col
      last_idx = idx
    }

    return "#{nameline}\n#{barline}"
  end

  #
  # Converts an hr to a string.
  #
  def hr_to_s # :nodoc:
    return "\n"
  end

  #
  # Converts a row to a string.
  #
  def row_to_s(row) # :nodoc:
    line = ' ' * indent
    last_cell = nil
    last_idx = nil
    row.each_with_index { |cell, idx|
      if (idx != 0)
        line << pad(' ', last_cell.to_s, last_idx)
      end
      # Limit wide cells
      if colprops[idx]['MaxChar']
        last_cell = cell.to_s[0..colprops[idx]['MaxChar'].to_i]
        line << style_table_field(last_cell, idx)
      else
        line << style_table_field(cell.to_s, idx)
        last_cell = cell
      end
      last_idx = idx
    }

    return line + "\n"
  end

  #
  # Pads out with the supplied character for the remainder of the space given
  # some text and a column index.
  #
  def pad(chr, buf, colidx, use_cell_pad = true) # :nodoc:
    # Ensure we pad the minimum required amount
    max = colprops[colidx]['MaxChar'] || colprops[colidx]['MaxWidth']
    max = colprops[colidx]['MaxWidth'] if max.to_i > colprops[colidx]['MaxWidth'].to_i
    encoding = buf.encoding.name
    if not ["UTF-8", "ASCII-8BIT", "US-ASCII"].include? encoding
      warn '**WARNING** In file ' << __FILE__ << '::' <<\
        __method__.to_s << ': String with unsupported encoding caught!'
    end
    utf8_buf = buf.dup.force_encoding("UTF-8")
    if !utf8_buf.valid_encoding?
      hans_size = 0
    else
      hans_size = utf8_buf.size - utf8_buf.gsub(/\p{Han}+/u, '').size
    end
    remainder = max - utf8_buf.length - hans_size
    remainder = 0 if remainder < 0
    val       = chr * remainder

    if (use_cell_pad)
      val << ' ' * cellpad
    end

    return val
  end

  def format_table_field(str, idx)
    str_cp = str.clone

    colprops[idx]['Formatters'].each do |f|
      str_cp = f.format(str_cp)
    end

    str_cp
  end

  def style_table_field(str, idx)
    str_cp = str.clone

    colprops[idx]['Stylers'].each do |s|
      str_cp = s.style(str_cp)
    end

    str_cp
  end

end

end
end

