/*
 * libjingle
 * Copyright 2016 Google Inc.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  1. Redistributions of source code must retain the above copyright notice,
 *     this list of conditions and the following disclaimer.
 *  2. Redistributions in binary form must reproduce the above copyright notice,
 *     this list of conditions and the following disclaimer in the documentation
 *     and/or other materials provided with the distribution.
 *  3. The name of the author may not be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
 * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package tee3.webrtc;

import android.media.AudioFormat;
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.os.Build;

import java.io.IOException;
import java.nio.ByteBuffer;

public class MediaCodecAudioEncoder{
    public enum AudioCodecType {
        AUDIO_CODEC_UNKNOWN,
        AUDIO_CODEC_AAC,
    }
    private static final String AAC_MIME_TYPE = "audio/mp4a-latm";
    private static final String TAG = "MediaCodecAudioEncoder";
    private static final int DEQUEUE_TIMEOUT = 0;  // Non-blocking, no wait.
    private static  int audioSampleRate = 44100;
    private static int audioChannel = AudioFormat.CHANNEL_IN_MONO;// changed by bob.liu, prev value: AudioFormat.CHANNEL_IN_STEREO;
    private static int audioBitsPerSample = AudioFormat.ENCODING_PCM_16BIT;
    public static int audioBitrate = 32 * 1000;  // 32kbps
    private AudioCodecType audioCodecType = AudioCodecType.AUDIO_CODEC_AAC;
    private MediaCodec audioEncoder;
    private ByteBuffer[] outputBuffers;
    private Thread mediaCodecThread;
    public static boolean isAacHwSupported(){
        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN)
            return false; // MediaCodec is missing.
        MediaCodec encoder;
        try {
            encoder = MediaCodec.createEncoderByType(AAC_MIME_TYPE);
        } catch (IOException e) {
            Logging.e(TAG, "Create audioEncoder failed, Hardware AAC encoder is not supported.");
            e.printStackTrace();
            return false;
        }
        return true;
    }

    MediaCodecAudioEncoder(){
        Logging.d(TAG,"construct MediaCodecAudioEncoder");
    }

    public boolean setPareams(int sampleRate, int channel, int bitsPerSample, int bitRate){
        if(sampleRate != audioSampleRate){
//            Logging.e(TAG, "Supported aac encoder with 44100 samplerate only. ");
//            return false;
            audioSampleRate = sampleRate;
        }
        if(1 == channel || 2 == channel){
            audioChannel =channel;
        }else{
            Logging.e(TAG, "Supported aac encoder with 1 or 2 channels only. ");
            return false;
        }

        if (bitRate != audioBitrate){
            audioBitrate = bitRate;
        }
        return configParams();
    }
    private String convertTypeToAndorid(AudioCodecType type){
        switch (type){
            case AUDIO_CODEC_AAC:
                return AAC_MIME_TYPE;
            default:
                return AAC_MIME_TYPE;
        }
    }
    private boolean configParams(){
        if(null != audioEncoder){
            try {
                MediaFormat audioFormat = new MediaFormat();
                audioFormat.setString(MediaFormat.KEY_MIME, convertTypeToAndorid(audioCodecType));
                audioFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, audioSampleRate);
                audioFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, audioChannel);
                audioBitrate = 64000;
                audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, audioBitrate);
                audioFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 0);
                audioEncoder.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
                return true;
            } catch (IllegalStateException e) {
                Logging.e(TAG, "configParams failed", e);
                return false;
            }
        }
        return  false;
    }

    private void checkOnMediaCodecThread() {
        if (mediaCodecThread.getId() != Thread.currentThread().getId()) {
            throw new RuntimeException(
                    "MediaCodecAudioEncoder previously operated on " + mediaCodecThread +
                            " but is now called on " + Thread.currentThread());
        }
    }

    public  ByteBuffer[] initEncode(AudioCodecType type, int sampleRate, int channel, int bitsPerSample, int bitRate){
        Logging.d(TAG,"initEncode, type = "+ type+",sampleRate = "+ sampleRate+",channel = "+channel+", bitsPerSample = "+bitsPerSample+",bitRate = "+bitRate);
        if (mediaCodecThread != null) {
            throw new RuntimeException("Forgot to release()?");
        }
        mediaCodecThread = Thread.currentThread();
        try {
            audioEncoder = MediaCodec.createEncoderByType(convertTypeToAndorid(type));
        } catch (IOException e) {
            Logging.e(TAG, "Create audioEncoder failed, Hardware AAC encoder is not supported.");
            e.printStackTrace();
            return null;
        }
        audioCodecType = type;
        if (!setPareams(sampleRate, channel, bitsPerSample, bitRate)){
            return null;
        }
        audioEncoder.start();
        ByteBuffer[] inputBuffers = audioEncoder.getInputBuffers();
        outputBuffers = audioEncoder.getOutputBuffers();

        Logging.v(TAG, "Input buffers: " + inputBuffers.length +
                ". Output buffers: " + outputBuffers.length);
        return inputBuffers;
    }

    private boolean encode(int inputBuffer, int size, long presentationTimestampUs) {
        checkOnMediaCodecThread();
        //Logging.v(TAG, "encode inputBufferIndex = "+ inputBuffer +",size="+size);
        try {
            audioEncoder.queueInputBuffer(inputBuffer, 0, size, presentationTimestampUs, 0);
            return true;
        }
        catch (IllegalStateException e) {
            Logging.e(TAG, "encode failed", e);
            return false;
        }
    }

    private int dequeueInputBuffer() {
        checkOnMediaCodecThread();
        //Logging.v(TAG, "dequeueInputBuffer");
        try {
            return audioEncoder.dequeueInputBuffer(DEQUEUE_TIMEOUT);
        } catch (IllegalStateException e) {
            Logging.e(TAG, "dequeueIntputBuffer failed", e);
            return -2;
        }
    }
    // Helper struct for dequeueOutputBuffer() below.
    private static class OutputBufferInfo {
        public OutputBufferInfo(
                int index, ByteBuffer buffer,
                boolean isKeyFrame, long presentationTimestampUs) {
            this.index = index;
            this.buffer = buffer;
            this.isKeyFrame = isKeyFrame;
            this.presentationTimestampUs = presentationTimestampUs;
        }

        private final int index;
        private final ByteBuffer buffer;
        private final boolean isKeyFrame;
        private final long presentationTimestampUs;
    }

    // Dequeue and return an output buffer, or null if no output is ready.  Return
    // a fake OutputBufferInfo with index -1 if the codec is no longer operable.
    private OutputBufferInfo dequeueOutputBuffer() {
        checkOnMediaCodecThread();
        //Logging.v(TAG, "dequeueOutputBuffer");
        try {
            MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
            int result = audioEncoder.dequeueOutputBuffer(info, DEQUEUE_TIMEOUT);
            // Check if this is config frame and save configuration data.
//            if (result >= 0) {
//                boolean isConfigFrame =
//                        (info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0;
//                if (isConfigFrame) {
//                    Logging.v(TAG, "Config frame generated. Offset: " + info.offset +
//                            ". Size: " + info.size);
//                    configData = ByteBuffer.allocateDirect(info.size);
//                    outputBuffers[result].position(info.offset);
//                    outputBuffers[result].limit(info.offset + info.size);
//                    configData.put(outputBuffers[result]);
//                    // Release buffer back.
//                    mediaCodec.releaseOutputBuffer(result, false);
//                    // Query next output.
//                    result = mediaCodec.dequeueOutputBuffer(info, DEQUEUE_TIMEOUT);
//                }
//            }
            if (result >= 0) {
                // MediaCodec doesn't care about Buffer position/remaining/etc so we can
                // mess with them to get a slice and avoid having to pass extra
                // (BufferInfo-related) parameters back to C++.
                ByteBuffer outputBuffer = outputBuffers[result].duplicate();
                outputBuffer.position(info.offset);
                outputBuffer.limit(info.offset + info.size);
                return new OutputBufferInfo(result, outputBuffer.slice(),false, info.presentationTimeUs);

            } else if (result == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                outputBuffers = audioEncoder.getOutputBuffers();
                return dequeueOutputBuffer();
            } else if (result == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                return dequeueOutputBuffer();
            } else if (result == MediaCodec.INFO_TRY_AGAIN_LATER) {
                Logging.d(TAG, "no acc out");
                return null;
            }
            throw new RuntimeException("dequeueOutputBuffer: " + result);
        } catch (IllegalStateException e) {
            Logging.e(TAG, "dequeueOutputBuffer failed", e);
            return new OutputBufferInfo(-1, null, false, -1);
        }
    }

    private boolean releaseOutputBuffer(int index) {
        checkOnMediaCodecThread();
        //Logging.d(TAG, "releaseOutputBuffer");
        try {
            audioEncoder.releaseOutputBuffer(index, false);
            return true;
        } catch (IllegalStateException e) {
            Logging.e(TAG, "releaseOutputBuffer failed", e);
            return false;
        }
    }

    private void release() {
        Logging.v(TAG, "Java releaseEncoder");
        //checkOnMediaCodecThread();
        try {
            audioEncoder.stop();
            audioEncoder.release();
        } catch (IllegalStateException e) {
            Logging.e(TAG, "release failed", e);
        }
        audioEncoder = null;
        mediaCodecThread = null;
    }
}