package cz.vutbr.bpdataacquisition;

import android.content.Context;
import android.media.AudioDeviceInfo;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTimestamp;
import android.media.AudioTrack;
import android.media.MediaRecorder;
import android.util.Log;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;

public class AudioDataAcquisition {

    // TAG
    private final String TAG = getClass().getSimpleName();

    // CONSTANTS
    private static final int RECORDER_SAMPLE_RATE = 8000;
    private static final int RECORDER_CHANNELS = AudioFormat.CHANNEL_IN_MONO;
    private static final int RECORDER_AUDIO_ENCODING = AudioFormat.ENCODING_PCM_16BIT;
    private static final int BYTES_PER_ELEMENT = 2;
    private static final int BUFFER_SIZE = 1024; //AudioRecord.getMinBufferSize(RECORDER_SAMPLE_RATE, RECORDER_CHANNELS, RECORDER_AUDIO_ENCODING);

    // OBJECTS
    private AudioRecord mAudioRecord;
    private AudioManager mAudioManager;
    private AudioTrack mAudioTrack;

    // FLAGS
    private boolean mIsRecording;

    // VARIABLES
    private double mGain;
    private String mFilePath;
    private int mOrigionalVolume;

    // TIME STAMP
    private AudioTimestamp mAudioTimestamp;

    // THREADS
    private Thread mAudioThread;

    // LISTENERS
    private AudioListener listener;

    // CONTEXT
    private Context appContext;

    // CONSTRUCTOR
    public AudioDataAcquisition(Context _appContext, AudioManager _audioManager) {
        appContext = _appContext;

        mIsRecording = false;
        mGain = 1;

        listener = null;

        // INITIALIZE OBJECTS
        mAudioManager = _audioManager;
        mOrigionalVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);

        mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, RECORDER_SAMPLE_RATE, AudioFormat.CHANNEL_OUT_MONO, RECORDER_AUDIO_ENCODING, BUFFER_SIZE, AudioTrack.MODE_STREAM);
    }

    // LISTENERS - METHODS
    public interface AudioListener {
        void onAudioValueChanged(short[] data);
    }

    // LISTENERS - SET UP
    public void setAudioListener(AudioListener _listener) {
        listener = _listener;
    }

    // GAIN - SET UP
    public void setGain(double gain) {
        mGain = gain;
    }

    // TIMESTAMP - FRAME POSITION
    public long getAudioSamplePosition() {
        if (mAudioTimestamp != null) {
            return mAudioTimestamp.framePosition;
        }
        return 0;
    }

    // TIMESTAMP - NANO TIME
    public long getAudioSampleTime() {
        if (mAudioTimestamp != null) {
            return mAudioTimestamp.nanoTime;
        }
        return 0;
    }

    // RECORDING STATE
    public boolean isRecording() {
        return mIsRecording;
    }

    // START RECORDING
    public void startRecording(String filePath) {
        list = new ArrayList<AudioTimeStamp>();

        mFilePath = filePath;
        mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, RECORDER_SAMPLE_RATE, RECORDER_CHANNELS, RECORDER_AUDIO_ENCODING, BUFFER_SIZE * BYTES_PER_ELEMENT);
        mAudioRecord.startRecording();

        mAudioTimestamp = null;

        mAudioThread = new Thread(new Runnable() {
            @Override
            public void run() {
                writeDataToFile();
            }
        }, "AudioThread");
        mAudioThread.setPriority(Thread.MAX_PRIORITY);
        mAudioThread.start();
    }

    // STOP RECORDING
    public void stopRecording() {
        if (mAudioRecord != null) {
            mIsRecording = false;
            mAudioRecord.stop();
            mAudioRecord.release();
            mAudioRecord = null;
            mAudioThread = null;
        }
    }

    // CORE - WRITE AUDIO STREAM
    private void writeDataToFile() {
        short rawData[] = new short[BUFFER_SIZE];
        short playData[] = new short[BUFFER_SIZE];

        FileOutputStream os = null;
        try {
            os = new FileOutputStream(mFilePath + ".pcm");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        if(mAudioTrack != null && isHeadphonesPlugged()) {
            mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC), 0);
            mAudioTrack.play();
        }

        mIsRecording = true;
        while (mIsRecording) {
            int numRead = mAudioRecord.read(rawData, 0, BUFFER_SIZE);

            if (numRead > 0) {
                // TESTING
                AudioTimestamp listAudioTimestamp = new AudioTimestamp();
                mAudioRecord.getTimestamp(listAudioTimestamp, AudioTimestamp.TIMEBASE_BOOTTIME);
                list.add(new AudioTimeStamp(listAudioTimestamp.framePosition, listAudioTimestamp.nanoTime));

                if (mAudioTimestamp == null) {
                    mAudioTimestamp = listAudioTimestamp;
                }

                plotData(rawData);


                if (mAudioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) {
                    for (int i = 0; i < numRead; ++i) {
                        playData[i] = (short) Math.min((int) (rawData[i] * mGain), (int) Short.MAX_VALUE);
                    }
                    mAudioTrack.write(playData, 0, numRead);
                }

                try {
                    byte byteData[] = short2byte(rawData);
                    os.write(byteData, 0, numRead * BYTES_PER_ELEMENT);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        try {
            os.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        mAudioTrack.stop();
        mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mOrigionalVolume, 0);
    }

    // CHART DATA METHODS
    private void plotData(short[] rawData){
        final short[] plotData = new short[rawData.length/32];
        for (int i = 0; i < rawData.length/32; i++) {
            plotData[i] = rawData[i*32];
        }
        if(listener != null) {
            listener.onAudioValueChanged(plotData);
        }
    }

    // HELPER METHODS
    private byte[] short2byte(short[] rawData) {
        int shortArrsize = rawData.length;
        byte[] bytes = new byte[shortArrsize * 2];
        for (int i = 0; i < shortArrsize; i++) {
            bytes[i * 2] = (byte) (rawData[i] & 0x00FF);
            bytes[(i * 2) + 1] = (byte) (rawData[i] >> 8);
            rawData[i] = 0;
        }
        return bytes;
    }

    private boolean isHeadphonesPlugged(){
        AudioDeviceInfo[] audioDevices = mAudioManager.getDevices(AudioManager.GET_DEVICES_ALL);
        for(AudioDeviceInfo deviceInfo : audioDevices){
            if(deviceInfo.getType()==AudioDeviceInfo.TYPE_WIRED_HEADPHONES || deviceInfo.getType()==AudioDeviceInfo.TYPE_BLUETOOTH_A2DP){
                return true;
            }
        }
        return false;
    }

    // SYNCHRONIZATION METHODS
    public void synchronize(int skipFrames) {
        File f1 = new File(mFilePath + ".pcm"); // The location of your PCM file
        File f2 = new File(mFilePath + ".wav"); // The location where you want your WAV file
        try {
            rawToWave(f1, f2, skipFrames);
            f1.delete();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // RAW TO WAV METHODS - MODIFIED
    // https://stackoverflow.com/questions/37281430/how-to-convert-pcm-file-to-wav-or-mp3
    private void rawToWave(final File rawFile, final File waveFile, int skipFrames) throws IOException {

        byte[] rawData = new byte[(int) rawFile.length()];
        DataInputStream input = null;
        try {
            input = new DataInputStream(new FileInputStream(rawFile));
            input.read(rawData);
        } finally {
            if (input != null) {
                input.close();
            }
        }

        DataOutputStream output = null;
        try {
            output = new DataOutputStream(new FileOutputStream(waveFile));
            // WAVE header
            // see http://ccrma.stanford.edu/courses/422/projects/WaveFormat/
            writeString(output, "RIFF"); // chunk id
            writeInt(output, 36 + rawData.length - (skipFrames * 2)); // chunk size
            writeString(output, "WAVE"); // format
            writeString(output, "fmt "); // subchunk 1 id
            writeInt(output, 16); // subchunk 1 size
            writeShort(output, (short) 1); // audio format (1 = PCM)
            writeShort(output, (short) 1); // number of channels
            writeInt(output, RECORDER_SAMPLE_RATE); // sample rate
            writeInt(output, RECORDER_SAMPLE_RATE * 2); // byte rate
            writeShort(output, (short) 2); // block align
            writeShort(output, (short) 16); // bits per sample
            writeString(output, "data"); // subchunk 2 id
            writeInt(output, rawData.length - (skipFrames * 2)); // subchunk 2 size
            // Audio data (conversion big endian -> little endian)
            short[] shorts = new short[rawData.length / 2];
            ByteBuffer.wrap(rawData).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(shorts);
            ByteBuffer bytes = ByteBuffer.allocate((shorts.length * 2) - (skipFrames * 2));
            for (int i = skipFrames; i < shorts.length; i++) {
                bytes.putShort(shorts[i]);
            }

            output.write(fullyReadFileToBytes(rawFile));
        } finally {
            if (output != null) {
                output.close();
            }
        }
    }

    byte[] fullyReadFileToBytes(File f) throws IOException {
        int size = (int) f.length();
        byte bytes[] = new byte[size];
        byte tmpBuff[] = new byte[size];
        FileInputStream fis= new FileInputStream(f);
        try {

            int read = fis.read(bytes, 0, size);
            if (read < size) {
                int remain = size - read;
                while (remain > 0) {
                    read = fis.read(tmpBuff, 0, remain);
                    System.arraycopy(tmpBuff, 0, bytes, size - remain, read);
                    remain -= read;
                }
            }
        }  catch (IOException e){
            throw e;
        } finally {
            fis.close();
        }
        return bytes;
    }

    private void writeInt(final DataOutputStream output, final int value) throws IOException {
        output.write(value >> 0);
        output.write(value >> 8);
        output.write(value >> 16);
        output.write(value >> 24);
    }

    private void writeShort(final DataOutputStream output, final short value) throws IOException {
        output.write(value >> 0);
        output.write(value >> 8);
    }

    private void writeString(final DataOutputStream output, final String value) throws IOException {
        for (int i = 0; i < value.length(); i++) {
            output.write(value.charAt(i));
        }
    }

    // TESTING

    public List<AudioTimeStamp> list;

    public class AudioTimeStamp {
        AudioTimeStamp(long position, long time) {
            samplePosition = position;
            sampleTimeStamp = time;
        }

        public long samplePosition;
        public long sampleTimeStamp;
    }
}
