// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.

#include <chrono>
#include <utility>
#include <vector>

#include <fmt/chrono.h>
#include <fmt/format.h>
#include <fmt/ranges.h>

#include "wpi/datalog/DataLogReader.hpp"
#include "wpi/util/DenseMap.hpp"
#include "wpi/util/MemoryBuffer.hpp"
#include "wpi/util/print.hpp"

int main(int argc, const char** argv) {
  if (argc != 2) {
    wpi::util::print(stderr, "Usage: printlog <file>\n");
    return EXIT_FAILURE;
  }
  auto fileBuffer = wpi::util::MemoryBuffer::GetFile(argv[1]);
  if (!fileBuffer) {
    wpi::util::print(stderr, "could not open file: {}\n",
                     fileBuffer.error().message());
    return EXIT_FAILURE;
  }
  wpi::log::DataLogReader reader{std::move(*fileBuffer)};
  if (!reader) {
    wpi::util::print(stderr, "not a log file\n");
    return EXIT_FAILURE;
  }

  wpi::util::DenseMap<int, wpi::log::StartRecordData> entries;
  for (auto&& record : reader) {
    if (record.IsStart()) {
      wpi::log::StartRecordData data;
      if (record.GetStartData(&data)) {
        wpi::util::print(
            "Start({}, name='{}', type='{}', metadata='{}') [{}]\n", data.entry,
            data.name, data.type, data.metadata,
            record.GetTimestamp() / 1000000.0);
        if (entries.find(data.entry) != entries.end()) {
          wpi::util::print("...DUPLICATE entry ID, overriding\n");
        }
        entries[data.entry] = data;
      } else {
        wpi::util::print("Start(INVALID)\n");
      }
    } else if (record.IsFinish()) {
      int entry;
      if (record.GetFinishEntry(&entry)) {
        wpi::util::print("Finish({}) [{}]\n", entry,
                         record.GetTimestamp() / 1000000.0);
        auto it = entries.find(entry);
        if (it == entries.end()) {
          wpi::util::print("...ID not found\n");
        } else {
          entries.erase(it);
        }
      } else {
        wpi::util::print("Finish(INVALID)\n");
      }
    } else if (record.IsSetMetadata()) {
      wpi::log::MetadataRecordData data;
      if (record.GetSetMetadataData(&data)) {
        wpi::util::print("SetMetadata({}, '{}') [{}]\n", data.entry,
                         data.metadata, record.GetTimestamp() / 1000000.0);
        auto it = entries.find(data.entry);
        if (it == entries.end()) {
          wpi::util::print("...ID not found\n");
        } else {
          it->second.metadata = data.metadata;
        }
      } else {
        wpi::util::print("SetMetadata(INVALID)\n");
      }
    } else if (record.IsControl()) {
      wpi::util::print("Unrecognized control record\n");
    } else {
      wpi::util::print("Data({}, size={}) ", record.GetEntry(),
                       record.GetSize());
      auto entry = entries.find(record.GetEntry());
      if (entry == entries.end()) {
        wpi::util::print("<ID not found>\n");
        continue;
      }
      wpi::util::print("<name='{}', type='{}'> [{}]\n", entry->second.name,
                       entry->second.type, record.GetTimestamp() / 1000000.0);

      // handle systemTime specially
      if (entry->second.name == "systemTime" && entry->second.type == "int64") {
        int64_t val;
        if (record.GetInteger(&val)) {
          auto timeval = std::chrono::system_clock::time_point(
              std::chrono::microseconds(val));
          wpi::util::print("  {:%Y-%m-%d %H:%M:%S}.{:06}\n", timeval,
                           val % 1000000);
        } else {
          wpi::util::print("  invalid\n");
        }
        continue;
      }

      if (entry->second.type == "double") {
        double val;
        if (record.GetDouble(&val)) {
          wpi::util::print("  {}\n", val);
        } else {
          wpi::util::print("  invalid\n");
        }
      } else if (entry->second.type == "int64") {
        int64_t val;
        if (record.GetInteger(&val)) {
          wpi::util::print("  {}\n", val);
        } else {
          wpi::util::print("  invalid\n");
        }
      } else if (entry->second.type == "string" ||
                 entry->second.type == "json") {
        std::string_view val;
        record.GetString(&val);
        wpi::util::print("  '{}'\n", val);
      } else if (entry->second.type == "boolean") {
        bool val;
        if (record.GetBoolean(&val)) {
          wpi::util::print("  {}\n", val);
        } else {
          wpi::util::print("  invalid\n");
        }
      } else if (entry->second.type == "boolean[]") {
        std::vector<int> val;
        if (record.GetBooleanArray(&val)) {
          wpi::util::print("  {}\n", fmt::join(val, ", "));
        } else {
          wpi::util::print("  invalid\n");
        }
      } else if (entry->second.type == "double[]") {
        std::vector<double> val;
        if (record.GetDoubleArray(&val)) {
          wpi::util::print("  {}\n", fmt::join(val, ", "));
        } else {
          wpi::util::print("  invalid\n");
        }
      } else if (entry->second.type == "float[]") {
        std::vector<float> val;
        if (record.GetFloatArray(&val)) {
          wpi::util::print("  {}\n", fmt::join(val, ", "));
        } else {
          wpi::util::print("  invalid\n");
        }
      } else if (entry->second.type == "int64[]") {
        std::vector<int64_t> val;
        if (record.GetIntegerArray(&val)) {
          wpi::util::print("  {}\n", fmt::join(val, ", "));
        } else {
          wpi::util::print("  invalid\n");
        }
      } else if (entry->second.type == "string[]") {
        std::vector<std::string_view> val;
        if (record.GetStringArray(&val)) {
          wpi::util::print("  {}\n", fmt::join(val, ", "));
        } else {
          wpi::util::print("  invalid\n");
        }
      }
    }
  }
}
