#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_stabilization_system.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 "[Near] CLIP_LEVEL";
  case 1: return "[Near] TARGET_POSITION_X";
  case 2: return "[Near] TARGET_POSITION_Y";
  case 3: return "[Near] CENTROID_X";
  case 4: return "[Near] CENTROID_Y";
  case 5: return "[Near] ERROR_X";
  case 6: return "[Near] ERROR_Y";
  case 7: return "[Near] DIAMETER";
  case 8: return "[Near] D4SIG_MAJOR";
  case 9: return "[Near] D4SIG_MINOR";
  case 10: return "[Near] FWHM_MAJOR";
  case 11: return "[Near] FWHM_MINOR";
  case 12: return "[Near] 1E2_MAJOR";
  case 13: return "[Near] 1E2_MINOR";
  case 14: return "[Near] ELLIPSE";
  case 15: return "[Near] ELLIPTICITY";
  case 16: return "[Near] ECCENTRICITY";
  case 17: return "[Near] POWER";
  case 18: return "[Near] MAX_POWER_DENSITY";
  case 19: return "[Near] CLIP_LEVEL_IRRADIATION_AREA";
  case 20: return "[Near] CLIP_LEVEL_AVERAGE_POWER_DENSITY";
  case 21: return "[Near] FLATNESS";
  case 22: return "[Near] UNIFORMITY";
  case 23: return "[Near] PLATEAU_UNIFORMITY";
  case 24: return "[Near] CROSS_SECTIONAL_AREA";
  case 25: return "[Near] PEAK";
  case 26: return "[Near] EDGE_STEEPNESS";
  case 27: return "[Far ] TARGET_POSITION_X";
  case 28: return "[Far ] TARGET_POSITION_Y";
  case 29: return "[Far ] CENTROID_X";
  case 30: return "[Far ] CENTROID_Y";
  case 31: return "[Far ] ERROR_X";
  case 32: return "[Far ] ERROR_Y";
  case 33: return "[Far ] PEAK";
  default: return "UNDEFINED";
  }
}

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 service_stub = hillab::v4::BeamStabilizationSystem::NewStub(channel);
  auto system_stub = hillab::v4::System::NewStub(channel);
 
  int choice = 0;
  float inp = 0;
  while (true) {
    system("cls");
    std::cout << "Beam StabilizationSystem Stub Client: " << addr << std::endl;
    std::cout << std::endl;
    std::cout << "1.  Get" << std::endl;
    std::cout << "2.  Set" << std::endl;
    std::cout << "3.  Monitor Beam Profiles" << std::endl;
    std::cout << "4.  Monitor System Log" << std::endl;
    std::cout << "5.  Get Preset List" << std::endl;
    std::cout << "6.  Load Preset" << std::endl;
    std::cout << "7.  Get Near Image Stream Server Info" << std::endl;
    std::cout << "8.  Get Far Image Stream Server Info" << std::endl;
    std::cout << "9.  Set Near Image Stream Server" << std::endl;
    std::cout << "10. Set Far 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, service_context;
      hillab::v4::GetRequest request;
      hillab::v4::GetResponse response;
      hillab::v4::PresetResponse preset_response;
      system_stub->GetCurrentPreset(&system_context, empty_request, &preset_response);
      std::cout << "Current preset name: " << preset_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 = service_stub->Get(&service_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, service_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::PresetResponse preset_response;
      system_stub->GetCurrentPreset(&system_get_context, empty_request, &preset_response);
      std::cout << "Current preset name: " << preset_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.c_str());
      }
      request.set_key(key);
      request.set_allocated_value(value);
      status = service_stub->Set(&service_context, request, &empty_response);

      if (!CheckError(status)) {
        // Saves settings
        grpc::ClientContext system_set_context;
        system_stub->SavePreset(&system_set_context, empty_request, &empty_response);
      }
      break;
    }
    case 3: {
      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>* >(
              service_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::left << std::setw(40) << "Index"
            << std::right << std::setw(15) << "Latest"
            << std::setw(15) << "Average"
            << std::setw(15) << "Std.Dev." << std::endl;
          std::cout << std::string(85, '-') << std::endl;

          // Print each beam profile
          for (const auto& profile : update.beam_profiles()) {
            std::cout << std::fixed << std::setprecision(3)
              << std::left << std::setw(40) << ProfileIndexToString(profile.index())
              << std::right << std::setw(15) << profile.latest_value() << std::right
              << std::setw(15) << profile.average() << std::right
              << std::setw(15) << profile.standard_deviation() << std::right
              << 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 4: {
      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 5: {
      std::cout << "[Get Preset List]" << std::endl;
      grpc::ClientContext system_context;
      hillab::v4::PresetListResponse preset_list_response;
      hillab::v4::PresetResponse preset_response;
      status = system_stub->GetPresetList(&system_context, empty_request, &preset_list_response);
      if (!CheckError(status)) {
        std::cout << "Preset List: ";
        for (int i = 0; i < preset_list_response.name_list_size(); i++) {
          std::cout << preset_list_response.name_list()[i];
          if (i != preset_list_response.name_list_size() - 1) std::cout << ", ";
        }
        std::cout << std::endl;
      }
      break;
    }
    case 6: { 
      std::cout << "[Load Preset]" << std::endl;
      grpc::ClientContext system_context;
      hillab::v4::PresetRequest preset_request;
      std::cout << "Enter the name of the preset." << std::endl;
      std::cout << " > ";
      std::string name;
      std::cin >> name;
      preset_request.set_name(name);
      status = system_stub->LoadPreset(&system_context, preset_request, &empty_response);
      CheckError(status);
      break;
    }
    case 7: {
      std::cout << "[Get Near Image Stream Server Info]" << std::endl;
      grpc::ClientContext context;
      hillab::v4::ImageStreamServerResponse response;
      status = system_stub->GetNearFieldImageStreamServerInfo(&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 8: {
      std::cout << "[Get Far Image Stream Server Info]" << std::endl;
      grpc::ClientContext context;
      hillab::v4::ImageStreamServerResponse response;
      status = system_stub->GetFarFieldImageStreamServerInfo(&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 9: {
      std::cout << "[Set Near 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->SetNearFieldImageStreamServer(&context, request, &empty_response);

      CheckError(status);
      break;
    }
    case 10: {
      std::cout << "[Set Far 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->SetFarFieldImageStreamServer(&context, request, &empty_response);

      CheckError(status);
      break;
    }
    default: 
      std::cout << "Invalid Number" << std::endl;
      break;
    }

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

  }
  return 0;
}