package cz.vutbr.bpdataacquisition;

import android.Manifest;
import android.app.ActivityManager;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;

import com.jjoe64.graphview.GraphView;
import com.jjoe64.graphview.GridLabelRenderer;
import com.jjoe64.graphview.series.DataPoint;
import com.jjoe64.graphview.series.LineGraphSeries;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class MainActivity extends AppCompatActivity {

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

    // PERMISSIONS
    private final static int REQUEST_CODE_ASK_PERMISSIONS = 1;
    private static final String[] REQUIRED_SDK_PERMISSIONS = new String[] {
            Manifest.permission.CAMERA,
            Manifest.permission.WRITE_EXTERNAL_STORAGE,
            Manifest.permission.RECORD_AUDIO
    };

    // CONSTANTS
    private final int VIDEO_MAX_DATA_POINTS = 120;
    private final int AUDIO_MAX_DATA_POINTS = 1000;

    // OBJECTS
    private VideoDataAcquisition ppg;
    private AudioDataAcquisition pcg;
    private AudioManager mAudioManager;

    // VARIABLES
    private double currentGain = 1d;
    //private boolean isRecording = false;
    private int delayInSamples;
    private static String filePath;

    // CHARTS
    private GraphView videoChart;
    private GraphView audioChart;

    private LineGraphSeries videoSeries;
    private LineGraphSeries audioSeries;

    private double videoLastXValue;
    private double audioLastXValue;

    // TIME STAMPS
    private long startTime;

    // WIDGETS
    private SeekBar gainControl;
    private TextView gainText;
    private MenuItem menuStartStop;

    // RECORDING STATE
    private boolean isRecording() {
        if (ppg != null && pcg != null) {
            if (ppg.isRecording() && pcg.isRecording()) {
                return true;
            }
        }
        return false;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // permissions
        checkPermissions();

        setContentView(R.layout.activity_main);

        // set up audio manager
        mAudioManager = (AudioManager) getSystemService(AUDIO_SERVICE);

        // set up charts
        setUpVideoChart();
        setUpAudioChart();

        // prepare
        prepareRecording();

        // widgets
        gainControl = (SeekBar) findViewById(R.id.gainControl);
        gainText = (TextView) findViewById(R.id.gainText);

        gainText.setText(String.valueOf(currentGain));

        gainControl.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
                currentGain = 1d + (i / 10d);
                gainText.setText(String.valueOf(currentGain));
                if (pcg != null) {
                    pcg.setGain(currentGain);
                }
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {

            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {
                if (pcg != null) {
                    pcg.setGain(currentGain);
                }
            }
        });

        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
    }

    @Override
    protected void onPause() {
        super.onPause();
        if (isRecording()) {
            ActivityManager activityManager = (ActivityManager) getApplicationContext().getSystemService(ACTIVITY_SERVICE);
            activityManager.moveTaskToFront(getTaskId(), 0);
        }
    }

    private void setUpVideoChart() {
        videoChart = (GraphView) findViewById(R.id.video);

        // set up
        videoChart.getViewport().setXAxisBoundsManual(true);
        videoChart.getViewport().setMinX(0);
        videoChart.getViewport().setMaxX(VIDEO_MAX_DATA_POINTS);
        videoChart.getViewport().setMinY(0);
        videoChart.getViewport().setMaxY(255);

        videoChart.getGridLabelRenderer().setGridStyle(GridLabelRenderer.GridStyle.HORIZONTAL);
        videoChart.getGridLabelRenderer().setHorizontalLabelsVisible(false);
        videoChart.getGridLabelRenderer().setVerticalLabelsVisible(false);
        videoChart.getGridLabelRenderer().setLabelVerticalWidth(0);
        videoChart.getGridLabelRenderer().setLabelHorizontalHeight(0);
        videoChart.getGridLabelRenderer().setHighlightZeroLines(false);

        videoChart.getViewport().setScalable(false);
        videoChart.getViewport().setScrollable(false);
        videoChart.getViewport().setScalableY(false);
        videoChart.getViewport().setScrollableY(false);
    }

    private void setUpAudioChart() {
        audioChart = (GraphView) findViewById(R.id.audio);

        // set up
        audioChart.getViewport().setXAxisBoundsManual(true);
        audioChart.getViewport().setMinX(0);
        audioChart.getViewport().setMaxX(AUDIO_MAX_DATA_POINTS);

        audioChart.getGridLabelRenderer().setGridStyle(GridLabelRenderer.GridStyle.HORIZONTAL);
        audioChart.getGridLabelRenderer().setHorizontalLabelsVisible(false);
        audioChart.getGridLabelRenderer().setVerticalLabelsVisible(false);
        audioChart.getGridLabelRenderer().setLabelVerticalWidth(0);
        audioChart.getGridLabelRenderer().setLabelHorizontalHeight(0);
        audioChart.getGridLabelRenderer().setHighlightZeroLines(false);

        audioChart.getViewport().setScalable(false);
        audioChart.getViewport().setScrollable(false);
        audioChart.getViewport().setScalableY(false);
        audioChart.getViewport().setScrollableY(false);
    }

    private void resetCharts() {
        videoLastXValue = 0d;
        audioLastXValue = 0d;

        videoChart.removeAllSeries();
        audioChart.removeAllSeries();

        videoSeries = new LineGraphSeries<>();
        videoSeries.setColor(Color.parseColor("#e4002b"));
        videoSeries.setThickness(8);

        audioSeries = new LineGraphSeries<>();
        audioSeries.setColor(Color.parseColor("#e4002b"));
        audioSeries.setThickness(4);

        videoChart.addSeries(videoSeries);
        audioChart.addSeries(audioSeries);
    }

    private void prepareRecording() {
        ppg = null;
        pcg = null;

        ppg = new VideoDataAcquisition(getApplicationContext());
        pcg = new AudioDataAcquisition(getApplicationContext(), mAudioManager);

        setUpListeners();
    }

    private void setUpListeners() {
        if (ppg != null) {
            ppg.setVideoListener(new VideoDataAcquisition.VideoListener() {
                @Override
                public void onVideoValueChanged(final float data) {
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            videoLastXValue += 1d;
                            videoSeries.appendData(new DataPoint(videoLastXValue, data), true, VIDEO_MAX_DATA_POINTS);
                        }
                    });
                }
            });
        }

        if (pcg != null) {
            pcg.setAudioListener(new AudioDataAcquisition.AudioListener() {
                @Override
                public void onAudioValueChanged(final short[] data) {
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            int dataLength = data.length;
                            for (int i = 0; i < dataLength - 1; i++) {
                                audioLastXValue += 1d;
                                audioSeries.appendData(new DataPoint(audioLastXValue, data[i]), true, AUDIO_MAX_DATA_POINTS, true);
                            }
                            audioLastXValue += 1d;
                            audioSeries.appendData(new DataPoint(audioLastXValue, data[dataLength - 1]), true, AUDIO_MAX_DATA_POINTS, false);
                        }
                    });
                }
            });
        }
    }

    private void startRecording() {
        checkPermissions();

        prepareRecording();

        if (!isHeadsetPlugged()) {
            if (ppg != null && pcg != null) {
                // charts
                resetCharts();

                // files
                filePath = getFilePath();

                // gain
                pcg.setGain(currentGain);

                // flag
                //isRecording = true;

                // timestamp
                startTime = System.currentTimeMillis();

                // start recording
                pcg.startRecording(filePath);
                ppg.startRecording(filePath);

                // menu item delay
                menuStartStop.setEnabled(true);
            }
        }
        else {
            menuStartStop.setIcon(R.drawable.ic_play_arrow_white_24dp);
            menuStartStop.setEnabled(true);
            Toast.makeText(this, "YOU CAN NOT USE HEADSET!", Toast.LENGTH_LONG).show();
        }
    }

    private void stopRecording() {
        // stop recording
        ppg.stopRecording();
        pcg.stopRecording();

        // synchronize
        int skipFrames = calculateDelay();
        pcg.synchronize(skipFrames);
        Toast.makeText(this, ("Synchronized (" + String.valueOf(delayInSamples) + " samples)"), Toast.LENGTH_LONG).show();

        //isRecording = false;

        // menu item delay
        menuStartStop.setEnabled(true);
    }

    private int calculateDelay() {
        Toast.makeText(this, "Synchronization...", Toast.LENGTH_SHORT).show();

        delayInSamples = 0;
        int audioFs = 8000;

        // video time stamp
        long videoFrameTime = ppg.getFirstFrameTime();

        // audio time stamp
        long audioSamplePosition = pcg.getAudioSamplePosition();
        long audioSampleTime = pcg.getAudioSampleTime();

        // calculate delay
        long delay = videoFrameTime - audioSampleTime;
        delayInSamples = (int)Math.round((audioFs * (delay * Math.pow(10, -9))) + audioSamplePosition);

        return delayInSamples;
    }

//    private int calculateDelay() {
//        Toast.makeText(this, "Synchronization...", Toast.LENGTH_SHORT).show();
//
//        // TESTING
//        List<VideoDataAcquisition.VideoTimeStamp> vvv = ppg.list;
//        writeToCSVVideo(vvv);
//
//        List<AudioDataAcquisition.AudioTimeStamp> aaa = pcg.list;
//        writeToCSVAudio(aaa);
//
//        delayInSamples = 0;
//        int audioFs = 8000;
//        int videoFs = 30;
//        int syncConstant = 0; // [ms]
//
//        long min = Long.MAX_VALUE;
//        int row = 0;
//        int col = 0;
//        long lowest = 0;
//
//        for (int i = 0; i < vvv.size(); i++) {
//            for (int j = 0; j < aaa.size(); j++) {
//                long vts = vvv.get(i).frameTimeStamp;
//                long ats = aaa.get(j).sampleTimeStamp;
//
//                long neco = vts-ats;
//
//                if (Math.abs(neco) < min) {
//                    min = Math.abs(neco);
//                    lowest = neco;
//                    row = i;
//                    col = j;
//                }
//            }
//        }
//
//        Toast.makeText(this, "Sync accuracy: " + String.valueOf(roundAvoid(lowest * Math.pow(10, -6), 3)) + " ms", Toast.LENGTH_LONG).show();
//
//        long video2audio_pos = (long)Math.round(row / (double)videoFs * audioFs);
//        long audio_sample_pos = aaa.get(col).samplePosition;
//
//        double syncConstInSamples = (syncConstant / 1000d) * audioFs;
//        delayInSamples = (int) (Math.toIntExact(audio_sample_pos - video2audio_pos) + syncConstInSamples);
//
//
//
//        return delayInSamples;
//    }

    public static double roundAvoid(double value, int places) {
        double scale = Math.pow(10, places);
        double neco = Math.round(value * scale) / scale;
        return neco;
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if ((keyCode == KeyEvent.KEYCODE_BACK)) {
            System.out.println("KEYCODE_BACK");
            return true;
        }
        return false;
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        menuStartStop = menu.findItem(R.id.start_stop);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        if (id == R.id.start_stop) {
            if(isRecording() && System.currentTimeMillis() - startTime > 1000) {
                menuStartStop.setEnabled(false);
                menuStartStop.setIcon(R.drawable.ic_play_arrow_white_24dp);
                Toast.makeText(this, "Stopping...", Toast.LENGTH_SHORT).show();
                stopRecording();
            }
            else if (System.currentTimeMillis() - startTime > 1000){
                menuStartStop.setEnabled(false);
                menuStartStop.setIcon(R.drawable.ic_stop_white_24dp);
                Toast.makeText(this, "Starting...", Toast.LENGTH_SHORT).show();
                startRecording();
            }
        }
        return super.onOptionsItemSelected(item);
    }

    // HELPERS

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

    private String getFilePath() {
        final File dir = getExternalFilesDir(null);
        return (dir == null ? "" : (dir.getAbsolutePath() + "/")) + System.currentTimeMillis();
    }

    // PERMISSIONS
    // https://developer.here.com/documentation/android-starter/topics/request-android-permissions.html
    // Checks the dynamically-controlled permissions and requests missing permissions from end user.

    protected void checkPermissions() {
        final List<String> missingPermissions = new ArrayList<String>();
        // check all required dynamic permissions
        for (final String permission : REQUIRED_SDK_PERMISSIONS) {
            final int result = ContextCompat.checkSelfPermission(this, permission);
            if (result != PackageManager.PERMISSION_GRANTED) {
                missingPermissions.add(permission);
            }
        }
        if (!missingPermissions.isEmpty()) {
            // request all missing permissions
            final String[] permissions = missingPermissions.toArray(new String[missingPermissions.size()]);
            ActivityCompat.requestPermissions(this, permissions, REQUEST_CODE_ASK_PERMISSIONS);
        } else {
            final int[] grantResults = new int[REQUIRED_SDK_PERMISSIONS.length];
            Arrays.fill(grantResults, PackageManager.PERMISSION_GRANTED);
            onRequestPermissionsResult(REQUEST_CODE_ASK_PERMISSIONS, REQUIRED_SDK_PERMISSIONS, grantResults);
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
        switch (requestCode) {
            case REQUEST_CODE_ASK_PERMISSIONS:
                for (int index = permissions.length - 1; index >= 0; --index) {
                    if (grantResults[index] != PackageManager.PERMISSION_GRANTED) {
                        // exit the app if one permission is not granted
                        Toast.makeText(this, "Required permission '" + permissions[index] + "' not granted, exiting", Toast.LENGTH_LONG).show();
                        finish();
                        return;
                    }
                }
                // all permissions were granted
                break;
        }
    }

    // TESTING
    // CSV
    private static final String CSV_SEPARATOR = ";";
    private static void writeToCSVVideo(List<VideoDataAcquisition.VideoTimeStamp> timeStamps)
    {
        try
        {
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(filePath + "-ppg.csv"), "UTF-8"));
            for (VideoDataAcquisition.VideoTimeStamp timeStamp : timeStamps)
            {
                StringBuffer oneLine = new StringBuffer();
                oneLine.append(timeStamp.frameTimeStamp);
                oneLine.append(CSV_SEPARATOR);
                oneLine.append(timeStamp.avgRedValue);
                bw.write(oneLine.toString());
                bw.newLine();
            }
            bw.flush();
            bw.close();
        }
        catch (UnsupportedEncodingException e) {}
        catch (FileNotFoundException e){}
        catch (IOException e){}
    }

    private static void writeToCSVAudio(List<AudioDataAcquisition.AudioTimeStamp> timeStamps)
    {
        try
        {
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(filePath + "-pcg.csv"), "UTF-8"));
            for (AudioDataAcquisition.AudioTimeStamp timeStamp : timeStamps)
            {
                StringBuffer oneLine = new StringBuffer();
                oneLine.append(timeStamp.samplePosition);
                oneLine.append(CSV_SEPARATOR);
                oneLine.append(timeStamp.sampleTimeStamp);
                bw.write(oneLine.toString());
                bw.newLine();
            }
            bw.flush();
            bw.close();
        }
        catch (UnsupportedEncodingException e) {}
        catch (FileNotFoundException e){}
        catch (IOException e){}
    }
}
