/*
 * Copyright (C) 2020 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.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.server.wm.ActivityManagerTestBase.executeShellCommand;
import static android.server.wm.WindowInsetsAnimationUtils.requestControlThenTransitionToVisibility;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.WindowInsets.Type.ime;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN;

import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;

import android.app.Activity;
import android.app.ActivityOptions;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Insets;
import android.graphics.Paint;
import android.graphics.Rect;
import android.inputmethodservice.InputMethodService;
import android.os.Bundle;
import android.platform.test.annotations.LargeTest;
import android.provider.Settings;
import android.server.wm.WindowInsetsAnimationControllerTests.LimitedErrorCollector;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowInsetsAnimation;
import android.view.WindowInsetsAnimation.Callback;
import android.view.WindowInsetsController;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.FrameLayout;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;

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

import org.junit.Rule;
import org.junit.Test;

import java.lang.reflect.Array;
import java.util.List;
import java.util.Locale;
import java.util.function.Supplier;

@LargeTest
public class WindowInsetsAnimationSynchronicityTests {
    private static final int APP_COLOR = 0xff01fe10; // green
    private static final int BEHIND_IME_COLOR = 0xfffeef00; // yellow
    private static final int IME_COLOR = 0xfffe01fd; // pink

    @Rule
    public LimitedErrorCollector mErrorCollector = new LimitedErrorCollector();

    private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();

    @Test
    public void testShowAndHide_renderSynchronouslyBetweenImeWindowAndAppContent() throws Throwable {
        runTest(false /* useControlApi */);
    }

    @Test
    public void testControl_rendersSynchronouslyBetweenImeWindowAndAppContent() throws Throwable {
        runTest(true /* useControlApi */);
    }

    private void runTest(boolean useControlApi) throws Exception {
        try (ImeSession imeSession = new ImeSession(SimpleIme.getName(mContext))) {
            TestActivity activity = launchActivity();
            activity.setUseControlApi(useControlApi);
            PollingCheck.waitFor(activity::hasWindowFocus);
            activity.setEvaluator(() -> {
                // This runs from time to time on the UI thread.
                Bitmap screenshot = getInstrumentation().getUiAutomation().takeScreenshot();
                final int center = screenshot.getWidth() / 2;
                int imePositionApp = lowestPixelWithColor(APP_COLOR, 1, screenshot);
                int contentBottomMiddle = lowestPixelWithColor(APP_COLOR, center, screenshot);
                int behindImeBottomMiddle =
                        lowestPixelWithColor(BEHIND_IME_COLOR, center, screenshot);
                int imePositionIme = Math.max(contentBottomMiddle, behindImeBottomMiddle);
                if (imePositionApp != imePositionIme) {
                    mErrorCollector.addError(new AssertionError(String.format(Locale.US,
                            "IME is positioned at %d (max of %d, %d),"
                                    + " app thinks it is positioned at %d",
                            imePositionIme, contentBottomMiddle, behindImeBottomMiddle,
                            imePositionApp)));
                }
            });
            Thread.sleep(2000);
            activity.setEvaluator(null);
        }
    }

    private TestActivity launchActivity() {
        final ActivityOptions options= ActivityOptions.makeBasic();
        options.setLaunchWindowingMode(WINDOWING_MODE_FULLSCREEN);
        final TestActivity[] activity = (TestActivity[]) Array.newInstance(TestActivity.class, 1);
        SystemUtil.runWithShellPermissionIdentity(() -> {
            activity[0] = (TestActivity) getInstrumentation().startActivitySync(
                    new Intent(getInstrumentation().getTargetContext(), TestActivity.class)
                            .addFlags(FLAG_ACTIVITY_NEW_TASK), options.toBundle());
        });
        return activity[0];
    }

    private static int lowestPixelWithColor(int color, int x, Bitmap bitmap) {
        int[] pixels = new int[bitmap.getHeight()];
        bitmap.getPixels(pixels, 0, 1, x, 0, 1, bitmap.getHeight());
        for (int y = pixels.length - 1; y >= 0; y--) {
            if (pixels[y] == color) {
                return y;
            }
        }
        return -1;
    }

    public static class TestActivity extends Activity implements
            WindowInsetsController.OnControllableInsetsChangedListener {

        private TestView mTestView;
        private EditText mEditText;
        private Runnable mEvaluator;
        private boolean mUseControlApi;

        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            getWindow().requestFeature(Window.FEATURE_NO_TITLE);
            getWindow().setDecorFitsSystemWindows(false);
            getWindow().setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_HIDDEN);
            mTestView = new TestView(this);
            mEditText = new EditText(this);
            mEditText.setImeOptions(EditorInfo.IME_FLAG_NO_FULLSCREEN);
            mTestView.addView(mEditText);
            mTestView.mEvaluator = () -> {
                if (mEvaluator != null) {
                    mEvaluator.run();
                }
            };
            mEditText.requestFocus();
            setContentView(mTestView);
            mEditText.getWindowInsetsController().addOnControllableInsetsChangedListener(this);
        }

        void setEvaluator(Runnable evaluator) {
            mEvaluator = evaluator;
        }

        void setUseControlApi(boolean useControlApi) {
            mUseControlApi = useControlApi;
        }

        @Override
        public void onControllableInsetsChanged(@NonNull WindowInsetsController controller,
                int typeMask) {
            if ((typeMask & ime()) != 0) {
                mEditText.getWindowInsetsController().removeOnControllableInsetsChangedListener(
                        this);
                showIme();
            }
        }

        private void showIme() {
            if (mUseControlApi) {
                requestControlThenTransitionToVisibility(mTestView.getWindowInsetsController(),
                        ime(), true);
            } else {
                mTestView.getWindowInsetsController().show(ime());
            }
        }

        private void hideIme() {
            if (mUseControlApi) {
                requestControlThenTransitionToVisibility(mTestView.getWindowInsetsController(),
                        ime(), false);
            } else {
                mTestView.getWindowInsetsController().hide(ime());
            }
        }

        private static class TestView extends FrameLayout {
            private WindowInsets mLayoutInsets;
            private WindowInsets mAnimationInsets;
            private final Rect mTmpRect = new Rect();
            private final Paint mContentPaint = new Paint();

            private final Callback mInsetsCallback = new Callback(Callback.DISPATCH_MODE_STOP) {
                @NonNull
                @Override
                public WindowInsets onProgress(@NonNull WindowInsets insets,
                        @NonNull List<WindowInsetsAnimation> runningAnimations) {
                    if (runningAnimations.stream().anyMatch(TestView::isImeAnimation)) {
                        mAnimationInsets = insets;
                        invalidate();
                    }
                    return WindowInsets.CONSUMED;
                }

                @Override
                public void onEnd(@NonNull WindowInsetsAnimation animation) {
                    if (isImeAnimation(animation)) {
                        mAnimationInsets = null;
                        post(() -> {
                            if (mLayoutInsets.isVisible(ime())) {
                                ((TestActivity) getContext()).hideIme();
                            } else {
                                ((TestActivity) getContext()).showIme();
                            }
                        });

                    }
                }
            };
            private final Runnable mRunEvaluator;
            private Runnable mEvaluator;

            TestView(Context context) {
                super(context);
                setWindowInsetsAnimationCallback(mInsetsCallback);
                mContentPaint.setColor(APP_COLOR);
                mContentPaint.setStyle(Paint.Style.FILL);
                setWillNotDraw(false);
                mRunEvaluator = () -> {
                    if (mEvaluator != null) {
                        mEvaluator.run();
                    }
                };
            }

            @Override
            public WindowInsets onApplyWindowInsets(WindowInsets insets) {
                mLayoutInsets = insets;
                return WindowInsets.CONSUMED;
            }

            private WindowInsets getEffectiveInsets() {
                return mAnimationInsets != null ? mAnimationInsets : mLayoutInsets;
            }

            @Override
            protected void onDraw(Canvas canvas) {
                canvas.drawColor(BEHIND_IME_COLOR);
                mTmpRect.set(0, 0, getWidth(), getHeight());
                Insets insets = getEffectiveInsets().getInsets(ime());
                insetRect(mTmpRect, insets);
                canvas.drawRect(mTmpRect, mContentPaint);
                removeCallbacks(mRunEvaluator);
                post(mRunEvaluator);
            }

            private static boolean isImeAnimation(WindowInsetsAnimation animation) {
                return (animation.getTypeMask() & ime()) != 0;
            }

            private static void insetRect(Rect rect, Insets insets) {
                rect.left += insets.left;
                rect.top += insets.top;
                rect.right -= insets.right;
                rect.bottom -= insets.bottom;
            }
        }
    }

    private static class ImeSession implements AutoCloseable {

        private static final long TIMEOUT = 2000;
        private final ComponentName mImeName;
        private Context mContext = InstrumentationRegistry.getInstrumentation().getContext();

        ImeSession(ComponentName ime) throws Exception {
            mImeName = ime;
            executeShellCommand("ime reset");
            executeShellCommand("ime enable " + ime.flattenToShortString());
            executeShellCommand("ime set " + ime.flattenToShortString());
            PollingCheck.check("Make sure that MockIME becomes available", TIMEOUT,
                    () -> ime.equals(getCurrentInputMethodId()));
        }

        @Override
        public void close() throws Exception {
            executeShellCommand("ime reset");
            PollingCheck.check("Make sure that MockIME becomes unavailable", TIMEOUT, () ->
                    mContext.getSystemService(InputMethodManager.class)
                            .getEnabledInputMethodList()
                            .stream()
                            .noneMatch(info -> mImeName.equals(info.getComponent())));
        }

        @Nullable
        private ComponentName getCurrentInputMethodId() {
            // TODO: Replace this with IMM#getCurrentInputMethodIdForTesting()
            return ComponentName.unflattenFromString(
                    Settings.Secure.getString(mContext.getContentResolver(),
                    Settings.Secure.DEFAULT_INPUT_METHOD));
        }
    }

    public static class SimpleIme extends InputMethodService {

        public static final int HEIGHT_DP = 200;
        public static final int SIDE_PADDING_DP = 50;

        @Override
        public View onCreateInputView() {
            final ViewGroup view = new FrameLayout(this);
            final View inner = new View(this);
            final float density = getResources().getDisplayMetrics().density;
            final int height = (int) (HEIGHT_DP * density);
            final int sidePadding = (int) (SIDE_PADDING_DP * density);
            view.setPadding(sidePadding, 0, sidePadding, 0);
            view.addView(inner, new FrameLayout.LayoutParams(MATCH_PARENT,
                    height));
            inner.setBackgroundColor(IME_COLOR);
            return view;
        }

        static ComponentName getName(Context context) {
            return new ComponentName(context, SimpleIme.class);
        }
    }
}
