/*
 * Copyright 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 androidx.car.app.sample.navigation.common.car;

import static android.Manifest.permission.RECORD_AUDIO;
import static android.media.AudioAttributes.CONTENT_TYPE_MUSIC;
import static android.media.AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE;
import static android.media.AudioFormat.CHANNEL_OUT_MONO;
import static android.media.AudioFormat.ENCODING_DEFAULT;
import static android.media.AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
import static android.os.Build.VERSION.SDK_INT;

import static androidx.car.app.media.CarAudioRecord.AUDIO_CONTENT_BUFFER_SIZE;
import static androidx.car.app.media.CarAudioRecord.AUDIO_CONTENT_SAMPLING_RATE;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.os.Build.VERSION_CODES;

import androidx.annotation.NonNull;
import androidx.annotation.RequiresPermission;
import androidx.car.app.CarContext;
import androidx.car.app.CarToast;
import androidx.car.app.media.CarAudioRecord;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/** Manages recording the microphone and accessing the stored data from the microphone. */
public class MicrophoneRecorder {
    private static final String FILE_NAME = "recording.wav";

    private final CarContext mCarContext;

    public MicrophoneRecorder(@NonNull CarContext carContext) {
        mCarContext = carContext;
    }

    /**
     * Starts recording the car microphone, then plays it back.
     */
    public void record() {
        if (mCarContext.checkSelfPermission(RECORD_AUDIO)
                != PackageManager.PERMISSION_GRANTED) {
            CarToast.makeText(mCarContext, "Grant mic permission on phone",
                    CarToast.LENGTH_LONG).show();
            List<String> permissions = Collections.singletonList(RECORD_AUDIO);
            mCarContext.requestPermissions(permissions, (grantedPermissions,
                    rejectedPermissions) -> {
                if (grantedPermissions.contains(RECORD_AUDIO)) {
                    record();
                }
            });
            return;
        }
        CarAudioRecord record = CarAudioRecord.create(mCarContext);

        Thread recordingThread =
                new Thread(
                        () -> doRecord(record),
                        "AudioRecorder Thread");
        recordingThread.start();
    }

    @SuppressLint("ClassVerificationFailure") // runtime check for < API 26
    @RequiresPermission(RECORD_AUDIO)
    private void play(AudioFocusRequest audioFocusRequest) {
        if (SDK_INT < VERSION_CODES.O) {
            return;
        }

        InputStream inputStream;
        try {
            inputStream = mCarContext.openFileInput(FILE_NAME);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            return;
        }

        AudioTrack audioTrack = new AudioTrack.Builder()
                .setAudioAttributes(new AudioAttributes.Builder()
                        .setUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
                        .setContentType(CONTENT_TYPE_MUSIC)
                        .build())
                .setAudioFormat(new AudioFormat.Builder()
                        .setEncoding(ENCODING_DEFAULT)
                        .setSampleRate(AUDIO_CONTENT_SAMPLING_RATE)
                        .setChannelMask(CHANNEL_OUT_MONO)
                        .build())
                .setBufferSizeInBytes(AUDIO_CONTENT_BUFFER_SIZE)
                .build();
        audioTrack.play();
        try {
            while (inputStream.available() > 0) {
                byte[] audioData = new byte[AUDIO_CONTENT_BUFFER_SIZE];
                int size = inputStream.read(audioData, 0, audioData.length);

                if (size < 0) {
                    // End of file
                    return;
                }
                audioTrack.write(audioData, 0, size);
            }
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }
        audioTrack.stop();
        // Abandon the FocusRequest so that user's media can be resumed
        mCarContext.getSystemService(AudioManager.class).abandonAudioFocusRequest(
                audioFocusRequest);
    }

    @SuppressLint("ClassVerificationFailure") // runtime check for < API 26
    @RequiresPermission(RECORD_AUDIO)
    private void doRecord(CarAudioRecord record) {
        if (SDK_INT < VERSION_CODES.O) {
            return;
        }

        // Take audio focus so that user's media is not recorded
        AudioAttributes audioAttributes =
                new AudioAttributes.Builder()
                        .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
                        .setUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
                        .build();

        AudioFocusRequest audioFocusRequest =
                new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
                        .setAudioAttributes(audioAttributes)
                        .setOnAudioFocusChangeListener(state -> {
                            if (state == AudioManager.AUDIOFOCUS_LOSS) {
                                // Stop recording if audio focus is lost
                                record.stopRecording();
                            }
                        })
                        .build();

        if (mCarContext.getSystemService(AudioManager.class).requestAudioFocus(audioFocusRequest)
                != AUDIOFOCUS_REQUEST_GRANTED) {
            return;
        }

        record.startRecording();

        List<Byte> bytes = new ArrayList<>();
        boolean isRecording = true;
        while (isRecording) {
            // gets the voice output from microphone to byte format
            byte[] bData = new byte[AUDIO_CONTENT_BUFFER_SIZE];
            int len = record.read(bData, 0, AUDIO_CONTENT_BUFFER_SIZE);

            if (len > 0) {
                for (int i = 0; i < len; i++) {
                    bytes.add(bData[i]);
                }
            } else {
                isRecording = false;
            }
        }

        try {
            OutputStream outputStream = mCarContext.openFileOutput(FILE_NAME, Context.MODE_PRIVATE);
            addHeader(outputStream, bytes.size());
            for (Byte b : bytes) {
                outputStream.write(b);
            }

            outputStream.flush();
            outputStream.close();
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }
        record.stopRecording();
        play(audioFocusRequest);
    }

    private void addHeader(OutputStream outputStream, int totalAudioLen) throws IOException {
        int totalDataLen = totalAudioLen + 36;
        byte[] header = new byte[44];
        int dataElementSize = 8;
        long longSampleRate = AUDIO_CONTENT_SAMPLING_RATE;

        // See http://soundfile.sapp.org/doc/WaveFormat/
        header[0] = 'R';  // RIFF/WAVE header
        header[1] = 'I';
        header[2] = 'F';
        header[3] = 'F';
        header[4] = (byte) (totalAudioLen & 0xff);
        header[5] = (byte) ((totalDataLen >> 8) & 0xff);
        header[6] = (byte) ((totalDataLen >> 16) & 0xff);
        header[7] = (byte) ((totalDataLen >> 24) & 0xff);
        header[8] = 'W';
        header[9] = 'A';
        header[10] = 'V';
        header[11] = 'E';
        header[12] = 'f';  // 'fmt ' chunk
        header[13] = 'm';
        header[14] = 't';
        header[15] = ' ';
        header[16] = 16;  // 4 bytes: size of 'fmt ' chunk
        header[17] = 0;
        header[18] = 0;
        header[19] = 0;
        header[20] = 1;  // format = 1 PCM
        header[21] = 0;
        header[22] = 1; // Num channels (mono)
        header[23] = 0;
        header[24] = (byte) (longSampleRate & 0xff); // sample rate
        header[25] = (byte) ((longSampleRate >> 8) & 0xff);
        header[26] = (byte) ((longSampleRate >> 16) & 0xff);
        header[27] = (byte) ((longSampleRate >> 24) & 0xff);
        header[28] = (byte) (longSampleRate & 0xff); // byte rate
        header[29] = (byte) ((longSampleRate >> 8) & 0xff);
        header[30] = (byte) ((longSampleRate >> 16) & 0xff);
        header[31] = (byte) ((longSampleRate >> 24) & 0xff);
        header[32] = 1;  // block align
        header[33] = 0;
        header[34] = (byte) (dataElementSize & 0xff);  // bits per sample
        header[35] = (byte) ((dataElementSize >> 8) & 0xff);
        header[36] = 'd';
        header[37] = 'a';
        header[38] = 't';
        header[39] = 'a';
        header[40] = (byte) (totalAudioLen & 0xff);
        header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
        header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
        header[43] = (byte) ((totalAudioLen >> 24) & 0xff);

        outputStream.write(header, 0, 44);
    }
}
