/*
 * Copyright (C) 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 android.server.wm;

import static android.server.wm.LocationOnScreenTests.TestActivity.COLOR_TOLERANCE;
import static android.server.wm.LocationOnScreenTests.TestActivity.EXTRA_LAYOUT_PARAMS;
import static android.server.wm.LocationOnScreenTests.TestActivity.TEST_COLOR_1;
import static android.server.wm.LocationOnScreenTests.TestActivity.TEST_COLOR_2;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;

import static androidx.test.InstrumentationRegistry.getInstrumentation;

import static org.hamcrest.Matchers.is;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.os.Bundle;
import android.platform.test.annotations.Presubmit;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager.LayoutParams;
import android.widget.FrameLayout;

import androidx.test.filters.FlakyTest;
import androidx.test.filters.SmallTest;
import androidx.test.rule.ActivityTestRule;

import com.android.compatibility.common.util.BitmapUtils;
import com.android.compatibility.common.util.PollingCheck;

import org.hamcrest.Matcher;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ErrorCollector;

import java.util.function.Supplier;

@SmallTest
@Presubmit
public class LocationOnScreenTests {

    @Rule
    public final ErrorCollector mErrorCollector = new ErrorCollector();

    @Rule
    public final ActivityTestRule<TestActivity> mDisplayCutoutActivity =
            new ActivityTestRule<>(TestActivity.class, false /* initialTouchMode */,
                    false /* launchActivity */);

    private LayoutParams mLayoutParams;
    private Context mContext;

    @Before
    public void setUp() {
        mContext = getInstrumentation().getContext();
        mLayoutParams = new LayoutParams(MATCH_PARENT, MATCH_PARENT, LayoutParams.TYPE_APPLICATION,
                LayoutParams.FLAG_LAYOUT_IN_SCREEN | LayoutParams.FLAG_LAYOUT_INSET_DECOR,
                PixelFormat.TRANSLUCENT);
    }

    @Test
    public void testLocationOnDisplay_appWindow() {
        runTest(mLayoutParams);
    }

    @Test
    public void testLocationOnDisplay_appWindow_fullscreen() {
        mLayoutParams.flags |= LayoutParams.FLAG_FULLSCREEN;
        runTest(mLayoutParams);
    }

    @Test
    public void testLocationOnDisplay_floatingWindow() {
        mLayoutParams.height = 50;
        mLayoutParams.width = 50;
        mLayoutParams.gravity = Gravity.CENTER;
        mLayoutParams.flags &= ~(FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR);
        runTest(mLayoutParams);
    }

    @Test
    public void testLocationOnDisplay_appWindow_displayCutoutNever() {
        mLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
        runTest(mLayoutParams);
    }

    @Test
    public void testLocationOnDisplay_appWindow_displayCutoutShortEdges() {
        mLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
        runTest(mLayoutParams);
    }

    private void runTest(LayoutParams lp) {
        final TestActivity activity = launchAndWait(mDisplayCutoutActivity, lp);
        PollingCheck.waitFor(() -> getOnMainSync(activity::isEnterAnimationComplete));

        Point actual = getOnMainSync(activity::getViewLocationOnScreen);
        Point expected = findTestColorsInScreenshot(actual);

        assertThat("View.locationOnScreen returned incorrect value", actual, is(expected));
    }

    private <T> void assertThat(String reason, T actual, Matcher<? super T> matcher) {
        mErrorCollector.checkThat(reason, actual, matcher);
    }

    private <R> R getOnMainSync(Supplier<R> f) {
        final Object[] result = new Object[1];
        runOnMainSync(() -> result[0] = f.get());
        //noinspection unchecked
        return (R) result[0];
    }

    private void runOnMainSync(Runnable runnable) {
        getInstrumentation().runOnMainSync(runnable);
    }

    private <T extends Activity> T launchAndWait(ActivityTestRule<T> rule,
            LayoutParams lp) {
        final T activity = rule.launchActivity(
                new Intent().putExtra(EXTRA_LAYOUT_PARAMS, lp));
        PollingCheck.waitFor(activity::hasWindowFocus);
        return activity;
    }

    private Point findTestColorsInScreenshot(Point guess) {
        final Bitmap screenshot = getInstrumentation().getUiAutomation().takeScreenshot();

        // We have a good guess from locationOnScreen - check there first to avoid having to go over
        // the entire bitmap. Also increases robustness in the extremely unlikely case that those
        // colors are visible elsewhere.
        if (isTestColors(screenshot, guess.x, guess.y)) {
            return guess;
        }

        for (int y = 0; y < screenshot.getHeight(); y++) {
            for (int x = 0; x < screenshot.getWidth() - 1; x++) {
                if (isTestColors(screenshot, x, y)) {
                    return new Point(x, y);
                }
            }
        }
        String path = mContext.getExternalFilesDir(null).getPath();
        String file = "location_on_screen_failure.png";
        BitmapUtils.saveBitmap(screenshot, path, file);
        Assert.fail("No match found for TEST_COLOR_1 and TEST_COLOR_2 pixels. Check "
                + path + "/" + file);
        return null;
    }

    private boolean isTestColors(Bitmap screenshot, int x, int y) {
        return sameColorWithinTolerance(screenshot.getPixel(x, y), TEST_COLOR_1)
                && sameColorWithinTolerance(screenshot.getPixel(x + 1, y), TEST_COLOR_2);
    }

    /**
     * Returns whether two colors are considered the same.
     *
     * Some tolerance is allowed to compensate for errors introduced when screenshots are scaled.
     */
    private static boolean sameColorWithinTolerance(int pixelColor, int testColor) {
        final Color pColor = Color.valueOf(pixelColor);
        final Color tColor = Color.valueOf(testColor);
        return pColor.alpha() == tColor.alpha()
                && Math.abs(pColor.red() - tColor.red()) <= COLOR_TOLERANCE
                && Math.abs(pColor.blue() - tColor.blue()) <= COLOR_TOLERANCE
                && Math.abs(pColor.green() - tColor.green()) <= COLOR_TOLERANCE;
    }

    public static class TestActivity extends Activity {

        static final int TEST_COLOR_1 = 0xff123456;
        static final int TEST_COLOR_2 = 0xfffedcba;
        static final int COLOR_TOLERANCE = 4;
        static final String EXTRA_LAYOUT_PARAMS = "extra.layout_params";
        private View mView;
        private boolean mEnterAnimationComplete;

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            getWindow().requestFeature(Window.FEATURE_NO_TITLE);

            FrameLayout frame = new FrameLayout(this);
            frame.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
            setContentView(frame);

            mView = new TestView(this);
            frame.addView(mView, new FrameLayout.LayoutParams(2, 1, Gravity.CENTER));

            if (getIntent() != null
                    && getIntent().getParcelableExtra(EXTRA_LAYOUT_PARAMS) != null) {
                getWindow().setAttributes(getIntent().getParcelableExtra(EXTRA_LAYOUT_PARAMS));
            }
        }

        public Point getViewLocationOnScreen() {
            final int[] location = new int[2];
            mView.getLocationOnScreen(location);
            return new Point(location[0], location[1]);
        }

        public boolean isEnterAnimationComplete() {
            return mEnterAnimationComplete;
        }

        @Override
        public void onEnterAnimationComplete() {
            super.onEnterAnimationComplete();
            mEnterAnimationComplete = true;
        }
    }

    private static class TestView extends View {
        private Paint mPaint = new Paint();

        public TestView(Context context) {
            super(context);
        }

        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);

            mPaint.setColor(TEST_COLOR_1);
            canvas.drawRect(0, 0, 1, 1, mPaint);
            mPaint.setColor(TEST_COLOR_2);
            canvas.drawRect(1, 0, 2, 1, mPaint);
        }
    }
}
