// inspired by github.com/dattanchu/bprinter

//#pragma once

#include <map>
#include <vector>
#include <string>
#include <algorithm>
#include <numeric>
#include <stdexcept>
#include <sstream>

#ifdef auto_table_USE_SCOPED_COLOR
#include "scoped_color.h"
#endif

namespace auto_table {

template <typename K, typename V>
struct sum_values {
  V operator()(V i, const std::pair<const K, V>& x) { return i + x.second; }
};

struct endl {};

template <typename my_stream_t = std::stringstream>
class auto_table {
  my_stream_t my_stream;
  typedef std::vector<std::string> row_t;
  typedef std::vector<row_t> rows_t;
  typedef std::map<size_t, size_t> column_widths_t;
  column_widths_t column_widths;
  rows_t rows;
  size_t header_every_nth_row;
  size_t horizontal_padding;

 public:
  auto_table() : header_every_nth_row(0), horizontal_padding(0) {}

 private:
  static size_t width(std::string const& s) { return s.length(); }

  static size_t combine_width(size_t one_width, size_t another_width) {
    return std::max(one_width, another_width);
  }

 private:
  template <typename stream_t>
  void print_horizontal_line(stream_t& stream) {
    stream << '+';
    size_t sum = std::accumulate(column_widths.begin(), column_widths.end(), 0,
                                 sum_values<size_t, size_t>());
    for (size_t i = 0;
         i < sum + column_widths.size() +
                 2 * column_widths.size() * horizontal_padding - 1;
         i++)
      stream << '-';
    stream << "+\n";
  }

  std::string pad_column(std::string const& s, size_t column) {
    size_t s_width = width(s);
    size_t column_width = column_widths[column];
    if (s_width > column_width) return s;

    return std::string(column_width - s_width + horizontal_padding, ' ') + s +
           std::string(horizontal_padding, ' ');
  }

  template <typename stream_t>
  void print_row(size_t r, stream_t& stream, int color = 7) {
    size_t column_count = column_widths.size();
    row_t const& row = rows[r];
    size_t header_count = row.size();

    for (size_t i = 0; i < header_count; i++) {
      stream << '|';
#ifdef auto_table_USE_SCOPED_COLOR
      scoped_console_color col(color);
#endif
      stream << pad_column(row[i], i);
    }

    for (size_t i = header_count; i < column_count; i++)
      stream << '|' << pad_column("", i);

    stream << "|\n";
  }

  template <typename stream_t>
  void print_header(stream_t& stream) {
    print_horizontal_line(stream);
    if (rows.size() > 0) {
      print_row(0, stream, 10);
      print_horizontal_line(stream);
    }
  }

  template <typename stream_t>
  void print_rows(stream_t& stream) {
    size_t row_count = rows.size();

    if (row_count == 0) return;

    for (size_t row = 1; row < row_count - 1; row++) {
      if (row > 1 && header_every_nth_row &&
          (row - 1) % header_every_nth_row == 0)
        print_header(stream);

      print_row(row, stream, 15);
    }

    if (rows[row_count - 1].size() > 0) print_row(row_count - 1, stream, 15);
  }

  template <typename stream_t>
  void print_footer(stream_t& stream) {
    print_horizontal_line(stream);
  }

 public:
  auto_table& add_column(std::string const& name, size_t min_width = 0) {
    size_t new_width = combine_width(width(name), min_width);
    column_widths[column_widths.size()] = new_width;

    if (rows.size() < 1) rows.push_back(row_t());

    rows.front().push_back(name);
    return *this;
  }

  auto_table& with_header_every_nth_row(size_t n) {
    header_every_nth_row = n;
    return *this;
  }

  auto_table& with_horizontal_padding(size_t n) {
    horizontal_padding = n;
    return *this;
  }

  auto_table& operator<<(::auto_table::endl const& input) {
    rows.push_back(row_t());
    return *this;
  }

  template <typename TPar>
  auto_table& operator<<(TPar const& input) {
    if (column_widths.size() == 0)
      throw std::runtime_error("no columns defined!");

    if (rows.size() < 1) rows.push_back(row_t());

    if (rows.back().size() >= column_widths.size()) rows.push_back(row_t());

    my_stream << input;
    std::string entry(my_stream.str());
    size_t column = rows.back().size();
    size_t new_width = combine_width(width(entry), column_widths[column]);
    column_widths[column] = new_width;

    rows.back().push_back(entry);

    my_stream.str("");

    return *this;
  }

  template <typename stream_t>
  void print(stream_t& stream) {
    this->print_header(stream);
    this->print_rows(stream);
    this->print_footer(stream);
  }

  my_stream_t& get_stream() { return my_stream; }
};

typedef auto_table<> printer;
}

#include <random>
#include <iostream>

int main() {
  auto_table::printer tp;
  tp
    .add_column("Name")
    .add_column("Age")
    .add_column("Position")
    .add_column("Allowance")
    .with_header_every_nth_row(3)
    .with_horizontal_padding(1)
  ;

  tp << "Dat Chu" << 25 << "Research Assistant" << -0.00000000001337;
  tp << "John Doe" << 26 << "Too much float" << 125456789.123456789;
  tp << "John Doe" << 26 << "Typical Int" << 1254;
  tp << "John Doe" << 26 << "Typical float" << 1254.36;
  tp << "John Doe" << 26 << "Too much negative" << -125456789.123456789;
  tp << "John Doe" << 26 << "Exact size int" << 125456789;
  tp << "John Doe" << 26 << "Exact size int" << -12545678;
  tp << "John Doe" << 26 << "Exact size float" << -1254567.8;
  tp << "John Doe" << 26 << "Negative Int" << -1254;
  tp << "Jane Doe" << auto_table::endl();
  tp << "Tom Doe" << 7 << "Student" << -3.14;
  
  tp.print(std::cout);

  tp
    .with_header_every_nth_row(0)
    .with_horizontal_padding(0)
    .print(std::cout)
  ;

}
