/*
 * Copyright (C) 2013 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.android.mediaframeworktest.integration;

import android.graphics.ImageFormat;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraMetadata;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.ICameraDeviceCallbacks;
import android.hardware.camera2.ICameraDeviceUser;
import android.hardware.camera2.impl.CameraMetadataNative;
import android.hardware.camera2.impl.CaptureResultExtras;
import android.hardware.camera2.params.OutputConfiguration;
import android.hardware.camera2.utils.BinderHolder;
import android.media.Image;
import android.media.ImageReader;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.RemoteException;
import android.os.SystemClock;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
import android.util.Log;
import android.view.Surface;

import static android.hardware.camera2.CameraDevice.TEMPLATE_PREVIEW;

import com.android.mediaframeworktest.MediaFrameworkIntegrationTestRunner;

import org.mockito.ArgumentMatcher;
import org.mockito.ArgumentCaptor;
import static org.mockito.Mockito.*;

public class CameraDeviceBinderTest extends AndroidTestCase {
    private static String TAG = "CameraDeviceBinderTest";
    // Number of streaming callbacks need to check.
    private static int NUM_CALLBACKS_CHECKED = 10;
    // Wait for capture result timeout value: 1500ms
    private final static int WAIT_FOR_COMPLETE_TIMEOUT_MS = 1500;
    // Wait for flush timeout value: 1000ms
    private final static int WAIT_FOR_FLUSH_TIMEOUT_MS = 1000;
    // Wait for idle timeout value: 2000ms
    private final static int WAIT_FOR_IDLE_TIMEOUT_MS = 2000;
    // Wait while camera device starts working on requests
    private final static int WAIT_FOR_WORK_MS = 300;
    // Default size is VGA, which is mandatory camera supported image size by CDD.
    private static final int DEFAULT_IMAGE_WIDTH = 640;
    private static final int DEFAULT_IMAGE_HEIGHT = 480;
    private static final int MAX_NUM_IMAGES = 5;

    private int mCameraId;
    private ICameraDeviceUser mCameraUser;
    private CameraBinderTestUtils mUtils;
    private ICameraDeviceCallbacks.Stub mMockCb;
    private Surface mSurface;
    private OutputConfiguration mOutputConfiguration;
    private HandlerThread mHandlerThread;
    private Handler mHandler;
    ImageReader mImageReader;

    public CameraDeviceBinderTest() {
    }

    private class ImageDropperListener implements ImageReader.OnImageAvailableListener {

        @Override
        public void onImageAvailable(ImageReader reader) {
            Image image = reader.acquireNextImage();
            if (image != null) image.close();
        }
    }

    public class DummyCameraDeviceCallbacks extends ICameraDeviceCallbacks.Stub {

        /*
         * (non-Javadoc)
         * @see
         * android.hardware.camera2.ICameraDeviceCallbacks#onDeviceError(int,
         * android.hardware.camera2.CaptureResultExtras)
         */
        public void onDeviceError(int errorCode, CaptureResultExtras resultExtras)
                throws RemoteException {
            // TODO Auto-generated method stub

        }

        /*
         * (non-Javadoc)
         * @see android.hardware.camera2.ICameraDeviceCallbacks#onDeviceIdle()
         */
        public void onDeviceIdle() throws RemoteException {
            // TODO Auto-generated method stub

        }

        /*
         * (non-Javadoc)
         * @see
         * android.hardware.camera2.ICameraDeviceCallbacks#onCaptureStarted(
         * android.hardware.camera2.CaptureResultExtras, long)
         */
        public void onCaptureStarted(CaptureResultExtras resultExtras, long timestamp)
                throws RemoteException {
            // TODO Auto-generated method stub

        }

        /*
         * (non-Javadoc)
         * @see
         * android.hardware.camera2.ICameraDeviceCallbacks#onResultReceived(
         * android.hardware.camera2.impl.CameraMetadataNative,
         * android.hardware.camera2.CaptureResultExtras)
         */
        public void onResultReceived(CameraMetadataNative result, CaptureResultExtras resultExtras)
                throws RemoteException {
            // TODO Auto-generated method stub

        }

        /*
         * (non-Javadoc)
         * @see android.hardware.camera2.ICameraDeviceCallbacks#onPrepared()
         */
        @Override
        public void onPrepared(int streamId) throws RemoteException {
            // TODO Auto-generated method stub

        }
    }

    class IsMetadataNotEmpty extends ArgumentMatcher<CameraMetadataNative> {
        @Override
        public boolean matches(Object obj) {
            return !((CameraMetadataNative) obj).isEmpty();
        }
    }

    private void createDefaultSurface() {
        mImageReader =
                ImageReader.newInstance(DEFAULT_IMAGE_WIDTH,
                        DEFAULT_IMAGE_HEIGHT,
                        ImageFormat.YUV_420_888,
                        MAX_NUM_IMAGES);
        mImageReader.setOnImageAvailableListener(new ImageDropperListener(), mHandler);
        mSurface = mImageReader.getSurface();
        mOutputConfiguration = new OutputConfiguration(mSurface);
    }

    private CaptureRequest.Builder createDefaultBuilder(boolean needStream) throws Exception {
        CameraMetadataNative metadata = new CameraMetadataNative();
        assertTrue(metadata.isEmpty());

        int status = mCameraUser.createDefaultRequest(TEMPLATE_PREVIEW, /* out */metadata);
        assertEquals(CameraBinderTestUtils.NO_ERROR, status);
        assertFalse(metadata.isEmpty());

        CaptureRequest.Builder request = new CaptureRequest.Builder(metadata, /*reprocess*/false,
                CameraCaptureSession.SESSION_ID_NONE);
        assertFalse(request.isEmpty());
        assertFalse(metadata.isEmpty());
        if (needStream) {
            int streamId = mCameraUser.createStream(mOutputConfiguration);
            assertEquals(0, streamId);
            request.addTarget(mSurface);
        }
        return request;
    }

    private int submitCameraRequest(CaptureRequest request, boolean streaming) throws Exception {
        int requestId = mCameraUser.submitRequest(request, streaming, null);
        assertTrue(
                "Request IDs should be non-negative (expected: >= 0, actual: " + requestId + ")",
                requestId >= 0);
        return requestId;
    }

    @Override
    protected void setUp() throws Exception {
        super.setUp();

        /**
         * Workaround for mockito and JB-MR2 incompatibility
         *
         * Avoid java.lang.IllegalArgumentException: dexcache == null
         * https://code.google.com/p/dexmaker/issues/detail?id=2
         */
        System.setProperty("dexmaker.dexcache", getContext().getCacheDir().toString());
        mUtils = new CameraBinderTestUtils(getContext());

        // This cannot be set in the constructor, since the onCreate isn't
        // called yet
        mCameraId = MediaFrameworkIntegrationTestRunner.mCameraId;

        ICameraDeviceCallbacks.Stub dummyCallbacks = new DummyCameraDeviceCallbacks();

        String clientPackageName = getContext().getPackageName();

        mMockCb = spy(dummyCallbacks);

        BinderHolder holder = new BinderHolder();
        mUtils.getCameraService().connectDevice(mMockCb, mCameraId,
                clientPackageName, CameraBinderTestUtils.USE_CALLING_UID, holder);
        mCameraUser = ICameraDeviceUser.Stub.asInterface(holder.getBinder());
        assertNotNull(String.format("Camera %s was null", mCameraId), mCameraUser);
        mHandlerThread = new HandlerThread(TAG);
        mHandlerThread.start();
        mHandler = new Handler(mHandlerThread.getLooper());
        createDefaultSurface();

        Log.v(TAG, String.format("Camera %s connected", mCameraId));
    }

    @Override
    protected void tearDown() throws Exception {
        mCameraUser.disconnect();
        mCameraUser = null;
        mSurface.release();
        mImageReader.close();
        mHandlerThread.quitSafely();
    }

    @SmallTest
    public void testCreateDefaultRequest() throws Exception {
        CameraMetadataNative metadata = new CameraMetadataNative();
        assertTrue(metadata.isEmpty());

        int status = mCameraUser.createDefaultRequest(TEMPLATE_PREVIEW, /* out */metadata);
        assertEquals(CameraBinderTestUtils.NO_ERROR, status);
        assertFalse(metadata.isEmpty());

    }

    @SmallTest
    public void testCreateStream() throws Exception {
        int streamId = mCameraUser.createStream(mOutputConfiguration);
        assertEquals(0, streamId);

        assertEquals(CameraBinderTestUtils.ALREADY_EXISTS,
                mCameraUser.createStream(mOutputConfiguration));

        assertEquals(CameraBinderTestUtils.NO_ERROR, mCameraUser.deleteStream(streamId));
    }

    @SmallTest
    public void testDeleteInvalidStream() throws Exception {
        assertEquals(CameraBinderTestUtils.BAD_VALUE, mCameraUser.deleteStream(-1));
        assertEquals(CameraBinderTestUtils.BAD_VALUE, mCameraUser.deleteStream(0));
        assertEquals(CameraBinderTestUtils.BAD_VALUE, mCameraUser.deleteStream(1));
        assertEquals(CameraBinderTestUtils.BAD_VALUE, mCameraUser.deleteStream(0xC0FFEE));
    }

    @SmallTest
    public void testCreateStreamTwo() throws Exception {

        // Create first stream
        int streamId = mCameraUser.createStream(mOutputConfiguration);
        assertEquals(0, streamId);

        assertEquals(CameraBinderTestUtils.ALREADY_EXISTS,
                mCameraUser.createStream(mOutputConfiguration));

        // Create second stream with a different surface.
        SurfaceTexture surfaceTexture = new SurfaceTexture(/* ignored */0);
        surfaceTexture.setDefaultBufferSize(640, 480);
        Surface surface2 = new Surface(surfaceTexture);
        OutputConfiguration output2 = new OutputConfiguration(surface2);

        int streamId2 = mCameraUser.createStream(output2);
        assertEquals(1, streamId2);

        // Clean up streams
        assertEquals(CameraBinderTestUtils.NO_ERROR, mCameraUser.deleteStream(streamId));
        assertEquals(CameraBinderTestUtils.NO_ERROR, mCameraUser.deleteStream(streamId2));
    }

    @SmallTest
    public void testSubmitBadRequest() throws Exception {

        CaptureRequest.Builder builder = createDefaultBuilder(/* needStream */false);
        CaptureRequest request1 = builder.build();
        int status = mCameraUser.submitRequest(request1, /* streaming */false, null);
        assertEquals("Expected submitRequest to return BAD_VALUE " +
                "since we had 0 surface targets set.", CameraBinderTestUtils.BAD_VALUE, status);

        builder.addTarget(mSurface);
        CaptureRequest request2 = builder.build();
        status = mCameraUser.submitRequest(request2, /* streaming */false, null);
        assertEquals("Expected submitRequest to return BAD_VALUE since " +
                "the target surface wasn't registered with createStream.",
                CameraBinderTestUtils.BAD_VALUE, status);
    }

    @SmallTest
    public void testSubmitGoodRequest() throws Exception {

        CaptureRequest.Builder builder = createDefaultBuilder(/* needStream */true);
        CaptureRequest request = builder.build();

        // Submit valid request twice.
        int requestId1 = submitCameraRequest(request, /* streaming */false);
        int requestId2 = submitCameraRequest(request, /* streaming */false);
        assertNotSame("Request IDs should be unique for multiple requests", requestId1, requestId2);

    }

    @SmallTest
    public void testSubmitStreamingRequest() throws Exception {

        CaptureRequest.Builder builder = createDefaultBuilder(/* needStream */true);

        CaptureRequest request = builder.build();

        // Submit valid request once (non-streaming), and another time
        // (streaming)
        int requestId1 = submitCameraRequest(request, /* streaming */false);

        int requestIdStreaming = submitCameraRequest(request, /* streaming */true);
        assertNotSame("Request IDs should be unique for multiple requests", requestId1,
                requestIdStreaming);

        int status = mCameraUser.cancelRequest(-1, null);
        assertEquals("Invalid request IDs should not be cancellable",
                CameraBinderTestUtils.BAD_VALUE, status);

        status = mCameraUser.cancelRequest(requestId1, null);
        assertEquals("Non-streaming request IDs should not be cancellable",
                CameraBinderTestUtils.BAD_VALUE, status);

        status = mCameraUser.cancelRequest(requestIdStreaming, null);
        assertEquals("Streaming request IDs should be cancellable", CameraBinderTestUtils.NO_ERROR,
                status);

    }

    @SmallTest
    public void testCameraInfo() throws RemoteException {
        CameraMetadataNative info = new CameraMetadataNative();

        int status = mCameraUser.getCameraInfo(/*out*/info);
        assertEquals(CameraBinderTestUtils.NO_ERROR, status);

        assertFalse(info.isEmpty());
        assertNotNull(info.get(CameraCharacteristics.SCALER_AVAILABLE_FORMATS));
    }

    @SmallTest
    public void testCameraCharacteristics() throws RemoteException {
        CameraMetadataNative info = new CameraMetadataNative();

        int status = mUtils.getCameraService().getCameraCharacteristics(mCameraId, /*out*/info);
        assertEquals(CameraBinderTestUtils.NO_ERROR, status);

        assertFalse(info.isEmpty());
        assertNotNull(info.get(CameraCharacteristics.SCALER_AVAILABLE_FORMATS));
    }

    @SmallTest
    public void testWaitUntilIdle() throws Exception {
        CaptureRequest.Builder builder = createDefaultBuilder(/* needStream */true);
        int requestIdStreaming = submitCameraRequest(builder.build(), /* streaming */true);

        // Test Bad case first: waitUntilIdle when there is active repeating request
        int status = mCameraUser.waitUntilIdle();
        assertEquals("waitUntilIdle is invalid operation when there is active repeating request",
            CameraBinderTestUtils.INVALID_OPERATION, status);

        // Test good case, waitUntilIdle when there is no active repeating request
        status = mCameraUser.cancelRequest(requestIdStreaming, null);
        assertEquals(CameraBinderTestUtils.NO_ERROR, status);
        status = mCameraUser.waitUntilIdle();
        assertEquals(CameraBinderTestUtils.NO_ERROR, status);
    }

    @SmallTest
    public void testCaptureResultCallbacks() throws Exception {
        IsMetadataNotEmpty matcher = new IsMetadataNotEmpty();
        CaptureRequest request = createDefaultBuilder(/* needStream */true).build();

        // Test both single request and streaming request.
        verify(mMockCb, timeout(WAIT_FOR_COMPLETE_TIMEOUT_MS).times(1)).onResultReceived(
                argThat(matcher),
                any(CaptureResultExtras.class));

        verify(mMockCb, timeout(WAIT_FOR_COMPLETE_TIMEOUT_MS).atLeast(NUM_CALLBACKS_CHECKED))
                .onResultReceived(
                        argThat(matcher),
                        any(CaptureResultExtras.class));
    }

    @SmallTest
    public void testCaptureStartedCallbacks() throws Exception {
        CaptureRequest request = createDefaultBuilder(/* needStream */true).build();

        ArgumentCaptor<Long> timestamps = ArgumentCaptor.forClass(Long.class);

        // Test both single request and streaming request.
        int requestId1 = submitCameraRequest(request, /* streaming */false);
        verify(mMockCb, timeout(WAIT_FOR_COMPLETE_TIMEOUT_MS).times(1)).onCaptureStarted(
                any(CaptureResultExtras.class),
                anyLong());

        int streamingId = submitCameraRequest(request, /* streaming */true);
        verify(mMockCb, timeout(WAIT_FOR_COMPLETE_TIMEOUT_MS).atLeast(NUM_CALLBACKS_CHECKED))
                .onCaptureStarted(
                        any(CaptureResultExtras.class),
                        timestamps.capture());

        long timestamp = 0; // All timestamps should be larger than 0.
        for (Long nextTimestamp : timestamps.getAllValues()) {
            Log.v(TAG, "next t: " + nextTimestamp + " current t: " + timestamp);
            assertTrue("Captures are out of order", timestamp < nextTimestamp);
            timestamp = nextTimestamp;
        }
    }

    @SmallTest
    public void testIdleCallback() throws Exception {
        int status;
        CaptureRequest request = createDefaultBuilder(/* needStream */true).build();

        // Try streaming
        int streamingId = submitCameraRequest(request, /* streaming */true);

        // Wait a bit to fill up the queue
        SystemClock.sleep(WAIT_FOR_WORK_MS);

        // Cancel and make sure we eventually quiesce
        status = mCameraUser.cancelRequest(streamingId, null);

        verify(mMockCb, timeout(WAIT_FOR_IDLE_TIMEOUT_MS).times(1)).onDeviceIdle();

        // Submit a few capture requests
        int requestId1 = submitCameraRequest(request, /* streaming */false);
        int requestId2 = submitCameraRequest(request, /* streaming */false);
        int requestId3 = submitCameraRequest(request, /* streaming */false);
        int requestId4 = submitCameraRequest(request, /* streaming */false);
        int requestId5 = submitCameraRequest(request, /* streaming */false);

        // And wait for more idle
        verify(mMockCb, timeout(WAIT_FOR_IDLE_TIMEOUT_MS).times(2)).onDeviceIdle();

    }

    @SmallTest
    public void testFlush() throws Exception {
        int status;

        // Initial flush should work
        status = mCameraUser.flush(null);
        assertEquals(CameraBinderTestUtils.NO_ERROR, status);

        // Then set up a stream
        CaptureRequest request = createDefaultBuilder(/* needStream */true).build();

        // Flush should still be a no-op, really
        status = mCameraUser.flush(null);
        assertEquals(CameraBinderTestUtils.NO_ERROR, status);

        // Submit a few capture requests
        int requestId1 = submitCameraRequest(request, /* streaming */false);
        int requestId2 = submitCameraRequest(request, /* streaming */false);
        int requestId3 = submitCameraRequest(request, /* streaming */false);
        int requestId4 = submitCameraRequest(request, /* streaming */false);
        int requestId5 = submitCameraRequest(request, /* streaming */false);

        // Then flush and wait for idle
        status = mCameraUser.flush(null);
        assertEquals(CameraBinderTestUtils.NO_ERROR, status);

        verify(mMockCb, timeout(WAIT_FOR_FLUSH_TIMEOUT_MS).times(1)).onDeviceIdle();

        // Now a streaming request
        int streamingId = submitCameraRequest(request, /* streaming */true);

        // Wait a bit to fill up the queue
        SystemClock.sleep(WAIT_FOR_WORK_MS);

        // Then flush and wait for the idle callback
        status = mCameraUser.flush(null);
        assertEquals(CameraBinderTestUtils.NO_ERROR, status);

        verify(mMockCb, timeout(WAIT_FOR_FLUSH_TIMEOUT_MS).times(2)).onDeviceIdle();

        // TODO: When errors are hooked up, count that errors + successful
        // requests equal to 5.
    }
}
