// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// C support functions for bindings. This file is copied into the
// generated gomobile_bind package and compiled along with the
// generated binding files.

#include <android/log.h>
#include <errno.h>
#include <jni.h>
#include <stdint.h>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include "seq.h"
#include "_cgo_export.h"

#define NULL_REFNUM 41

static JavaVM *jvm;
// jnienvs holds the per-thread JNIEnv* for Go threads where we called AttachCurrentThread.
// A pthread key destructor is supplied to call DetachCurrentThread on exit. This trick is
// documented in http://developer.android.com/training/articles/perf-jni.html under "Threads".
static pthread_key_t jnienvs;

static jclass seq_class;
static jmethodID seq_throw_exc;
static jmethodID seq_getRef;
static jmethodID seq_decRef;
static jmethodID seq_incRef;
static jmethodID seq_incRefnum;
static jmethodID seq_wrapThrowable;

static jmethodID throwable_getMessage;

static jfieldID ref_objField;

// env_destructor is registered as a thread data key destructor to
// clean up a Go thread that is attached to the JVM.
static void env_destructor(void *env) {
	if ((*jvm)->DetachCurrentThread(jvm) != JNI_OK) {
		LOG_INFO("failed to detach current thread");
	}
}

static JNIEnv *go_seq_get_thread_env(void) {
	JNIEnv *env;
	jint ret = (*jvm)->GetEnv(jvm, (void **)&env, JNI_VERSION_1_6);
	if (ret != JNI_OK) {
		if (ret != JNI_EDETACHED) {
			LOG_FATAL("failed to get thread env");
		}
		if ((*jvm)->AttachCurrentThread(jvm, &env, NULL) != JNI_OK) {
			LOG_FATAL("failed to attach current thread");
		}
		pthread_setspecific(jnienvs, env);
	}
	return env;
}

void go_seq_maybe_throw_exception(JNIEnv *env, jobject msg) {
	if (msg != NULL) {
		(*env)->CallStaticVoidMethod(env, seq_class, seq_throw_exc, msg);
	}
}

jobject go_seq_wrap_exception(JNIEnv *env) {
	jthrowable exc = (*env)->ExceptionOccurred(env);
	if (!exc) {
		return NULL;
	}
	(*env)->ExceptionClear(env);
	return (*env)->CallStaticObjectMethod(env, seq_class, seq_wrapThrowable, exc);
}

jbyteArray go_seq_to_java_bytearray(JNIEnv *env, nbyteslice s, int copy) {
	if (s.ptr == NULL) {
		return NULL;
	}
	jbyteArray res = (*env)->NewByteArray(env, s.len);
	if (res == NULL) {
		LOG_FATAL("NewByteArray failed");
	}
	(*env)->SetByteArrayRegion(env, res, 0, s.len, s.ptr);
	if (copy) {
		free(s.ptr);
	}
	return res;
}

#define surr1 0xd800
#define surr2 0xdc00
#define surr3 0xe000

// Unicode replacement character
#define replacementChar 0xFFFD

#define rune1Max ((1<<7) - 1)
#define rune2Max ((1<<11) - 1)
#define rune3Max ((1<<16) - 1)
// Maximum valid Unicode code point.
#define MaxRune 0x0010FFFF

#define surrogateMin 0xD800
#define surrogateMax 0xDFFF
// 0011 1111
#define maskx 0x3F
// 1000 0000
#define tx 0x80
// 1100 0000
#define t2 0xC0
// 1110 0000
#define t3 0xE0
// 1111 0000
#define t4 0xF0

// encode_rune writes into p (which must be large enough) the UTF-8 encoding
// of the rune. It returns the number of bytes written.
static int encode_rune(uint8_t *p, uint32_t r) {
	if (r <= rune1Max) {
		p[0] = (uint8_t)r;
		return 1;
	} else if (r <= rune2Max) {
		p[0] = t2 | (uint8_t)(r>>6);
		p[1] = tx | (((uint8_t)(r))&maskx);
		return 2;
	} else {
		if (r > MaxRune || (surrogateMin <= r && r <= surrogateMax)) {
			r = replacementChar;
		}
		if (r <= rune3Max) {
			p[0] = t3 | (uint8_t)(r>>12);
			p[1] = tx | (((uint8_t)(r>>6))&maskx);
			p[2] = tx | (((uint8_t)(r))&maskx);
			return 3;
		} else {
			p[0] = t4 | (uint8_t)(r>>18);
			p[1] = tx | (((uint8_t)(r>>12))&maskx);
			p[2] = tx | (((uint8_t)(r>>6))&maskx);
			p[3] = tx | (((uint8_t)(r))&maskx);
			return 4;
		}
	}
}

// utf16_decode decodes an array of UTF16 characters to a UTF-8 encoded
// nstring copy. The support functions and utf16_decode itself are heavily
// based on the unicode/utf8 and unicode/utf16 Go packages.
static nstring utf16_decode(jchar *chars, jsize len) {
	jsize worstCaseLen = 4*len;
	uint8_t *buf = malloc(worstCaseLen);
	if (buf == NULL) {
		LOG_FATAL("utf16Decode: malloc failed");
	}
	jsize nsrc = 0;
	jsize ndst = 0;
	while (nsrc < len) {
		uint32_t r = chars[nsrc];
		nsrc++;
		if (surr1 <= r && r < surr2 && nsrc < len) {
			uint32_t r2 = chars[nsrc];
			if (surr2 <= r2 && r2 < surr3) {
				nsrc++;
				r = (((r-surr1)<<10) | (r2 - surr2)) + 0x10000;
			}
		}
		if (ndst + 4 > worstCaseLen) {
			LOG_FATAL("utf16Decode: buffer overflow");
		}
		ndst += encode_rune(buf + ndst, r);
	}
	struct nstring res = {.chars = buf, .len = ndst};
	return res;
}

nstring go_seq_from_java_string(JNIEnv *env, jstring str) {
	struct nstring res = {NULL, 0};
	if (str == NULL) {
		return res;
	}
	jsize nchars = (*env)->GetStringLength(env, str);
	if (nchars == 0) {
		return res;
	}
	jchar *chars = (jchar *)(*env)->GetStringChars(env, str, NULL);
	if (chars == NULL) {
		LOG_FATAL("GetStringChars failed");
	}
	nstring nstr = utf16_decode(chars, nchars);
	(*env)->ReleaseStringChars(env, str, chars);
	return nstr;
}

nbyteslice go_seq_from_java_bytearray(JNIEnv *env, jbyteArray arr, int copy) {
	struct nbyteslice res = {NULL, 0};
	if (arr == NULL) {
		return res;
	}

	jsize len = (*env)->GetArrayLength(env, arr);
	if (len == 0) {
		return res;
	}
	jbyte *ptr = (*env)->GetByteArrayElements(env, arr, NULL);
	if (ptr == NULL) {
		LOG_FATAL("GetByteArrayElements failed");
	}
	if (copy) {
		void *ptr_copy = (void *)malloc(len);
		if (ptr_copy == NULL) {
			LOG_FATAL("malloc failed");
		}
		memcpy(ptr_copy, ptr, len);
		(*env)->ReleaseByteArrayElements(env, arr, ptr, JNI_ABORT);
		ptr = (jbyte *)ptr_copy;
	}
	res.ptr = ptr;
	res.len = len;
	return res;
}

int32_t go_seq_to_refnum(JNIEnv *env, jobject o) {
	if (o == NULL) {
		return NULL_REFNUM;
	}
	return (int32_t)(*env)->CallStaticIntMethod(env, seq_class, seq_incRef, o);
}

jobject go_seq_from_refnum(JNIEnv *env, int32_t refnum, jclass proxy_class, jmethodID proxy_cons) {
	if (refnum == NULL_REFNUM) {
		return NULL;
	}
	// Seq.Ref ref = Seq.getRef(refnum)
	jobject ref = (*env)->CallStaticObjectMethod(env, seq_class, seq_getRef, (jint)refnum);
	if (ref == NULL) {
		LOG_FATAL("Unknown reference: %d", refnum);
	}
	if (refnum > 0) { // Java object
		// Go incremented the reference count just before passing the refnum. Decrement it here.
		(*env)->CallStaticVoidMethod(env, seq_class, seq_decRef, (jint)refnum);
		// return ref.obj
		return (*env)->GetObjectField(env, ref, ref_objField);
	} else {
		// return new <Proxy>(ref)
		return (*env)->NewObject(env, proxy_class, proxy_cons, ref);
	}
}

// go_seq_to_java_string converts a nstring to a jstring.
jstring go_seq_to_java_string(JNIEnv *env, nstring str) {
	jstring s = (*env)->NewString(env, str.chars, str.len/2);
	if (str.chars != NULL) {
		free(str.chars);
	}
	return s;
}

// go_seq_push_local_frame retrieves or creates the JNIEnv* for the current thread
// and pushes a JNI reference frame. Must be matched with call to go_seq_pop_local_frame.
JNIEnv *go_seq_push_local_frame(jint cap) {
	JNIEnv *env = go_seq_get_thread_env();
	if ((*env)->PushLocalFrame(env, cap) < 0) {
		LOG_FATAL("PushLocalFrame failed");
	}
	return env;
}

// Pop the current local frame, freeing all JNI local references in it
void go_seq_pop_local_frame(JNIEnv *env) {
	(*env)->PopLocalFrame(env, NULL);
}

void go_seq_inc_ref(int32_t ref) {
	JNIEnv *env = go_seq_get_thread_env();
	(*env)->CallStaticVoidMethod(env, seq_class, seq_incRefnum, (jint)ref);
}

void go_seq_dec_ref(int32_t ref) {
	JNIEnv *env = go_seq_get_thread_env();
	(*env)->CallStaticVoidMethod(env, seq_class, seq_decRef, (jint)ref);
}

JNIEXPORT void JNICALL
Java_go_Seq_init(JNIEnv *env, jclass clazz) {
	if ((*env)->GetJavaVM(env, &jvm) != 0) {
		LOG_FATAL("failed to get JVM");
	}
	if (pthread_key_create(&jnienvs, env_destructor) != 0) {
		LOG_FATAL("failed to initialize jnienvs thread local storage");
	}

	seq_class = (*env)->NewGlobalRef(env, clazz);
	seq_throw_exc = (*env)->GetStaticMethodID(env, seq_class, "throwException", "(Lgo/error;)V");
	if (seq_throw_exc == NULL) {
		LOG_FATAL("failed to find method Seq.throwException");
	}

	seq_getRef = (*env)->GetStaticMethodID(env, seq_class, "getRef", "(I)Lgo/Seq$Ref;");
	if (seq_getRef == NULL) {
		LOG_FATAL("failed to find method Seq.getRef");
	}
	seq_decRef = (*env)->GetStaticMethodID(env, seq_class, "decRef", "(I)V");
	if (seq_decRef == NULL) {
		LOG_FATAL("failed to find method Seq.decRef");
	}
	seq_incRefnum = (*env)->GetStaticMethodID(env, seq_class, "incRefnum", "(I)V");
	if (seq_incRefnum == NULL) {
		LOG_FATAL("failed to find method Seq.incRefnum");
	}
	seq_incRef = (*env)->GetStaticMethodID(env, seq_class, "incRef", "(Ljava/lang/Object;)I");
	if (seq_incRef == NULL) {
		LOG_FATAL("failed to find method Seq.incRef");
	}
	seq_wrapThrowable = (*env)->GetStaticMethodID(env, seq_class, "wrapThrowable", "(Ljava/lang/Throwable;)Lgo/error;");
	if (seq_wrapThrowable == NULL) {
		LOG_FATAL("failed to find method Seq.wrapThrowable");
	}
	jclass throwable_class = (*env)->FindClass(env, "java/lang/Throwable");
	if (throwable_class == NULL) {
		LOG_FATAL("failed to find Throwable class");
	}
	throwable_getMessage = (*env)->GetMethodID(env, throwable_class, "getMessage", "()Ljava/lang/String;");
	if (throwable_getMessage == NULL) {
		LOG_FATAL("failed to find method Throwable.getMessage");
	}
	jclass ref_class = (*env)->FindClass(env, "go/Seq$Ref");
	if (ref_class == NULL) {
		LOG_FATAL("failed to find the Seq.Ref class");
	}
	ref_objField = (*env)->GetFieldID(env, ref_class, "obj", "Ljava/lang/Object;");
	if (ref_objField == NULL) {
		LOG_FATAL("failed to find the Seq.Ref.obj field");
	}
}

JNIEXPORT void JNICALL
Java_go_Seq_destroyRef(JNIEnv *env, jclass clazz, jint refnum) {
	DestroyRef(refnum);
}

JNIEXPORT void JNICALL
Java_go_Seq_incGoRef(JNIEnv *env, jclass clazz, jint refnum) {
	IncGoRef(refnum);
}
