/*
 * Copyright (C) 2015 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.setupwizardlib.util;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Dialog;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Handler;
import androidx.annotation.RequiresPermission;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowManager;

/**
 * A helper class to manage the system navigation bar and status bar. This will add various
 * systemUiVisibility flags to the given Window or View to make them follow the Setup Wizard style.
 *
 * <p>When the useImmersiveMode intent extra is true, a screen in Setup Wizard should hide the
 * system bars using methods from this class. For Lollipop, {@link
 * #hideSystemBars(android.view.Window)} will completely hide the system navigation bar and change
 * the status bar to transparent, and layout the screen contents (usually the illustration) behind
 * it.
 */
public class SystemBarHelper {

  private static final String TAG = "SystemBarHelper";

  @SuppressLint("InlinedApi")
  private static final int DEFAULT_IMMERSIVE_FLAGS =
      View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
          | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
          | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
          | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;

  @SuppressLint("InlinedApi")
  private static final int DIALOG_IMMERSIVE_FLAGS =
      View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;

  /** Needs to be equal to View.STATUS_BAR_DISABLE_BACK */
  private static final int STATUS_BAR_DISABLE_BACK = 0x00400000;

  /**
   * The maximum number of retries when peeking the decor view. When polling for the decor view,
   * waiting it to be installed, set a maximum number of retries.
   */
  private static final int PEEK_DECOR_VIEW_RETRIES = 3;

  /**
   * Hide the navigation bar for a dialog.
   *
   * <p>This will only take effect in versions Lollipop or above. Otherwise this is a no-op.
   */
  public static void hideSystemBars(final Dialog dialog) {
    if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
      final Window window = dialog.getWindow();
      temporarilyDisableDialogFocus(window);
      addVisibilityFlag(window, DIALOG_IMMERSIVE_FLAGS);
      addImmersiveFlagsToDecorView(window, DIALOG_IMMERSIVE_FLAGS);

      // Also set the navigation bar and status bar to transparent color. Note that this
      // doesn't work if android.R.boolean.config_enableTranslucentDecor is false.
      window.setNavigationBarColor(0);
      window.setStatusBarColor(0);
    }
  }

  /**
   * Hide the navigation bar, make the color of the status and navigation bars transparent, and
   * specify {@link View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN} flag so that the content is laid-out
   * behind the transparent status bar. This is commonly used with {@link
   * android.app.Activity#getWindow()} to make the navigation and status bars follow the Setup
   * Wizard style.
   *
   * <p>This will only take effect in versions Lollipop or above. Otherwise this is a no-op.
   */
  public static void hideSystemBars(final Window window) {
    if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
      addVisibilityFlag(window, DEFAULT_IMMERSIVE_FLAGS);
      addImmersiveFlagsToDecorView(window, DEFAULT_IMMERSIVE_FLAGS);

      // Also set the navigation bar and status bar to transparent color. Note that this
      // doesn't work if android.R.boolean.config_enableTranslucentDecor is false.
      window.setNavigationBarColor(0);
      window.setStatusBarColor(0);
    }
  }

  /**
   * Revert the actions of hideSystemBars. Note that this will remove the system UI visibility flags
   * regardless of whether it is originally present. You should also manually reset the navigation
   * bar and status bar colors, as this method doesn't know what value to revert it to.
   */
  public static void showSystemBars(final Dialog dialog, final Context context) {
    showSystemBars(dialog.getWindow(), context);
  }

  /**
   * Revert the actions of hideSystemBars. Note that this will remove the system UI visibility flags
   * regardless of whether it is originally present. You should also manually reset the navigation
   * bar and status bar colors, as this method doesn't know what value to revert it to.
   */
  public static void showSystemBars(final Window window, final Context context) {
    if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
      removeVisibilityFlag(window, DEFAULT_IMMERSIVE_FLAGS);
      removeImmersiveFlagsFromDecorView(window, DEFAULT_IMMERSIVE_FLAGS);

      if (context != null) {
        //noinspection AndroidLintInlinedApi
        final TypedArray typedArray =
            context.obtainStyledAttributes(
                new int[] {android.R.attr.statusBarColor, android.R.attr.navigationBarColor});
        final int statusBarColor = typedArray.getColor(0, 0);
        final int navigationBarColor = typedArray.getColor(1, 0);
        window.setStatusBarColor(statusBarColor);
        window.setNavigationBarColor(navigationBarColor);
        typedArray.recycle();
      }
    }
  }

  /** Convenience method to add a visibility flag in addition to the existing ones. */
  public static void addVisibilityFlag(final View view, final int flag) {
    if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) {
      final int vis = view.getSystemUiVisibility();
      view.setSystemUiVisibility(vis | flag);
    }
  }

  /** Convenience method to add a visibility flag in addition to the existing ones. */
  public static void addVisibilityFlag(final Window window, final int flag) {
    if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) {
      WindowManager.LayoutParams attrs = window.getAttributes();
      attrs.systemUiVisibility |= flag;
      window.setAttributes(attrs);
    }
  }

  /**
   * Convenience method to remove a visibility flag from the view, leaving other flags that are not
   * specified intact.
   */
  public static void removeVisibilityFlag(final View view, final int flag) {
    if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) {
      final int vis = view.getSystemUiVisibility();
      view.setSystemUiVisibility(vis & ~flag);
    }
  }

  /**
   * Convenience method to remove a visibility flag from the window, leaving other flags that are
   * not specified intact.
   */
  public static void removeVisibilityFlag(final Window window, final int flag) {
    if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) {
      WindowManager.LayoutParams attrs = window.getAttributes();
      attrs.systemUiVisibility &= ~flag;
      window.setAttributes(attrs);
    }
  }

  /**
   * Sets whether the back button on the software navigation bar is visible. This only works if you
   * have the STATUS_BAR permission. Otherwise framework will filter out this flag and this method
   * call will not have any effect.
   *
   * <p>IMPORTANT: Do not assume that users have no way to go back when the back button is hidden.
   * Many devices have physical back buttons, and accessibility services like TalkBack may have
   * gestures mapped to back. Please use onBackPressed, onKeyDown, or other similar ways to make
   * sure back button events are still handled (or ignored) properly.
   */
  @RequiresPermission("android.permission.STATUS_BAR")
  public static void setBackButtonVisible(final Window window, final boolean visible) {
    if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) {
      if (visible) {
        removeVisibilityFlag(window, STATUS_BAR_DISABLE_BACK);
        removeImmersiveFlagsFromDecorView(window, STATUS_BAR_DISABLE_BACK);
      } else {
        addVisibilityFlag(window, STATUS_BAR_DISABLE_BACK);
        addImmersiveFlagsToDecorView(window, STATUS_BAR_DISABLE_BACK);
      }
    }
  }

  /**
   * Set a view to be resized when the keyboard is shown. This will set the bottom margin of the
   * view to be immediately above the keyboard, and assumes that the view sits immediately above the
   * navigation bar.
   *
   * <p>Note that you must set {@link android.R.attr#windowSoftInputMode} to {@code adjustResize}
   * for this class to work. Otherwise window insets are not dispatched and this method will have no
   * effect.
   *
   * <p>This will only take effect in versions Lollipop or above. Otherwise this is a no-op.
   *
   * @param view The view to be resized when the keyboard is shown.
   */
  public static void setImeInsetView(final View view) {
    if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
      view.setOnApplyWindowInsetsListener(new WindowInsetsListener());
    }
  }

  /**
   * Add the specified immersive flags to the decor view of the window, because {@link
   * View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN} only takes effect when it is added to a view instead of
   * the window.
   */
  @TargetApi(VERSION_CODES.HONEYCOMB)
  private static void addImmersiveFlagsToDecorView(final Window window, final int vis) {
    getDecorView(
        window,
        new OnDecorViewInstalledListener() {
          @Override
          public void onDecorViewInstalled(View decorView) {
            addVisibilityFlag(decorView, vis);
          }
        });
  }

  @TargetApi(VERSION_CODES.HONEYCOMB)
  private static void removeImmersiveFlagsFromDecorView(final Window window, final int vis) {
    getDecorView(
        window,
        new OnDecorViewInstalledListener() {
          @Override
          public void onDecorViewInstalled(View decorView) {
            removeVisibilityFlag(decorView, vis);
          }
        });
  }

  private static void getDecorView(Window window, OnDecorViewInstalledListener callback) {
    new DecorViewFinder().getDecorView(window, callback, PEEK_DECOR_VIEW_RETRIES);
  }

  private static class DecorViewFinder {

    private final Handler handler = new Handler();
    private Window window;
    private int retries;
    private OnDecorViewInstalledListener callback;

    private final Runnable checkDecorViewRunnable =
        new Runnable() {
          @Override
          public void run() {
            // Use peekDecorView instead of getDecorView so that clients can still set window
            // features after calling this method.
            final View decorView = window.peekDecorView();
            if (decorView != null) {
              callback.onDecorViewInstalled(decorView);
            } else {
              retries--;
              if (retries >= 0) {
                // If the decor view is not installed yet, try again in the next loop.
                handler.post(checkDecorViewRunnable);
              } else {
                Log.w(TAG, "Cannot get decor view of window: " + window);
              }
            }
          }
        };

    public void getDecorView(Window window, OnDecorViewInstalledListener callback, int retries) {
      this.window = window;
      this.retries = retries;
      this.callback = callback;
      checkDecorViewRunnable.run();
    }
  }

  private interface OnDecorViewInstalledListener {

    void onDecorViewInstalled(View decorView);
  }

  /**
   * Apply a hack to temporarily set the window to not focusable, so that the navigation bar will
   * not show up during the transition.
   */
  private static void temporarilyDisableDialogFocus(final Window window) {
    window.setFlags(
        WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
        WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
    // Add the SOFT_INPUT_IS_FORWARD_NAVIGATION_FLAG. This is normally done by the system when
    // FLAG_NOT_FOCUSABLE is not set. Setting this flag allows IME to be shown automatically
    // if the dialog has editable text fields.
    window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION);
    new Handler()
        .post(
            new Runnable() {
              @Override
              public void run() {
                window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
              }
            });
  }

  @TargetApi(VERSION_CODES.LOLLIPOP)
  private static class WindowInsetsListener implements View.OnApplyWindowInsetsListener {
    private int bottomOffset;
    private boolean hasCalculatedBottomOffset = false;

    @Override
    public WindowInsets onApplyWindowInsets(View view, WindowInsets insets) {
      if (!hasCalculatedBottomOffset) {
        bottomOffset = getBottomDistance(view);
        hasCalculatedBottomOffset = true;
      }

      int bottomInset = insets.getSystemWindowInsetBottom();

      final int bottomMargin = Math.max(insets.getSystemWindowInsetBottom() - bottomOffset, 0);

      final ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
      // Check that we have enough space to apply the bottom margins before applying it.
      // Otherwise the framework may think that the view is empty and exclude it from layout.
      if (bottomMargin < lp.bottomMargin + view.getHeight()) {
        lp.setMargins(lp.leftMargin, lp.topMargin, lp.rightMargin, bottomMargin);
        view.setLayoutParams(lp);
        bottomInset = 0;
      }

      return insets.replaceSystemWindowInsets(
          insets.getSystemWindowInsetLeft(),
          insets.getSystemWindowInsetTop(),
          insets.getSystemWindowInsetRight(),
          bottomInset);
    }
  }

  private static int getBottomDistance(View view) {
    int[] coords = new int[2];
    view.getLocationInWindow(coords);
    return view.getRootView().getHeight() - coords[1] - view.getHeight();
  }
}
