// 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 "HALUtil.hpp"

#include <jni.h>

#include <cassert>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <string>

#include <fmt/format.h>

#include "org_wpilib_hardware_hal_HALUtil.h"
#include "wpi/hal/CAN.h"
#include "wpi/hal/DriverStation.h"
#include "wpi/hal/Errors.h"
#include "wpi/hal/HAL.h"
#include "wpi/hal/Value.h"
#include "wpi/util/jni_util.hpp"

using namespace wpi::util::java;

static_assert(org_wpilib_hardware_hal_HALUtil_RUNTIME_SIMULATION ==
              HAL_RUNTIME_SIMULATION);
static_assert(org_wpilib_hardware_hal_HALUtil_RUNTIME_SYSTEMCORE ==
              HAL_RUNTIME_SYSTEMCORE);

static JavaVM* jvm = nullptr;
static JException illegalArgExCls;
static JException indexOobExCls;
static JException boundaryExCls;
static JException allocationExCls;
static JException halHandleExCls;
static JException uncleanStatusExCls;
static JException nullPointerEx;
static JClass powerDistributionVersionCls;
static JClass canStatusCls;
static JClass matchInfoDataCls;
static JClass canReceiveMessageCls;
static JClass canStreamMessageCls;
static JClass halValueCls;
static JClass opModeOptionCls;
static JClass revPHVersionCls;
static JClass canStreamOverflowExCls;

static const JClassInit classes[] = {
    {"org/wpilib/hardware/hal/PowerDistributionVersion",
     &powerDistributionVersionCls},
    {"org/wpilib/hardware/hal/can/CANStatus", &canStatusCls},
    {"org/wpilib/hardware/hal/MatchInfoData", &matchInfoDataCls},
    {"org/wpilib/hardware/hal/can/CANReceiveMessage", &canReceiveMessageCls},
    {"org/wpilib/hardware/hal/can/CANStreamMessage", &canStreamMessageCls},
    {"org/wpilib/hardware/hal/OpModeOption", &opModeOptionCls},
    {"org/wpilib/hardware/hal/HALValue", &halValueCls},
    {"org/wpilib/hardware/hal/REVPHVersion", &revPHVersionCls},
    {"org/wpilib/hardware/hal/can/CANStreamOverflowException",
     &canStreamOverflowExCls}};

static const JExceptionInit exceptions[] = {
    {"java/lang/IllegalArgumentException", &illegalArgExCls},
    {"java/lang/IndexOutOfBoundsException", &indexOobExCls},
    {"org/wpilib/hardware/hal/util/BoundaryException", &boundaryExCls},
    {"org/wpilib/hardware/hal/util/AllocationException", &allocationExCls},
    {"org/wpilib/hardware/hal/util/HalHandleException", &halHandleExCls},
    {"org/wpilib/hardware/hal/util/UncleanStatusException",
     &uncleanStatusExCls},
    {"java/lang/NullPointerException", &nullPointerEx}};

namespace wpi::hal {

void ThrowUncleanStatusException(JNIEnv* env, std::string_view msg,
                                 int32_t status) {
  static jmethodID func =
      env->GetMethodID(uncleanStatusExCls, "<init>", "(ILjava/lang/String;)V");

  jobject exception =
      env->NewObject(uncleanStatusExCls, func, static_cast<jint>(status),
                     MakeJString(env, msg));
  env->Throw(static_cast<jthrowable>(exception));
}

void ThrowAllocationException(JNIEnv* env, const char* lastError,
                              int32_t status) {
  allocationExCls.Throw(env,
                        fmt::format("Code: {}\n{}", status, lastError).c_str());
}

void ThrowHalHandleException(JNIEnv* env, int32_t status) {
  const char* message = HAL_GetLastError(&status);
  halHandleExCls.Throw(env,
                       fmt::format(" Code: {}. {}", status, message).c_str());
}

void ReportError(JNIEnv* env, int32_t status, bool doThrow) {
  if (status == 0) {
    return;
  }
  const char* message = HAL_GetLastError(&status);
  if (status == HAL_HANDLE_ERROR) {
    ThrowHalHandleException(env, status);
    return;
  }
  if (doThrow && status < 0) {
    ThrowUncleanStatusException(
        env, fmt::format(" Code: {}. {}", status, message).c_str(), status);
  } else {
    std::string func;
    auto stack = GetJavaStackTrace(env, &func, "org.wpilib");
    // Make a copy of message for safety, calling back into the HAL might
    // invalidate the string.
    std::string lastMessage{message};
    HAL_SendError(1, status, 0, lastMessage.c_str(), func.c_str(),
                  stack.c_str(), 1);
  }
}

void ThrowError(JNIEnv* env, int32_t status, int32_t minRange, int32_t maxRange,
                int32_t requestedValue) {
  if (status == 0) {
    return;
  }
  const char* lastError = HAL_GetLastError(&status);
  if (status == HAL_NO_AVAILABLE_RESOURCES ||
      status == HAL_RESOURCE_IS_ALLOCATED ||
      status == HAL_RESOURCE_OUT_OF_RANGE) {
    ThrowAllocationException(env, lastError, status);
    return;
  }
  if (status == HAL_HANDLE_ERROR) {
    ThrowHalHandleException(env, status);
    return;
  }
  ThrowUncleanStatusException(
      env, fmt::format(" Code: {}. {}", status, lastError).c_str(), status);
}

void ThrowNullPointerException(JNIEnv* env, std::string_view msg) {
  nullPointerEx.Throw(env, msg);
}

void ThrowCANStreamOverflowException(JNIEnv* env, jobjectArray messages,
                                     jint length) {
  static jmethodID constructor =
      env->GetMethodID(canStreamOverflowExCls, "<init>",
                       "([Lorg/wpilib/hardware/hal/CANStreamMessage;I)V");
  jobject exception =
      env->NewObject(canStreamOverflowExCls, constructor, messages, length);
  env->Throw(static_cast<jthrowable>(exception));
}

void ThrowIllegalArgumentException(JNIEnv* env, std::string_view msg) {
  illegalArgExCls.Throw(env, msg);
}

void ThrowIndexOutOfBoundsException(JNIEnv* env, std::string_view msg) {
  indexOobExCls.Throw(env, msg);
}

void ThrowBoundaryException(JNIEnv* env, double value, double lower,
                            double upper) {
  static jmethodID getMessage = nullptr;
  if (!getMessage) {
    getMessage = env->GetStaticMethodID(boundaryExCls, "getMessage",
                                        "(DDD)Ljava/lang/String;");
  }

  static jmethodID constructor = nullptr;
  if (!constructor) {
    constructor =
        env->GetMethodID(boundaryExCls, "<init>", "(Ljava/lang/String;)V");
  }

  jobject msg = env->CallStaticObjectMethod(
      boundaryExCls, getMessage, static_cast<jdouble>(value),
      static_cast<jdouble>(lower), static_cast<jdouble>(upper));
  jobject ex = env->NewObject(boundaryExCls, constructor, msg);
  env->Throw(static_cast<jthrowable>(ex));
}

jobject CreateOpModeOption(JNIEnv* env, const HAL_OpModeOption& option) {
  static jmethodID constructor = env->GetMethodID(
      opModeOptionCls, "<init>",
      "(JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;II)V");
  JLocal<jstring> name{
      env, MakeJString(env, wpi::util::to_string_view(&option.name))};
  JLocal<jstring> group{
      env, MakeJString(env, wpi::util::to_string_view(&option.group))};
  JLocal<jstring> desc{
      env, MakeJString(env, wpi::util::to_string_view(&option.description))};
  return env->NewObject(opModeOptionCls, constructor,
                        static_cast<jlong>(option.id), name.obj(), group.obj(),
                        desc.obj(), static_cast<jint>(option.textColor),
                        static_cast<jint>(option.backgroundColor));
}

jobjectArray CreateOpModeOptionArray(
    JNIEnv* env, std::span<const HAL_OpModeOption> options) {
  jobjectArray arr =
      env->NewObjectArray(options.size(), opModeOptionCls, nullptr);
  if (!arr) {
    return nullptr;
  }
  size_t i = 0;
  for (auto& option : options) {
    JLocal<jobject> elem{env, CreateOpModeOption(env, option)};
    env->SetObjectArrayElement(arr, i++, elem);
  }
  return arr;
}

HAL_OpModeOption CreateOpModeOptionFromJava(JNIEnv* env, jobject option) {
  static jfieldID idField = env->GetFieldID(opModeOptionCls, "id", "J");
  static jfieldID nameField =
      env->GetFieldID(opModeOptionCls, "name", "Ljava/lang/String;");
  static jfieldID groupField =
      env->GetFieldID(opModeOptionCls, "group", "Ljava/lang/String;");
  static jfieldID descriptionField =
      env->GetFieldID(opModeOptionCls, "description", "Ljava/lang/String;");
  static jfieldID textColorField =
      env->GetFieldID(opModeOptionCls, "textColor", "I");
  static jfieldID backgroundColorField =
      env->GetFieldID(opModeOptionCls, "backgroundColor", "I");
  if (!idField || !nameField || !groupField || !descriptionField ||
      !textColorField || !backgroundColorField) {
    ThrowIllegalArgumentException(env, "Missing field in OpModeOption");
    return {0, {}, {}, {}, 0, 0};
  }
  int64_t id = env->GetLongField(option, idField);
  JLocal<jstring> name{
      env, static_cast<jstring>(env->GetObjectField(option, nameField))};
  JLocal<jstring> group{
      env, static_cast<jstring>(env->GetObjectField(option, groupField))};
  JLocal<jstring> description{
      env, static_cast<jstring>(env->GetObjectField(option, descriptionField))};
  int32_t textColor = env->GetIntField(option, textColorField);
  int32_t backgroundColor = env->GetIntField(option, backgroundColorField);
  return {id,
          wpi::util::alloc_wpi_string(JStringRef{env, name}),
          wpi::util::alloc_wpi_string(JStringRef{env, group}),
          wpi::util::alloc_wpi_string(JStringRef{env, description}),
          textColor,
          backgroundColor};
}

jobject CreateREVPHVersion(JNIEnv* env, uint32_t firmwareMajor,
                           uint32_t firmwareMinor, uint32_t firmwareFix,
                           uint32_t hardwareMinor, uint32_t hardwareMajor,
                           uint32_t uniqueId) {
  static jmethodID constructor =
      env->GetMethodID(revPHVersionCls, "<init>", "(IIIIII)V");
  return env->NewObject(
      revPHVersionCls, constructor, static_cast<jint>(firmwareMajor),
      static_cast<jint>(firmwareMinor), static_cast<jint>(firmwareFix),
      static_cast<jint>(hardwareMinor), static_cast<jint>(hardwareMajor),
      static_cast<jint>(uniqueId));
}

void SetCanStatusObject(JNIEnv* env, jobject canStatus,
                        float percentBusUtilization, uint32_t busOffCount,
                        uint32_t txFullCount, uint32_t receiveErrorCount,
                        uint32_t transmitErrorCount) {
  static jmethodID func =
      env->GetMethodID(canStatusCls, "setStatus", "(DIIII)V");
  env->CallVoidMethod(
      canStatus, func, static_cast<jdouble>(percentBusUtilization),
      static_cast<jint>(busOffCount), static_cast<jint>(txFullCount),
      static_cast<jint>(receiveErrorCount),
      static_cast<jint>(transmitErrorCount));
}

void SetMatchInfoObject(JNIEnv* env, jobject matchStatus,
                        const HAL_MatchInfo& matchInfo) {
  static jmethodID func =
      env->GetMethodID(matchInfoDataCls, "setData", "(Ljava/lang/String;III)V");

  env->CallVoidMethod(matchStatus, func, MakeJString(env, matchInfo.eventName),
                      static_cast<jint>(matchInfo.matchNumber),
                      static_cast<jint>(matchInfo.replayNumber),
                      static_cast<jint>(matchInfo.matchType));
}

jbyteArray SetCANReceiveMessageObject(JNIEnv* env, jobject canData,
                                      int32_t length, int32_t flags,
                                      uint64_t timestamp) {
  static jmethodID func =
      env->GetMethodID(canReceiveMessageCls, "setReceiveData", "(IIJ)[B");

  jbyteArray retVal = static_cast<jbyteArray>(env->CallObjectMethod(
      canData, func, static_cast<jint>(length), static_cast<jint>(flags),
      static_cast<jlong>(timestamp)));
  return retVal;
}

jbyteArray SetCANStreamObject(JNIEnv* env, jobject canStreamData,
                              int32_t length, int32_t flags, uint32_t messageId,
                              uint64_t timestamp) {
  static jmethodID func =
      env->GetMethodID(canStreamMessageCls, "setStreamData", "(IIIJ)[B");

  jbyteArray retVal = static_cast<jbyteArray>(env->CallObjectMethod(
      canStreamData, func, static_cast<jint>(length), static_cast<jint>(flags),
      static_cast<jint>(messageId), static_cast<jlong>(timestamp)));
  return retVal;
}

jobject CreateHALValue(JNIEnv* env, const HAL_Value& value) {
  static jmethodID fromNative = env->GetStaticMethodID(
      halValueCls, "fromNative", "(IJD)Lorg/wpilib/hardware/hal/HALValue;");
  jlong value1 = 0;
  jdouble value2 = 0.0;
  switch (value.type) {
    case HAL_BOOLEAN:
      value1 = value.data.v_boolean;
      break;
    case HAL_DOUBLE:
      value2 = value.data.v_double;
      break;
    case HAL_ENUM:
      value1 = value.data.v_enum;
      break;
    case HAL_INT:
      value1 = value.data.v_int;
      break;
    case HAL_LONG:
      value1 = value.data.v_long;
      break;
    default:
      break;
  }
  return env->CallStaticObjectMethod(
      halValueCls, fromNative, static_cast<jint>(value.type), value1, value2);
}

jobject CreatePowerDistributionVersion(JNIEnv* env, uint32_t firmwareMajor,
                                       uint32_t firmwareMinor,
                                       uint32_t firmwareFix,
                                       uint32_t hardwareMinor,
                                       uint32_t hardwareMajor,
                                       uint32_t uniqueId) {
  static jmethodID constructor =
      env->GetMethodID(powerDistributionVersionCls, "<init>", "(IIIIII)V");
  return env->NewObject(
      powerDistributionVersionCls, constructor,
      static_cast<jint>(firmwareMajor), static_cast<jint>(firmwareMinor),
      static_cast<jint>(firmwareFix), static_cast<jint>(hardwareMinor),
      static_cast<jint>(hardwareMajor), static_cast<jint>(uniqueId));
}

jobject CreateCANStreamMessage(JNIEnv* env) {
  static jmethodID constructor =
      env->GetMethodID(canStreamMessageCls, "<init>", "()V");
  return env->NewObject(canStreamMessageCls, constructor);
}

JavaVM* GetJVM() {
  return jvm;
}

namespace sim {
jint SimOnLoad(JavaVM* vm, void* reserved);
void SimOnUnload(JavaVM* vm, void* reserved);
}  // namespace sim

}  // namespace wpi::hal

using namespace wpi::hal;

extern "C" {

//
// indicate JNI version support desired and load classes
//
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
  jvm = vm;

  JNIEnv* env;
  if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
    return JNI_ERR;
  }

  for (auto& c : classes) {
    *c.cls = JClass(env, c.name);
    if (!*c.cls) {
      return JNI_ERR;
    }
  }

  for (auto& c : exceptions) {
    *c.cls = JException(env, c.name);
    if (!*c.cls) {
      return JNI_ERR;
    }
  }

  return sim::SimOnLoad(vm, reserved);
}

JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved) {
  sim::SimOnUnload(vm, reserved);

  JNIEnv* env;
  if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
    return;
  }
  // Delete global references

  for (auto& c : classes) {
    c.cls->free(env);
  }
  for (auto& c : exceptions) {
    c.cls->free(env);
  }
  jvm = nullptr;
}

/*
 * Class:     org_wpilib_hardware_hal_HALUtil
 * Method:    getSerialNumber
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL
Java_org_wpilib_hardware_hal_HALUtil_getSerialNumber
  (JNIEnv* env, jclass)
{
  WPI_String serialNum;
  HAL_GetSerialNumber(&serialNum);
  jstring ret = MakeJString(env, wpi::util::to_string_view(&serialNum));
  WPI_FreeString(&serialNum);
  return ret;
}

/*
 * Class:     org_wpilib_hardware_hal_HALUtil
 * Method:    getComments
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL
Java_org_wpilib_hardware_hal_HALUtil_getComments
  (JNIEnv* env, jclass)
{
  WPI_String comments;
  HAL_GetComments(&comments);
  jstring ret = MakeJString(env, wpi::util::to_string_view(&comments));
  WPI_FreeString(&comments);
  return ret;
}

/*
 * Class:     org_wpilib_hardware_hal_HALUtil
 * Method:    getTeamNumber
 * Signature: ()I
 */
JNIEXPORT jint JNICALL
Java_org_wpilib_hardware_hal_HALUtil_getTeamNumber
  (JNIEnv* env, jclass)
{
  return HAL_GetTeamNumber();
}

/*
 * Class:     org_wpilib_hardware_hal_HALUtil
 * Method:    getMonotonicTime
 * Signature: ()J
 */
JNIEXPORT jlong JNICALL
Java_org_wpilib_hardware_hal_HALUtil_getMonotonicTime
  (JNIEnv* env, jclass)
{
  return HAL_GetMonotonicTime();
}

/*
 * Class:     org_wpilib_hardware_hal_HALUtil
 * Method:    getHALRuntimeType
 * Signature: ()I
 */
JNIEXPORT jint JNICALL
Java_org_wpilib_hardware_hal_HALUtil_getHALRuntimeType
  (JNIEnv* env, jclass)
{
  jint returnValue = HAL_GetRuntimeType();
  return returnValue;
}

/*
 * Class:     org_wpilib_hardware_hal_HALUtil
 * Method:    getHALErrorMessage
 * Signature: (I)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL
Java_org_wpilib_hardware_hal_HALUtil_getHALErrorMessage
  (JNIEnv* paramEnv, jclass, jint paramId)
{
  const char* msg = HAL_GetErrorMessage(paramId);
  return MakeJString(paramEnv, msg);
}

/*
 * Class:     org_wpilib_hardware_hal_HALUtil
 * Method:    getHALErrno
 * Signature: ()I
 */
JNIEXPORT jint JNICALL
Java_org_wpilib_hardware_hal_HALUtil_getHALErrno
  (JNIEnv*, jclass)
{
  return errno;
}

/*
 * Class:     org_wpilib_hardware_hal_HALUtil
 * Method:    getHALstrerror
 * Signature: (I)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL
Java_org_wpilib_hardware_hal_HALUtil_getHALstrerror
  (JNIEnv* env, jclass, jint errorCode)
{
  const char* msg = std::strerror(errno);
  return MakeJString(env, msg);
}

}  // extern "C"
