/*
 * 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.app.Dialog;
import android.content.Intent;
import android.media.MediaRouter.RouteInfo;
import android.media.projection.StopReason;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.service.quicksettings.Tile;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Button;

import androidx.annotation.Nullable;

import com.android.internal.app.MediaRouteDialogPresenter;
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.animation.DialogCuj;
import com.android.systemui.animation.DialogTransitionAnimator;
import com.android.systemui.animation.Expandable;
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.QSTile.BooleanState;
import com.android.systemui.plugins.qs.TileDetailsViewModel;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.qs.QSHost;
import com.android.systemui.qs.QsEventLogger;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.qs.tileimpl.QSTileImpl;
import com.android.systemui.qs.tiles.dialog.CastDetailsViewModel;
import com.android.systemui.res.R;
import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor;
import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.statusbar.pipeline.shared.data.model.DefaultConnectionModel;
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository;
import com.android.systemui.statusbar.policy.CastController;
import com.android.systemui.statusbar.policy.CastDevice;
import com.android.systemui.statusbar.policy.HotspotController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.DialogKt;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

import javax.inject.Inject;

/** Quick settings tile: Cast **/
public class CastTile extends QSTileImpl<BooleanState> {

    public static final String TILE_SPEC = "cast";

    private static final String INTERACTION_JANK_TAG = TILE_SPEC;

    private final CastController mController;
    private final KeyguardStateController mKeyguard;
    private final DialogTransitionAnimator mDialogTransitionAnimator;
    private final Callback mCallback = new Callback();
    private final TileJavaAdapter mJavaAdapter;
    private final ShadeDialogContextInteractor mShadeDialogContextInteractor;
    private boolean mCastTransportAllowed;
    private boolean mHotspotConnected;
    private final CastDetailsViewModel.Factory mCastDetailsViewModelFactory;

    @Inject
    public CastTile(
            QSHost host,
            QsEventLogger uiEventLogger,
            @Background Looper backgroundLooper,
            @Main Handler mainHandler,
            FalsingManager falsingManager,
            MetricsLogger metricsLogger,
            StatusBarStateController statusBarStateController,
            ActivityStarter activityStarter,
            QSLogger qsLogger,
            CastController castController,
            KeyguardStateController keyguardStateController,
            HotspotController hotspotController,
            DialogTransitionAnimator dialogTransitionAnimator,
            ConnectivityRepository connectivityRepository,
            TileJavaAdapter javaAdapter,
            ShadeDialogContextInteractor shadeDialogContextInteractor,
            CastDetailsViewModel.Factory castDetailsViewModelFactory
    ) {
        super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger,
                statusBarStateController, activityStarter, qsLogger);
        mController = castController;
        mKeyguard = keyguardStateController;
        mDialogTransitionAnimator = dialogTransitionAnimator;
        mJavaAdapter = javaAdapter;
        mShadeDialogContextInteractor = shadeDialogContextInteractor;
        mCastDetailsViewModelFactory = castDetailsViewModelFactory;
        mController.observe(this, mCallback);
        mKeyguard.observe(this, mCallback);
        mJavaAdapter.bind(
                this,
                connectivityRepository.getDefaultConnections(),
                mNetworkModelConsumer
        );
        hotspotController.observe(this, mHotspotCallback);
    }

    @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 Expandable expandable) {
        handleClick(expandable);
    }

    @Override
    protected void handleClick(@Nullable Expandable expandable) {
        handleClick(() -> {
            if (!mKeyguard.isShowing()) {
                showDialog(expandable);
            } else {
                mActivityStarter.postQSRunnableDismissingKeyguard(() -> showDialog(expandable));
            }
        });
    }

    @Override
    public boolean getDetailsViewModel(Consumer<TileDetailsViewModel> callback) {
        CastDetailsViewModel viewModel = mCastDetailsViewModelFactory
                .create(mShadeDialogContextInteractor.getContext(), ROUTE_TYPE_REMOTE_DISPLAY);
        handleClick(() -> {
            if (!mKeyguard.isShowing()) {
                callback.accept(viewModel);
            } else {
                mActivityStarter.dismissKeyguardThenExecute(() -> {
                    callback.accept(viewModel);
                    return false;
                },  null /* cancelAction */,  true/* afterKeyguardGone */);
            }
        });
        return true;
    }

    private void handleClick(Runnable showPromptCallback) {
        if (getState().state == Tile.STATE_UNAVAILABLE) {
            return;
        }

        List<CastDevice> activeDevices = getActiveDevices();
        if (willShowPrompt()) {
            showPromptCallback.run();
        } else {
            mController.stopCasting(activeDevices.get(0), StopReason.STOP_QS_TILE);
        }
    }

    // We want to pop up the media route selection dialog (or show the cast details view) 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 willShowPrompt() {
        List<CastDevice> activeDevices = getActiveDevices();
        return activeDevices.isEmpty() || (activeDevices.get(0).getTag() instanceof RouteInfo);
    }

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

        return activeDevices;
    }

    private static class DialogHolder {
        private Dialog mDialog;

        private void init(Dialog dialog) {
            mDialog = dialog;
        }
    }

    private void showDialog(@Nullable Expandable expandable) {
        mUiHandler.post(() -> {
            final DialogHolder holder = new DialogHolder();
            final Dialog dialog = MediaRouteDialogPresenter.createDialog(
                    mShadeDialogContextInteractor.getContext(),
                    ROUTE_TYPE_REMOTE_DISPLAY,
                    v -> {
                        ActivityTransitionAnimator.Controller controller =
                                mDialogTransitionAnimator.createActivityTransitionController(v);

                        if (controller == null) {
                            holder.mDialog.dismiss();
                        }

                        mActivityStarter
                                .postStartActivityDismissingKeyguard(getLongClickIntent(), 0,
                                        controller);
                    }, R.style.Theme_SystemUI_Dialog_Cast, false /* showProgressBarWhenEmpty */);
            holder.init(dialog);
            SystemUIDialog.setShowForAllUsers(dialog, true);
            SystemUIDialog.registerDismissListener(dialog);
            SystemUIDialog.setWindowOnTop(dialog, mKeyguard.isShowing());
            SystemUIDialog.setDialogSize(dialog);

            mUiHandler.post(() -> {
                if (expandable != null) {
                    DialogTransitionAnimator.Controller controller =
                            expandable.dialogTransitionController(
                                    new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
                                            INTERACTION_JANK_TAG));
                    if (controller != null) {
                        mDialogTransitionAnimator.show(dialog, controller);
                        return;
                    }
                }
                if (dialog.getWindow() != null) {
                    DialogKt.registerAnimationOnBackInvoked(dialog,
                            dialog.getWindow().getDecorView());
                }
                dialog.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;
        ArrayList<CharSequence> stateDescriptionParts = new ArrayList<>();
        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.getState() == CastDevice.CastState.Connected) {
                state.value = true;
                state.secondaryLabel = getDeviceName(device);
                stateDescriptionParts
                        .add(mContext.getString(R.string.accessibility_cast_name, state.label));
                connecting = false;
                break;
            } else if (device.getState() == CastDevice.CastState.Connecting) {
                connecting = true;
            }
        }
        if (connecting && !state.value) {
            state.secondaryLabel = mContext.getString(R.string.quick_settings_connecting);
        }
        state.icon = maybeLoadResourceIcon(state.value
                ? R.drawable.ic_cast_connected : R.drawable.ic_cast);
        if (canCastToNetwork() || state.value) {
            state.state = state.value ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
            if (!state.value) {
                state.secondaryLabel = "";
            }
            state.expandedAccessibilityClassName = Button.class.getName();
            state.forceExpandIcon = willShowPrompt();
        } else {
            state.state = Tile.STATE_UNAVAILABLE;
            String noWifi = mContext.getString(R.string.quick_settings_cast_no_network);
            state.secondaryLabel = noWifi;
            state.forceExpandIcon = false;
        }
        if (!TextUtils.isEmpty(state.secondaryLabel)) {
            stateDescriptionParts.add(state.secondaryLabel);
        }
        state.stateDescription = String.join(", ", stateDescriptionParts);
    }

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

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

    private boolean canCastToNetwork() {
        return mCastTransportAllowed || mHotspotConnected;
    }

    private void setCastTransportAllowed(boolean connected) {
        if (connected != mCastTransportAllowed) {
            mCastTransportAllowed = connected;
            // Hotspot is not connected, so changes here should update
            if (!mHotspotConnected) {
                refreshState();
            }
        }
    }

    private void setHotspotConnected(boolean connected) {
        if (connected != mHotspotConnected) {
            mHotspotConnected = connected;
            // Wifi is not connected, so changes here should update
            if (!mCastTransportAllowed) {
                refreshState();
            }
        }
    }

    private final Consumer<DefaultConnectionModel> mNetworkModelConsumer = (model) -> {
        boolean isWifiDefault = model.getWifi().isDefault();
        boolean isEthernetDefault = model.getEthernet().isDefault();
        boolean hasCellularTransport = model.getMobile().isDefault();
        setCastTransportAllowed((isWifiDefault || isEthernetDefault) && !hasCellularTransport);
    };

    private final HotspotController.Callback mHotspotCallback =
            new HotspotController.Callback() {
                @Override
                public void onHotspotChanged(boolean enabled, int numDevices) {
                    boolean enabledAndConnected = enabled && numDevices > 0;
                    setHotspotConnected(enabledAndConnected);
                }
            };

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

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