/*
 * 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 com.android.systemui.car.window;

import static android.view.WindowInsets.Type.navigationBars;
import static android.view.WindowInsets.Type.statusBars;

import android.annotation.Nullable;
import android.util.Log;
import android.view.WindowInsets;
import android.view.WindowInsets.Side.InsetsSide;
import android.view.WindowInsets.Type.InsetsType;
import android.view.WindowInsetsController;

import androidx.annotation.VisibleForTesting;

import com.android.systemui.dagger.SysUISingleton;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

import javax.inject.Inject;

/**
 * This controller is responsible for the following:
 * <p><ul>
 * <li>Holds the global state for SystemUIOverlayWindow.
 * <li>Allows {@link SystemUIOverlayWindowManager} to register {@link OverlayViewMediator}(s).
 * <li>Enables {@link OverlayViewController)(s) to reveal/conceal themselves while respecting the
 * global state of SystemUIOverlayWindow.
 * </ul>
 */
@SysUISingleton
public class OverlayViewGlobalStateController {
    private static final boolean DEBUG = false;
    private static final String TAG = OverlayViewGlobalStateController.class.getSimpleName();
    private static final int UNKNOWN_Z_ORDER = -1;
    private final SystemUIOverlayWindowController mSystemUIOverlayWindowController;
    private final WindowInsetsController mWindowInsetsController;
    @VisibleForTesting
    Map<OverlayViewController, Integer> mZOrderMap;
    @VisibleForTesting
    SortedMap<Integer, OverlayViewController> mZOrderVisibleSortedMap;
    @VisibleForTesting
    Set<OverlayViewController> mViewsHiddenForOcclusion;
    @VisibleForTesting
    OverlayViewController mHighestZOrder;
    private boolean mIsOccluded;

    @Inject
    public OverlayViewGlobalStateController(
            SystemUIOverlayWindowController systemUIOverlayWindowController) {
        mSystemUIOverlayWindowController = systemUIOverlayWindowController;
        mSystemUIOverlayWindowController.attach();
        mSystemUIOverlayWindowController.registerOutsideTouchListener((v, event) -> {
            if (mHighestZOrder != null) {
                mHighestZOrder.onTouchEvent(v, event);
            }
        });
        mWindowInsetsController =
                mSystemUIOverlayWindowController.getBaseLayout().getWindowInsetsController();
        mZOrderMap = new HashMap<>();
        mZOrderVisibleSortedMap = new TreeMap<>();
        mViewsHiddenForOcclusion = new HashSet<>();
    }

    /**
     * Register {@link OverlayViewMediator} to use in SystemUIOverlayWindow.
     */
    public void registerMediator(OverlayViewMediator overlayViewMediator) {
        Log.d(TAG, "Registering content mediator: " + overlayViewMediator.getClass().getName());

        overlayViewMediator.registerListeners();
        overlayViewMediator.setUpOverlayContentViewControllers();
    }

    /**
     * Show content in Overlay Window using {@link OverlayPanelViewController}.
     *
     * This calls {@link OverlayViewGlobalStateController#showView(OverlayViewController, Runnable)}
     * where the runnable is nullified since the actual showing of the panel is handled by the
     * controller itself.
     */
    public void showView(OverlayPanelViewController panelViewController) {
        showView(panelViewController, /* show= */ null);
    }

    /**
     * Show content in Overlay Window using {@link OverlayViewController}.
     */
    public void showView(OverlayViewController viewController, @Nullable Runnable show) {
        debugLog();
        if (mIsOccluded && !viewController.shouldShowWhenOccluded()) {
            mViewsHiddenForOcclusion.add(viewController);
            return;
        }
        if (mZOrderVisibleSortedMap.isEmpty()) {
            setWindowVisible(true);
        }

        if (!(viewController instanceof OverlayPanelViewController)) {
            inflateView(viewController);
        }

        if (show != null) {
            show.run();
        }

        updateInternalsWhenShowingView(viewController);
        refreshUseStableInsets();
        refreshInsetsToFit();
        refreshWindowFocus();
        refreshSystemBarVisibility();
        refreshStatusBarVisibility();
        refreshRotaryFocusIfNeeded();

        Log.d(TAG, "Content shown: " + viewController.getClass().getName());
        debugLog();
    }

    private void updateInternalsWhenShowingView(OverlayViewController viewController) {
        int zOrder;
        if (mZOrderMap.containsKey(viewController)) {
            zOrder = mZOrderMap.get(viewController);
        } else {
            zOrder = mSystemUIOverlayWindowController.getBaseLayout().indexOfChild(
                    viewController.getLayout());
            mZOrderMap.put(viewController, zOrder);
        }

        mZOrderVisibleSortedMap.put(zOrder, viewController);

        refreshHighestZOrderWhenShowingView(viewController);
    }

    private void refreshHighestZOrderWhenShowingView(OverlayViewController viewController) {
        if (mZOrderMap.getOrDefault(mHighestZOrder, UNKNOWN_Z_ORDER) < mZOrderMap.get(
                viewController)) {
            mHighestZOrder = viewController;
        }
    }

    /**
     * Hide content in Overlay Window using {@link OverlayPanelViewController}.
     *
     * This calls {@link OverlayViewGlobalStateController#hideView(OverlayViewController, Runnable)}
     * where the runnable is nullified since the actual hiding of the panel is handled by the
     * controller itself.
     */
    public void hideView(OverlayPanelViewController panelViewController) {
        hideView(panelViewController, /* hide= */ null);
    }

    /**
     * Hide content in Overlay Window using {@link OverlayViewController}.
     */
    public void hideView(OverlayViewController viewController, @Nullable Runnable hide) {
        debugLog();
        if (mIsOccluded && mViewsHiddenForOcclusion.contains(viewController)) {
            mViewsHiddenForOcclusion.remove(viewController);
            return;
        }
        if (!viewController.isInflated()) {
            Log.d(TAG, "Content cannot be hidden since it isn't inflated: "
                    + viewController.getClass().getName());
            return;
        }
        if (!mZOrderMap.containsKey(viewController)) {
            Log.d(TAG, "Content cannot be hidden since it has never been shown: "
                    + viewController.getClass().getName());
            return;
        }
        if (!mZOrderVisibleSortedMap.containsKey(mZOrderMap.get(viewController))) {
            Log.d(TAG, "Content cannot be hidden since it isn't currently shown: "
                    + viewController.getClass().getName());
            return;
        }

        if (hide != null) {
            hide.run();
        }

        mZOrderVisibleSortedMap.remove(mZOrderMap.get(viewController));
        refreshHighestZOrderWhenHidingView(viewController);
        refreshUseStableInsets();
        refreshInsetsToFit();
        refreshWindowFocus();
        refreshSystemBarVisibility();
        refreshStatusBarVisibility();
        refreshRotaryFocusIfNeeded();

        if (mZOrderVisibleSortedMap.isEmpty()) {
            setWindowVisible(false);
        }

        Log.d(TAG, "Content hidden: " + viewController.getClass().getName());
        debugLog();
    }

    private void refreshHighestZOrderWhenHidingView(OverlayViewController viewController) {
        if (mZOrderVisibleSortedMap.isEmpty()) {
            mHighestZOrder = null;
            return;
        }
        if (!mHighestZOrder.equals(viewController)) {
            return;
        }

        mHighestZOrder = mZOrderVisibleSortedMap.get(mZOrderVisibleSortedMap.lastKey());
    }

    private void refreshSystemBarVisibility() {
        if (mZOrderVisibleSortedMap.isEmpty()) {
            mWindowInsetsController.show(navigationBars());
            return;
        }

        // Do not hide navigation bar insets if the window is not focusable.
        if (mHighestZOrder.shouldFocusWindow() && !mHighestZOrder.shouldShowNavigationBarInsets()) {
            mWindowInsetsController.hide(navigationBars());
        } else {
            mWindowInsetsController.show(navigationBars());
        }
    }

    private void refreshStatusBarVisibility() {
        if (mZOrderVisibleSortedMap.isEmpty()) {
            mWindowInsetsController.show(statusBars());
            return;
        }

        // Do not hide status bar insets if the window is not focusable.
        if (mHighestZOrder.shouldFocusWindow() && !mHighestZOrder.shouldShowStatusBarInsets()) {
            mWindowInsetsController.hide(statusBars());
        } else {
            mWindowInsetsController.show(statusBars());
        }
    }

    private void refreshWindowFocus() {
        setWindowFocusable(mHighestZOrder == null ? false : mHighestZOrder.shouldFocusWindow());
    }

    private void refreshUseStableInsets() {
        mSystemUIOverlayWindowController.setUsingStableInsets(
                mHighestZOrder == null ? false : mHighestZOrder.shouldUseStableInsets());
    }

    /**
     * Refreshes the insets to fit (or honor) either by {@link InsetsType} or {@link InsetsSide}.
     *
     * By default, the insets to fit are defined by the {@link InsetsType}. But if an
     * {@link OverlayViewController} overrides {@link OverlayViewController#getInsetSidesToFit()} to
     * return an {@link InsetsSide}, then that takes precedence over {@link InsetsType}.
     */
    private void refreshInsetsToFit() {
        if (mZOrderVisibleSortedMap.isEmpty()) {
            setFitInsetsTypes(statusBars());
        } else {
            if (mHighestZOrder.getInsetSidesToFit() != OverlayViewController.INVALID_INSET_SIDE) {
                // First fit all system bar insets as setFitInsetsSide defines which sides of system
                // bar insets to actually honor.
                setFitInsetsTypes(WindowInsets.Type.systemBars());
                setFitInsetsSides(mHighestZOrder.getInsetSidesToFit());
            } else {
                setFitInsetsTypes(mHighestZOrder.getInsetTypesToFit());
            }
        }
    }

    private void refreshRotaryFocusIfNeeded() {
        for (OverlayViewController controller : mZOrderVisibleSortedMap.values()) {
            boolean isTop = Objects.equals(controller, mHighestZOrder);
            controller.setAllowRotaryFocus(isTop);
        }

        if (!mZOrderVisibleSortedMap.isEmpty()) {
            mHighestZOrder.refreshRotaryFocusIfNeeded();
        }
    }

    /** Returns {@code true} is the window is visible. */
    public boolean isWindowVisible() {
        return mSystemUIOverlayWindowController.isWindowVisible();
    }

    private void setWindowVisible(boolean visible) {
        mSystemUIOverlayWindowController.setWindowVisible(visible);
    }

    /** Sets the insets to fit based on the {@link InsetsType} */
    private void setFitInsetsTypes(@InsetsType int types) {
        mSystemUIOverlayWindowController.setFitInsetsTypes(types);
    }

    /** Sets the insets to fit based on the {@link InsetsSide} */
    private void setFitInsetsSides(@InsetsSide int sides) {
        mSystemUIOverlayWindowController.setFitInsetsSides(sides);
    }

    /**
     * Sets the {@link android.view.WindowManager.LayoutParams#FLAG_ALT_FOCUSABLE_IM} flag of the
     * sysui overlay window.
     */
    public void setWindowNeedsInput(boolean needsInput) {
        mSystemUIOverlayWindowController.setWindowNeedsInput(needsInput);
    }

    /** Returns {@code true} if the window is focusable. */
    public boolean isWindowFocusable() {
        return mSystemUIOverlayWindowController.isWindowFocusable();
    }

    /** Sets the focusable flag of the sysui overlawy window. */
    public void setWindowFocusable(boolean focusable) {
        mSystemUIOverlayWindowController.setWindowFocusable(focusable);
    }

    /** Inflates the view controlled by the given view controller. */
    public void inflateView(OverlayViewController viewController) {
        if (!viewController.isInflated()) {
            viewController.inflate(mSystemUIOverlayWindowController.getBaseLayout());
        }
    }

    /**
     * Return {@code true} if OverlayWindow is in a state where HUNs should be displayed above it.
     */
    public boolean shouldShowHUN() {
        return mZOrderVisibleSortedMap.isEmpty() || mHighestZOrder.shouldShowHUN();
    }

    /**
     * Set the OverlayViewWindow to be in occluded or unoccluded state. When OverlayViewWindow is
     * occluded, all views mounted to it that are not configured to be shown during occlusion will
     * be hidden.
     */
    public void setOccluded(boolean occluded) {
        if (occluded) {
            // Hide views before setting mIsOccluded to true so the regular hideView logic is used,
            // not the one used during occlusion.
            hideViewsForOcclusion();
            mIsOccluded = true;
        } else {
            mIsOccluded = false;
            // show views after setting mIsOccluded to false so the regular showView logic is used,
            // not the one used during occlusion.
            showViewsHiddenForOcclusion();
        }
    }

    private void hideViewsForOcclusion() {
        HashSet<OverlayViewController> viewsCurrentlyShowing = new HashSet<>(
                mZOrderVisibleSortedMap.values());
        viewsCurrentlyShowing.forEach(overlayController -> {
            if (!overlayController.shouldShowWhenOccluded()) {
                hideView(overlayController, overlayController::hideInternal);
                mViewsHiddenForOcclusion.add(overlayController);
            }
        });
    }

    private void showViewsHiddenForOcclusion() {
        mViewsHiddenForOcclusion.forEach(overlayViewController -> {
            showView(overlayViewController, overlayViewController::showInternal);
        });
        mViewsHiddenForOcclusion.clear();
    }

    private void debugLog() {
        if (!DEBUG) {
            return;
        }

        Log.d(TAG, "mHighestZOrder: " + mHighestZOrder);
        Log.d(TAG, "mZOrderVisibleSortedMap.size(): " + mZOrderVisibleSortedMap.size());
        Log.d(TAG, "mZOrderVisibleSortedMap: " + mZOrderVisibleSortedMap);
        Log.d(TAG, "mZOrderMap.size(): " + mZOrderMap.size());
        Log.d(TAG, "mZOrderMap: " + mZOrderMap);
        Log.d(TAG, "mIsOccluded: " + mIsOccluded);
        Log.d(TAG, "mViewsHiddenForOcclusion: " + mViewsHiddenForOcclusion);
        Log.d(TAG, "mViewsHiddenForOcclusion.size(): " + mViewsHiddenForOcclusion.size());
    }
}
