/*
 * Copyright (C) 2016 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.google.android.cameraview;

import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.Espresso.registerIdlingResources;
import static android.support.test.espresso.Espresso.unregisterIdlingResources;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.assertThat;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withId;

import static com.google.android.cameraview.AspectRatioIsCloseTo.closeToOrInverse;
import static com.google.android.cameraview.CameraViewActions.setAspectRatio;
import static com.google.android.cameraview.CameraViewMatchers.hasAspectRatio;

import static junit.framework.Assert.assertFalse;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;

import android.graphics.Bitmap;
import android.os.SystemClock;
import android.support.test.espresso.IdlingResource;
import android.support.test.espresso.NoMatchingViewException;
import android.support.test.espresso.UiController;
import android.support.test.espresso.ViewAction;
import android.support.test.espresso.ViewAssertion;
import android.support.test.filters.FlakyTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.view.TextureView;
import android.view.View;
import android.view.ViewGroup;

import com.google.android.cameraview.test.R;

import org.hamcrest.Matcher;
import org.hamcrest.core.IsAnything;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.io.Closeable;
import java.io.IOException;
import java.util.Set;

@RunWith(AndroidJUnit4.class)
public class CameraViewTest {

    @Rule
    public final ActivityTestRule<CameraViewActivity> rule;

    private CameraViewIdlingResource mCameraViewIdlingResource;

    public CameraViewTest() {
        rule = new ActivityTestRule<>(CameraViewActivity.class);
    }

    @Before
    public void setUpIdlingResource() {
        mCameraViewIdlingResource = new CameraViewIdlingResource(
                (CameraView) rule.getActivity().findViewById(R.id.camera));
        registerIdlingResources(mCameraViewIdlingResource);
    }

    @After
    public void tearDownIdlingResource() throws Exception {
        unregisterIdlingResources(mCameraViewIdlingResource);
        mCameraViewIdlingResource.close();
    }

    @Test
    public void testSetup() {
        onView(withId(R.id.camera))
                .check(matches(isDisplayed()));
        try {
            onView(withId(R.id.texture_view))
                    .check(matches(isDisplayed()));
        } catch (NoMatchingViewException e) {
            onView(withId(R.id.surface_view))
                    .check(matches(isDisplayed()));
        }
    }

    @Test
    @FlakyTest
    public void preview_isShowing() throws Exception {
        onView(withId(R.id.camera))
                .perform(waitFor(1000))
                .check(showingPreview());
    }

    @Test
    public void testAspectRatio() {
        final CameraView cameraView = (CameraView) rule.getActivity().findViewById(R.id.camera);
        final Set<AspectRatio> ratios = cameraView.getSupportedAspectRatios();
        for (AspectRatio ratio : ratios) {
            onView(withId(R.id.camera))
                    .perform(setAspectRatio(ratio))
                    .check(matches(hasAspectRatio(ratio)));
        }
    }

    @Test
    public void testAdjustViewBounds() {
        onView(withId(R.id.camera))
                .check(new ViewAssertion() {
                    @Override
                    public void check(View view, NoMatchingViewException noViewFoundException) {
                        CameraView cameraView = (CameraView) view;
                        assertThat(cameraView.getAdjustViewBounds(), is(false));
                        cameraView.setAdjustViewBounds(true);
                        assertThat(cameraView.getAdjustViewBounds(), is(true));
                    }
                })
                .perform(new AnythingAction("layout") {
                    @Override
                    public void perform(UiController uiController, View view) {
                        ViewGroup.LayoutParams params = view.getLayoutParams();
                        params.height = ViewGroup.LayoutParams.WRAP_CONTENT;
                        view.setLayoutParams(params);
                    }
                })
                .check(new ViewAssertion() {
                    @Override
                    public void check(View view, NoMatchingViewException noViewFoundException) {
                        CameraView cameraView = (CameraView) view;
                        AspectRatio cameraRatio = cameraView.getAspectRatio();
                        AspectRatio viewRatio = AspectRatio.of(view.getWidth(), view.getHeight());
                        assertThat(cameraRatio, is(closeToOrInverse(viewRatio)));
                    }
                });
    }

    @Test
    public void testPreviewViewSize() {
        onView(withId(R.id.camera))
                .check(new ViewAssertion() {
                    @Override
                    public void check(View view, NoMatchingViewException noViewFoundException) {
                        CameraView cameraView = (CameraView) view;
                        View preview = view.findViewById(R.id.texture_view);
                        if (preview == null) {
                            preview = view.findViewById(R.id.surface_view);
                        }
                        AspectRatio cameraRatio = cameraView.getAspectRatio();
                        assert cameraRatio != null;
                        AspectRatio textureRatio = AspectRatio.of(
                                preview.getWidth(), preview.getHeight());
                        assertThat(textureRatio, is(closeToOrInverse(cameraRatio)));
                    }
                });
    }

    @Test
    public void testAutoFocus() {
        onView(withId(R.id.camera))
                .check(new ViewAssertion() {
                    @Override
                    public void check(View view, NoMatchingViewException noViewFoundException) {
                        CameraView cameraView = (CameraView) view;
                        // This can fail on devices without auto-focus support
                        assertThat(cameraView.getAutoFocus(), is(true));
                        cameraView.setAutoFocus(false);
                        assertThat(cameraView.getAutoFocus(), is(false));
                        cameraView.setAutoFocus(true);
                        assertThat(cameraView.getAutoFocus(), is(true));
                    }
                });
    }

    @Test
    public void testFacing() {
        onView(withId(R.id.camera))
                .check(new ViewAssertion() {
                    @Override
                    public void check(View view, NoMatchingViewException noViewFoundException) {
                        CameraView cameraView = (CameraView) view;
                        assertThat(cameraView.getFacing(), is(CameraView.FACING_BACK));
                        cameraView.setFacing(CameraView.FACING_FRONT);
                        assertThat(cameraView.getFacing(), is(CameraView.FACING_FRONT));
                    }
                })
                .perform(waitFor(1000))
                .check(showingPreview());
    }

    @Test
    public void testFlash() {
        onView(withId(R.id.camera))
                .check(new ViewAssertion() {
                    @Override
                    public void check(View view, NoMatchingViewException noViewFoundException) {
                        CameraView cameraView = (CameraView) view;
                        assertThat(cameraView.getFlash(), is(CameraView.FLASH_AUTO));
                        cameraView.setFlash(CameraView.FLASH_TORCH);
                        assertThat(cameraView.getFlash(), is(CameraView.FLASH_TORCH));
                    }
                });
    }

    @Test
    public void testTakePicture() throws Exception {
        TakePictureIdlingResource resource = new TakePictureIdlingResource(
                (CameraView) rule.getActivity().findViewById(R.id.camera));
        onView(withId(R.id.camera))
                .perform(new AnythingAction("take picture") {
                    @Override
                    public void perform(UiController uiController, View view) {
                        CameraView cameraView = (CameraView) view;
                        cameraView.takePicture();
                    }
                });
        try {
            registerIdlingResources(resource);
            onView(withId(R.id.camera))
                    .perform(waitFor(1000))
                    .check(showingPreview());
            assertThat("Didn't receive valid JPEG data.", resource.receivedValidJpeg(), is(true));
        } finally {
            unregisterIdlingResources(resource);
            resource.close();
        }
    }

    private static ViewAction waitFor(final long ms) {
        return new AnythingAction("wait") {
            @Override
            public void perform(UiController uiController, View view) {
                SystemClock.sleep(ms);
            }
        };
    }

    private static ViewAssertion showingPreview() {
        return new ViewAssertion() {
            @Override
            public void check(View view, NoMatchingViewException noViewFoundException) {
                if (android.os.Build.VERSION.SDK_INT < 14) {
                    return;
                }
                CameraView cameraView = (CameraView) view;
                TextureView textureView = (TextureView) cameraView.findViewById(R.id.texture_view);
                Bitmap bitmap = textureView.getBitmap();
                int topLeft = bitmap.getPixel(0, 0);
                int center = bitmap.getPixel(bitmap.getWidth() / 2, bitmap.getHeight() / 2);
                int bottomRight = bitmap.getPixel(
                        bitmap.getWidth() - 1, bitmap.getHeight() - 1);
                assertFalse("Preview possibly blank: " + Integer.toHexString(topLeft),
                        topLeft == center && center == bottomRight);
            }
        };
    }

    /**
     * Wait for a camera to open.
     */
    private static class CameraViewIdlingResource implements IdlingResource, Closeable {

        private final CameraView.Callback mCallback
                = new CameraView.Callback() {

            @Override
            public void onCameraOpened(CameraView cameraView) {
                if (!mIsIdleNow) {
                    mIsIdleNow = true;
                    if (mResourceCallback != null) {
                        mResourceCallback.onTransitionToIdle();
                    }
                }
            }

            @Override
            public void onCameraClosed(CameraView cameraView) {
                mIsIdleNow = false;
            }
        };

        private final CameraView mCameraView;

        private ResourceCallback mResourceCallback;

        private boolean mIsIdleNow;

        public CameraViewIdlingResource(CameraView cameraView) {
            mCameraView = cameraView;
            mCameraView.addCallback(mCallback);
            mIsIdleNow = mCameraView.isCameraOpened();
        }

        @Override
        public void close() throws IOException {
            mCameraView.removeCallback(mCallback);
        }

        @Override
        public String getName() {
            return CameraViewIdlingResource.class.getSimpleName();
        }

        @Override
        public boolean isIdleNow() {
            return mIsIdleNow;
        }

        @Override
        public void registerIdleTransitionCallback(ResourceCallback callback) {
            mResourceCallback = callback;
        }

    }

    private static class TakePictureIdlingResource implements IdlingResource, Closeable {

        private final CameraView.Callback mCallback
                = new CameraView.Callback() {
            @Override
            public void onPictureTaken(CameraView cameraView, byte[] data) {
                if (!mIsIdleNow) {
                    mIsIdleNow = true;
                    mValidJpeg = data.length > 2 &&
                            data[0] == (byte) 0xFF && data[1] == (byte) 0xD8;
                    if (mResourceCallback != null) {
                        mResourceCallback.onTransitionToIdle();
                    }
                }
            }
        };

        private final CameraView mCameraView;

        private ResourceCallback mResourceCallback;

        private boolean mIsIdleNow;

        private boolean mValidJpeg;

        public TakePictureIdlingResource(CameraView cameraView) {
            mCameraView = cameraView;
            mCameraView.addCallback(mCallback);
        }

        @Override
        public void close() throws IOException {
            mCameraView.removeCallback(mCallback);
        }

        @Override
        public String getName() {
            return TakePictureIdlingResource.class.getSimpleName();
        }

        @Override
        public boolean isIdleNow() {
            return mIsIdleNow;
        }

        @Override
        public void registerIdleTransitionCallback(ResourceCallback callback) {
            mResourceCallback = callback;
        }

        public boolean receivedValidJpeg() {
            return mValidJpeg;
        }

    }

    private static abstract class AnythingAction implements ViewAction {

        private final String mDescription;

        public AnythingAction(String description) {
            mDescription = description;
        }

        @Override
        public Matcher<View> getConstraints() {
            return new IsAnything<>();
        }

        @Override
        public String getDescription() {
            return mDescription;
        }

    }

}