/*
 * Copyright (C) 2014 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 android.hardware.soundtrigger;

import static org.mockito.Matchers.any;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;

import android.content.Context;
import android.hardware.soundtrigger.SoundTrigger.GenericRecognitionEvent;
import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel;
import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionEvent;
import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
import android.media.soundtrigger.SoundTriggerManager;
import android.os.ParcelUuid;
import android.os.ServiceManager;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.LargeTest;
import android.test.suitebuilder.annotation.SmallTest;

import com.android.internal.app.ISoundTriggerService;

import org.mockito.MockitoAnnotations;

import java.io.DataOutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Random;
import java.util.UUID;

public class GenericSoundModelTest extends AndroidTestCase {
    static final int MSG_DETECTION_ERROR = -1;
    static final int MSG_DETECTION_RESUME = 0;
    static final int MSG_DETECTION_PAUSE = 1;
    static final int MSG_KEYPHRASE_TRIGGER = 2;
    static final int MSG_GENERIC_TRIGGER = 4;

    private Random random = new Random();
    private HashSet<UUID> loadedModelUuids;
    private ISoundTriggerService soundTriggerService;
    private SoundTriggerManager soundTriggerManager;

    @Override
    public void setUp() throws Exception {
        super.setUp();
        MockitoAnnotations.initMocks(this);

        Context context = getContext();
        soundTriggerService = ISoundTriggerService.Stub.asInterface(
                ServiceManager.getService(Context.SOUND_TRIGGER_SERVICE));
        soundTriggerManager = (SoundTriggerManager) context.getSystemService(
                Context.SOUND_TRIGGER_SERVICE);

        loadedModelUuids = new HashSet<UUID>();
    }

    @Override
    public void tearDown() throws Exception {
        for (UUID modelUuid : loadedModelUuids) {
            soundTriggerService.deleteSoundModel(new ParcelUuid(modelUuid));
        }
        super.tearDown();
    }

    GenericSoundModel new_sound_model() {
        // Create sound model
        byte[] data = new byte[1024];
        random.nextBytes(data);
        UUID modelUuid = UUID.randomUUID();
        UUID mVendorUuid = UUID.randomUUID();
        return new GenericSoundModel(modelUuid, mVendorUuid, data);
    }

    @SmallTest
    public void testUpdateGenericSoundModel() throws Exception {
        GenericSoundModel model = new_sound_model();

        // Update sound model
        soundTriggerService.updateSoundModel(model);
        loadedModelUuids.add(model.getUuid());

        // Confirm it was updated
        GenericSoundModel returnedModel =
                soundTriggerService.getSoundModel(new ParcelUuid(model.getUuid()));
        assertEquals(model, returnedModel);
    }

    @SmallTest
    public void testDeleteGenericSoundModel() throws Exception {
        GenericSoundModel model = new_sound_model();

        // Update sound model
        soundTriggerService.updateSoundModel(model);
        loadedModelUuids.add(model.getUuid());

        // Delete sound model
        soundTriggerService.deleteSoundModel(new ParcelUuid(model.getUuid()));
        loadedModelUuids.remove(model.getUuid());

        // Confirm it was deleted
        GenericSoundModel returnedModel =
                soundTriggerService.getSoundModel(new ParcelUuid(model.getUuid()));
        assertEquals(null, returnedModel);
    }

    @LargeTest
    public void testStartStopGenericSoundModel() throws Exception {
        GenericSoundModel model = new_sound_model();

        boolean captureTriggerAudio = true;
        boolean allowMultipleTriggers = true;
        RecognitionConfig config = new RecognitionConfig(captureTriggerAudio, allowMultipleTriggers,
                null, null);
        TestRecognitionStatusCallback spyCallback = spy(new TestRecognitionStatusCallback());

        // Update and start sound model recognition
        soundTriggerService.updateSoundModel(model);
        loadedModelUuids.add(model.getUuid());
        int r = soundTriggerService.startRecognition(new ParcelUuid(model.getUuid()), spyCallback,
                config);
        assertEquals("Could Not Start Recognition with code: " + r,
                android.hardware.soundtrigger.SoundTrigger.STATUS_OK, r);

        // Stop recognition
        r = soundTriggerService.stopRecognition(new ParcelUuid(model.getUuid()), spyCallback);
        assertEquals("Could Not Stop Recognition with code: " + r,
                android.hardware.soundtrigger.SoundTrigger.STATUS_OK, r);
    }

    @LargeTest
    public void testTriggerGenericSoundModel() throws Exception {
        GenericSoundModel model = new_sound_model();

        boolean captureTriggerAudio = true;
        boolean allowMultipleTriggers = true;
        RecognitionConfig config = new RecognitionConfig(captureTriggerAudio, allowMultipleTriggers,
                null, null);
        TestRecognitionStatusCallback spyCallback = spy(new TestRecognitionStatusCallback());

        // Update and start sound model
        soundTriggerService.updateSoundModel(model);
        loadedModelUuids.add(model.getUuid());
        soundTriggerService.startRecognition(new ParcelUuid(model.getUuid()), spyCallback, config);

        // Send trigger to stub HAL
        Socket socket = new Socket(InetAddress.getLocalHost(), 14035);
        DataOutputStream out = new DataOutputStream(socket.getOutputStream());
        out.writeBytes("trig " + model.getUuid().toString() + "\r\n");
        out.flush();
        socket.close();

        // Verify trigger was received
        verify(spyCallback, timeout(100)).onGenericSoundTriggerDetected(any());
    }

    /**
     * Tests a more complicated pattern of loading, unloading, triggering, starting and stopping
     * recognition. Intended to find unexpected errors that occur in unexpected states.
     */
    @LargeTest
    public void testFuzzGenericSoundModel() throws Exception {
        int numModels = 2;

        final int STATUS_UNLOADED = 0;
        final int STATUS_LOADED = 1;
        final int STATUS_STARTED = 2;

        class ModelInfo {
            int status;
            GenericSoundModel model;

            public ModelInfo(GenericSoundModel model, int status) {
                this.status = status;
                this.model = model;
            }
        }

        Random predictableRandom = new Random(100);

        ArrayList modelInfos = new ArrayList<ModelInfo>();
        for(int i=0; i<numModels; i++) {
            // Create sound model
            byte[] data = new byte[1024];
            predictableRandom.nextBytes(data);
            UUID modelUuid = UUID.randomUUID();
            UUID mVendorUuid = UUID.randomUUID();
            GenericSoundModel model = new GenericSoundModel(modelUuid, mVendorUuid, data);
            ModelInfo modelInfo = new ModelInfo(model, STATUS_UNLOADED);
            modelInfos.add(modelInfo);
        }

        boolean captureTriggerAudio = true;
        boolean allowMultipleTriggers = true;
        RecognitionConfig config = new RecognitionConfig(captureTriggerAudio, allowMultipleTriggers,
                null, null);
        TestRecognitionStatusCallback spyCallback = spy(new TestRecognitionStatusCallback());


        int numOperationsToRun = 100;
        for(int i=0; i<numOperationsToRun; i++) {
            // Select a random model
            int modelInfoIndex = predictableRandom.nextInt(modelInfos.size());
            ModelInfo modelInfo = (ModelInfo) modelInfos.get(modelInfoIndex);

            // Perform a random operation
            int operation = predictableRandom.nextInt(5);

            if (operation == 0 && modelInfo.status == STATUS_UNLOADED) {
                // Update and start sound model
                soundTriggerService.updateSoundModel(modelInfo.model);
                loadedModelUuids.add(modelInfo.model.getUuid());
                modelInfo.status = STATUS_LOADED;
            } else if (operation == 1 && modelInfo.status == STATUS_LOADED) {
                // Start the sound model
                int r = soundTriggerService.startRecognition(new ParcelUuid(
                                modelInfo.model.getUuid()),
                        spyCallback, config);
                assertEquals("Could Not Start Recognition with code: " + r,
                        android.hardware.soundtrigger.SoundTrigger.STATUS_OK, r);
                modelInfo.status = STATUS_STARTED;
            } else if (operation == 2 && modelInfo.status == STATUS_STARTED) {
                // Send trigger to stub HAL
                Socket socket = new Socket(InetAddress.getLocalHost(), 14035);
                DataOutputStream out = new DataOutputStream(socket.getOutputStream());
                out.writeBytes("trig " + modelInfo.model.getUuid() + "\r\n");
                out.flush();
                socket.close();

                // Verify trigger was received
                verify(spyCallback, timeout(100)).onGenericSoundTriggerDetected(any());
                reset(spyCallback);
            } else if (operation == 3 && modelInfo.status == STATUS_STARTED) {
                // Stop recognition
                int r = soundTriggerService.stopRecognition(new ParcelUuid(
                                modelInfo.model.getUuid()),
                        spyCallback);
                assertEquals("Could Not Stop Recognition with code: " + r,
                        android.hardware.soundtrigger.SoundTrigger.STATUS_OK, r);
                modelInfo.status = STATUS_LOADED;
            } else if (operation == 4 && modelInfo.status != STATUS_UNLOADED) {
                // Delete sound model
                soundTriggerService.deleteSoundModel(new ParcelUuid(modelInfo.model.getUuid()));
                loadedModelUuids.remove(modelInfo.model.getUuid());

                // Confirm it was deleted
                GenericSoundModel returnedModel = soundTriggerService.getSoundModel(
                        new ParcelUuid(modelInfo.model.getUuid()));
                assertEquals(null, returnedModel);
                modelInfo.status = STATUS_UNLOADED;
            }
        }
    }

    public class TestRecognitionStatusCallback extends IRecognitionStatusCallback.Stub {
        @Override
        public void onGenericSoundTriggerDetected(GenericRecognitionEvent recognitionEvent) {
        }

        @Override
        public void onKeyphraseDetected(KeyphraseRecognitionEvent recognitionEvent) {
        }

        @Override
        public void onError(int status) {
        }

        @Override
        public void onRecognitionPaused() {
        }

        @Override
        public void onRecognitionResumed() {
        }
    }
}
