/*
 * Copyright 2017 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 android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.Toast;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;

/**
 * Base class for other Activities.
 */
abstract class TestAudioActivity extends Activity {
    public static final String TAG = "OboeTester";

    protected static final int FADER_PROGRESS_MAX = 1000;
    private static final int INTENT_TEST_DELAY_MILLIS = 1100;

    public static final int AUDIO_STATE_OPEN = 0;
    public static final int AUDIO_STATE_STARTED = 1;
    public static final int AUDIO_STATE_PAUSED = 2;
    public static final int AUDIO_STATE_STOPPED = 3;
    public static final int AUDIO_STATE_CLOSING = 4;
    public static final int AUDIO_STATE_CLOSED = 5;

    public static final int COLOR_ACTIVE = 0xFFD0D0A0;
    public static final int COLOR_IDLE = 0xFFD0D0D0;

    // Pass the activity index to native so it can know how to respond to the start and stop calls.
    // WARNING - must match definitions in NativeAudioContext.h ActivityType
    public static final int ACTIVITY_TEST_OUTPUT = 0;
    public static final int ACTIVITY_TEST_INPUT = 1;
    public static final int ACTIVITY_TAP_TO_TONE = 2;
    public static final int ACTIVITY_RECORD_PLAY = 3;
    public static final int ACTIVITY_ECHO = 4;
    public static final int ACTIVITY_RT_LATENCY = 5;
    public static final int ACTIVITY_GLITCHES = 6;
    public static final int ACTIVITY_TEST_DISCONNECT = 7;
    public static final int ACTIVITY_DATA_PATHS = 8;

    private static final int MY_PERMISSIONS_REQUEST_EXTERNAL_STORAGE = 1001;

    private int mAudioState = AUDIO_STATE_CLOSED;

    protected ArrayList<StreamContext> mStreamContexts;
    private Button mOpenButton;
    private Button mStartButton;
    private Button mPauseButton;
    private Button mStopButton;
    private Button mCloseButton;
    private MyStreamSniffer mStreamSniffer;
    private CheckBox mCallbackReturnStopBox;
    private int mSampleRate;
    private int mSingleTestIndex = -1;
    private static boolean mBackgroundEnabled;

    protected Bundle mBundleFromIntent;
    protected boolean mTestRunningByIntent;
    protected String mResultFileName;
    private String mTestResults;

    public String getTestName() {
        return "TestAudio";
    }

    public static class StreamContext {
        StreamConfigurationView configurationView;
        AudioStreamTester tester;

        boolean isInput() {
            return tester.getCurrentAudioStream().isInput();
        }
    }

    // Periodically query the status of the streams.
    protected class MyStreamSniffer {
        public static final int SNIFFER_UPDATE_PERIOD_MSEC = 150;
        public static final int SNIFFER_UPDATE_DELAY_MSEC = 300;

        private Handler mHandler;

        // Display status info for the stream.
        private Runnable runnableCode = new Runnable() {
            @Override
            public void run() {
                boolean streamClosed = false;
                boolean gotViews = false;
                for (StreamContext streamContext : mStreamContexts) {
                    AudioStreamBase.StreamStatus status = streamContext.tester.getCurrentAudioStream().getStreamStatus();
                    AudioStreamBase.DoubleStatistics latencyStatistics =
                            streamContext.tester.getCurrentAudioStream().getLatencyStatistics();
                    if (streamContext.configurationView != null) {
                        // Handler runs this on the main UI thread.
                        int framesPerBurst = streamContext.tester.getCurrentAudioStream().getFramesPerBurst();
                        status.framesPerCallback = getFramesPerCallback();
                        String msg = "";
                        msg += "timestamp.latency = " + latencyStatistics.dump() + "\n";
                        msg += status.dump(framesPerBurst);
                        streamContext.configurationView.setStatusText(msg);
                        updateStreamDisplay();
                        gotViews = true;
                    }

                    streamClosed = streamClosed || (status.state >= 12);
                }

                if (streamClosed) {
                    onStreamClosed();
                } else {
                    // Repeat this runnable code block again.
                    if (gotViews) {
                        mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_PERIOD_MSEC);
                    }
                }
            }
        };

        private void startStreamSniffer() {
            stopStreamSniffer();
            mHandler = new Handler(Looper.getMainLooper());
            // Start the initial runnable task by posting through the handler
            mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_DELAY_MSEC);
        }

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

    }

    public static void setBackgroundEnabled(boolean enabled) {
        mBackgroundEnabled = enabled;
    }

    public static boolean isBackgroundEnabled() {
        return mBackgroundEnabled;
    }

    public void onStreamClosed() {
    }

    protected abstract void inflateActivity();

    void updateStreamDisplay() {
    }

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

        mBundleFromIntent = getIntent().getExtras();
    }

    @Override
    public void onNewIntent(Intent intent) {
        mBundleFromIntent = intent.getExtras();
    }

    public boolean isTestConfiguredUsingBundle() {
        return mBundleFromIntent != null;
    }

    public void hideSettingsViews() {
        for (StreamContext streamContext : mStreamContexts) {
            if (streamContext.configurationView != null) {
                streamContext.configurationView.hideSettingsView();
            }
        }
    }

    abstract int getActivityType();

    public void setSingleTestIndex(int testIndex) {
        mSingleTestIndex = testIndex;
    }

    public int getSingleTestIndex() {
        return mSingleTestIndex;
    }

    @Override
    protected void onStart() {
        super.onStart();
        resetConfiguration();
        setActivityType(getActivityType());
    }

    protected void resetConfiguration() {
    }

    @Override
    public void onResume() {
        super.onResume();
        if (mBundleFromIntent != null) {
            processBundleFromIntent();
        }
    }

    private void setVolumeFromIntent() {
        float normalizedVolume = IntentBasedTestSupport.getNormalizedVolumeFromBundle(mBundleFromIntent);
        if (normalizedVolume >= 0.0) {
            int streamType = IntentBasedTestSupport.getVolumeStreamTypeFromBundle(mBundleFromIntent);
            AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
            int maxVolume = audioManager.getStreamMaxVolume(streamType);
            int requestedVolume = (int) (maxVolume * normalizedVolume);
            audioManager.setStreamVolume(streamType, requestedVolume, 0);
        }
    }

    private void processBundleFromIntent() {
        if (mTestRunningByIntent) {
            return;
        }

        // Delay the test start to avoid race conditions. See Oboe Issue #1533
        mTestRunningByIntent = true;
        Handler handler = new Handler(Looper.getMainLooper()); // UI thread
        handler.postDelayed(new DelayedTestByIntentRunnable(),
                INTENT_TEST_DELAY_MILLIS); // Delay long enough to get past the onStop() call!

    }

    private class DelayedTestByIntentRunnable implements Runnable {
        @Override
        public void run() {
            try {
                mResultFileName = mBundleFromIntent.getString(IntentBasedTestSupport.KEY_FILE_NAME);
                setVolumeFromIntent();
                startTestUsingBundle();
            } catch( Exception e) {
                showErrorToast(e.getMessage());
            }
        }
    }

    public void startTestUsingBundle() {
    }

    @Override
    protected void onPause() {
        super.onPause();
    }

    @Override
    protected void onStop() {
        if (!isBackgroundEnabled()) {
            Log.i(TAG, "onStop() called so stop the test =========================");
            onStopTest();
        }
        super.onStop();
    }

    @Override
    protected void onDestroy() {
        if (isBackgroundEnabled()) {
            Log.i(TAG, "onDestroy() called so stop the test =========================");
            onStopTest();
        }
        mAudioState = AUDIO_STATE_CLOSED;
        super.onDestroy();
    }

    protected void updateEnabledWidgets() {
        if (mOpenButton != null) {
            mOpenButton.setBackgroundColor(mAudioState == AUDIO_STATE_OPEN ? COLOR_ACTIVE : COLOR_IDLE);
            mStartButton.setBackgroundColor(mAudioState == AUDIO_STATE_STARTED ? COLOR_ACTIVE : COLOR_IDLE);
            mPauseButton.setBackgroundColor(mAudioState == AUDIO_STATE_PAUSED ? COLOR_ACTIVE : COLOR_IDLE);
            mStopButton.setBackgroundColor(mAudioState == AUDIO_STATE_STOPPED ? COLOR_ACTIVE : COLOR_IDLE);
            mCloseButton.setBackgroundColor(mAudioState == AUDIO_STATE_CLOSED ? COLOR_ACTIVE : COLOR_IDLE);
        }
        setConfigViewsEnabled(mAudioState == AUDIO_STATE_CLOSED);
    }

    private void setConfigViewsEnabled(boolean b) {
        for (StreamContext streamContext : mStreamContexts) {
            if (streamContext.configurationView != null) {
                streamContext.configurationView.setChildrenEnabled(b);
            }
        }
    }

    private void applyConfigurationViewsToModels() {
        for (StreamContext streamContext : mStreamContexts) {
            if (streamContext.configurationView != null) {
                streamContext.configurationView.applyToModel(streamContext.tester.requestedConfiguration);
            }
        }
    }

    abstract boolean isOutput();

    public void clearStreamContexts() {
        mStreamContexts.clear();
    }

    public StreamContext addOutputStreamContext() {
        StreamContext streamContext = new StreamContext();
        streamContext.tester = AudioOutputTester.getInstance();
        streamContext.configurationView = (StreamConfigurationView)
                findViewById(R.id.outputStreamConfiguration);
        if (streamContext.configurationView == null) {
            streamContext.configurationView = (StreamConfigurationView)
                    findViewById(R.id.streamConfiguration);
        }
        if (streamContext.configurationView != null) {
            streamContext.configurationView.setOutput(true);
        }
        mStreamContexts.add(streamContext);
        return streamContext;
    }

    public AudioOutputTester addAudioOutputTester() {
        StreamContext streamContext = addOutputStreamContext();
        return (AudioOutputTester) streamContext.tester;
    }

    public StreamContext addInputStreamContext() {
        StreamContext streamContext = new StreamContext();
        streamContext.tester = AudioInputTester.getInstance();
        streamContext.configurationView = (StreamConfigurationView)
                findViewById(R.id.inputStreamConfiguration);
        if (streamContext.configurationView == null) {
            streamContext.configurationView = (StreamConfigurationView)
                    findViewById(R.id.streamConfiguration);
        }
        if (streamContext.configurationView != null) {
            streamContext.configurationView.setOutput(false);
        }
        streamContext.tester = AudioInputTester.getInstance();
        mStreamContexts.add(streamContext);
        return streamContext;
    }

    public AudioInputTester addAudioInputTester() {
        StreamContext streamContext = addInputStreamContext();
        return (AudioInputTester) streamContext.tester;
    }

    void updateStreamConfigurationViews() {
        for (StreamContext streamContext : mStreamContexts) {
            if (streamContext.configurationView != null) {
                streamContext.configurationView.updateDisplay(streamContext.tester.actualConfiguration);
            }
        }
    }

    StreamContext getFirstInputStreamContext() {
        for (StreamContext streamContext : mStreamContexts) {
            if (streamContext.isInput())
                return streamContext;
        }
        return null;
    }

    StreamContext getFirstOutputStreamContext() {
        for (StreamContext streamContext : mStreamContexts) {
            if (!streamContext.isInput())
                return streamContext;
        }
        return null;
    }

    protected void findAudioCommon() {
        mOpenButton = (Button) findViewById(R.id.button_open);
        if (mOpenButton != null) {
            mStartButton = (Button) findViewById(R.id.button_start);
            mPauseButton = (Button) findViewById(R.id.button_pause);
            mStopButton = (Button) findViewById(R.id.button_stop);
            mCloseButton = (Button) findViewById(R.id.button_close);
        }
        mStreamContexts = new ArrayList<StreamContext>();

        mCallbackReturnStopBox = (CheckBox) findViewById(R.id.callbackReturnStop);
        if (mCallbackReturnStopBox != null) {
            mCallbackReturnStopBox.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    OboeAudioStream.setCallbackReturnStop(mCallbackReturnStopBox.isChecked());
                }
            });
        }
        OboeAudioStream.setCallbackReturnStop(false);

        mStreamSniffer = new MyStreamSniffer();
    }

    private void updateNativeAudioParameters() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
            String text = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
            int audioManagerSampleRate = Integer.parseInt(text);
            text = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
            int audioManagerFramesPerBurst = Integer.parseInt(text);
            setDefaultAudioValues(audioManagerSampleRate, audioManagerFramesPerBurst);
        }
    }

    protected void showErrorToast(String message) {
        String text = "Error: " + message;
        Log.e(TAG, text);
        showToast(text);
    }

    protected void showToast(final String message) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(TestAudioActivity.this,
                        message,
                        Toast.LENGTH_SHORT).show();
            }
        });
    }

    public void openAudio(View view) {
        try {
            openAudio();
        } catch (Exception e) {
            showErrorToast(e.getMessage());
        }
    }

    public void startAudio(View view) {
        Log.i(TAG, "startAudio() called =======================================");
        try {
            startAudio();
        } catch (Exception e) {
            showErrorToast(e.getMessage());
        }
        keepScreenOn(true);
    }

    protected void keepScreenOn(boolean on) {
        if (on) {
            getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        } else {
            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        }
    }

    public void stopAudio(View view) {
        stopAudio();
        keepScreenOn(false);
    }

    public void pauseAudio(View view) {
        pauseAudio();
        keepScreenOn(false);
    }

    public void closeAudio(View view) {
        closeAudio();
    }

    public int getSampleRate() {
        return mSampleRate;
    }

    public void openAudio() throws IOException {
        closeAudio();

        updateNativeAudioParameters();

        if (!isTestConfiguredUsingBundle()) {
            applyConfigurationViewsToModels();
        }

        int sampleRate = 0;

        // Open output streams then open input streams.
        // This is so that the capacity of input stream can be expanded to
        // match the burst size of the output for full duplex.
        for (StreamContext streamContext : mStreamContexts) {
            if (!streamContext.isInput()) {
                openStreamContext(streamContext);
                int streamSampleRate = streamContext.tester.actualConfiguration.getSampleRate();
                if (sampleRate == 0) {
                    sampleRate = streamSampleRate;
                }
            }
        }
        for (StreamContext streamContext : mStreamContexts) {
            if (streamContext.isInput()) {
                if (sampleRate != 0) {
                    streamContext.tester.requestedConfiguration.setSampleRate(sampleRate);
                }
                openStreamContext(streamContext);
            }
        }
        updateEnabledWidgets();
        mStreamSniffer.startStreamSniffer();
    }

    /**
     * @param deviceId
     * @return true if the device is TYPE_BLUETOOTH_SCO
     */
    boolean isScoDevice(int deviceId) {
        if (deviceId == 0) return false; // Unspecified
        AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
        final AudioDeviceInfo[] devices = audioManager.getDevices(
                AudioManager.GET_DEVICES_INPUTS | AudioManager.GET_DEVICES_OUTPUTS);
        for (AudioDeviceInfo device : devices) {
            if (device.getId() == deviceId) {
                return device.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_SCO;
            }
        }
        return false;
    }

    private void openStreamContext(StreamContext streamContext) throws IOException {
        StreamConfiguration requestedConfig = streamContext.tester.requestedConfiguration;
        StreamConfiguration actualConfig = streamContext.tester.actualConfiguration;

        streamContext.tester.open(); // OPEN the stream

        mSampleRate = actualConfig.getSampleRate();
        mAudioState = AUDIO_STATE_OPEN;
        int sessionId = actualConfig.getSessionId();
        if (streamContext.configurationView != null) {
            if (sessionId > 0) {
                try {
                    streamContext.configurationView.setupEffects(sessionId);
                } catch (Exception e) {
                    showErrorToast(e.getMessage());
                }
            }
            streamContext.configurationView.updateDisplay(streamContext.tester.actualConfiguration);
        }
    }

    // Native methods
    private native int startNative();

    private native int pauseNative();

    private native int stopNative();

    protected native void setActivityType(int activityType);

    private native int getFramesPerCallback();

    private static native void setDefaultAudioValues(int audioManagerSampleRate, int audioManagerFramesPerBurst);

    public void startAudio() throws IOException {
        Log.i(TAG, "startAudio() called =========================");
        int result = startNative();
        if (result < 0) {
            showErrorToast("Start failed with " + result);
            throw new IOException("startNative returned " + result);
        } else {
            for (StreamContext streamContext : mStreamContexts) {
                StreamConfigurationView configView = streamContext.configurationView;
                if (configView != null) {
                    configView.updateDisplay(streamContext.tester.actualConfiguration);
                }
            }
            mAudioState = AUDIO_STATE_STARTED;
            updateEnabledWidgets();
        }
    }

    protected void toastPauseError(int result) {
        showErrorToast("Pause failed with " + result);
    }

    public void pauseAudio() {
        int result = pauseNative();
        if (result < 0) {
            toastPauseError(result);
        } else {
            mAudioState = AUDIO_STATE_PAUSED;
            updateEnabledWidgets();
        }
    }

    public void stopAudio() {
        int result = stopNative();
        if (result < 0) {
            showErrorToast("Stop failed with " + result);
        } else {
            mAudioState = AUDIO_STATE_STOPPED;
            updateEnabledWidgets();
        }
    }

    public void runTest() {
    }

    public void saveIntentLog() {
    }

    // This should only be called from UI events such as onStop or a button press.
    public void onStopTest() {
        stopTest();
    }

    public void stopTest() {
        stopAudio();
        closeAudio();
    }

    public void stopAudioQuiet() {
        stopNative();
        mAudioState = AUDIO_STATE_STOPPED;
        updateEnabledWidgets();
    }

    // Make synchronized so we don't close from two streams at the same time.
    public synchronized void closeAudio() {
        if (mAudioState >= AUDIO_STATE_CLOSING) {
            Log.d(TAG, "closeAudio() already closing");
            return;
        }
        mAudioState = AUDIO_STATE_CLOSING;

        mStreamSniffer.stopStreamSniffer();
        // Close output streams first because legacy callbacks may still be active
        // and an output stream may be calling the input stream.
        for (StreamContext streamContext : mStreamContexts) {
            if (!streamContext.isInput()) {
                streamContext.tester.close();
            }
        }
        for (StreamContext streamContext : mStreamContexts) {
            if (streamContext.isInput()) {
                streamContext.tester.close();
            }
        }

        mAudioState = AUDIO_STATE_CLOSED;
        updateEnabledWidgets();
    }

    void startBluetoothSco() {
        AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
        myAudioMgr.startBluetoothSco();
    }

    void stopBluetoothSco() {
        AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
        myAudioMgr.stopBluetoothSco();
    }

    @Override
    public void onRequestPermissionsResult(int requestCode,
                                           String[] permissions,
                                           int[] grantResults) {

        if (MY_PERMISSIONS_REQUEST_EXTERNAL_STORAGE != requestCode) {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
            return;
        }
        // If request is cancelled, the result arrays are empty.
        if (grantResults.length > 0
                && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            writeTestResult(mTestResults);
        } else {
            showToast("Writing external storage needed for test results.");
        }
    }

    @NonNull
    protected String getCommonTestReport() {
        StringBuffer report = new StringBuffer();
        // Add some extra information for the remote tester.
        report.append("build.fingerprint = " + Build.FINGERPRINT + "\n");
        try {
            PackageInfo pinfo = getPackageManager().getPackageInfo(getPackageName(), 0);
            report.append(String.format("test.version = %s\n", pinfo.versionName));
            report.append(String.format("test.version.code = %d\n", pinfo.versionCode));
        } catch (PackageManager.NameNotFoundException e) {
        }
        report.append("time.millis = " + System.currentTimeMillis() + "\n");

        if (mStreamContexts.size() == 0) {
            report.append("ERROR: no active streams" + "\n");
        } else {
            StreamContext streamContext = mStreamContexts.get(0);
            AudioStreamTester streamTester = streamContext.tester;
            report.append(streamTester.actualConfiguration.dump());
            AudioStreamBase.StreamStatus status = streamTester.getCurrentAudioStream().getStreamStatus();
            AudioStreamBase.DoubleStatistics latencyStatistics =
                    streamTester.getCurrentAudioStream().getLatencyStatistics();
            int framesPerBurst = streamTester.getCurrentAudioStream().getFramesPerBurst();
            status.framesPerCallback = getFramesPerCallback();
            report.append("timestamp.latency = " + latencyStatistics.dump() + "\n");
            report.append(status.dump(framesPerBurst));
        }

        return report.toString();
    }

    void writeTestResultIfPermitted(String resultString) {
        // Here, thisActivity is the current activity
        if (ContextCompat.checkSelfPermission(this,
                Manifest.permission.WRITE_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED) {
            mTestResults = resultString;
            ActivityCompat.requestPermissions(this,
                    new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
                    MY_PERMISSIONS_REQUEST_EXTERNAL_STORAGE);
        } else {
            // Permission has already been granted
            writeTestResult(resultString);
        }
    }

    void maybeWriteTestResult(String resultString) {
        if (mResultFileName != null) {
            writeTestResultIfPermitted(resultString);
        };
    }

    // Run this in a background thread.
    void writeTestResult(String resultString) {
        File resultFile = new File(mResultFileName);
        Writer writer = null;
        try {
            writer = new OutputStreamWriter(new FileOutputStream(resultFile));
            writer.write(resultString);
        } catch (
                IOException e) {
            e.printStackTrace();
            showErrorToast(" writing result file. " + e.getMessage());
        } finally {
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        mResultFileName = null;
    }
}
