/*
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2.1
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 */

#include <math.h>
#include <unistd.h>

#include <chrono>
#include <climits>
#include <csignal>
#include <deque>
#include <functional>
#include <iomanip>
#include <iostream>
#include <memory>
#include <numeric>
#include <thread>

#include "../sh4lt/follower.hpp"
#include "../sh4lt/logger/console.hpp"
#include "../sh4lt/utils/data-printer.hpp"

static const size_t max_frame_time_count{30};

using namespace sh4lt;

static std::unique_ptr<Follower> follower;
static bool debug = false;
static bool show_frame_timings = false;
static bool show_version = false;
static bool cartridge_return = false;
static auto max_val_displayed = 10u;
static std::string sockpath;
static std::string val_format = "x";
static bool print_info = true;
static std::deque<int64_t> frames_time;
static std::string separator = "";
static size_t precision = 3;
static size_t min_width = 0;
static sh4lt::utils::data_printer_t data_printer = nullptr;

void leave(int sig) {
  follower.reset();
  exit(sig);
}

void usage(const char* prog_name) {
  printf("usage: %s [OPTIONS] label [group]\n", prog_name);
  printf(R""""(
sh4lt-flow prints data from an existing Sh4lt.
It aims at providing a debugging tool with the Sh4lt library.

OPTIONS:
  -s path    print from a Sh4lt socket file instead of the label/group
  -c         print sh4lt type and then information about last frame (carriage return)
  -d         print debug
  -v         print Sh4lt version and exit
  -t         print frame timing information (delay and frame-per-seconds)
  -n         print data only (no info, no frame number, no size and no timing information)

DATA FORMATING OPTIONS:
  -m num     print first 'num' values for each frame. Default is 10 (0 for all values)
  -i sep     value separator 
  -f format  print values using 'format',
               'x'   for hexadecimal (default)
               'c'   for char
               'f'   for 32 bits float
               'd'   for 64 bits float
               'i8'  for 8 bits integer
               'i16' for 16 bits integer
               'i32' for 32 bits integer
               'i64' for 64 bits integer
               'ui8'  for unsigned 8 bits integer
               'ui16' for unsigned 16 bits integer
               'ui32' for unsigned 32 bits integer
               'ui64' for unsigned 64 bits integer
  -p size    precision for float (default to 3)
  -w size    minimum width for a printed value (default is 0 for 'disabled') 

Statistics:
ph: Playhead date (hours:minutes:seconds.milliseconds)
bd: Duration of the date placed in the buffer (milliseconds)
dT: Average time duration between successive buffers (milliseconds)
bps: Average number of buffer per second 
)"""");
  exit(1);
}

auto parse_options(int argc, char* argv[]) -> bool {
  opterr = 0;
  int c = 0;
  while ((c = getopt(argc, argv, "cdf:tvm:ns:i:p:w:")) != -1) switch (c) {
      case 'c':
        cartridge_return = true;
        break;
      case 'd':
        debug = true;
        break;
      case 'f':
        val_format = std::string(optarg);
        break;
      case 'i':
        separator = std::string(optarg);
        break;
      case 'm':
        max_val_displayed = static_cast<unsigned int>(atoi(optarg));
        break;
      case 'n':
        print_info = false;
        break;
      case 'p':
        precision = static_cast<size_t>(atoi(optarg));
        break;
      case 's':
        sockpath = std::string(optarg);
        break;
      case 't':
        show_frame_timings = true;
        break;
      case 'v':
        show_version = true;
        break;
      case 'w':
        min_width = static_cast<size_t>(atoi(optarg));
        break;
      case '?':
        break;
      default:
        return false;
    }

  if (show_version) {
    std::printf("%s\n", SH4LT_VERSION_STRING);
    exit(1);
  }
  if (precision < 1 || precision > 16) {
    std::cerr << "invalid precision \"" << precision << "\", min is 1, max is 16\n";
    return false;
  }
  std::string label;
  std::string group = ShType::default_group();
  if (sockpath.empty() && optind + 1 <= argc) {
    label = argv[optind];
    if (optind + 2 == argc) group = argv[optind + 1];
  }
  if (!label.empty() && !group.empty()) {
    sockpath = ShType::get_path(label, group);
    if (sockpath.empty()) {
      std::cerr << ShType::get_log_for_path_issue() << '\n';
      return false;
    }
  }
  if (sockpath.empty()) return false;
  if (print_info) {
    if (!label.empty())
      std::cout << "label \"" << label << "\" group \"" << group << "\" socket path \"" << sockpath
                << "\"\n";
    else
      std::cout << "socket path \"" << sockpath << "\"\n";
  }
  return true;
}

void on_data(void* data, size_t size, const sh4lt_time_info_t* info) {
  if (cartridge_return) std::cout << "\33[2K\r";
  if (print_info) {
    float frame_duration = 0.f;
    float framerate = 0.f;
    if (show_frame_timings) {
      frames_time.push_back(info->steady_clock_date / 1000000.f);

      while (frames_time.size() > max_frame_time_count) frames_time.pop_front();

      if (frames_time.size() > 1) {
        frame_duration = frames_time.back() - frames_time[frames_time.size() - 2];
        int64_t total_frames_duration = frames_time.back() - frames_time.front();
        float mean_time_per_frame =
            static_cast<float>(total_frames_duration) / static_cast<float>(frames_time.size() - 1);
        framerate = 1000.f / mean_time_per_frame;
      }
    }

    // print informations
    std::cout << info->buffer_number;

    if (show_frame_timings) {
      using namespace std::chrono;
      // writing playhead date
      if (info->buffer_date != -1) {
        auto date = nanoseconds(info->buffer_date);
        std::cout << " ph: " << duration_cast<hours>(date).count() << ":";
        std::cout << std::setfill('0') << std::setprecision(0) << std::setw(2)
                  << duration_cast<minutes>(date).count() % 60 << ":";
        std::cout << std::setfill('0') << std::setprecision(0) << std::setw(2)
                  << duration_cast<seconds>(date).count() % 60 << ".";
        std::cout << std::setfill('0') << std::setprecision(0) << std::setw(3)
                  << duration_cast<milliseconds>(date).count() % 1000;
      } else {
        std::cout << "  ph: -1";
      }

      auto buf_dur = info->buffer_duration == -1 ? -1.f : info->buffer_duration / 1000000.f;
      std::cout << std::setprecision(2) << std::fixed << "  bd: " << buf_dur;
      std::cout << std::setprecision(0) << std::fixed << "  dT: " << frame_duration << "ms";
      std::cout << std::setprecision(2) << std::fixed << "  bps: " << framerate;
      // auto date = static_cast<time_t>(info->system_clock_date / 1000000000);
      // auto msec = info->system_clock_date % 1000000000 / 1000000.f;
      // std::cout << " lt: " << std::put_time(std::localtime(&date), "%b %d %Y %H:%M:%S") << "."
      //           << std::setfill('0') << std::setprecision(0) << std::setw(3) << msec
      //           << std::put_time(std::localtime(&date), " %Z");

      // speed
      std::cout << std::setprecision(3) << std::fixed << " sp: " << info->speed;
    }

    std::cout << "  size: " << size << "  data: ";
  }  // end print_info
  std::cout << data_printer(data, size, max_val_displayed);
  if (cartridge_return)
    std::cout << std::flush;
  else
    std::cout << std::endl;
}

auto main(int argc, char* argv[]) -> int {
  (void)signal(SIGINT, leave);
  (void)signal(SIGABRT, leave);
  (void)signal(SIGQUIT, leave);
  (void)signal(SIGTERM, leave);

  if (!parse_options(argc, argv)) {
    usage(argv[0]);
    return 1;
  }

  auto data_pr = sh4lt::utils::make_data_printer_fun(val_format, separator, precision, min_width);
  if (!data_pr) {
    std::cerr << "format not supported: " << val_format << '\n';
    return 1;
  }
  data_printer = *data_pr;
  follower = std::make_unique<Follower>(
      sockpath,
      [&](void* data, size_t size, const sh4lt_time_info_t* info) { on_data(data, size, info); },
      [&](const ShType& shtype) {
        if (print_info) {
          std::cout << "connected: type " << ShType::serialize(shtype) << std::endl;
        }
      },
      [&]() {
        if (print_info) {
          std::cout << "disconnected" << std::endl;
        }
      },
      std::make_shared<logger::Console>(debug));

  // wait
  while (true) {
    std::this_thread::sleep_for(std::chrono::seconds(1));
  }

  return 0;
}
