/*
 * Copyright (C) 2021 The Android Open Source Project
 *
 * 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 com.android.tv.settings.system.development.audio;

import android.annotation.Nullable;
import android.content.Context;
import android.media.AudioFormat;
import android.media.AudioTrack;
import android.os.Handler;
import android.util.Log;

import java.nio.ShortBuffer;

/** Manages audio recording, audio metrics, and audio playback for debugging purposes. */
public class AudioDebug {

    private static final String TAG = "AudioDebug";

    public static final int CHANNELS = 1;
    public static final int SAMPLE_RATE = 16000;
    public static final int BITRATE = 16;
    public static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT;

    private final Context mContext;

    private final AudioRecordedCallback mAudioRecordedCallback;

    private final AudioMetrics mMetrics;

    @Nullable
    private AudioReader mAudioReader;

    @Nullable
    private ShortBuffer mAudioBuffer;

    @Nullable
    private AudioTrack mAudioTrack;

    /** Interface for receiving a notification when audio recording finishes. */
    public interface AudioRecordedCallback {
        /** Callback for receiving a notification when audio recording finishes. */
        void onAudioRecorded(boolean successful);
    }

    /**
     * @param context               The parent context
     * @param audioRecordedCallback Callback for notification on audio recording completion
     * @param metricsCallback       Callback for metrics updates
     */
    public AudioDebug(Context context, AudioRecordedCallback audioRecordedCallback,
            AudioMetrics.UpdateMetricsCallback metricsCallback) {
        this.mContext = context;
        this.mAudioRecordedCallback = audioRecordedCallback;

        mMetrics = new AudioMetrics(metricsCallback);
    }

    /** Starts recording audio. */
    public void startRecording(int audioSource) throws AudioReaderException {
        if (mAudioReader != null) {
            mAudioReader.stop();
        }

        mMetrics.start();

        mAudioReader = new AudioReader(audioSource, mMetrics);
        mAudioReader.addListener((ShortBuffer buffer) -> onAudioRecorded(buffer));

        Thread audioReaderThread = new Thread(mAudioReader);
        audioReaderThread.setPriority(10);
        audioReaderThread.start();
    }

    /**
     * Stores a buffer containing recorded audio in an AudioTrack. Overwrites any previously
     * recorded audio.
     *
     * @param audioBuffer The buffer containing the recorded audio.
     */
    private void onAudioRecorded(ShortBuffer audioBuffer) {
        if (audioBuffer.position() == 0) {
            Log.e(TAG, "Empty buffer recorded");
            return;
        }

        this.mAudioBuffer = audioBuffer;

        int numShorts = audioBuffer.position();
        int numBytes = numShorts * 2;

        Handler mainHandler = new Handler(mContext.getMainLooper());

        try {
            mAudioTrack =
                    new AudioTrack.Builder()
                            .setAudioFormat(
                                    new AudioFormat.Builder()
                                            .setSampleRate(SAMPLE_RATE)
                                            .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
                                            .setEncoding(ENCODING)
                                            .build()
                            )
                            .setTransferMode(AudioTrack.MODE_STATIC)
                            .setBufferSizeInBytes(numBytes)
                            .build();
        } catch (UnsupportedOperationException | IllegalArgumentException e) {
            Log.e(TAG, "Failed to create AudioTrack", e);
            mainHandler.post(() -> mAudioRecordedCallback.onAudioRecorded(false));
            return;
        }

        Log.i(TAG, String.format("AudioTrack state: %d", mAudioTrack.getState()));

        int writeStatus = mAudioTrack.write(audioBuffer.array(), 0, numShorts,
                AudioTrack.WRITE_BLOCKING);
        if (writeStatus > 0) {
            Log.i(TAG, String.format("Wrote %d bytes to an AudioTrack", numBytes));
            mainHandler.post(() -> mAudioRecordedCallback.onAudioRecorded(true));
        } else if (writeStatus == 0) {
            Log.e(TAG, "Received empty audio buffer");
        } else {
            Log.e(TAG, String.format("Error calling AudioTrack.write(): %d", writeStatus));
            mainHandler.post(() -> mAudioRecordedCallback.onAudioRecorded(false));
        }
    }


    /** Stops recording audio. */
    public void stopRecording() {
        if (mAudioReader != null) {
            mAudioReader.stop();
            mAudioReader = null;
        }
    }

    /** Stops recording audio, and discards the recorded audio. */
    public void cancelRecording() {
        if (mAudioReader != null) {
            mAudioReader.cancel();
            mAudioReader = null;
        }
    }


    /** Plays the recorded audio. */
    public void playAudio() {
        if (mAudioTrack == null) {
            Log.e(TAG, "No audio track recorded");
        } else {
            mAudioTrack.stop();
            mAudioTrack.reloadStaticData();
            mAudioTrack.setPlaybackHeadPosition(0);
            mAudioTrack.setVolume(1.0f);
            mAudioTrack.play();
        }
    }

    /** Writes the recorded audio to a WAV file. */
    public void writeAudioToFile() {
        WavWriter.writeToFile(mContext.getExternalFilesDir(null), mAudioBuffer);
    }
}
