/*
 * Copyright (C) 2014 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.qs.tiles;

import static android.media.MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY;

import android.annotation.NonNull;
import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.media.MediaRouter.RouteInfo;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.service.quicksettings.Tile;
import android.util.Log;
import android.view.View;
import android.view.View.OnAttachStateChangeListener;
import android.view.ViewGroup;
import android.widget.Button;

import androidx.annotation.Nullable;

import com.android.internal.app.MediaRouteDialogPresenter;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.systemui.R;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.qs.DetailAdapter;
import com.android.systemui.plugins.qs.QSTile.BooleanState;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.qs.QSDetailItems;
import com.android.systemui.qs.QSDetailItems.Item;
import com.android.systemui.qs.QSHost;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.qs.tileimpl.QSTileImpl;
import com.android.systemui.statusbar.connectivity.NetworkController;
import com.android.systemui.statusbar.connectivity.SignalCallback;
import com.android.systemui.statusbar.connectivity.WifiIndicators;
import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.statusbar.policy.CastController;
import com.android.systemui.statusbar.policy.CastController.CastDevice;
import com.android.systemui.statusbar.policy.HotspotController;
import com.android.systemui.statusbar.policy.KeyguardStateController;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;

import javax.inject.Inject;

/** Quick settings tile: Cast **/
public class CastTile extends QSTileImpl<BooleanState> {
    private static final Intent CAST_SETTINGS =
            new Intent(Settings.ACTION_CAST_SETTINGS);

    private final CastController mController;
    private final CastDetailAdapter mDetailAdapter;
    private final KeyguardStateController mKeyguard;
    private final NetworkController mNetworkController;
    private final DialogLaunchAnimator mDialogLaunchAnimator;
    private final Callback mCallback = new Callback();
    private Dialog mDialog;
    private boolean mWifiConnected;
    private boolean mHotspotConnected;

    @Inject
    public CastTile(
            QSHost host,
            @Background Looper backgroundLooper,
            @Main Handler mainHandler,
            FalsingManager falsingManager,
            MetricsLogger metricsLogger,
            StatusBarStateController statusBarStateController,
            ActivityStarter activityStarter,
            QSLogger qsLogger,
            CastController castController,
            KeyguardStateController keyguardStateController,
            NetworkController networkController,
            HotspotController hotspotController,
            DialogLaunchAnimator dialogLaunchAnimator
    ) {
        super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger,
                statusBarStateController, activityStarter, qsLogger);
        mController = castController;
        mDetailAdapter = new CastDetailAdapter();
        mKeyguard = keyguardStateController;
        mNetworkController = networkController;
        mDialogLaunchAnimator = dialogLaunchAnimator;
        mController.observe(this, mCallback);
        mKeyguard.observe(this, mCallback);
        mNetworkController.observe(this, mSignalCallback);
        hotspotController.observe(this, mHotspotCallback);
    }

    @Override
    public DetailAdapter getDetailAdapter() {
        return mDetailAdapter;
    }

    @Override
    public BooleanState newTileState() {
        BooleanState state = new BooleanState();
        state.handlesLongClick = false;
        return state;
    }

    @Override
    public void handleSetListening(boolean listening) {
        super.handleSetListening(listening);
        if (DEBUG) Log.d(TAG, "handleSetListening " + listening);
        if (!listening) {
            mController.setDiscovering(false);
        }
    }

    @Override
    protected void handleUserSwitch(int newUserId) {
        super.handleUserSwitch(newUserId);
        mController.setCurrentUserId(newUserId);
    }

    @Override
    public Intent getLongClickIntent() {
        return new Intent(Settings.ACTION_CAST_SETTINGS);
    }

    @Override
    protected void handleLongClick(@Nullable View view) {
        handleClick(view);
    }

    @Override
    protected void handleClick(@Nullable View view) {
        if (getState().state == Tile.STATE_UNAVAILABLE) {
            return;
        }

        List<CastDevice> activeDevices = getActiveDevices();
        if (willPopDetail()) {
            if (!mKeyguard.isShowing()) {
                showDetail(view);
            } else {
                mActivityStarter.postQSRunnableDismissingKeyguard(() -> {
                    // Dismissing the keyguard will collapse the shade, so we don't animate from the
                    // view here as it would not look good.
                    showDetail(null /* view */);
                });
            }
        } else {
            mController.stopCasting(activeDevices.get(0));
        }
    }

    // We want to pop up the media route selection dialog if we either have no active devices
    // (neither routes nor projection), or if we have an active route. In other cases, we assume
    // that a projection is active. This is messy, but this tile never correctly handled the
    // case where multiple devices were active :-/.
    private boolean willPopDetail() {
        List<CastDevice> activeDevices = getActiveDevices();
        return activeDevices.isEmpty() || (activeDevices.get(0).tag instanceof RouteInfo);
    }

    private List<CastDevice> getActiveDevices() {
        ArrayList<CastDevice> activeDevices = new ArrayList<>();
        for (CastDevice device : mController.getCastDevices()) {
            if (device.state == CastDevice.STATE_CONNECTED
                    || device.state == CastDevice.STATE_CONNECTING) {
                activeDevices.add(device);
            }
        }

        return activeDevices;
    }

    @Override
    public void showDetail(boolean show) {
        showDetail(null /* view */);
    }

    private void showDetail(@Nullable View view) {
        mUiHandler.post(() -> {
            mDialog = MediaRouteDialogPresenter.createDialog(mContext, ROUTE_TYPE_REMOTE_DISPLAY,
                    v -> {
                        mDialogLaunchAnimator.disableAllCurrentDialogsExitAnimations();
                        mDialog.dismiss();
                        mActivityStarter
                                .postStartActivityDismissingKeyguard(getLongClickIntent(), 0);
                    });
            SystemUIDialog.setShowForAllUsers(mDialog, true);
            SystemUIDialog.registerDismissListener(mDialog);
            SystemUIDialog.setWindowOnTop(mDialog);

            mUiHandler.post(() -> {
                if (view != null) {
                    mDialogLaunchAnimator.showFromView(mDialog, view);
                } else {
                    mDialog.show();
                }
            });
        });
    }

    @Override
    public CharSequence getTileLabel() {
        return mContext.getString(R.string.quick_settings_cast_title);
    }

    @Override
    protected void handleUpdateState(BooleanState state, Object arg) {
        state.label = mContext.getString(R.string.quick_settings_cast_title);
        state.contentDescription = state.label;
        state.stateDescription = "";
        state.value = false;
        final List<CastDevice> devices = mController.getCastDevices();
        boolean connecting = false;
        // We always choose the first device that's in the CONNECTED state in the case where
        // multiple devices are CONNECTED at the same time.
        for (CastDevice device : devices) {
            if (device.state == CastDevice.STATE_CONNECTED) {
                state.value = true;
                state.secondaryLabel = getDeviceName(device);
                state.stateDescription = state.stateDescription + ","
                        + mContext.getString(
                                R.string.accessibility_cast_name, state.label);
                connecting = false;
                break;
            } else if (device.state == CastDevice.STATE_CONNECTING) {
                connecting = true;
            }
        }
        if (connecting && !state.value) {
            state.secondaryLabel = mContext.getString(R.string.quick_settings_connecting);
        }
        state.icon = ResourceIcon.get(state.value ? R.drawable.ic_cast_connected
                : R.drawable.ic_cast);
        if (canCastToWifi() || state.value) {
            state.state = state.value ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
            if (!state.value) {
                state.secondaryLabel = "";
            }
            state.contentDescription = state.contentDescription + ","
                    + mContext.getString(R.string.accessibility_quick_settings_open_details);
            state.expandedAccessibilityClassName = Button.class.getName();
            state.forceExpandIcon = willPopDetail();
        } else {
            state.state = Tile.STATE_UNAVAILABLE;
            String noWifi = mContext.getString(R.string.quick_settings_cast_no_wifi);
            state.secondaryLabel = noWifi;
            state.forceExpandIcon = false;
        }
        state.stateDescription = state.stateDescription + ", " + state.secondaryLabel;
        mDetailAdapter.updateItems(devices);
    }

    @Override
    public int getMetricsCategory() {
        return MetricsEvent.QS_CAST;
    }

    @Override
    protected String composeChangeAnnouncement() {
        if (!mState.value) {
            // We only announce when it's turned off to avoid vocal overflow.
            return mContext.getString(R.string.accessibility_casting_turned_off);
        }
        return null;
    }

    private String getDeviceName(CastDevice device) {
        return device.name != null ? device.name
                : mContext.getString(R.string.quick_settings_cast_device_default_name);
    }

    private boolean canCastToWifi() {
        return mWifiConnected || mHotspotConnected;
    }

    private final SignalCallback mSignalCallback = new SignalCallback() {
                @Override
                public void setWifiIndicators(@NonNull WifiIndicators indicators) {
                    // statusIcon.visible has the connected status information
                    boolean enabledAndConnected = indicators.enabled
                            && (indicators.qsIcon == null ? false : indicators.qsIcon.visible);
                    if (enabledAndConnected != mWifiConnected) {
                        mWifiConnected = enabledAndConnected;
                        // Hotspot is not connected, so changes here should update
                        if (!mHotspotConnected) {
                            refreshState();
                        }
                    }
                }
            };

    private final HotspotController.Callback mHotspotCallback =
            new HotspotController.Callback() {
                @Override
                public void onHotspotChanged(boolean enabled, int numDevices) {
                    boolean enabledAndConnected = enabled && numDevices > 0;
                    if (enabledAndConnected != mHotspotConnected) {
                        mHotspotConnected = enabledAndConnected;
                        // Wifi is not connected, so changes here should update
                        if (!mWifiConnected) {
                            refreshState();
                        }
                    }
                }
            };

    private final class Callback implements CastController.Callback,
            KeyguardStateController.Callback {
        @Override
        public void onCastDevicesChanged() {
            refreshState();
        }

        @Override
        public void onKeyguardShowingChanged() {
            refreshState();
        }
    };

    private final class CastDetailAdapter implements DetailAdapter, QSDetailItems.Callback {
        private final LinkedHashMap<String, CastDevice> mVisibleOrder = new LinkedHashMap<>();

        private QSDetailItems mItems;

        @Override
        public CharSequence getTitle() {
            return mContext.getString(R.string.quick_settings_cast_title);
        }

        @Override
        public Boolean getToggleState() {
            return null;
        }

        @Override
        public Intent getSettingsIntent() {
            return CAST_SETTINGS;
        }

        @Override
        public void setToggleState(boolean state) {
            // noop
        }

        @Override
        public int getMetricsCategory() {
            return MetricsEvent.QS_CAST_DETAILS;
        }

        @Override
        public View createDetailView(Context context, View convertView, ViewGroup parent) {
            mItems = QSDetailItems.convertOrInflate(context, convertView, parent);
            mItems.setTagSuffix("Cast");
            if (convertView == null) {
                if (DEBUG) Log.d(TAG, "addOnAttachStateChangeListener");
                mItems.addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
                    @Override
                    public void onViewAttachedToWindow(View v) {
                        if (DEBUG) Log.d(TAG, "onViewAttachedToWindow");
                    }

                    @Override
                    public void onViewDetachedFromWindow(View v) {
                        if (DEBUG) Log.d(TAG, "onViewDetachedFromWindow");
                        mVisibleOrder.clear();
                    }
                });
            }
            mItems.setEmptyState(R.drawable.ic_qs_cast_detail_empty,
                    R.string.quick_settings_cast_detail_empty_text);
            mItems.setCallback(this);
            updateItems(mController.getCastDevices());
            mController.setDiscovering(true);
            return mItems;
        }

        private void updateItems(List<CastDevice> devices) {
            if (mItems == null) return;
            Item[] items = null;
            if (devices != null && !devices.isEmpty()) {
                // if we are connected, simply show that device
                for (CastDevice device : devices) {
                    if (device.state == CastDevice.STATE_CONNECTED) {
                        final Item item = new Item();
                        item.iconResId = R.drawable.ic_cast_connected;
                        item.line1 = getDeviceName(device);
                        item.line2 = mContext.getString(R.string.quick_settings_connected);
                        item.tag = device;
                        item.canDisconnect = true;
                        items = new Item[] { item };
                        break;
                    }
                }
                // otherwise list all available devices, and don't move them around
                if (items == null) {
                    for (CastDevice device : devices) {
                        mVisibleOrder.put(device.id, device);
                    }
                    items = new Item[devices.size()];
                    int i = 0;
                    for (String id : mVisibleOrder.keySet()) {
                        final CastDevice device = mVisibleOrder.get(id);
                        if (!devices.contains(device)) continue;
                        final Item item = new Item();
                        item.iconResId = R.drawable.ic_cast;
                        item.line1 = getDeviceName(device);
                        if (device.state == CastDevice.STATE_CONNECTING) {
                            item.line2 = mContext.getString(R.string.quick_settings_connecting);
                        }
                        item.tag = device;
                        items[i++] = item;
                    }
                }
            }
            mItems.setItems(items);
        }

        @Override
        public void onDetailItemClick(Item item) {
            if (item == null || item.tag == null) return;
            MetricsLogger.action(mContext, MetricsEvent.QS_CAST_SELECT);
            final CastDevice device = (CastDevice) item.tag;
            mController.startCasting(device);
        }

        @Override
        public void onDetailItemDisconnect(Item item) {
            if (item == null || item.tag == null) return;
            MetricsLogger.action(mContext, MetricsEvent.QS_CAST_DISCONNECT);
            final CastDevice device = (CastDevice) item.tag;
            mController.stopCasting(device);
        }
    }
}
