#include <iostream>
#include <iomanip>

#include <grpc/grpc.h>
#include <grpcpp/channel.h>
#include <grpcpp/client_context.h>
#include <grpcpp/create_channel.h>
#include <grpcpp/security/credentials.h>

#include "hillab_beam_profiler.grpc.pb.h"

using google::protobuf::Empty;

#ifdef _WIN32
#include <windows.h>
#endif

void MoveCursorToTop() {
#ifdef _WIN32
  // Windows 환경의 커서 이동
  HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
  COORD coordScreen = { 0, 0 };
  SetConsoleCursorPosition(hConsole, coordScreen);
#else
  // ANSI 코드를 사용하여 커서 이동 (Linux, macOS)
  std::cout << "\033[H" << std::flush;
#endif
}

#define _CRT_SECURE_NO_WARNINGS
std::string TimestampToReadableString(const google::protobuf::Timestamp& timestamp) {
  std::chrono::system_clock::time_point tp(std::chrono::seconds(timestamp.seconds()));
  int64_t millis = timestamp.nanos() / 1'000'000;
  std::time_t time = std::chrono::system_clock::to_time_t(tp);
  std::ostringstream oss;
  struct tm _tm;
  localtime_s(&_tm, &time);
  oss << std::put_time(&_tm, "%Y-%m-%d %H:%M:%S");
  oss << '.' << std::setw(3) << std::setfill('0') << millis;
  return oss.str();
}

std::string ProfileIndexToString(const int i) {
  switch (i) {
  case 0: return "CLIP_LEVEL";
  case 1: return "TARGET_POSITION_X";
  case 2: return "TARGET_POSITION_Y";
  case 3: return "CENTROID_X";
  case 4: return "CENTROID_Y";
  case 5: return "ERROR_X";
  case 6: return "ERROR_Y";
  case 7: return "DIAMETER";
  case 8: return "D4SIG_MAJOR";
  case 9: return "D4SIG_MINOR";
  case 10: return "FWHM_MAJOR";
  case 11: return "FWHM_MINOR";
  case 12: return "1E2_MAJOR";
  case 13: return "1E2_MINOR";
  case 14: return "ELLIPSE";
  case 15: return "ELLIPTICITY";
  case 16: return "ECCENTRICITY";
  case 17: return "POWER";
  case 18: return "MAX_POWER_DENSITY";
  case 19: return "CLIP_LEVEL_IRRADIATION_AREA";
  case 20: return "CLIP_LEVEL_AVERAGE_POWER_DENSITY";
  case 21: return "FLATNESS";
  case 22: return "UNIFORMITY";
  case 23: return "PLATEAU_UNIFORMITY";
  case 24: return "CROSS_SECTIONAL_AREA";
  case 25: return "PEAK";
  case 26: return "PEAK_COUNT"; 
  case 27: return "PEAK_X";
  case 28: return "PEAK_Y";
  case 29: return "PEAK_ERROR_X";
  case 30: return "PEAK_ERROR_Y";
  case 31: return "EDGE_STEEPNESS";
  }
}

bool CheckError(grpc::Status status)
{
  if (status.error_code() != grpc::StatusCode::OK) {
    std::cout << "Failed with error code " << status.error_code() 
      << ": " << status.error_message() << std::endl;
    return true;
  }
  else {
    return false;
  }
}

int main()
{
  std::string addr = "127.0.0.1:50051";
  auto channel = grpc::CreateChannel(addr, grpc::InsecureChannelCredentials());
  auto beam_profiler_stub = hillab::v4::BeamProfiler::NewStub(channel);
  auto system_stub = hillab::v4::System::NewStub(channel);
 
  int choice = 0;
  float inp = 0;
  while (true) {
    system("cls");
    std::cout << "Beam Profiler Stub Client: " << addr << std::endl;
    std::cout << std::endl;
    std::cout << "1.  Get" << std::endl;
    std::cout << "2.  Set" << std::endl;
    std::cout << "3.  Reset Data" << std::endl;
    std::cout << "4.  Monitor Beam Profiles" << std::endl;
    std::cout << "5.  Get Available Device List" << std::endl;
    std::cout << "6.  Connect Device" << std::endl;
    std::cout << "7.  Disonnect Device" << std::endl;
    std::cout << "8.  Monitor System Log" << std::endl;
    std::cout << "9.  Get Raw Image Stream Server Info" << std::endl;
    std::cout << "10. Get Profiled Image Stream Server Info" << std::endl;
    std::cout << "11. Set Raw Image Stream Server" << std::endl;
    std::cout << "12. Set Profiled Image Stream Server" << std::endl;
    std::cout << std::endl;
    std::cout << "Enter the service number." << std::endl;
    std::cout << " > ";
    std::cin >> choice;
    system("cls");

    grpc::Status status;
    Empty empty_request, empty_response;
    switch (choice) { 

    case 1: {
      std::cout << "[Get]" << std::endl;
      grpc::ClientContext system_context, beam_profiler_context;
      hillab::v4::GetRequest request;
      hillab::v4::GetResponse response;
      hillab::v4::SettingResponse setting_response;
      system_stub->GetCurrentSetting(&system_context, empty_request, &setting_response);
      std::cout << "Current setting name: " << setting_response.name() << std::endl;
      std::cout << "Enter the name of key." << std::endl;
      std::cout << " > ";
      std::string key;
      std::cin >> key;
      request.set_key(key);
      status = beam_profiler_stub->Get(&beam_profiler_context, request, &response);

      if (!CheckError(status)) {
        const std::string res_key = response.key();
        const hillab::v4::Value res_value = response.value();
        const hillab::v4::Value res_min_value = response.min_value();
        const hillab::v4::Value res_max_value = response.max_value();
        const std::string res_unit = response.unit();

        std::cout << "Key: " << res_key << std::endl;
        std::cout << "Value: ";
        switch (res_value.value_case()) {
        case hillab::v4::Value::kBoolValue:
          std::cout << (res_value.bool_value() ? "true" : "false"); break;
        case hillab::v4::Value::kIntValue:
          std::cout << res_value.int_value()
            << " (" << res_min_value.int_value() << " to "
            << res_max_value.int_value() << ")";
          break;
        case hillab::v4::Value::kFloatValue:
          std::cout << res_value.float_value()
            << " (" << res_min_value.float_value() << " to "
            << res_max_value.float_value() << ")";
          break;
        case hillab::v4::Value::kStringValue:
          std::cout << res_value.string_value(); break;
        default: 
          std::cout << "undefined type"; break;
        }
        std::cout << std::endl;
        std::cout << "Unit: " << res_unit << std::endl;
      }
      break;
    }
    case 2: {
      std::cout << "[Set]" << std::endl;
      grpc::ClientContext system_get_context, beam_profiler_context;
      hillab::v4::SetRequest request;

      // This allocated memory will be deleted by the protoc.
      // See at @set_allocated_foo(string* value) part in following URL for more details:
      // https://protobuf.dev/reference/cpp/cpp-generated/#string
      auto* value = new hillab::v4::Value;
      std::string type;
      std::string value_str;
      hillab::v4::SettingResponse setting_response;
      system_stub->GetCurrentSetting(&system_get_context, empty_request, &setting_response);
      std::cout << "Current setting name: " << setting_response.name() << std::endl;
      std::cout << "Enter the name of key." << std::endl;
      std::cout << " > ";
      std::string key;
      std::cin >> key;
      std::cout << "Enter the type. (bool | float | int | string)" << std::endl;
      std::cout << " > ";
      std::cin >> type;
      if (type != "bool" && type != "int" && type != "float" && type != "string") {
        std::cout << "Invalid type " << type << std::endl;
        break;
      }
      std::cout << "Enter the value." << std::endl;
      std::cout << " > ";
      std::cin >> value_str;
      if (type == "bool") {
        value->set_bool_value(value_str == "true" ? true : false);
      }
      else if (type == "int") {
        value->set_int_value(std::stoi(value_str));
      }
      else if (type == "float") {
        value->set_float_value(std::stof(value_str));
      }
      else if (type == "string") {
        value->set_string_value(value_str);
      }
      request.set_key(key);
      request.set_allocated_value(value);
      status = beam_profiler_stub->Set(&beam_profiler_context, request, &empty_response);

      if (!CheckError(status)) {
        // Saves settings
        grpc::ClientContext system_set_context;
        system_stub->SaveSetting(&system_set_context, empty_request, &empty_response);
      }
      break;
    }
    case 3: {
      std::cout << "[Reset Data]" << std::endl;
      grpc::ClientContext context;
      status = beam_profiler_stub->ResetData(&context, empty_request, &empty_response);

      CheckError(status);
      break;
    }
    case 4: {
      std::cout << "[Monitor Beam Profiles]" << std::endl;
      grpc::ClientContext context;
      auto reader 
        = std::unique_ptr<grpc::ClientReader<hillab::v4::BeamProfileStream>>(
            static_cast< grpc::ClientReader<hillab::v4::BeamProfileStream>* >(
              beam_profiler_stub->MonitorBeamProfile(&context, empty_request).release()
            )
          );

      hillab::v4::BeamProfileStream update;
      bool is_monitoring = true;

      // Start monitoring in a separate thread
      std::thread monitoring_thread([&reader, &update, &is_monitoring]() {
        while (is_monitoring) {
          // Clear console
          MoveCursorToTop();

          if (!reader->Read(&update)) {
            system("cls");
            std::cout << "Failed to read data from the server. "
              << "Press any key and Enter to stop monitoring." << std::endl;
            break;
          }

          std::string timestamp = TimestampToReadableString(update.time());

          std::cout << "[Beam Profiles Monitor]" << std::endl;
          std::cout << "Press any key and Enter to stop monitoring." << std::endl;
          std::cout << "Time: " << timestamp << std::endl;

          // Print header
          std::cout << std::setw(32) << "Index"
            << std::setw(15) << "Latest"
            << std::setw(15) << "Average"
            << std::setw(15) << "Std Dev" << std::endl;
          std::cout << std::string(77, '-') << std::endl;

          // Print each beam profile
          for (const auto& profile : update.beam_profiles()) {
            std::cout << std::fixed << std::setprecision(3)
              << std::setw(32) << ProfileIndexToString(profile.index())
              << std::setw(15) << profile.latest_value()
              << std::setw(15) << profile.average()
              << std::setw(15) << profile.standard_deviation()
              << std::endl;
          }
        }
        });

      // Wait for user input to stop monitoring
      std::string input;
      while (true) {
        std::cin >> input;
        context.TryCancel();
        break;
      }

      // Wait for monitoring thread to finish
      monitoring_thread.join();

      // Check the status
      status = reader->Finish();
      CheckError(status);
      break;
    }
    case 5: {
      std::cout << "[Get Available Device List]" << std::endl;
      grpc::ClientContext context;
      hillab::v4::DeviceInfoList response;
      status = system_stub->GetDeviceList(&context, empty_request, &response);
      auto devices = response.devices();
      std::cout << devices.size() << " devices are detected." << std::endl;
      if (!devices.empty()) {
        std::cout << std::setw(15) << "Serial"
          << std::setw(15) << "Alias" << std::endl;
        std::cout << std::string(30, '-') << std::endl;
        for (auto device : devices) {
          std::cout << std::setw(15) << device.serial_number()
            << std::setw(15) << device.alias() << std::endl;;
        }
      }

      CheckError(status);
      break;
    }
    case 6: {
      std::cout << "[Connect Device]" << std::endl;
      grpc::ClientContext context;
      hillab::v4::ConnectDeviceRequest request;
      std::string serial_number;
      std::cout << "Enter the serial number." << std::endl;
      std::cout << " > ";
      std::cin >> serial_number;
      request.set_serial_number(serial_number);
      status = system_stub->ConnectDevice(&context, request, &empty_response);

      CheckError(status);
      break;
    }
    case 7: {
      std::cout << "[Disonnect Device]" << std::endl;
      grpc::ClientContext context;
      status = system_stub->DisconnectDevice(&context, empty_request, &empty_response);

      CheckError(status);
      break;
    }
    case 8: {
      grpc::ClientContext context;
      auto reader
        = std::unique_ptr<grpc::ClientReader<hillab::v4::SystemLogStream>>(
          static_cast<grpc::ClientReader<hillab::v4::SystemLogStream>*>(
            system_stub->MonitorSystemLog(&context, empty_request).release()
            )
        );

      hillab::v4::SystemLogStream update;
      bool monitoring_system_error = true;

      std::cout << "[Monitor System Log]" << std::endl;
      std::cout << "Press any key and Enter to stop monitoring." << std::endl;

      // Start monitoring in a separate thread
      std::thread monitoring_thread([&reader, &update, &monitoring_system_error]() {
        while (monitoring_system_error) {
          if (!reader->Read(&update)) {
            std::cout << "Failed to read data from the server. "
              << "Press any key and Enter to stop monitoring." << std::endl;
            break;
          }

          std::string timestamp = TimestampToReadableString(update.time());
          std::string type_str;
          hillab::v4::SystemLogType type = update.type();
          switch (type) {
          case hillab::v4::SYSTEMLOGTYPE_INFO:
            type_str = "INFO"; break;
          case hillab::v4::SYSTEMLOGTYPE_ERROR: 
            type_str = "ERROR"; break;
          default: 
            type_str = "UNDEFINED"; break;
          }
          std::cout << "[" << timestamp << "] " 
            << type_str << ": " << update.message() << std::endl;
        }
        });

      // Wait for user input to stop monitoring
      std::string input;
      while (true) {
        std::cin >> input;
        context.TryCancel();
        break;
      }

      // Wait for monitoring thread to finish
      monitoring_thread.join();

      // Check the status
      status = reader->Finish();
      CheckError(status);
      break;
    }   
    case 9: {
      std::cout << "[Get Raw Image Stream Server Info]" << std::endl;
      grpc::ClientContext context;
      hillab::v4::ImageStreamServerResponse response;
      status = system_stub->GetRawImageStreamServerInfo(&context, empty_request, &response);

      if (CheckError(status)) break;
      std::cout << "IP: " << response.ip() << std::endl;
      std::cout << "PORT: " << response.port() << std::endl;
      std::cout << "MTU: " << response.mtu() << std::endl;
      std::cout << "Resolution Downscale: 1/" << std::pow(2, response.downscale()) << std::endl;
      std::cout << "Running: " << (response.running() ? "true" : "false") << std::endl;
      std::cout << "Auto Running Option: " << (response.auto_running() ? "true" : "false") << std::endl;
      std::cout << "SDP:\n" << response.sdp() << std::endl;
      break;
    }
    case 10: {
      std::cout << "[Get Profiled Image Stream Server Info]" << std::endl;
      grpc::ClientContext context;
      hillab::v4::ImageStreamServerResponse response;
      status = system_stub->GetProfiledImageStreamServerInfo(&context, empty_request, &response);

      if (CheckError(status)) break;
      std::cout << "IP: " << response.ip() << std::endl;
      std::cout << "PORT: " << response.port() << std::endl;
      std::cout << "MTU: " << response.mtu() << std::endl;
      std::cout << "Resolution Downscale: 1/" << std::pow(2, response.downscale()) << std::endl;
      std::cout << "Running: " << (response.running() ? "true" : "false") << std::endl;
      std::cout << "Auto Running Option: " << (response.auto_running() ? "true" : "false") << std::endl;
      std::cout << "SDP:\n" << response.sdp() << std::endl;
      break;
    }
    case 11: {
      std::cout << "[Set Raw Image Stream Server]" << std::endl;
      grpc::ClientContext context;
      hillab::v4::ImageStreamServerRequest request;
      std::string str;
      int mtu, downscale;
      bool running, auto_running;
      std::cout << "Enter the MTU. Enter -1 to make no modifications." << std::endl;
      std::cout << " > "; std::cin >> mtu;
      std::cout << "Enter the downscale level. Enter -1 to make no modifications." << std::endl;
      std::cout << " > "; std::cin >> downscale;
      std::cout << "Enter the running (true/false)" << std::endl;
      std::cout << " > "; std::cin >> str;
      running = (str == "true" ? true : false);
      std::cout << "Enter the auto running (true/false)" << std::endl;
      std::cout << " > "; std::cin >> str;
      auto_running = (str == "true" ? true : false);
      if (mtu > -1) request.set_mtu(mtu);
      if (downscale > -1) request.set_downscale(downscale);
      request.set_running(running);
      request.set_auto_running(auto_running);
      status = system_stub->SetRawImageStreamServer(&context, request, &empty_response);

      CheckError(status);
      break;
    }
    case 12: {
      std::cout << "[Set Profiled Image Stream Server]" << std::endl;
      grpc::ClientContext context;
      hillab::v4::ImageStreamServerRequest request;
      std::string str;
      int mtu, downscale;
      bool running, auto_running;
      std::cout << "Enter the MTU. Enter -1 to make no modifications." << std::endl;
      std::cout << " > "; std::cin >> mtu;
      std::cout << "Enter the downscale level. Enter -1 to make no modifications." << std::endl;
      std::cout << " > "; std::cin >> downscale;
      std::cout << "Enter the running (true/false)" << std::endl;
      std::cout << " > "; std::cin >> str;
      running = (str == "true" ? true : false);
      std::cout << "Enter the auto running (true/false)" << std::endl;
      std::cout << " > "; std::cin >> str;
      auto_running = (str == "true" ? true : false);
      if (mtu > -1) request.set_mtu(mtu);
      if (downscale > -1) request.set_downscale(downscale);
      request.set_running(running);
      request.set_auto_running(auto_running);
      status = system_stub->SetProfiledImageStreamServer(&context, request, &empty_response);

      CheckError(status);
      break;
    }

    default: 
      std::cout << "Invalid Number" << std::endl;
      break;
    }

    system("pause"); // Press any key to continue

  }
  return 0;
}