#include "SerialPort.h"

#include <termios.h>
#include <pthread.h>
#include <unistd.h>

#define MAX_THREAD_LIMIT 32


static speed_t getBaudrate(jint baudrate)
{
    switch(baudrate) {
        case 0: return B0;
        case 50: return B50;
        case 75: return B75;
        case 110: return B110;
        case 134: return B134;
        case 150: return B150;
        case 200: return B200;
        case 300: return B300;
        case 600: return B600;
        case 1200: return B1200;
        case 1800: return B1800;
        case 2400: return B2400;
        case 4800: return B4800;
        case 9600: return B9600;
        case 19200: return B19200;
        case 38400: return B38400;
        case 57600: return B57600;
        case 115200: return B115200;
        case 230400: return B230400;
        case 460800: return B460800;
        case 500000: return B500000;
        case 576000: return B576000;
        case 921600: return B921600;
        case 1000000: return B1000000;
        case 1152000: return B1152000;
        case 1500000: return B1500000;
        case 2000000: return B2000000;
        case 2500000: return B2500000;
        case 3000000: return B3000000;
        case 3500000: return B3500000;
        case 4000000: return B4000000;
        default: return -1;
    }
}

struct {
    pthread_t tid;
    jobject clazz;
} threads[MAX_THREAD_LIMIT];
JavaVM *vm;
jfieldID mFD;
jmethodID onNativeData;

void *comm_read() {
    JNIEnv *env;
    (*vm)->AttachCurrentThread(vm, &env, NULL);
    jclass clazz = NULL;
    for (int i = 0; i < MAX_THREAD_LIMIT; i++) {
        if (pthread_equal(pthread_self(), threads[i].tid)) {
            clazz = threads[i].clazz;
            if (mFD == NULL || onNativeData == NULL) {
                jclass obj = (*env)->GetObjectClass(env, clazz);
                mFD = (*env)->GetFieldID(env, obj, "mFD", "I");
                onNativeData = (*env)->GetMethodID(env, obj, "onNativeData", "([B)V");
            }
            break;
        }
    }
    if (clazz != NULL) {
        int fd;
        while ((fd = (*env)->GetIntField(env, clazz, mFD)) != -1) {
            int size = 1024;
            jbyte data[size];
            int len = read(fd, data, size);
            if (len > 0) {
                jbyteArray bytes = (*env)->NewByteArray(env, len);
                (*env)->SetByteArrayRegion(env, bytes, 0, len, data);
                (*env)->CallVoidMethod(env, clazz, onNativeData, bytes);
            } else if (len < 0) {
                // DETECT: Hardware level error (e.g. unplugged)
                // Send a specific error byte like 0x0E to trigger the Error result in Repository
                jbyteArray errorBytes = (*env)->NewByteArray(env, 1);
                jbyte err = 0x0E;
                (*env)->SetByteArrayRegion(env, errorBytes, 0, 1, &err);
                (*env)->CallVoidMethod(env, clazz, onNativeData, errorBytes);
            }
            usleep(10000);
        }
    }
    if (vm != NULL) {
        int isFree = 1;
        for (int i = 0; i < MAX_THREAD_LIMIT; i++) {
            if (threads[i].clazz != NULL) {
                if (threads[i].clazz == clazz) {
                    (*env)->DeleteGlobalRef(env, clazz);
                    threads[i].clazz = clazz = NULL;
                } else {
                    isFree = 0;
                    if (clazz == NULL) break;
                }
            }
        }
        (*vm)->DetachCurrentThread(vm);
        if (isFree) {
            onNativeData = NULL;
            mFD = NULL;
            vm = NULL;
        }
    }
    return 0;
}

JNIEXPORT void JNICALL
Java_com_laseroptek_raman_data_source_serial_SerialPort_open(JNIEnv *env, jclass clazz, int fd, int baudrate) {
    tcflush(fd, TCIOFLUSH);
    struct termios cfg;
    tcgetattr(fd, &cfg);
    cfmakeraw(&cfg);
    speed_t speed = getBaudrate(baudrate);
    cfsetispeed(&cfg, speed);
    cfsetospeed(&cfg, speed);
    tcsetattr(fd, TCSANOW, &cfg);
    for (int i = 0; i < MAX_THREAD_LIMIT; i++) {
        if (threads[i].clazz == NULL) {
            if (vm == NULL) {
                (*env)->GetJavaVM(env, &vm);
            }
            threads[i].clazz = (*env)->NewGlobalRef(env, clazz);
            pthread_create(&threads[i].tid, NULL, comm_read, NULL);
            break;
        }
    }
}

JNIEXPORT void JNICALL
Java_com_laseroptek_raman_data_source_serial_SerialPort_write(JNIEnv *env, __unused jclass _, int fd, jbyteArray bytes) {
    jbyte *data = (*env)->GetByteArrayElements(env, bytes, NULL);
    if (data != NULL) {
        write(fd, data, (*env)->GetArrayLength(env, bytes));
        (*env)->ReleaseByteArrayElements(env, bytes, data, JNI_ABORT);
    }
}

JNIEXPORT void JNICALL
Java_com_laseroptek_raman_data_source_serial_SerialPort_close(__unused JNIEnv *_, __unused jclass __, int fd) {
    close(fd);
}
