/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2016 Piasy
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package com.github.piasy.rxandroidaudio;

import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.util.Log;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;

import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;

/**
 * Created by Piasy{github.com/Piasy} on 16/2/24.
 *
 * <em>NOTE: users should only have one instance active at the same time.</em>
 */

@SuppressWarnings({"WeakerAccess"})
public final class StreamAudioRecorder {
    public static final int DEFAULT_SAMPLE_RATE = 44100;
    public static final int DEFAULT_BUFFER_SIZE = 2048;

    private static final String TAG = "StreamAudioRecorder";

    private final AtomicBoolean mIsRecording;
    private ExecutorService mExecutorService;

    private StreamAudioRecorder() {
        // singleton
        mIsRecording = new AtomicBoolean(false);
    }

    public static StreamAudioRecorder getInstance() {
        return StreamAudioRecorderHolder.INSTANCE;
    }

    public synchronized boolean start(@NonNull AudioDataCallback audioDataCallback) {
        return start(DEFAULT_SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO,
                AudioFormat.ENCODING_PCM_16BIT, DEFAULT_BUFFER_SIZE, audioDataCallback);
    }

    /**
     * AudioFormat.CHANNEL_IN_MONO
     * AudioFormat.ENCODING_PCM_16BIT
     */
    public synchronized boolean start(int sampleRate, int channelConfig, int audioFormat,
            int bufferSize, @NonNull AudioDataCallback audioDataCallback) {
        stop();

        mExecutorService = Executors.newSingleThreadExecutor();
        if (mIsRecording.compareAndSet(false, true)) {
            mExecutorService.execute(
                    new AudioRecordRunnable(sampleRate, channelConfig, audioFormat, bufferSize,
                            audioDataCallback));
            return true;
        }
        return false;
    }

    public synchronized void stop() {
        mIsRecording.compareAndSet(true, false);

        if (mExecutorService != null) {
            mExecutorService.shutdown();
            mExecutorService = null;
        }
    }

    /**
     * Although Android frameworks jni implementation are the same for ENCODING_PCM_16BIT and
     * ENCODING_PCM_8BIT, the Java doc declared that the buffer type should be the corresponding
     * type, so we use different ways.
     */
    public interface AudioDataCallback {
        @WorkerThread
        void onAudioData(byte[] data, int size);

        void onError();
    }

    private static final class StreamAudioRecorderHolder {
        private static final StreamAudioRecorder INSTANCE = new StreamAudioRecorder();
    }

    private class AudioRecordRunnable implements Runnable {

        private final AudioRecord mAudioRecord;
        private final AudioDataCallback mAudioDataCallback;

        private final byte[] mByteBuffer;
        private final short[] mShortBuffer;
        private final int mByteBufferSize;
        private final int mShortBufferSize;
        private final int mAudioFormat;

        AudioRecordRunnable(int sampleRate, int channelConfig, int audioFormat, int byteBufferSize,
                @NonNull AudioDataCallback audioDataCallback) {
            mAudioFormat = audioFormat;
            int minBufferSize =
                    AudioRecord.getMinBufferSize(sampleRate, channelConfig, mAudioFormat);
            mByteBufferSize = byteBufferSize;
            mShortBufferSize = mByteBufferSize / 2;
            mByteBuffer = new byte[mByteBufferSize];
            mShortBuffer = new short[mShortBufferSize];
            mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRate, channelConfig,
                    audioFormat, Math.max(minBufferSize, byteBufferSize));
            mAudioDataCallback = audioDataCallback;
        }

        @Override
        public void run() {
            if (mAudioRecord.getState() == AudioRecord.STATE_INITIALIZED) {
                try {
                    mAudioRecord.startRecording();
                } catch (IllegalStateException e) {
                    Log.w(TAG, "startRecording fail: " + e.getMessage());
                    mAudioDataCallback.onError();
                    return;
                }
                while (mIsRecording.get()) {
                    int ret;
                    if (mAudioFormat == AudioFormat.ENCODING_PCM_16BIT) {
                        ret = mAudioRecord.read(mShortBuffer, 0, mShortBufferSize);
                        if (ret > 0) {
                            mAudioDataCallback.onAudioData(
                                    short2byte(mShortBuffer, ret, mByteBuffer), ret * 2);
                        } else {
                            onError(ret);
                            break;
                        }
                    } else {
                        ret = mAudioRecord.read(mByteBuffer, 0, mByteBufferSize);
                        if (ret > 0) {
                            mAudioDataCallback.onAudioData(mByteBuffer, ret);
                        } else {
                            onError(ret);
                            break;
                        }
                    }
                }
            }
            mAudioRecord.release();
        }

        private byte[] short2byte(short[] sData, int size, byte[] bData) {
            if (size > sData.length || size * 2 > bData.length) {
                Log.w(TAG, "short2byte: too long short data array");
            }
            for (int i = 0; i < size; i++) {
                bData[i * 2] = (byte) (sData[i] & 0x00FF);
                bData[(i * 2) + 1] = (byte) (sData[i] >> 8);
            }
            return bData;
        }

        private void onError(int errorCode) {
            if (errorCode == AudioRecord.ERROR_INVALID_OPERATION) {
                Log.w(TAG, "record fail: ERROR_INVALID_OPERATION");
                mAudioDataCallback.onError();
            } else if (errorCode == AudioRecord.ERROR_BAD_VALUE) {
                Log.w(TAG, "record fail: ERROR_BAD_VALUE");
                mAudioDataCallback.onError();
            }
        }
    }
}