/*
 * Copyright (C) 2017 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.example.android.themednavbarkeyboard;

import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;

import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.GradientDrawable;
import android.inputmethodservice.InputMethodService;
import android.os.Build;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.Window;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;

/**
 * A sample {@link InputMethodService} to demonstrates how to integrate the software keyboard with
 * custom themed navigation bar.
 */
public class ThemedNavBarKeyboard extends InputMethodService {

    private final int MINT_COLOR = 0xff98fb98;
    private final int LIGHT_RED = 0xff98fb98;

    private static final class BuildCompat {
        private static final boolean IS_RELEASE_BUILD = Build.VERSION.CODENAME.equals("REL");

        /**
         * The "effective" API version.
         * {@link android.os.Build.VERSION#SDK_INT} if the platform is a release build.
         * {@link android.os.Build.VERSION#SDK_INT} plus 1 if the platform is a development build.
         */
        private static final int EFFECTIVE_SDK_INT = IS_RELEASE_BUILD
                ? Build.VERSION.SDK_INT
                : Build.VERSION.SDK_INT + 1;
    }

    private KeyboardLayoutView mLayout;

    @Override
    public void onCreate() {
        super.onCreate();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            // Disable contrast for extended navbar gradient.
            getWindow().getWindow().setNavigationBarContrastEnforced(false);
        }
    }

    @Override
    public View onCreateInputView() {
        mLayout = new KeyboardLayoutView(this, getWindow().getWindow());
        return mLayout;
    }

    @Override
    public void onComputeInsets(Insets outInsets) {
        super.onComputeInsets(outInsets);

        // For floating mode, tweak Insets to avoid relayout in the target app.
        if (mLayout != null && mLayout.isFloatingMode()) {
            // Lying that the visible keyboard height is 0.
            outInsets.visibleTopInsets = getWindow().getWindow().getDecorView().getHeight();
            outInsets.contentTopInsets = getWindow().getWindow().getDecorView().getHeight();

            // But make sure that touch events are still sent to the IME.
            final int[] location = new int[2];
            mLayout.getLocationInWindow(location);
            final int x = location[0];
            final int y = location[1];
            outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_REGION;
            outInsets.touchableRegion.set(x, y, x + mLayout.getWidth(), y + mLayout.getHeight());
        }
    }

    private enum InputViewMode {
        /**
         * The input view is adjacent to the bottom Navigation Bar (if present). In this mode the
         * IME is expected to control Navigation Bar appearance, including button color.
         *
         * <p>Call {@link Window#setNavigationBarColor(int)} to change the navigation bar color.</p>
         *
         * <p>Call {@link View#setSystemUiVisibility(int)} with
         * {@link View#SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR} to optimize the navigation bar for
         * light color.</p>
         */
        SYSTEM_OWNED_NAV_BAR_LAYOUT,
        /**
         * The input view is extended to the bottom Navigation Bar (if present). In this mode the
         * IME is expected to control Navigation Bar appearance, including button color.
         *
         * <p>In this state, the system does not automatically place the input view above the
         * navigation bar.  You need to take care of the inset manually.</p>
         *
         * <p>Call {@link View#setSystemUiVisibility(int)} with
         * {@link View#SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR} to optimize the navigation bar for
         * light color.</p>

         * @see View#SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
         * @see View#SYSTEM_UI_FLAG_LAYOUT_STABLE
         */
        IME_OWNED_NAV_BAR_LAYOUT,
        /**
         * The input view is floating off of the bottom Navigation Bar region (if present). In this
         * mode the target application is expected to control Navigation Bar appearance, including
         * button color.
         */
        FLOATING_LAYOUT,
    }

    private final class KeyboardLayoutView extends LinearLayout {

        private final Window mWindow;
        private InputViewMode mMode = InputViewMode.SYSTEM_OWNED_NAV_BAR_LAYOUT;

        private void updateBottomPaddingIfNecessary(int newPaddingBottom) {
            if (getPaddingBottom() != newPaddingBottom) {
                setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), newPaddingBottom);
            }
        }

        @Override
        public WindowInsets onApplyWindowInsets(WindowInsets insets) {
            if (insets.isConsumed()
                    || (getSystemUiVisibility() & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0) {
                // In this case we are not interested in consuming NavBar region.
                // Make sure that the bottom padding is empty.
                updateBottomPaddingIfNecessary(0);
                return insets;
            }

            // In some cases the bottom system window inset is not a navigation bar. Wear devices
            // that have bottom chin are examples.  For now, assume that it's a navigation bar if it
            // has the same height as the root window's stable bottom inset.
            final WindowInsets rootWindowInsets = getRootWindowInsets();
            if (rootWindowInsets != null && (rootWindowInsets.getStableInsetBottom() !=
                    insets.getSystemWindowInsetBottom())) {
                // This is probably not a NavBar.
                updateBottomPaddingIfNecessary(0);
                return insets;
            }

            final int possibleNavBarHeight = insets.getSystemWindowInsetBottom();
            updateBottomPaddingIfNecessary(possibleNavBarHeight);
            return possibleNavBarHeight <= 0
                    ? insets
                    : insets.replaceSystemWindowInsets(
                            insets.getSystemWindowInsetLeft(),
                            insets.getSystemWindowInsetTop(),
                            insets.getSystemWindowInsetRight(),
                            0 /* bottom */);
        }

        public KeyboardLayoutView(Context context, final Window window) {
            super(context);
            mWindow = window;
            setOrientation(VERTICAL);

            if (BuildCompat.EFFECTIVE_SDK_INT <= Build.VERSION_CODES.O_MR1) {
                final TextView textView = new TextView(context);
                textView.setText("ThemedNavBarKeyboard works only on API 28 and higher devices");
                textView.setGravity(Gravity.CENTER);
                textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
                textView.setPadding(20, 10, 20, 20);
                addView(textView);
                setBackgroundColor(LIGHT_RED);
                return;
            }

            // By default use "SeparateNavBarMode" mode.
            switchToSeparateNavBarMode(Color.DKGRAY, false /* lightNavBar */);
            setBackgroundColor(MINT_COLOR);

            {
                final LinearLayout subLayout = new LinearLayout(context);
                {
                    final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
                            LinearLayout.LayoutParams.MATCH_PARENT,
                            LinearLayout.LayoutParams.WRAP_CONTENT);
                    lp.weight = 50;
                    subLayout.addView(createButton("BACK_DISPOSITION\nDEFAULT", () -> {
                        setBackDisposition(BACK_DISPOSITION_DEFAULT);
                    }), lp);
                }
                {
                    final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
                            LinearLayout.LayoutParams.MATCH_PARENT,
                            LinearLayout.LayoutParams.WRAP_CONTENT);
                    lp.weight = 50;
                    subLayout.addView(createButton("BACK_DISPOSITION\nADJUST_NOTHING", () -> {
                        setBackDisposition(BACK_DISPOSITION_ADJUST_NOTHING);
                    }), lp);
                }
                addView(subLayout);
            }

            addView(createButton("Floating Mode", () -> {
                switchToFloatingMode();
                setBackgroundColor(Color.TRANSPARENT);
            }));
            addView(createButton("Extended Dark Navigation Bar", () -> {
                switchToExtendedNavBarMode(false /* lightNavBar */);
                final GradientDrawable drawable = new GradientDrawable(
                        GradientDrawable.Orientation.TOP_BOTTOM,
                        new int[] {MINT_COLOR, Color.DKGRAY});
                setBackground(drawable);
            }));
            addView(createButton("Extended Light Navigation Bar", () -> {
                switchToExtendedNavBarMode(true /* lightNavBar */);
                final GradientDrawable drawable = new GradientDrawable(
                        GradientDrawable.Orientation.TOP_BOTTOM,
                        new int[] {MINT_COLOR, Color.WHITE});
                setBackground(drawable);
            }));
            addView(createButton("Separate Dark Navigation Bar", () -> {
                switchToSeparateNavBarMode(Color.DKGRAY, false /* lightNavBar */);
                setBackgroundColor(MINT_COLOR);
            }));
            addView(createButton("Separate Light Navigation Bar", () -> {
                switchToSeparateNavBarMode(Color.GRAY, true /* lightNavBar */);
                setBackgroundColor(MINT_COLOR);
            }));

            // Spacer
            addView(new View(getContext()), 0, 40);
        }

        public boolean isFloatingMode() {
            return mMode == InputViewMode.FLOATING_LAYOUT;
        }

        private View createButton(String text, final Runnable onClickCallback) {
            final Button button = new Button(getContext());
            button.setText(text);
            button.setOnClickListener(view -> onClickCallback.run());
            return button;
        }

        private void updateSystemUiFlag(int flags) {
            final int maskFlags = SYSTEM_UI_FLAG_LAYOUT_STABLE
                    | SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                    | SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
            final int visFlags = getSystemUiVisibility();
            setSystemUiVisibility((visFlags & ~maskFlags) | (flags & maskFlags));
        }

        /**
         * Updates the current input view mode to {@link InputViewMode#FLOATING_LAYOUT}.
         */
        private void switchToFloatingMode() {
            mMode = InputViewMode.FLOATING_LAYOUT;

            final int prevFlags = mWindow.getAttributes().flags;

            // This allows us to keep the navigation bar appearance based on the target application,
            // rather than the IME itself.
            mWindow.setFlags(0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);

            updateSystemUiFlag(0);

            // View#onApplyWindowInsets() will not be called if direct or indirect parent View
            // consumes all the insets.  Hence we need to make sure that the bottom padding is
            // cleared here.
            updateBottomPaddingIfNecessary(0);

            // For some reasons, seems that we need to post another requestLayout() when
            // FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS is changed.
            // TODO: Investigate the reason.
            if ((prevFlags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0) {
                post(() -> requestLayout());
            }
        }

        /**
         * Updates the current input view mode to {@link InputViewMode#SYSTEM_OWNED_NAV_BAR_LAYOUT}.
         *
         * @param navBarColor color to be passed to {@link Window#setNavigationBarColor(int)}.
         *                    {@link Color#TRANSPARENT} cannot be used here because it hides the
         *                    color view itself. Consider floating mode for that use case.
         * @param isLightNavBar {@code true} when the navigation bar should be optimized for light
         *                      color
         */
        private void switchToSeparateNavBarMode(int navBarColor, boolean isLightNavBar) {
            mMode = InputViewMode.SYSTEM_OWNED_NAV_BAR_LAYOUT;
            mWindow.setNavigationBarColor(navBarColor);

            // This allows us to use setNavigationBarColor() + SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.
            mWindow.setFlags(FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);

            updateSystemUiFlag(isLightNavBar ? SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR : 0);

            // View#onApplyWindowInsets() will not be called if direct or indirect parent View
            // consumes all the insets.  Hence we need to make sure that the bottom padding is
            // cleared here.
            updateBottomPaddingIfNecessary(0);
        }

        /**
         * Updates the current input view mode to {@link InputViewMode#IME_OWNED_NAV_BAR_LAYOUT}.
         *
         * @param isLightNavBar {@code true} when the navigation bar should be optimized for light
         *                      color
         */
        private void switchToExtendedNavBarMode(boolean isLightNavBar) {
            mMode = InputViewMode.IME_OWNED_NAV_BAR_LAYOUT;

            // This hides the ColorView.
            mWindow.setNavigationBarColor(Color.TRANSPARENT);

            // This allows us to use setNavigationBarColor() + SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.
            mWindow.setFlags(FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);

            updateSystemUiFlag(SYSTEM_UI_FLAG_LAYOUT_STABLE
                    | SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                    | (isLightNavBar ? SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR : 0));
        }
    }
}
