/*
 * Copyright (C) 2016 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.documentsui;

import static android.view.View.GONE;
import static android.view.View.VISIBLE;

import static com.android.documentsui.base.SharedMinimal.VERBOSE;
import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled;
import static com.android.documentsui.util.FlagUtils.isZipNgFlagEnabled;
import static com.android.documentsui.util.Material3Config.getRes;

import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Outline;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.ViewParent;
import android.view.Window;
import android.view.WindowManager;

import androidx.annotation.ColorRes;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;

import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.State;
import com.android.documentsui.base.UserId;
import com.android.documentsui.dirlist.AnimationView;
import com.android.documentsui.util.VersionUtils;
import com.android.modules.utils.build.SdkLevel;

import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.CollapsingToolbarLayout;

import java.util.function.IntConsumer;

/** A facade over the portions of the app and drawer toolbars. */
public class NavigationViewManager implements AppBarLayout.OnOffsetChangedListener {

    private static final String TAG = "NavigationViewManager";

    private final DrawerController mDrawer;
    private final Toolbar mToolbar;
    private final BaseActivity mActivity;
    private final View mHeader;
    private final State mState;
    private final NavigationViewManager.Environment mEnv;
    private final Breadcrumb mBreadcrumb;
    private final ProfileTabs mProfileTabs;
    private final View mSearchBarView;
    private final CollapsingToolbarLayout mCollapsingBarLayout;
    private final Drawable mDefaultActionBarBackground;
    private final ViewOutlineProvider mDefaultOutlineProvider;
    private final ViewOutlineProvider mSearchBarOutlineProvider;
    private final boolean mShowSearchBar;
    private final ConfigStore mConfigStore;
    private boolean mIsActionModeActivated = false;
    @ColorRes private int mDefaultStatusBarColorResId;

    // The offset of the app bar layout, it can only be 0 or negative, 0 means it's fully expanded,
    // negative value means it's collapsed.
    private int mCurrentVerticalOffset = 0;

    public NavigationViewManager(
            BaseActivity activity,
            DrawerController drawer,
            State state,
            NavigationViewManager.Environment env,
            Breadcrumb breadcrumb,
            View tabLayoutContainer,
            UserIdManager userIdManager,
            ConfigStore configStore) {
        this(
                activity,
                drawer,
                state,
                env,
                breadcrumb,
                tabLayoutContainer,
                userIdManager,
                null,
                configStore);
    }

    public NavigationViewManager(
            BaseActivity activity,
            DrawerController drawer,
            State state,
            NavigationViewManager.Environment env,
            Breadcrumb breadcrumb,
            View tabLayoutContainer,
            UserManagerState userManagerState,
            ConfigStore configStore) {
        this(
                activity,
                drawer,
                state,
                env,
                breadcrumb,
                tabLayoutContainer,
                null,
                userManagerState,
                configStore);
    }

    public NavigationViewManager(
            BaseActivity activity,
            DrawerController drawer,
            State state,
            NavigationViewManager.Environment env,
            Breadcrumb breadcrumb,
            View tabLayoutContainer,
            UserIdManager userIdManager,
            UserManagerState userManagerState,
            ConfigStore configStore) {

        mActivity = activity;
        mToolbar = activity.findViewById(getRes(R.id.toolbar));
        mHeader = activity.findViewById(getRes(R.id.directory_header));
        mDrawer = drawer;
        mState = state;
        mEnv = env;
        mBreadcrumb = breadcrumb;
        mBreadcrumb.setup(
                env,
                state,
                this::onNavigationItemSelected,
                isUseMaterial3FlagEnabled()
                        ? activity.findViewById(getRes(R.id.breadcrumb_top_divider))
                        : null);
        mConfigStore = configStore;
        mProfileTabs =
                getProfileTabs(tabLayoutContainer, userIdManager, userManagerState, activity);

        mToolbar.setNavigationOnClickListener(
                new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        onNavigationIconClicked();
                    }
                });
        mSearchBarView = activity.findViewById(getRes(R.id.searchbar_title));
        mCollapsingBarLayout = activity.findViewById(getRes(R.id.collapsing_toolbar));
        mDefaultActionBarBackground = mToolbar.getBackground();
        mDefaultOutlineProvider = mToolbar.getOutlineProvider();
        mShowSearchBar = isUseMaterial3FlagEnabled() ? false : activity.getResources().getBoolean(
                R.bool.show_search_bar);

        final int[] styledAttrs = {android.R.attr.statusBarColor};
        TypedArray a = mActivity.obtainStyledAttributes(styledAttrs);
        mDefaultStatusBarColorResId = a.getResourceId(0, -1);
        if (mDefaultStatusBarColorResId == -1) {
            Log.w(TAG, "Retrieve statusBarColorResId from theme failed, assigned default");
            mDefaultStatusBarColorResId = getRes(R.color.app_background_color);
        }
        a.recycle();

        final Resources resources = mToolbar.getResources();
        final int radius = resources.getDimensionPixelSize(getRes(R.dimen.search_bar_radius));
        final int marginStart =
                resources.getDimensionPixelSize(getRes(R.dimen.search_bar_background_margin_start));
        final int marginEnd =
                resources.getDimensionPixelSize(getRes(R.dimen.search_bar_background_margin_end));
        mSearchBarOutlineProvider = new ViewOutlineProvider() {
            @Override
            public void getOutline(View view, Outline outline) {
                outline.setRoundRect(marginStart, 0,
                        view.getWidth() - marginEnd, view.getHeight(), radius);
            }
        };

        // In CollapsingToolbarLayout, when the file list is scrolled up, the content inside
        // CollapsingToolbarLayout will be collapsed (i.e. to be pushed up out of the screen
        // boundary). Now if we use Shift + Tab to move the focus from the top app bar, the
        // content inside CollapsingToolbarLayout will be focused but not visible because the
        // layout is collapsed, in this case we need to expand the AppBarLayout (which is the
        // parent of CollapsingToolbarLayout because the collapse/expand happens on this level)
        // to make the focused view visible.
        if (isUseMaterial3FlagEnabled() && mCollapsingBarLayout != null) {
            View collapsingContent = mCollapsingBarLayout.findViewById(R.id.collapsing_content);
            collapsingContent.getViewTreeObserver()
                    .addOnGlobalFocusChangeListener(
                            (oldFocus, newFocus) -> {
                                onChildViewFocused(collapsingContent, newFocus);
                            });
        }
    }

    /** Called when a child view of the parent view is focused. */
    public void onChildViewFocused(View parentView, View childView) {
        // Only expand when the child view get focused and the layout is in collapsed
        // state (offset < 0).
        if (mCurrentVerticalOffset < 0 && childView != null
                && isDescendantOf(parentView, childView)) {
            // app_bar is the parent of CollapsingToolbarLayout, expand happens on this level.
            AppBarLayout appBarLayout = mActivity.findViewById(R.id.app_bar);
            appBarLayout.setExpanded(true, true);
        }
    }

    /** Returns true if the child view is a descendant of the parent view. */
    private boolean isDescendantOf(View parent, View child) {
        ViewParent current = child.getParent();
        while (current != null) {
            if (current == parent) {
                return true;
            }
            current = current.getParent();
        }
        return false;
    }

    private ProfileTabs getProfileTabs(View tabLayoutContainer, UserIdManager userIdManager,
            UserManagerState userManagerState, BaseActivity activity) {
        return mConfigStore.isPrivateSpaceInDocsUIEnabled()
                ? new ProfileTabs(tabLayoutContainer, mState, userManagerState, mEnv, activity,
                mConfigStore)
                : new ProfileTabs(tabLayoutContainer, mState, userIdManager, mEnv, activity,
                        mConfigStore);
    }

    @Override
    public void onOffsetChanged(AppBarLayout appBarLayout, int offset) {
        mCurrentVerticalOffset = offset;
        if (!VersionUtils.isAtLeastS()) {
            return;
        }

        // For S+ Only. Change toolbar color dynamically based on scroll offset.
        // Usually this can be done in xml using app:contentScrim and app:statusBarScrim, however
        // in our case since we also put directory_header.xml inside the CollapsingToolbarLayout,
        // the scrim will also cover the directory header. Long term need to think about how to
        // move directory_header out of the AppBarLayout.

        Window window = mActivity.getWindow();
        View actionBar =
                window.getDecorView().findViewById(androidx.appcompat.R.id.action_mode_bar);
        int dynamicHeaderColor =
                ContextCompat.getColor(
                        mActivity,
                        offset == 0
                                ? mDefaultStatusBarColorResId
                                : getRes(R.color.color_surface_header));
        if (actionBar != null) {
            // Action bar needs to be updated separately for selection mode.
            actionBar.setBackgroundColor(dynamicHeaderColor);
        }

        window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
        window.setStatusBarColor(dynamicHeaderColor);
        if (shouldShowSearchBar()) {
            // Do not change search bar background.
        } else {
            mToolbar.setBackground(new ColorDrawable(dynamicHeaderColor));
        }
    }

    public void setSearchBarClickListener(View.OnClickListener listener) {
        mSearchBarView.setOnClickListener(listener);
        if (SdkLevel.isAtLeastU()) {
            try {
                mSearchBarView.setHandwritingDelegatorCallback(
                        () -> listener.onClick(mSearchBarView));
            } catch (LinkageError e) {
                // Running on a device with an older build of Android U
                // TODO(b/274154553): Remove try/catch block after Android U Beta 1 is released
            }
        }
    }

    public ProfileTabsAddons getProfileTabsAddons() {
        return mProfileTabs;
    }

    /**
     * Sets a listener to the profile tabs.
     */
    public void setProfileTabsListener(ProfileTabs.Listener listener) {
        mProfileTabs.setListener(listener);
    }

    private void onNavigationIconClicked() {
        if (mDrawer.isPresent()) {
            mDrawer.setOpen(true);
        }
    }

    void onNavigationItemSelected(int position) {
        boolean changed = false;
        while (mState.stack.size() > position + 1) {
            changed = true;
            mState.stack.pop();
        }
        if (changed) {
            mEnv.refreshCurrentRootAndDirectory(AnimationView.ANIM_LEAVE);
        }
    }

    public UserId getSelectedUser() {
        return mProfileTabs.getSelectedUser();
    }

    public void setActionModeActivated(boolean actionModeActivated) {
        mIsActionModeActivated = actionModeActivated;
        update();
    }

    public void update() {
        updateScrollFlag();
        updateToolbar();
        mProfileTabs.updateView();

        // When the search view is expanded, most of the toolbar is hidden. Except when docked
        // search is enabled, in which case the toolbar is shown as normal.
        boolean showDockedSearch =
                mActivity.getResources().getBoolean(getRes(R.bool.show_docked_search));
        if (mEnv.isSearchExpanded() && !(isUseMaterial3FlagEnabled() && showDockedSearch)) {
            mToolbar.setTitle(null);
            mBreadcrumb.show(false);
            return;
        }

        mDrawer.setTitle(mEnv.getDrawerTitle());

        // Show burger menu on toolbar unless `use_material3` flag is on and the nav rail exists
        // (the burger menu will show on the nav rail instead).
        boolean showBurgerMenuOnToolbar =
                !isUseMaterial3FlagEnabled()
                        || mActivity.findViewById(getRes(R.id.nav_rail_container_roots)) == null;
        if (showBurgerMenuOnToolbar) {
            mToolbar.setNavigationIcon(getActionBarIcon());
            mToolbar.setNavigationContentDescription(getRes(R.string.drawer_open));
        } else {
            mToolbar.setNavigationIcon(null);
            mToolbar.setNavigationContentDescription(null);
        }

        if (shouldShowSearchBar()) {
            mBreadcrumb.show(false);
            mToolbar.setTitle(null);
            mSearchBarView.setVisibility(VISIBLE);
            return;
        }

        mSearchBarView.setVisibility(GONE);
        String title =
                mState.stack.size() <= 1 ? mEnv.getCurrentRoot().title : mState.stack.getTitle();
        if (VERBOSE) Log.v(TAG, "New toolbar title is: " + title);
        mToolbar.setTitle(title);
        mBreadcrumb.show(true);
        mBreadcrumb.postUpdate();
    }

    private void updateScrollFlag() {
        if (mCollapsingBarLayout == null) {
            return;
        }

        AppBarLayout.LayoutParams lp =
                (AppBarLayout.LayoutParams) mCollapsingBarLayout.getLayoutParams();
        lp.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL
                | AppBarLayout.LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED);
        mCollapsingBarLayout.setLayoutParams(lp);
    }

    private void updateToolbar() {
        // Hide or show the "Read-only" label.
        if (isZipNgFlagEnabled()) {
            final View label = mToolbar.findViewById(getRes(R.id.read_only_label));
            if (label != null) {
                final DocumentInfo dir = mActivity.getCurrentDirectory();
                label.setVisibility(dir != null && dir.isInArchive() ? VISIBLE : GONE);
            }
        }

        if (mCollapsingBarLayout == null) {
            // Tablet mode does not use CollapsingBarLayout
            // (res/layout-sw720dp/directory_app_bar.xml or res/layout/fixed_layout.xml)
            if (shouldShowSearchBar()) {
                mToolbar.setBackgroundResource(getRes(R.drawable.search_bar_background));
                mToolbar.setOutlineProvider(mSearchBarOutlineProvider);
            } else {
                mToolbar.setBackground(mDefaultActionBarBackground);
                mToolbar.setOutlineProvider(null);
            }
            return;
        }

        CollapsingToolbarLayout.LayoutParams toolbarLayoutParams =
                (CollapsingToolbarLayout.LayoutParams) mToolbar.getLayoutParams();

        int headerTopOffset = 0;
        if (shouldShowSearchBar() && !mIsActionModeActivated) {
            mToolbar.setBackgroundResource(getRes(R.drawable.search_bar_background));
            mToolbar.setOutlineProvider(mSearchBarOutlineProvider);
            int searchBarMargin =
                    mToolbar.getResources()
                            .getDimensionPixelSize(getRes(R.dimen.search_bar_margin));
            toolbarLayoutParams.setMargins(searchBarMargin, searchBarMargin, searchBarMargin,
                    searchBarMargin);
            mToolbar.setLayoutParams(toolbarLayoutParams);
            mToolbar.setElevation(
                    mToolbar.getResources()
                            .getDimensionPixelSize(getRes(R.dimen.search_bar_elevation)));
            headerTopOffset = toolbarLayoutParams.height + searchBarMargin * 2;
        } else {
            mToolbar.setBackground(mDefaultActionBarBackground);
            mToolbar.setOutlineProvider(mDefaultOutlineProvider);
            int actionBarMargin =
                    mToolbar.getResources()
                            .getDimensionPixelSize(getRes(R.dimen.action_bar_margin));
            toolbarLayoutParams.setMargins(0, 0, 0, /* bottom= */ actionBarMargin);
            mToolbar.setLayoutParams(toolbarLayoutParams);
            mToolbar.setElevation(
                    mToolbar.getResources()
                            .getDimensionPixelSize(getRes(R.dimen.action_bar_elevation)));
            headerTopOffset = toolbarLayoutParams.height + actionBarMargin;
        }

        if (!mIsActionModeActivated) {
            // This could be either FrameLayout.LayoutParams (when use_material3 flag is OFF) or
            // LinearLayout.LayoutParams (when use_material3 flag is ON), so use the common parent
            // class instead to make it work for both scenarios.
            ViewGroup.MarginLayoutParams headerLayoutParams =
                    (ViewGroup.MarginLayoutParams) mHeader.getLayoutParams();
            headerLayoutParams.setMargins(0, /* top= */ headerTopOffset, 0, 0);
            mHeader.setLayoutParams(headerLayoutParams);
        }
    }

    private boolean shouldShowSearchBar() {
        return mState.stack.isRecents() && !mEnv.isSearchExpanded() && mShowSearchBar;
    }

    // Hamburger if drawer is present, else sad nullness.
    private @Nullable
    Drawable getActionBarIcon() {
        if (mDrawer.isPresent()) {
            return mToolbar.getContext().getDrawable(getRes(R.drawable.ic_hamburger));
        } else {
            return null;
        }
    }

    void revealRootsDrawer(boolean open) {
        mDrawer.setOpen(open);
    }

    interface Breadcrumb {
        void setup(Environment env, State state, IntConsumer listener, @Nullable View topDivider);

        void show(boolean visibility);

        void postUpdate();
    }

    interface Environment {
        @Deprecated
            // Use CommonAddones#getCurrentRoot
        RootInfo getCurrentRoot();

        String getDrawerTitle();

        @Deprecated
            // Use CommonAddones#refreshCurrentRootAndDirectory
        void refreshCurrentRootAndDirectory(int animation);

        boolean isSearchExpanded();
    }
}
