// Copyright 2016 Hajime Hoshi
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package oto

/*

#include <jni.h>
#include <stdlib.h>

static jclass android_media_AudioFormat;
static jclass android_media_AudioManager;
static jclass android_media_AudioTrack;

static char* initAudioTrack(uintptr_t java_vm, uintptr_t jni_env,
    int sampleRate, int channelNum, int bitDepthInBytes, jobject* audioTrack, int bufferSize) {
  JavaVM* vm = (JavaVM*)java_vm;
  JNIEnv* env = (JNIEnv*)jni_env;

  jclass android_os_Build_VERSION = (*env)->FindClass(env, "android/os/Build$VERSION");
  const jint availableSDK =
      (*env)->GetStaticIntField(
          env, android_os_Build_VERSION,
          (*env)->GetStaticFieldID(env, android_os_Build_VERSION, "SDK_INT", "I"));
  (*env)->DeleteLocalRef(env, android_os_Build_VERSION);

  jclass local = (*env)->FindClass(env, "android/media/AudioFormat");
  android_media_AudioFormat = (*env)->NewGlobalRef(env, local);
  (*env)->DeleteLocalRef(env, local);

  local = (*env)->FindClass(env, "android/media/AudioManager");
  android_media_AudioManager = (*env)->NewGlobalRef(env, local);
  (*env)->DeleteLocalRef(env, local);

  local = (*env)->FindClass(env, "android/media/AudioTrack");
  android_media_AudioTrack = (*env)->NewGlobalRef(env, local);
  (*env)->DeleteLocalRef(env, local);

  const jint android_media_AudioManager_STREAM_MUSIC =
      (*env)->GetStaticIntField(
          env, android_media_AudioManager,
          (*env)->GetStaticFieldID(env, android_media_AudioManager, "STREAM_MUSIC", "I"));
  const jint android_media_AudioTrack_MODE_STREAM =
      (*env)->GetStaticIntField(
          env, android_media_AudioTrack,
          (*env)->GetStaticFieldID(env, android_media_AudioTrack, "MODE_STREAM", "I"));
  const jint android_media_AudioFormat_CHANNEL_OUT_MONO =
      (*env)->GetStaticIntField(
          env, android_media_AudioFormat,
          (*env)->GetStaticFieldID(env, android_media_AudioFormat, "CHANNEL_OUT_MONO", "I"));
  const jint android_media_AudioFormat_CHANNEL_OUT_STEREO =
      (*env)->GetStaticIntField(
          env, android_media_AudioFormat,
          (*env)->GetStaticFieldID(env, android_media_AudioFormat, "CHANNEL_OUT_STEREO", "I"));
  const jint android_media_AudioFormat_ENCODING_PCM_8BIT =
      (*env)->GetStaticIntField(
          env, android_media_AudioFormat,
          (*env)->GetStaticFieldID(env, android_media_AudioFormat, "ENCODING_PCM_8BIT", "I"));
  const jint android_media_AudioFormat_ENCODING_PCM_16BIT =
      (*env)->GetStaticIntField(
          env, android_media_AudioFormat,
          (*env)->GetStaticFieldID(env, android_media_AudioFormat, "ENCODING_PCM_16BIT", "I"));

  jint channel = android_media_AudioFormat_CHANNEL_OUT_MONO;
  switch (channelNum) {
  case 1:
    channel = android_media_AudioFormat_CHANNEL_OUT_MONO;
    break;
  case 2:
    channel = android_media_AudioFormat_CHANNEL_OUT_STEREO;
    break;
  default:
    return "invalid channel";
  }

  jint encoding = android_media_AudioFormat_ENCODING_PCM_8BIT;
  switch (bitDepthInBytes) {
  case 1:
    encoding = android_media_AudioFormat_ENCODING_PCM_8BIT;
    break;
  case 2:
    encoding = android_media_AudioFormat_ENCODING_PCM_16BIT;
    break;
  default:
    return "invalid bitDepthInBytes";
  }

  // If the available Android SDK is at least 24 (7.0 Nougat), the FLAG_LOW_LATENCY is available.
  // This requires a different constructor.
  if (availableSDK >= 24) {
    jclass android_media_AudioAttributes_Builder;
    jclass android_media_AudioFormat_Builder;
    jclass android_media_AudioAttributes;

    local = (*env)->FindClass(env, "android/media/AudioAttributes$Builder");
    android_media_AudioAttributes_Builder = (*env)->NewGlobalRef(env, local);
    (*env)->DeleteLocalRef(env, local);

    local = (*env)->FindClass(env, "android/media/AudioFormat$Builder");
    android_media_AudioFormat_Builder = (*env)->NewGlobalRef(env, local);
    (*env)->DeleteLocalRef(env, local);

    local = (*env)->FindClass(env, "android/media/AudioAttributes");
    android_media_AudioAttributes = (*env)->NewGlobalRef(env, local);
    (*env)->DeleteLocalRef(env, local);

    jint android_media_AudioAttributes_USAGE_UNKNOWN =
        (*env)->GetStaticIntField(
            env, android_media_AudioAttributes,
            (*env)->GetStaticFieldID(env, android_media_AudioAttributes, "USAGE_UNKNOWN", "I"));
    jint android_media_AudioAttributes_CONTENT_TYPE_UNKNOWN =
        (*env)->GetStaticIntField(
            env, android_media_AudioAttributes,
            (*env)->GetStaticFieldID(env, android_media_AudioAttributes, "CONTENT_TYPE_UNKNOWN", "I"));
    jint android_media_AudioAttributes_FLAG_LOW_LATENCY =
        (*env)->GetStaticIntField(
            env, android_media_AudioAttributes,
            (*env)->GetStaticFieldID(env, android_media_AudioAttributes, "FLAG_LOW_LATENCY", "I"));

    const jobject aattrBld =
        (*env)->NewObject(
            env, android_media_AudioAttributes_Builder,
            (*env)->GetMethodID(env, android_media_AudioAttributes_Builder, "<init>", "()V"));

    (*env)->CallObjectMethod(
        env, aattrBld,
        (*env)->GetMethodID(env, android_media_AudioAttributes_Builder, "setUsage", "(I)Landroid/media/AudioAttributes$Builder;"),
        android_media_AudioAttributes_USAGE_UNKNOWN);
    (*env)->CallObjectMethod(
        env, aattrBld,
        (*env)->GetMethodID(env, android_media_AudioAttributes_Builder, "setContentType", "(I)Landroid/media/AudioAttributes$Builder;"),
        android_media_AudioAttributes_CONTENT_TYPE_UNKNOWN);
    (*env)->CallObjectMethod(
        env, aattrBld,
        (*env)->GetMethodID(env, android_media_AudioAttributes_Builder, "setFlags", "(I)Landroid/media/AudioAttributes$Builder;"),
        android_media_AudioAttributes_FLAG_LOW_LATENCY);
    const jobject aattr =
        (*env)->CallObjectMethod(
            env, aattrBld,
            (*env)->GetMethodID(env, android_media_AudioAttributes_Builder, "build", "()Landroid/media/AudioAttributes;"));
    (*env)->DeleteLocalRef(env, aattrBld);

    const jobject afmtBld =
        (*env)->NewObject(
            env, android_media_AudioFormat_Builder,
            (*env)->GetMethodID(env, android_media_AudioFormat_Builder, "<init>", "()V"));
    (*env)->CallObjectMethod(
        env, afmtBld,
        (*env)->GetMethodID(env, android_media_AudioFormat_Builder, "setSampleRate", "(I)Landroid/media/AudioFormat$Builder;"),
        sampleRate);
    (*env)->CallObjectMethod(
        env, afmtBld,
        (*env)->GetMethodID(env, android_media_AudioFormat_Builder, "setEncoding", "(I)Landroid/media/AudioFormat$Builder;"),
        encoding);
    (*env)->CallObjectMethod(
        env, afmtBld,
        (*env)->GetMethodID(env, android_media_AudioFormat_Builder, "setChannelMask", "(I)Landroid/media/AudioFormat$Builder;"),
        channel);
    const jobject afmt =
        (*env)->CallObjectMethod(
            env, afmtBld,
            (*env)->GetMethodID(env, android_media_AudioFormat_Builder, "build", "()Landroid/media/AudioFormat;"));
    (*env)->DeleteLocalRef(env, afmtBld);

    const jobject tmpAudioTrack =
        (*env)->NewObject(
            env, android_media_AudioTrack,
            (*env)->GetMethodID(env, android_media_AudioTrack, "<init>", "(Landroid/media/AudioAttributes;Landroid/media/AudioFormat;III)V"),
            aattr, afmt, bufferSize, android_media_AudioTrack_MODE_STREAM, 0);
    *audioTrack = (*env)->NewGlobalRef(env, tmpAudioTrack);
    (*env)->DeleteLocalRef(env, tmpAudioTrack);
    (*env)->DeleteLocalRef(env, aattr);
    (*env)->DeleteLocalRef(env, afmt);
  } else {
    const jobject tmpAudioTrack =
        (*env)->NewObject(
            env, android_media_AudioTrack,
            (*env)->GetMethodID(env, android_media_AudioTrack, "<init>", "(IIIIII)V"),
            android_media_AudioManager_STREAM_MUSIC,
            sampleRate, channel, encoding, bufferSize,
            android_media_AudioTrack_MODE_STREAM);
    *audioTrack = (*env)->NewGlobalRef(env, tmpAudioTrack);
    (*env)->DeleteLocalRef(env, tmpAudioTrack);
  }

  (*env)->CallVoidMethod(
      env, *audioTrack,
      (*env)->GetMethodID(env, android_media_AudioTrack, "play", "()V"));

  return NULL;
}

static char* writeToAudioTrack(uintptr_t java_vm, uintptr_t jni_env,
    jobject audioTrack, int bitDepthInBytes, void* data, int length) {
  JavaVM* vm = (JavaVM*)java_vm;
  JNIEnv* env = (JNIEnv*)jni_env;

  jbyteArray arrInBytes;
  jshortArray arrInShorts;
  switch (bitDepthInBytes) {
  case 1:
    arrInBytes = (*env)->NewByteArray(env, length);
    (*env)->SetByteArrayRegion(env, arrInBytes, 0, length, data);
    break;
  case 2:
    arrInShorts = (*env)->NewShortArray(env, length);
    (*env)->SetShortArrayRegion(env, arrInShorts, 0, length, data);
    break;
  }

  jint result;
  static jmethodID write1 = NULL;
  static jmethodID write2 = NULL;
  if (!write1) {
    write1 = (*env)->GetMethodID(env, android_media_AudioTrack, "write", "([BII)I");
  }
  if (!write2) {
    write2 = (*env)->GetMethodID(env, android_media_AudioTrack, "write", "([SII)I");
  }
  switch (bitDepthInBytes) {
  case 1:
    result = (*env)->CallIntMethod(env, audioTrack, write1, arrInBytes, 0, length);
    (*env)->DeleteLocalRef(env, arrInBytes);
    break;
  case 2:
    result = (*env)->CallIntMethod(env, audioTrack, write2, arrInShorts, 0, length);
    (*env)->DeleteLocalRef(env, arrInShorts);
    break;
  }

  switch (result) {
  case -3: // ERROR_INVALID_OPERATION
    return "invalid operation";
  case -2: // ERROR_BAD_VALUE
    return "bad value";
  case -1: // ERROR
    return "error";
  }
  if (result < 0) {
    return "unknown error";
  }
  return NULL;
}

static char* releaseAudioTrack(uintptr_t java_vm, uintptr_t jni_env,
    jobject audioTrack) {
  JavaVM* vm = (JavaVM*)java_vm;
  JNIEnv* env = (JNIEnv*)jni_env;

  (*env)->CallVoidMethod(
      env, audioTrack,
      (*env)->GetMethodID(env, android_media_AudioTrack, "release", "()V"));
  return NULL;
}

*/
import "C"

import (
	"errors"
	"runtime"
	"unsafe"

	"golang.org/x/mobile/app"
)

type driver struct {
	sampleRate      int
	channelNum      int
	bitDepthInBytes int
	audioTrack      C.jobject
	chErr           chan error
	chBuffer        chan []byte
	tmp             []byte
	bufferSize      int
}

func newDriver(sampleRate, channelNum, bitDepthInBytes, bufferSizeInBytes int) (tryWriteCloser, error) {
	p := &driver{
		sampleRate:      sampleRate,
		channelNum:      channelNum,
		bitDepthInBytes: bitDepthInBytes,
		chErr:           make(chan error),
		chBuffer:        make(chan []byte),
	}
	runtime.SetFinalizer(p, (*driver).Close)

	if err := app.RunOnJVM(func(vm, env, ctx uintptr) error {
		audioTrack := C.jobject(0)
		bufferSize := C.int(bufferSizeInBytes)
		if msg := C.initAudioTrack(C.uintptr_t(vm), C.uintptr_t(env),
			C.int(sampleRate), C.int(channelNum), C.int(bitDepthInBytes),
			&audioTrack, bufferSize); msg != nil {
			return errors.New("oto: initAutioTrack failed: " + C.GoString(msg))
		}
		p.audioTrack = audioTrack
		p.bufferSize = int(bufferSize)
		return nil
	}); err != nil {
		return nil, err
	}

	go p.loop()
	return p, nil
}

func (p *driver) loop() {
	for bufInBytes := range p.chBuffer {
		var bufInShorts []int16
		if p.bitDepthInBytes == 2 {
			bufInShorts = make([]int16, len(bufInBytes)/2)
			for i := 0; i < len(bufInShorts); i++ {
				bufInShorts[i] = int16(bufInBytes[2*i]) | (int16(bufInBytes[2*i+1]) << 8)
			}
		}
		if err := app.RunOnJVM(func(vm, env, ctx uintptr) error {
			msg := (*C.char)(nil)
			switch p.bitDepthInBytes {
			case 1:
				msg = C.writeToAudioTrack(C.uintptr_t(vm), C.uintptr_t(env),
					p.audioTrack, C.int(p.bitDepthInBytes),
					unsafe.Pointer(&bufInBytes[0]), C.int(len(bufInBytes)))
			case 2:
				msg = C.writeToAudioTrack(C.uintptr_t(vm), C.uintptr_t(env),
					p.audioTrack, C.int(p.bitDepthInBytes),
					unsafe.Pointer(&bufInShorts[0]), C.int(len(bufInShorts)))
			default:
				panic("not reach")
			}
			if msg != nil {
				return errors.New("oto: loop failed: " + C.GoString(msg))
			}
			return nil
		}); err != nil {
			p.chErr <- err
			return
		}
	}
}

func (p *driver) TryWrite(data []byte) (int, error) {
	n := min(len(data), p.bufferSize-len(p.tmp))
	p.tmp = append(p.tmp, data[:n]...)

	if len(p.tmp) < p.bufferSize {
		return n, nil
	}

	select {
	case p.chBuffer <- p.tmp:
	case err := <-p.chErr:
		return 0, err
	}

	p.tmp = nil
	return n, nil
}

func (p *driver) Close() error {
	if p.audioTrack == 0 {
		return nil
	}

	runtime.SetFinalizer(p, nil)
	err := app.RunOnJVM(func(vm, env, ctx uintptr) error {
		if msg := C.releaseAudioTrack(C.uintptr_t(vm), C.uintptr_t(env),
			p.audioTrack); msg != nil {
			return errors.New("oto: release failed: " + C.GoString(msg))
		}
		return nil
	})

	p.audioTrack = 0
	return err
}

func (d *driver) tryWriteCanReturnWithoutWaiting() bool {
	return true
}
