/*
 * Copyright 2018 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.mobileer.oboetester;

import static com.mobileer.oboetester.IntentBasedTestSupport.configureStreamsFromBundle;

import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.text.method.ScrollingMovementMethod;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import java.io.IOException;

/**
 * Activity to measure latency on a full duplex stream.
 */
public class RoundTripLatencyActivity extends AnalyzerActivity {

    // STATEs defined in LatencyAnalyzer.h
    private static final int STATE_MEASURE_BACKGROUND = 0;
    private static final int STATE_IN_PULSE = 1;
    private static final int STATE_GOT_DATA = 2;
    private final static String LATENCY_FORMAT = "%4.2f";
    // When I use 5.3g I only get one digit after the decimal point!
    private final static String CONFIDENCE_FORMAT = "%5.3f";

    private TextView mAnalyzerView;
    private Button   mMeasureButton;
    private Button   mAverageButton;
    private Button   mCancelButton;
    private Button   mShareButton;
    private boolean  mHasRecording = false;

    private int     mBufferBursts = -1;
    private Handler mHandler = new Handler(Looper.getMainLooper()); // UI thread

    DoubleStatistics mTimestampLatencyStats = new DoubleStatistics(); // for single measurement

    // Run the test several times and report the average latency.
    protected class AverageLatencyTestRunner {
        private final static int AVERAGE_TEST_DELAY_MSEC = 1000; // arbitrary
        private static final int GOOD_RUNS_REQUIRED = 5; // arbitrary
        private static final int MAX_BAD_RUNS_ALLOWED = 5; // arbitrary
        private int mBadCount = 0; // number of bad measurements

        DoubleStatistics mLatencies = new DoubleStatistics();
        DoubleStatistics mConfidences = new DoubleStatistics();
        DoubleStatistics mTimestampLatencies = new DoubleStatistics(); // for multiple measurements
        private boolean mActive;
        private String  mLastReport = "";

        private int getGoodCount() {
            return mLatencies.count();
        }

        // Called on UI thread.
        String onAnalyserDone() {
            String message;
            boolean reschedule = false;
            if (!mActive) {
                message = "";
            } else if (getMeasuredResult() != 0) {
                mBadCount++;
                if (mBadCount > MAX_BAD_RUNS_ALLOWED) {
                    cancel();
                    updateButtons(false);
                    message = "averaging cancelled due to error\n";
                } else {
                    message = "skipping this bad run, "
                            + mBadCount + " of " + MAX_BAD_RUNS_ALLOWED + " max\n";
                    reschedule = true;
                }
            } else {
                double latency = getMeasuredLatencyMillis();
                mLatencies.add(latency);
                double confidence = getMeasuredConfidence();
                mConfidences.add(confidence);

                double timestampLatency = getTimestampLatencyMillis();
                if (timestampLatency > 0.0) {
                    mTimestampLatencies.add(timestampLatency);
                }
                if (getGoodCount() < GOOD_RUNS_REQUIRED) {
                    reschedule = true;
                } else {
                    mActive = false;
                    updateButtons(false);
                }
                message = reportAverage();
            }
            if (reschedule) {
                mHandler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        measureSingleLatency();
                    }
                }, AVERAGE_TEST_DELAY_MSEC);
            }
            return message;
        }

        private String reportAverage() {
            String message;
            if (getGoodCount() == 0 || mConfidences.getSum() == 0.0) {
                message = "num.iterations = " + getGoodCount() + "\n";
            } else {
                final double mAverageConfidence = mConfidences.calculateMean();
                double meanLatency = mLatencies.calculateMean();
                double meanAbsoluteDeviation = mLatencies.calculateMeanAbsoluteDeviation(meanLatency);
                double timestampLatencyMean = -1;
                double timestampLatencyMAD = 0.0;
                if (mTimestampLatencies.count() > 0) {
                    timestampLatencyMean = mTimestampLatencies.calculateMean();
                    timestampLatencyMAD =
                            mTimestampLatencies.calculateMeanAbsoluteDeviation(timestampLatencyMean);
                }
                message = "average.latency.msec = "
                        + String.format(LATENCY_FORMAT, meanLatency) + "\n"
                        + "mean.absolute.deviation = "
                        + String.format(LATENCY_FORMAT, meanAbsoluteDeviation) + "\n"
                        + "average.confidence = "
                        + String.format(CONFIDENCE_FORMAT, mAverageConfidence) + "\n"
                        + "min.latency.msec = " + String.format(LATENCY_FORMAT, mLatencies.getMin()) + "\n"
                        + "max.latency.msec = " + String.format(LATENCY_FORMAT, mLatencies.getMax()) + "\n"
                        + "num.iterations = " + mLatencies.count() + "\n"
                        + "timestamp.latency.msec = "
                        + String.format(LATENCY_FORMAT, timestampLatencyMean) + "\n"
                        + "timestamp.latency.mad = "
                        + String.format(LATENCY_FORMAT, timestampLatencyMAD) + "\n";
            }
            message += "num.failed = " + mBadCount + "\n";
            message += "\n"; // mark end of average report
            mLastReport = message;
            return message;
        }

        // Called on UI thread.
        public void start() {
            mLatencies = new DoubleStatistics();
            mConfidences = new DoubleStatistics();
            mTimestampLatencies = new DoubleStatistics();
            mBadCount = 0;
            mActive = true;
            mLastReport = "";
            measureSingleLatency();
        }

        public void clear() {
            mActive = false;
            mLastReport = "";
        }

        public void cancel() {
            mActive = false;
        }

        public boolean isActive() {
            return mActive;
        }

        public String getLastReport() {
            return mLastReport;
        }
    }
    AverageLatencyTestRunner mAverageLatencyTestRunner = new AverageLatencyTestRunner();

    // Periodically query the status of the stream.
    protected class LatencySniffer {
        private int counter = 0;
        public static final int SNIFFER_UPDATE_PERIOD_MSEC = 150;
        public static final int SNIFFER_UPDATE_DELAY_MSEC = 300;

        // Display status info for the stream.
        private Runnable runnableCode = new Runnable() {
            @Override
            public void run() {
                double timestampLatency = -1.0;
                int state = getAnalyzerState();
                if (state == STATE_MEASURE_BACKGROUND || state == STATE_IN_PULSE) {
                    timestampLatency = measureTimestampLatency();
                    // Some configurations do not support input timestamps.
                    if (timestampLatency > 0) {
                        mTimestampLatencyStats.add(timestampLatency);
                    }
                }

                String message;
                if (isAnalyzerDone()) {
                    if (mAverageLatencyTestRunner.isActive()) {
                        message = mAverageLatencyTestRunner.onAnalyserDone();
                    } else {
                        message = getResultString();
                    }
                    onAnalyzerDone();
                } else {
                    message = getProgressText();
                    message += "please wait... " + counter + "\n";
                    message += convertStateToString(getAnalyzerState()) + "\n";

                    // Repeat this runnable code block again.
                    mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_PERIOD_MSEC);
                }
                setAnalyzerText(message);
                counter++;
            }
        };

        private void startSniffer() {
            counter = 0;
            // Start the initial runnable task by posting through the handler
            mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_DELAY_MSEC);
        }

        private void stopSniffer() {
            if (mHandler != null) {
                mHandler.removeCallbacks(runnableCode);
            }
        }
    }

    static String convertStateToString(int state) {
        switch (state) {
            case STATE_MEASURE_BACKGROUND: return "BACKGROUND";
            case STATE_IN_PULSE: return "RECORDING";
            case STATE_GOT_DATA: return "ANALYZING";
            default: return "DONE";
        }
    }

    private String getProgressText() {
        int progress = getAnalyzerProgress();
        int state = getAnalyzerState();
        int resetCount = getResetCount();
        String message = String.format("progress = %d\nstate = %d\n#resets = %d\n",
                progress, state, resetCount);
        message += mAverageLatencyTestRunner.getLastReport();
        return message;
    }

    private void onAnalyzerDone() {
        if (mTestRunningByIntent) {
            String report = getCommonTestReport();
            report += getResultString();
            maybeWriteTestResult(report);
        }
        mTestRunningByIntent = false;
        mHasRecording = true;
        stopAudioTest();
    }

    @NonNull
    private String getResultString() {
        int result = getMeasuredResult();
        int resetCount = getResetCount();
        double confidence = getMeasuredConfidence();
        String message = "";

        message += String.format("confidence = " + CONFIDENCE_FORMAT + "\n", confidence);
        message += String.format("result.text = %s\n", resultCodeToString(result));

        // Only report valid latencies.
        if (result == 0) {
            int latencyFrames = getMeasuredLatency();
            double latencyMillis = getMeasuredLatencyMillis();
            int bufferSize = mAudioOutTester.getCurrentAudioStream().getBufferSizeInFrames();
            int latencyEmptyFrames = latencyFrames - bufferSize;
            double latencyEmptyMillis = latencyEmptyFrames * 1000.0 / getSampleRate();
            message += String.format("latency.msec = " + LATENCY_FORMAT + "\n", latencyMillis);
            message += String.format("latency.frames = %d\n", latencyFrames);
            message += String.format("latency.empty.msec = " + LATENCY_FORMAT + "\n", latencyEmptyMillis);
            message += String.format("latency.empty.frames = %d\n", latencyEmptyFrames);
        }

        message += String.format("rms.signal = %7.5f\n", getSignalRMS());
        message += String.format("rms.noise = %7.5f\n", getBackgroundRMS());
        message += String.format("correlation = " + CONFIDENCE_FORMAT + "\n",
                getMeasuredCorrelation());
        double timestampLatency = getTimestampLatencyMillis();
        message += String.format("timestamp.latency.msec = " + LATENCY_FORMAT + "\n",
                timestampLatency);
        if (mTimestampLatencyStats.count() > 0) {
            message += String.format("timestamp.latency.mad = " + LATENCY_FORMAT + "\n",
                    mTimestampLatencyStats.calculateMeanAbsoluteDeviation(timestampLatency));
        }
        message +=  "timestamp.latency.count = " + mTimestampLatencyStats.count() + "\n";
        message += String.format("reset.count = %d\n", resetCount);
        message += String.format("result = %d\n", result);

        return message;
    }

    private LatencySniffer mLatencySniffer = new LatencySniffer();

    double getMeasuredLatencyMillis() {
        return getMeasuredLatency() * 1000.0 / getSampleRate();
    }

    double getTimestampLatencyMillis() {
        if (mTimestampLatencyStats.count() == 0) return -1.0;
        else return mTimestampLatencyStats.calculateMean();
    }

    native int getAnalyzerProgress();
    native int getMeasuredLatency();
    native double measureTimestampLatency();
    native double getMeasuredConfidence();
    native double getMeasuredCorrelation();
    native double getBackgroundRMS();
    native double getSignalRMS();

    private void setAnalyzerText(String s) {
        mAnalyzerView.setText(s);
    }

    @Override
    protected void inflateActivity() {
        setContentView(R.layout.activity_rt_latency);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mMeasureButton = (Button) findViewById(R.id.button_measure);
        mAverageButton = (Button) findViewById(R.id.button_average);
        mCancelButton = (Button) findViewById(R.id.button_cancel);
        mShareButton = (Button) findViewById(R.id.button_share);
        mShareButton.setEnabled(false);
        mAnalyzerView = (TextView) findViewById(R.id.text_status);
        mAnalyzerView.setMovementMethod(new ScrollingMovementMethod());
        updateEnabledWidgets();

        hideSettingsViews();

        mBufferSizeView.setFaderNormalizedProgress(0.0); // for lowest latency
    }

    @Override
    int getActivityType() {
        return ACTIVITY_RT_LATENCY;
    }

    @Override
    protected void onStart() {
        super.onStart();
        mHasRecording = false;
        updateButtons(false);
    }

    @Override
    public void startTestUsingBundle() {
        try {
            StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration;
            StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration;
            configureStreamsFromBundle(mBundleFromIntent, requestedInConfig, requestedOutConfig);

            mBufferBursts = mBundleFromIntent.getInt(IntentBasedTestSupport.KEY_BUFFER_BURSTS, mBufferBursts);

            onMeasure(null);
        } finally {
            mBundleFromIntent = null;
        }
    }

    @Override
    protected void onStop() {
        mLatencySniffer.stopSniffer();
        super.onStop();
    }

    public void onMeasure(View view) {
        mAverageLatencyTestRunner.clear();
        measureSingleLatency();
    }

    void updateButtons(boolean running) {
        boolean busy = running || mAverageLatencyTestRunner.isActive();
        mMeasureButton.setEnabled(!busy);
        mAverageButton.setEnabled(!busy);
        mCancelButton.setEnabled(running);
        mShareButton.setEnabled(!busy && mHasRecording);
    }

    private void measureSingleLatency() {
        try {
            openAudio();
            if (mBufferBursts >= 0) {
                AudioStreamBase stream = mAudioOutTester.getCurrentAudioStream();
                int framesPerBurst = stream.getFramesPerBurst();
                stream.setBufferSizeInFrames(framesPerBurst * mBufferBursts);
                // override buffer size fader
                mBufferSizeView.setEnabled(false);
                mBufferBursts = -1;
            }
            startAudio();
            mTimestampLatencyStats  = new DoubleStatistics();
            mLatencySniffer.startSniffer();
            updateButtons(true);
        } catch (IOException e) {
            showErrorToast(e.getMessage());
        }
    }

    public void onAverage(View view) {
        mAverageLatencyTestRunner.start();
    }

    public void onCancel(View view) {
        mAverageLatencyTestRunner.cancel();
        stopAudioTest();
    }

    // Call on UI thread
    public void stopAudioTest() {
        mLatencySniffer.stopSniffer();
        stopAudio();
        closeAudio();
        updateButtons(false);
    }

    @Override
    String getWaveTag() {
        return "rtlatency";
    }

    @Override
    boolean isOutput() {
        return false;
    }
}
