#include <output/GnuplotWriter.hpp>

#include <mesh/Connectivity.hpp>
#include <mesh/ItemValue.hpp>
#include <mesh/Mesh.hpp>
#include <mesh/MeshData.hpp>
#include <mesh/MeshDataManager.hpp>
#include <utils/Messenger.hpp>
#include <utils/PugsTraits.hpp>
#include <utils/RevisionInfo.hpp>

#include <utils/Demangle.hpp>

#include <fstream>
#include <iomanip>

std::string
GnuplotWriter::_getDateAndVersionComment() const
{
  std::ostringstream os;

  std::time_t now = std::time(nullptr);
  os << "#  Generated by pugs: " << std::ctime(&now);
  os << "#  version: " << RevisionInfo::version() << '\n';
  os << "#  tag:  " << RevisionInfo::gitTag() << '\n';
  os << "#  HEAD: " << RevisionInfo::gitHead() << '\n';
  os << "#  hash: " << RevisionInfo::gitHash() << " (" << ((RevisionInfo::gitIsClean()) ? "clean" : "dirty") << ")\n";
  os << '\n';

  return os.str();
}

std::string
GnuplotWriter::_getFilename() const
{
  std::ostringstream sout;
  sout << m_base_filename;
  sout << '.' << std::setfill('0') << std::setw(4) << m_saved_times.size() << ".gnu";
  return sout.str();
}

template <size_t Dimension>
void
GnuplotWriter::_writePreamble(const OutputNamedItemValueSet& output_named_item_value_set, std::ostream& fout) const
{
  fout << "# list of data\n";
  fout << "# 1:x";
  if constexpr (Dimension > 1) {
    fout << " 2:y";
  }
  uint64_t i = Dimension + 1;
  for (const auto& i_named_item_value : output_named_item_value_set) {
    const std::string name         = i_named_item_value.first;
    const auto& item_value_variant = i_named_item_value.second;
    std::visit(
      [&](auto&& item_value) {
        using ItemValueType = std::decay_t<decltype(item_value)>;
        using DataType      = std::decay_t<typename ItemValueType::data_type>;
        if constexpr (std::is_arithmetic_v<DataType>) {
          fout << ' ' << i++ << ':' << name;
        } else if constexpr (is_tiny_vector_v<DataType>) {
          for (size_t j = 0; j < DataType{}.dimension(); ++j) {
            fout << ' ' << i++ << ':' << name << '[' << j << ']';
          }
        } else if constexpr (is_tiny_matrix_v<DataType>) {
          for (size_t j = 0; j < DataType{}.nbRows(); ++j) {
            for (size_t k = 0; k < DataType{}.nbColumns(); ++k) {
              fout << ' ' << i++ << ':' << name << '(' << j << ',' << k << ')';
            }
          }
        } else {
          throw UnexpectedError("invalid data type");
        }
      },
      item_value_variant);
  }
  fout << "\n\n";
}

template <typename DataType>
void
GnuplotWriter::_writeCellValue(const CellValue<DataType>& cell_value, CellId cell_id, std::ostream& fout) const
{
  const auto& value = cell_value[cell_id];
  if constexpr (std::is_arithmetic_v<DataType>) {
    fout << ' ' << value;
  } else if constexpr (is_tiny_vector_v<std::decay_t<DataType>>) {
    for (size_t i = 0; i < value.dimension(); ++i) {
      fout << ' ' << value[i];
    }
  } else if constexpr (is_tiny_matrix_v<std::decay_t<DataType>>) {
    for (size_t i = 0; i < value.nbRows(); ++i) {
      for (size_t j = 0; j < value.nbColumns(); ++j) {
        fout << ' ' << value(i, j);
      }
    }
  } else {
    throw UnexpectedError("invalid data type for cell value output: " + demangle<DataType>());
  }
}

template <typename DataType, ItemType item_type>
void
GnuplotWriter::_writeValue(const ItemValue<DataType, item_type>& item_value,
                           [[maybe_unused]] CellId cell_id,
                           [[maybe_unused]] NodeId node_id,
                           std::ostream& fout) const
{
  if constexpr (item_type == ItemType::cell) {
    this->_writeCellValue(item_value, cell_id, fout);
  } else if constexpr (item_type == ItemType::node) {
    this->_writeNodeValue(item_value, node_id, fout);
  } else {
    throw UnexpectedError{"invalid item type"};
  }
}

template <typename MeshType>
void
GnuplotWriter::_writeValues(const std::shared_ptr<const MeshType>& mesh,
                            const OutputNamedItemValueSet& output_named_item_value_set,
                            std::ostream& fout) const
{
  if constexpr (MeshType::Dimension == 1) {
    auto cell_to_node_matrix = mesh->connectivity().cellToNodeMatrix();
    auto cell_is_owned       = mesh->connectivity().cellIsOwned();

    for (CellId cell_id = 0; cell_id < mesh->numberOfCells(); ++cell_id) {
      if (cell_is_owned[cell_id]) {
        const auto& cell_nodes = cell_to_node_matrix[cell_id];
        for (size_t i_node = 0; i_node < cell_nodes.size(); ++i_node) {
          const NodeId& node_id                     = cell_nodes[i_node];
          const TinyVector<MeshType::Dimension>& xr = mesh->xr()[node_id];
          fout << xr[0];
          for (auto [name, item_value] : output_named_item_value_set) {
            std::visit([&](auto&& cell_value) { _writeValue(cell_value, cell_id, node_id, fout); }, item_value);
          }
          fout << '\n';
        }

        fout << "\n\n";
      }
    }
  } else if constexpr (MeshType::Dimension == 2) {
    auto cell_to_node_matrix = mesh->connectivity().cellToNodeMatrix();
    auto cell_is_owned       = mesh->connectivity().cellIsOwned();

    for (CellId cell_id = 0; cell_id < mesh->numberOfCells(); ++cell_id) {
      if (cell_is_owned[cell_id]) {
        const auto& cell_nodes = cell_to_node_matrix[cell_id];
        for (size_t i_node = 0; i_node < cell_nodes.size(); ++i_node) {
          const NodeId& node_id                     = cell_nodes[i_node];
          const TinyVector<MeshType::Dimension>& xr = mesh->xr()[node_id];
          fout << xr[0] << ' ' << xr[1];
          for (auto [name, item_value] : output_named_item_value_set) {
            std::visit([&](auto&& cell_value) { _writeValue(cell_value, cell_id, node_id, fout); }, item_value);
          }
          fout << '\n';
        }
        const NodeId& node_id                     = cell_nodes[0];
        const TinyVector<MeshType::Dimension>& xr = mesh->xr()[node_id];
        fout << xr[0] << ' ' << xr[1];
        for (auto [name, item_value] : output_named_item_value_set) {
          std::visit([&](auto&& cell_value) { _writeValue(cell_value, cell_id, node_id, fout); }, item_value);
        }
        fout << "\n\n\n";
      }
    }
  } else {
    throw UnexpectedError("invalid mesh dimension");
  }
}

template <typename ValueType>
void
GnuplotWriter::_writeNodeValue(const NodeValue<ValueType>& node_value, NodeId node_id, std::ostream& fout) const
{
  const auto& value = node_value[node_id];
  if constexpr (std::is_arithmetic_v<ValueType>) {
    fout << ' ' << value;
  } else if constexpr (is_tiny_vector_v<std::decay_t<ValueType>>) {
    for (size_t i = 0; i < value.dimension(); ++i) {
      fout << ' ' << value[i];
    }
  } else if constexpr (is_tiny_matrix_v<std::decay_t<ValueType>>) {
    for (size_t i = 0; i < value.nbRows(); ++i) {
      for (size_t j = 0; j < value.nbColumns(); ++j) {
        fout << ' ' << value(i, j);
      }
    }
  } else {
    throw UnexpectedError("invalid data type for cell value output: " + demangle<ValueType>());
  }
}

template <typename MeshType>
void
GnuplotWriter::_write(const std::shared_ptr<const MeshType>& mesh,
                      const OutputNamedItemValueSet& output_named_item_value_set,
                      double time) const
{
  if (parallel::rank() == 0) {
    std::ofstream fout{_getFilename()};
    fout.precision(15);
    fout.setf(std::ios_base::scientific);

    fout << this->_getDateAndVersionComment();
    fout << "# time = " << time << "\n\n";

    this->_writePreamble<MeshType::Dimension>(output_named_item_value_set, fout);
  }

  for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
    if (i_rank == parallel::rank()) {
      std::ofstream fout(_getFilename(), std::ios_base::app);
      fout.precision(15);
      fout.setf(std::ios_base::scientific);

      this->_writeValues(mesh, output_named_item_value_set, fout);
    }
    parallel::barrier();
  }
}

void
GnuplotWriter::writeMesh(const std::shared_ptr<const IMesh>& p_mesh) const
{
  OutputNamedItemValueSet output_named_item_value_set{};

  switch (p_mesh->dimension()) {
  case 1: {
    this->_write(std::dynamic_pointer_cast<const Mesh<Connectivity<1>>>(p_mesh), output_named_item_value_set, 0);
    break;
  }
  case 2: {
    this->_write(std::dynamic_pointer_cast<const Mesh<Connectivity<2>>>(p_mesh), output_named_item_value_set, 0);
    break;
  }
  default: {
    throw NormalError("gnuplot format is not available in dimension " + std::to_string(p_mesh->dimension()));
  }
  }
}

void
GnuplotWriter::write(const std::vector<std::shared_ptr<const NamedDiscreteFunction>>& named_discrete_function_list,
                     double time) const
{
  std::shared_ptr mesh = this->_getMesh(named_discrete_function_list);

  OutputNamedItemValueSet output_named_item_value_set = this->_getOutputNamedItemValueSet(named_discrete_function_list);

  switch (mesh->dimension()) {
  case 1: {
    this->_write(std::dynamic_pointer_cast<const Mesh<Connectivity<1>>>(mesh), output_named_item_value_set, time);
    break;
  }
  case 2: {
    this->_write(std::dynamic_pointer_cast<const Mesh<Connectivity<2>>>(mesh), output_named_item_value_set, time);
    break;
  }
  default: {
    throw NormalError("gnuplot format is not available in dimension " + std::to_string(mesh->dimension()));
  }
  }
}