/**
 * \file sdk/load-and-run/src/text_table.h
 * MegEngine is Licensed under the Apache License, Version 2.0 (the "License")
 *
 * Copyright (c) 2014-2021 Megvii Inc. All rights reserved.
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied.
 */

#pragma once

#include <array>
#include <iomanip>
#include <ostream>
#include <sstream>
#include <string>
#include <tuple>
#include <type_traits>
#include <vector>

namespace helper {

class TextTable {
 public:
  enum Level { Summary, Detail };
  enum class Align : int { Left, Right, Mid };
  TextTable() = default;
  explicit TextTable(const std::string& table_name) : m_name(table_name) {}
  TextTable& horizontal(char c) {
    m_row.params.horizontal = c;
    return *this;
  }
  TextTable& vertical(char c) {
    m_row.params.vertical = c;
    return *this;
  }
  TextTable& corner(char c) {
    m_row.params.corner = c;
    return *this;
  }
  TextTable& align(Align v) {
    m_row.params.align = v;
    return *this;
  }
  TextTable& padding(size_t w) {
    m_padding = w;
    return *this;
  }
  TextTable& prefix(const std::string& str) {
    m_prefix = str;
    return *this;
  }

  template <typename T>
  TextTable& add(const T& value) {
    m_row.values.emplace_back(value);
    if (m_cols_max_w.size() < m_row.values.size()) {
      m_cols_max_w.emplace_back(m_row.values.back().length());
    } else {
      size_t i = m_row.values.size() - 1;
      m_cols_max_w[i] = std::max(m_cols_max_w[i], m_row.values.back().length());
    }
    return *this;
  }

  template <typename T, typename std::enable_if<std::is_floating_point<T>::value, bool>::type = 0>
  TextTable& add(const T& value) {
    std::stringstream ss;
    ss << std::setiosflags(std::ios::fixed) << std::setprecision(2);
    ss << value;
    m_row.values.emplace_back(ss.str());
    if (m_cols_max_w.size() < m_row.values.size()) {
      m_cols_max_w.emplace_back(m_row.values.back().length());
    } else {
      size_t i = m_row.values.size() - 1;
      m_cols_max_w[i] = std::max(m_cols_max_w[i], m_row.values.back().length());
    }
    return *this;
  }

  template <typename T, typename std::enable_if<std::is_integral<T>::value, bool>::type = 0>
  TextTable& add(const T& value) {
    m_row.values.emplace_back(std::to_string(value));
    return *this;
  }

  void eor() {
    m_rows.emplace_back(m_row);
    adjuster_last_row();
    m_row.values.clear();
  }

  void reset() {
    m_row = {};
    m_cols_max_w.clear();
    m_padding = 0;
    m_rows.clear();
  }

  void show(std::ostream& os) {
    if (m_rows.empty()) return;
    auto& last_row = m_rows.front();
    bool first = true;
    for (auto& row : m_rows) {
      auto& lrow = (last_row.values.size() * char_length(last_row.params.horizontal)) >
                           (row.values.size() * char_length(row.params.horizontal))
                       ? last_row
                       : row;
      // line before row
      if (lrow.params.horizontal) {
        if (not first) os << std::endl;
        os << m_prefix;
        if (lrow.params.corner) os << lrow.params.corner;
        size_t skip_size = 0;
        // table name
        if (first) {
          os << m_name;
          skip_size = m_name.length();
        }
        for (size_t i = 0; i < lrow.values.size(); ++i) {
          auto max_w = m_cols_max_w.at(i) + m_padding * 2;
          if (max_w + char_length(lrow.params.corner) <= skip_size) {
            skip_size = skip_size - max_w - char_length(lrow.params.corner);
            continue;
          }
          size_t rest = max_w + char_length(lrow.params.corner) - skip_size;
          skip_size = 0;
          if (rest > char_length(lrow.params.corner)) {
            os << std::string(rest - char_length(lrow.params.corner), lrow.params.horizontal);
            rest = char_length(lrow.params.corner);
          }
          if (rest > 0 && lrow.params.corner) os << lrow.params.corner;
        }
      } else if (first) {
        os << m_prefix << ' ' << m_name;
      }
      first = false;
      os << std::endl << m_prefix;
      if (row.params.vertical) os << row.params.vertical;
      // row
      for (size_t i = 0; i < row.values.size(); ++i) {
        auto& str = row.values.at(i);
        auto max_w = m_cols_max_w.at(i) + 2 * m_padding;
        if (row.params.align == Align::Mid) {
          mid(os, str, max_w);
        } else if (row.params.align == Align::Left) {
          os << std::setw(max_w) << std::left << str;
        } else {
          os << std::setw(max_w) << std::right << str;
        }
        if (row.params.vertical) os << row.params.vertical;
      }
      last_row = row;
    }
    if (last_row.params.horizontal) {
      os << std::endl << m_prefix;
      if (last_row.params.corner) os << last_row.params.corner;
      for (size_t i = 0; i < last_row.values.size(); ++i) {
        auto max_w = m_cols_max_w.at(i);
        std::string tmp(max_w + m_padding * 2, last_row.params.horizontal);
        os << tmp;
        if (last_row.params.corner) os << last_row.params.corner;
      }
    }
  }

 private:
  void adjuster_last_row() {
    if (m_rows.empty()) return;
    auto& row = m_rows.back();
    if (row.params.horizontal == 0 or row.params.vertical == 0) {
      row.params.corner = 0;
    }
    if (row.params.horizontal != 0 && row.params.vertical != 0 && row.params.corner == 0) {
      row.params.corner = row.params.horizontal;
    }
  }

  inline void mid(std::ostream& os, const std::string& str, size_t max_w) {
    size_t l = (max_w - str.length()) / 2 + str.length();
    size_t r = max_w - l;
    os << std::setw(l) << std::right << str;
    if (r > 0) os << std::setw(r) << ' ';
  }
  inline size_t char_length(char c) { return c ? 1 : 0; }
  std::string m_name;
  std::vector<size_t> m_cols_max_w;
  size_t m_padding = 0;
  std::string m_prefix = "";
  struct Row {
    std::vector<std::string> values;
    struct Params {
      Align align = Align::Left;
      char horizontal = '-', vertical = '|', corner = '+';
    } params;
  };
  std::vector<Row> m_rows;
  Row m_row;
};

inline std::ostream& operator<<(std::ostream& stream, TextTable& table) {
  table.show(stream);
  return stream;
}

}  // namespace helper
