/*
 * 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.android.systemui.statusbar.phone.ui;

import static com.android.systemui.statusbar.phone.ui.StatusBarIconList.Slot;

import static com.google.common.base.Preconditions.checkArgument;

import android.annotation.NonNull;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.view.ViewGroup;

import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import com.android.internal.statusbar.StatusBarIcon;
import com.android.systemui.Dumpable;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.demomode.DemoMode;
import com.android.systemui.demomode.DemoModeController;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.StatusIconDisplayable;
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays;
import com.android.systemui.statusbar.phone.StatusBarIconHolder;
import com.android.systemui.statusbar.phone.StatusBarIconHolder.BindableIconHolder;
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
import com.android.systemui.statusbar.pipeline.icons.shared.BindableIconsRegistry;
import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
import com.android.systemui.tuner.TunerService;
import com.android.systemui.tuner.TunerService.Tunable;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.inject.Inject;

/**
 * Receives the callbacks from CommandQueue related to icons and tracks the state of
 * all the icons. Dispatches this state to any IconManagers that are currently
 * registered with it.
 */
@SysUISingleton
public class StatusBarIconControllerImpl implements Tunable,
        ConfigurationListener, Dumpable, StatusBarIconController, DemoMode {

    private static final String TAG = "StatusBarIconController";
    // Use this suffix to prevent external icon slot names from unintentionally overriding our
    // internal, system-level slot names. See b/255428281.
    @VisibleForTesting
    protected static final String EXTERNAL_SLOT_SUFFIX = "__external";

    private final StatusBarIconList mStatusBarIconList;
    private final ArrayList<IconManager> mIconGroups = new ArrayList<>();
    private final ArraySet<String> mIconHideList = new ArraySet<>();
    private final StatusBarPipelineFlags mStatusBarPipelineFlags;
    private final Context mContext;

    /**  */
    @Inject
    public StatusBarIconControllerImpl(
            Context context,
            CommandQueue commandQueue,
            DemoModeController demoModeController,
            ConfigurationController configurationController,
            TunerService tunerService,
            DumpManager dumpManager,
            StatusBarIconList statusBarIconList,
            StatusBarPipelineFlags statusBarPipelineFlags,
            BindableIconsRegistry modernIconsRegistry
    ) {
        mStatusBarIconList = statusBarIconList;
        mContext = context;
        mStatusBarPipelineFlags = statusBarPipelineFlags;

        if (StatusBarConnectedDisplays.isEnabled()) {
            // refresh requests are dispatched by StatusBarIconRefreshInteractor, per display.
        } else {
            configurationController.addCallback(this);
        }
        commandQueue.addCallback(mCommandQueueCallbacks);
        tunerService.addTunable(this, ICON_HIDE_LIST);
        demoModeController.addCallback(this);
        dumpManager.registerDumpable(getClass().getSimpleName(), this);

        addModernBindableIcons(modernIconsRegistry);
    }

    /**
     * BindableIcons will always produce ModernStatusBarViews, which will be initialized and bound
     * upon being added to any icon group. Because their view policy does not require subsequent
     * calls to setIcon(), we can simply register them all statically here and not have to build
     * CoreStartables for each modern icon.
     *
     * @param registry a statically defined provider of the modern icons
     */
    private void addModernBindableIcons(BindableIconsRegistry registry) {
        List<BindableIcon> icons = registry.getBindableIcons();

        // Initialization point for the bindable (modern) icons. These icons get their own slot
        // allocated immediately, and are required to control their own display properties
        for (BindableIcon i : icons) {
            if (i.getShouldBindIcon()) {
                addBindableIcon(i);
            }
        }
    }

    /**  */
    @Override
    public void addIconGroup(IconManager group) {
        for (IconManager existingIconManager : mIconGroups) {
            if (existingIconManager.mGroup == group.mGroup) {
                Log.e(TAG, "Adding new IconManager for the same ViewGroup. This could cause "
                        + "unexpected results.");
            }
        }

        group.setController(this);
        group.reloadDimens();
        mIconGroups.add(group);
        List<Slot> allSlots = mStatusBarIconList.getSlots();
        for (int i = 0; i < allSlots.size(); i++) {
            Slot slot = allSlots.get(i);
            List<StatusBarIconHolder> holders = slot.getHolderListInViewOrder();
            boolean hidden = mIconHideList.contains(slot.getName());

            for (StatusBarIconHolder holder : holders) {
                int viewIndex = mStatusBarIconList.getViewIndex(slot.getName(), holder.getTag());
                group.onIconAdded(viewIndex, slot.getName(), hidden, holder);
            }
        }
    }

    @Override
    public void refreshIconGroup(IconManager iconManager) {
        removeIconGroup(iconManager);
        addIconGroup(iconManager);
    }

    @Override
    public void refreshIconGroups(int displayId) {
        if (!StatusBarConnectedDisplays.isEnabled()) return;
        for (int i = mIconGroups.size() - 1; i >= 0; --i) {
            IconManager group = mIconGroups.get(i);
            if (group.getDisplayId() == displayId) {
                removeIconGroup(group);
                addIconGroup(group);
            }
        }
    }

    @Deprecated // Use refreshIconGroups(int displayId) instead
    private void refreshIconGroups() {
        StatusBarConnectedDisplays.assertInLegacyMode();
        for (int i = mIconGroups.size() - 1; i >= 0; --i) {
            IconManager group = mIconGroups.get(i);
            removeIconGroup(group);
            addIconGroup(group);
        }
    }

    /**  */
    @Override
    public void removeIconGroup(IconManager group) {
        group.destroy();
        mIconGroups.remove(group);
    }

    /**  */
    @Override
    public void onTuningChanged(String key, String newValue) {
        if (!ICON_HIDE_LIST.equals(key)) {
            return;
        }
        mIconHideList.clear();
        mIconHideList.addAll(StatusBarIconController.getIconHideList(mContext, newValue));
        List<Slot> currentSlots = mStatusBarIconList.getSlots();
        ArrayMap<Slot, List<StatusBarIconHolder>> slotsToReAdd = new ArrayMap<>();

        // This is a little hacky... Peel off all of the holders on all of the slots
        // but keep them around so they can be re-added

        // Remove all the icons.
        for (int i = currentSlots.size() - 1; i >= 0; i--) {
            Slot s = currentSlots.get(i);
            slotsToReAdd.put(s, s.getHolderList());
            removeAllIconsForSlot(s.getName(), /* fromNewPipeline */ false);
        }

        // Add them all back
        for (int i = 0; i < currentSlots.size(); i++) {
            Slot item = currentSlots.get(i);
            List<StatusBarIconHolder> iconsForSlot = slotsToReAdd.get(item);
            if (iconsForSlot == null) continue;
            for (StatusBarIconHolder holder : iconsForSlot) {
                setIcon(item.getName(), holder);
            }
        }
    }

    private void addSystemIcon(String slot, StatusBarIconHolder holder) {
        int viewIndex = mStatusBarIconList.getViewIndex(slot, holder.getTag());
        boolean hidden = mIconHideList.contains(slot);

        mIconGroups.forEach(l -> l.onIconAdded(viewIndex, slot, hidden, holder));
    }

    void addBindableIcon(BindableIcon icon) {
        StatusBarIconHolder existingHolder = mStatusBarIconList.getIconHolder(icon.getSlot(), 0);
        // Expected to be null
        if (existingHolder == null) {
            BindableIconHolder bindableIcon =
                    new BindableIconHolder(icon.getInitializer(), icon.getSlot());
            setIcon(icon.getSlot(), bindableIcon);
        } else {
            Log.e(TAG, "addBindableIcon called, but icon has already been added. Ignoring");
        }
    }

    @Override
    public void setIcon(String slot, int resourceId, CharSequence contentDescription) {
        setResourceIconInternal(
                slot,
                Icon.createWithResource(mContext, resourceId),
                /* preloadedIcon= */ null,
                contentDescription,
                StatusBarIcon.Type.SystemIcon,
                StatusBarIcon.Shape.WRAP_CONTENT);
    }

    @Override
    public void setResourceIcon(String slot, @Nullable String resPackage,
            @DrawableRes int iconResId, @Nullable Drawable preloadedIcon,
            CharSequence contentDescription, StatusBarIcon.Shape shape) {
        Icon icon = resPackage != null
                ? Icon.createWithResource(resPackage, iconResId)
                : Icon.createWithResource(mContext, iconResId);
        setResourceIconInternal(
                slot,
                icon,
                preloadedIcon,
                contentDescription,
                StatusBarIcon.Type.ResourceIcon,
                shape);
    }

    private void setResourceIconInternal(String slot, Icon resourceIcon,
            @Nullable Drawable preloadedIcon, CharSequence contentDescription,
            StatusBarIcon.Type type, StatusBarIcon.Shape shape) {
        checkArgument(resourceIcon.getType() == Icon.TYPE_RESOURCE,
                "Expected Icon of TYPE_RESOURCE, but got " + resourceIcon.getType());
        String resPackage = resourceIcon.getResPackage();
        if (TextUtils.isEmpty(resPackage)) {
            resPackage = mContext.getPackageName();
        }

        StatusBarIconHolder holder = mStatusBarIconList.getIconHolder(slot, 0);
        if (holder == null) {
            StatusBarIcon icon = new StatusBarIcon(UserHandle.SYSTEM, resPackage,
                    resourceIcon, /* iconLevel= */ 0, /* number=*/ 0,
                    contentDescription, type, shape);
            icon.preloadedIcon = preloadedIcon;
            holder = StatusBarIconHolder.fromIcon(icon);
            setIcon(slot, holder);
        } else {
            holder.getIcon().pkg = resPackage;
            holder.getIcon().icon = resourceIcon;
            holder.getIcon().contentDescription = contentDescription;
            holder.getIcon().type = type;
            holder.getIcon().shape = shape;
            holder.getIcon().preloadedIcon = preloadedIcon;
            handleSet(slot, holder);
        }
    }

    @Override
    public void setNewWifiIcon() {
        String slot = mContext.getString(com.android.internal.R.string.status_bar_wifi);
        StatusBarIconHolder holder = mStatusBarIconList.getIconHolder(slot, /* tag= */ 0);
        if (holder == null) {
            holder = StatusBarIconHolder.forNewWifiIcon();
            setIcon(slot, holder);
        } else {
            // Don't have to do anything in the new world
        }
    }

    /**
     * Accept a list of MobileIconStates, which all live in the same slot(?!), and then are sorted
     * by subId. Don't worry this definitely makes sense and works.
     *
     * @param subIds list of subscription ID integers that provide the key to the icon to display.
     */
    @Override
    public void setNewMobileIconSubIds(List<Integer> subIds) {
        String slotName = mContext.getString(com.android.internal.R.string.status_bar_mobile);
        Slot mobileSlot = mStatusBarIconList.getSlot(slotName);

        // Because of the way we cache the icon holders, we need to remove everything any time
        // we get a new set of subscriptions. This might change in the future, but is required
        // to support demo mode for now
        removeAllIconsForSlot(slotName, /* fromNewPipeline */ true);

        Collections.reverse(subIds);

        for (Integer subId : subIds) {
            StatusBarIconHolder holder = mobileSlot.getHolderForTag(subId);
            if (holder == null) {
                holder = StatusBarIconHolder.fromSubIdForModernMobileIcon(subId);
                setIcon(slotName, holder);
            } else {
                // Don't have to do anything in the new world
            }
        }
    }

    private final CommandQueue.Callbacks mCommandQueueCallbacks = new CommandQueue.Callbacks() {
        @Override
        public void setIcon(String slot, StatusBarIcon icon) {
            // Icons that come from CommandQueue are from external services.
            setExternalIcon(slot, icon);
        }

        @Override
        public void removeIcon(String slot) {
            removeAllIconsForExternalSlot(slot);
        }
    };

    @Override
    public void setIconFromTile(String slot, StatusBarIcon icon) {
        setExternalIcon(slot, icon);
    }

    @Override
    public void removeIconForTile(String slot) {
        removeAllIconsForExternalSlot(slot);
    }

    private void setExternalIcon(String slot, StatusBarIcon icon) {
        if (icon == null) {
            removeAllIconsForExternalSlot(slot);
            return;
        }
        String slotName = createExternalSlotName(slot);
        StatusBarIconHolder holder = StatusBarIconHolder.fromIcon(icon);
        setIcon(slotName, holder);
    }

    private void setIcon(String slot, @NonNull StatusBarIconHolder holder) {
        boolean isNew = mStatusBarIconList.getIconHolder(slot, holder.getTag()) == null;
        mStatusBarIconList.setIcon(slot, holder);

        if (isNew) {
            addSystemIcon(slot, holder);
        } else {
            handleSet(slot, holder);
        }
    }

    /**  */
    public void setIconVisibility(String slot, boolean visibility) {
        setIconVisibility(slot, visibility, 0);
    }

    /**  */
    public void setIconVisibility(String slot, boolean visibility, int tag) {
        StatusBarIconHolder holder = mStatusBarIconList.getIconHolder(slot, tag);
        if (holder == null || holder.isVisible() == visibility) {
            return;
        }

        holder.setVisible(visibility);
        handleSet(slot, holder);
    }

    /**  */
    @Override
    public void removeIcon(String slot, int tag) {
        // If the new pipeline is on for this icon, don't allow removal, since the new pipeline
        // will never call this method
        if (mStatusBarPipelineFlags.isIconControlledByFlags(slot)) {
            Log.i(TAG, "Ignoring removal of (" + slot + "). "
                    + "It should be controlled elsewhere");
            return;
        }

        if (mStatusBarIconList.getIconHolder(slot, tag) == null) {
            return;
        }
        int viewIndex = mStatusBarIconList.getViewIndex(slot, tag);
        mStatusBarIconList.removeIcon(slot, tag);
        mIconGroups.forEach(l -> l.onRemoveIcon(viewIndex));
    }

    private void removeAllIconsForExternalSlot(String slotName) {
        removeAllIconsForSlot(createExternalSlotName(slotName), /* fromNewPipeline= */ false);
    }

    private void removeAllIconsForSlot(String slotName, boolean fromNewPipeline) {
        // If the new pipeline is on for this icon, don't allow removal, since the new pipeline
        // will never call this method
        if (!fromNewPipeline && mStatusBarPipelineFlags.isIconControlledByFlags(slotName)) {
            Log.i(TAG, "Ignoring removal of (" + slotName + "). "
                    + "It should be controlled elsewhere");
            return;
        }

        Slot slot = mStatusBarIconList.getSlot(slotName);
        if (!slot.hasIconsInSlot()) {
            return;
        }

        List<StatusBarIconHolder> iconsToRemove = slot.getHolderListInViewOrder();
        for (StatusBarIconHolder holder : iconsToRemove) {
            int viewIndex = mStatusBarIconList.getViewIndex(slotName, holder.getTag());
            slot.removeForTag(holder.getTag());
            mIconGroups.forEach(l -> l.onRemoveIcon(viewIndex));
        }
    }

    private void handleSet(String slotName, StatusBarIconHolder holder) {
        int viewIndex = mStatusBarIconList.getViewIndex(slotName, holder.getTag());
        mIconGroups.forEach(l -> l.onSetIconHolder(viewIndex, holder));
    }

    /**  */
    @Override
    public void dump(PrintWriter pw, String[] args) {
        pw.println(TAG + " state:");
        for (IconManager manager : mIconGroups) {
            if (manager.shouldLog()) {
                ViewGroup group = manager.mGroup;
                int n = group.getChildCount();
                pw.println("  icon views: " + n);
                for (int i = 0; i < n; i++) {
                    StatusIconDisplayable ic = (StatusIconDisplayable) group.getChildAt(i);
                    pw.println("    [" + i + "] icon=" + ic);
                }
            }
        }

        mStatusBarIconList.dump(pw);
    }

    /**  */
    @Override
    public void onDemoModeStarted() {
        for (IconManager manager : mIconGroups) {
            if (manager.isDemoable()) {
                manager.onDemoModeStarted();
            }
        }
    }

    /**  */
    @Override
    public void onDemoModeFinished() {
        for (IconManager manager : mIconGroups) {
            if (manager.isDemoable()) {
                manager.onDemoModeFinished();
            }
        }
    }

    /**  */
    @Override
    public void dispatchDemoCommand(String command, Bundle args) {
        for (IconManager manager : mIconGroups) {
            if (manager.isDemoable()) {
                manager.dispatchDemoCommand(command, args);
            }
        }
    }

    /**  */
    @Override
    public List<String> demoCommands() {
        List<String> s = new ArrayList<>();
        s.add(DemoMode.COMMAND_STATUS);
        return s;
    }

    /**  */
    @Override
    public void onDensityOrFontScaleChanged() {
        StatusBarConnectedDisplays.assertInLegacyMode();
        refreshIconGroups();
    }

    private String createExternalSlotName(String slot) {
        if (slot.endsWith(EXTERNAL_SLOT_SUFFIX)) {
            return slot;
        } else {
            return slot + EXTERNAL_SLOT_SUFFIX;
        }
    }
}
