/*
 * Copyright (C) 2022 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 android.companion.virtual.audio;

import static android.media.AudioRecord.READ_BLOCKING;
import static android.media.AudioRecord.RECORDSTATE_RECORDING;
import static android.media.AudioRecord.RECORDSTATE_STOPPED;
import static android.media.AudioRecord.STATE_INITIALIZED;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.util.Log;

import com.android.internal.annotations.GuardedBy;

import java.nio.ByteBuffer;

/**
 * Wrapper around {@link AudioRecord} that allows for the underlying {@link AudioRecord} to
 * be swapped out while recording is ongoing.
 *
 * @hide
 */
// The stop() actually doesn't release resources, so should not force implementing Closeable.
@SuppressLint("NotCloseable")
@SystemApi
public final class AudioCapture {
    private static final String TAG = "AudioCapture";

    private final AudioFormat mAudioFormat;
    private final Object mLock = new Object();

    @GuardedBy("mLock")
    @Nullable
    private AudioRecord mAudioRecord;
    @GuardedBy("mLock")
    private int mRecordingState = RECORDSTATE_STOPPED;

    /**
     * Sets the {@link AudioRecord} to handle audio capturing.
     *
     * <p>Callers may call this multiple times with different audio records to change the underlying
     * {@link AudioRecord} without stopping and re-starting recording.
     *
     * @param audioRecord The underlying {@link AudioRecord} to use for capture, or null if no audio
     *   (i.e. silence) should be captured while still keeping the record in a recording state.
     */
    void setAudioRecord(@Nullable AudioRecord audioRecord) {
        Log.d(TAG, "set AudioRecord with " + audioRecord);
        synchronized (mLock) {
            // Sync recording state for new reference.
            if (audioRecord != null) {
                if (audioRecord.getState() != STATE_INITIALIZED) {
                    throw new IllegalStateException("set an uninitialized AudioRecord.");
                }

                if (mRecordingState == RECORDSTATE_RECORDING
                        && audioRecord.getRecordingState() != RECORDSTATE_RECORDING) {
                    audioRecord.startRecording();
                }
                if (mRecordingState == RECORDSTATE_STOPPED
                        && audioRecord.getRecordingState() != RECORDSTATE_STOPPED) {
                    audioRecord.stop();
                }
            }

            // Release old reference before assigning the new reference.
            if (mAudioRecord != null) {
                mAudioRecord.release();
            }
            mAudioRecord = audioRecord;
        }
    }

    AudioCapture(@NonNull AudioFormat audioFormat) {
        mAudioFormat = audioFormat;
    }

    void close() {
        synchronized (mLock) {
            if (mAudioRecord != null) {
                mAudioRecord.release();
                mAudioRecord = null;
            }
        }
    }

    /** See {@link AudioRecord#getFormat()} */
    public @NonNull AudioFormat getFormat() {
        return mAudioFormat;
    }

    /** See {@link AudioRecord#read(byte[], int, int)} */
    public int read(@NonNull byte[] audioData, int offsetInBytes, int sizeInBytes) {
        return read(audioData, offsetInBytes, sizeInBytes, READ_BLOCKING);
    }

    /** See {@link AudioRecord#read(byte[], int, int, int)} */
    public int read(@NonNull byte[] audioData, int offsetInBytes, int sizeInBytes,
            @AudioRecord.ReadMode int readMode) {
        final int sizeRead;
        synchronized (mLock) {
            if (mAudioRecord != null) {
                sizeRead = mAudioRecord.read(audioData, offsetInBytes, sizeInBytes, readMode);
            } else {
                sizeRead = 0;
            }
        }
        return sizeRead;
    }

    /** See {@link AudioRecord#read(ByteBuffer, int)}. */
    public int read(@NonNull ByteBuffer audioBuffer, int sizeInBytes) {
        return read(audioBuffer, sizeInBytes, READ_BLOCKING);
    }

    /** See {@link AudioRecord#read(ByteBuffer, int, int)}. */
    public int read(@NonNull ByteBuffer audioBuffer, int sizeInBytes,
            @AudioRecord.ReadMode int readMode) {
        final int sizeRead;
        synchronized (mLock) {
            if (mAudioRecord != null) {
                sizeRead = mAudioRecord.read(audioBuffer, sizeInBytes, readMode);
            } else {
                sizeRead = 0;
            }
        }
        return sizeRead;
    }

    /** See {@link AudioRecord#read(float[], int, int, int)}. */
    public int read(@NonNull float[] audioData, int offsetInFloats, int sizeInFloats,
            @AudioRecord.ReadMode int readMode) {
        final int sizeRead;
        synchronized (mLock) {
            if (mAudioRecord != null) {
                sizeRead = mAudioRecord.read(audioData, offsetInFloats, sizeInFloats, readMode);
            } else {
                sizeRead = 0;
            }
        }
        return sizeRead;
    }

    /** See {@link AudioRecord#read(short[], int, int)}. */
    public int read(@NonNull short[] audioData, int offsetInShorts, int sizeInShorts) {
        return read(audioData, offsetInShorts, sizeInShorts, READ_BLOCKING);
    }

    /** See {@link AudioRecord#read(short[], int, int, int)}. */
    public int read(@NonNull short[] audioData, int offsetInShorts, int sizeInShorts,
            @AudioRecord.ReadMode int readMode) {
        final int sizeRead;
        synchronized (mLock) {
            if (mAudioRecord != null) {
                sizeRead = mAudioRecord.read(audioData, offsetInShorts, sizeInShorts, readMode);
            } else {
                sizeRead = 0;
            }
        }
        return sizeRead;
    }

    /** See {@link AudioRecord#startRecording()}. */
    public void startRecording() {
        synchronized (mLock) {
            mRecordingState = RECORDSTATE_RECORDING;
            if (mAudioRecord != null && mAudioRecord.getRecordingState() != RECORDSTATE_RECORDING) {
                mAudioRecord.startRecording();
            }
        }
    }

    /** See {@link AudioRecord#stop()}. */
    public void stop() {
        synchronized (mLock) {
            mRecordingState = RECORDSTATE_STOPPED;
            if (mAudioRecord != null && mAudioRecord.getRecordingState() != RECORDSTATE_STOPPED) {
                mAudioRecord.stop();
            }
        }
    }

    /** See {@link AudioRecord#getRecordingState()}. */
    public int getRecordingState() {
        synchronized (mLock) {
            return mRecordingState;
        }
    }
}
