/*
 * Copyright (C) 2023 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.settingslib.media;

import android.annotation.SuppressLint;
import android.content.Context;
import android.media.MediaRoute2Info;
import android.media.MediaRouter2;
import android.media.MediaRouter2.RoutingController;
import android.media.MediaRouter2Manager;
import android.media.RouteDiscoveryPreference;
import android.media.RouteListingPreference;
import android.media.RoutingSessionInfo;
import android.media.session.MediaController;
import android.os.UserHandle;
import android.text.TextUtils;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.android.media.flags.Flags;
import com.android.settingslib.bluetooth.LocalBluetoothManager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/** Implements {@link InfoMediaManager} using {@link MediaRouter2}. */
@SuppressLint("MissingPermission")
public final class RouterInfoMediaManager extends InfoMediaManager {

    private static final String TAG = "RouterInfoMediaManager";

    private final MediaRouter2 mRouter;
    private final MediaRouter2Manager mRouterManager;

    private final Executor mExecutor = Executors.newSingleThreadExecutor();

    private final RouteCallback mRouteCallback = new RouteCallback();
    private final TransferCallback mTransferCallback = new TransferCallback();
    private final ControllerCallback mControllerCallback = new ControllerCallback();
    private final Consumer<RouteListingPreference> mRouteListingPreferenceCallback =
            (preference) -> {
                notifyRouteListingPreferenceUpdated(preference);
                refreshDevices();
            };

    private final AtomicReference<MediaRouter2.ScanToken> mScanToken = new AtomicReference<>();

    // TODO (b/321969740): Plumb target UserHandle between UMO and RouterInfoMediaManager.
    /* package */ RouterInfoMediaManager(
            Context context,
            @NonNull String packageName,
            @NonNull UserHandle userHandle,
            LocalBluetoothManager localBluetoothManager,
            @Nullable MediaController mediaController)
            throws PackageNotAvailableException {
        super(context, packageName, userHandle, localBluetoothManager, mediaController);

        MediaRouter2 router = null;

        if (Flags.enableCrossUserRoutingInMediaRouter2()) {
            try {
                router = MediaRouter2.getInstance(context, packageName, userHandle);
            } catch (IllegalArgumentException ex) {
                // Do nothing
            }
        } else {
            router = MediaRouter2.getInstance(context, packageName);
        }
        if (router == null) {
            throw new PackageNotAvailableException(
                    "Package name " + packageName + " does not exist.");
        }
        // We have to defer initialization because mRouter is final.
        mRouter = router;

        mRouterManager = MediaRouter2Manager.getInstance(context);
    }

    @Override
    protected void startScanOnRouter() {
        if (Flags.enableScreenOffScanning()) {
            MediaRouter2.ScanRequest request = new MediaRouter2.ScanRequest.Builder().build();
            mScanToken.compareAndSet(null, mRouter.requestScan(request));
        } else {
            mRouter.startScan();
        }
    }

    @Override
    protected void registerRouter() {
        mRouter.registerRouteCallback(mExecutor, mRouteCallback, RouteDiscoveryPreference.EMPTY);
        mRouter.registerRouteListingPreferenceUpdatedCallback(
                mExecutor, mRouteListingPreferenceCallback);
        mRouter.registerTransferCallback(mExecutor, mTransferCallback);
        mRouter.registerControllerCallback(mExecutor, mControllerCallback);
    }

    @Override
    protected void stopScanOnRouter() {
        if (Flags.enableScreenOffScanning()) {
            MediaRouter2.ScanToken token = mScanToken.getAndSet(null);
            if (token != null) {
                mRouter.cancelScanRequest(token);
            }
        } else {
            mRouter.stopScan();
        }
    }

    @Override
    protected void unregisterRouter() {
        mRouter.unregisterControllerCallback(mControllerCallback);
        mRouter.unregisterTransferCallback(mTransferCallback);
        mRouter.unregisterRouteListingPreferenceUpdatedCallback(mRouteListingPreferenceCallback);
        mRouter.unregisterRouteCallback(mRouteCallback);
    }

    @Override
    protected void transferToRoute(@NonNull MediaRoute2Info route) {
        mRouter.transferTo(route);
    }

    @Override
    protected void selectRoute(@NonNull MediaRoute2Info route, @NonNull RoutingSessionInfo info) {
        RoutingController controller = getControllerForSession(info);
        if (controller != null) {
            controller.selectRoute(route);
        }
    }

    @Override
    protected void deselectRoute(@NonNull MediaRoute2Info route, @NonNull RoutingSessionInfo info) {
        RoutingController controller = getControllerForSession(info);
        if (controller != null) {
            controller.deselectRoute(route);
        }
    }

    @Override
    protected void releaseSession(@NonNull RoutingSessionInfo sessionInfo) {
        RoutingController controller = getControllerForSession(sessionInfo);
        if (controller != null) {
            controller.release();
        }
    }

    @NonNull
    @Override
    protected List<MediaRoute2Info> getSelectableRoutes(@NonNull RoutingSessionInfo info) {
        RoutingController controller = getControllerForSession(info);
        if (controller == null) {
            return Collections.emptyList();
        }

        // Filter out selected routes.
        List<String> selectedRouteIds = controller.getRoutingSessionInfo().getSelectedRoutes();
        return controller.getSelectableRoutes().stream()
                .filter(route -> !selectedRouteIds.contains(route.getId()))
                .collect(Collectors.toList());
    }

    @NonNull
    @Override
    protected List<MediaRoute2Info> getDeselectableRoutes(@NonNull RoutingSessionInfo info) {
        RoutingController controller = getControllerForSession(info);
        if (controller == null) {
            return Collections.emptyList();
        }

        return controller.getDeselectableRoutes();
    }

    @NonNull
    @Override
    protected List<MediaRoute2Info> getSelectedRoutes(@NonNull RoutingSessionInfo info) {
        RoutingController controller = getControllerForSession(info);
        if (controller == null) {
            return Collections.emptyList();
        }
        return controller.getSelectedRoutes();
    }

    @Override
    protected void setSessionVolume(@NonNull RoutingSessionInfo info, int volume) {
        // TODO: b/291277292 - Implement MediaRouter2-based solution. Keeping MR2Manager call as
        //      MR2 filters information by package name.
        mRouterManager.setSessionVolume(info, volume);
    }

    @Override
    protected void setRouteVolume(@NonNull MediaRoute2Info route, int volume) {
        mRouter.setRouteVolume(route, volume);
    }

    @Nullable
    @Override
    protected RouteListingPreference getRouteListingPreference() {
        return mRouter.getRouteListingPreference();
    }

    @NonNull
    @Override
    protected List<RoutingSessionInfo> getRemoteSessions() {
        // TODO: b/291277292 - Implement MediaRouter2-based solution. Keeping MR2Manager call as
        //      MR2 filters information by package name.
        return mRouterManager.getRemoteSessions();
    }

    @NonNull
    @Override
    protected List<RoutingSessionInfo> getRoutingSessionsForPackage() {
        return mRouter.getControllers().stream()
                .map(RoutingController::getRoutingSessionInfo)
                .collect(Collectors.toList());
    }

    @Nullable
    @Override
    protected RoutingSessionInfo getRoutingSessionById(@NonNull String sessionId) {
        // TODO: b/291277292 - Implement MediaRouter2-based solution. Keeping MR2Manager calls as
        //      MR2 filters information by package name.

        for (RoutingSessionInfo sessionInfo : getRemoteSessions()) {
            if (TextUtils.equals(sessionInfo.getId(), sessionId)) {
                return sessionInfo;
            }
        }

        RoutingSessionInfo systemSession = mRouterManager.getSystemRoutingSession(null);
        return TextUtils.equals(systemSession.getId(), sessionId) ? systemSession : null;
    }

    @NonNull
    @Override
    protected List<MediaRoute2Info> getAvailableRoutesFromRouter() {
        return mRouter.getRoutes();
    }

    @NonNull
    @Override
    protected List<MediaRoute2Info> getTransferableRoutes(@NonNull String packageName) {
        List<RoutingController> controllers = mRouter.getControllers();
        RoutingController activeController = controllers.get(controllers.size() - 1);
        HashMap<String, MediaRoute2Info> transferableRoutes = new HashMap<>();

        activeController
                .getTransferableRoutes()
                .forEach(route -> transferableRoutes.put(route.getId(), route));

        if (activeController.getRoutingSessionInfo().isSystemSession()) {
            mRouter.getRoutes().stream()
                    .filter(route -> !route.isSystemRoute())
                    .forEach(route -> transferableRoutes.put(route.getId(), route));
        } else {
            mRouter.getRoutes().stream()
                    .filter(route -> route.isSystemRoute())
                    .forEach(route -> transferableRoutes.put(route.getId(), route));
        }

        return new ArrayList<>(transferableRoutes.values());
    }

    @Nullable
    private RoutingController getControllerForSession(@NonNull RoutingSessionInfo sessionInfo) {
        return mRouter.getController(sessionInfo.getId());
    }

    private final class RouteCallback extends MediaRouter2.RouteCallback {
        @Override
        public void onRoutesUpdated(@NonNull List<MediaRoute2Info> routes) {
            refreshDevices();
        }

        @Override
        public void onPreferredFeaturesChanged(@NonNull List<String> preferredFeatures) {
            refreshDevices();
        }
    }

    private final class TransferCallback extends MediaRouter2.TransferCallback {
        @Override
        public void onTransfer(
                @NonNull RoutingController oldController,
                @NonNull RoutingController newController) {
            rebuildDeviceList();
            notifyCurrentConnectedDeviceChanged();
        }

        @Override
        public void onTransferFailure(@NonNull MediaRoute2Info requestedRoute) {
            // Do nothing.
        }

        @Override
        public void onStop(@NonNull RoutingController controller) {
            refreshDevices();
        }

        @Override
        public void onRequestFailed(int reason) {
            dispatchOnRequestFailed(reason);
        }
    }

    private final class ControllerCallback extends MediaRouter2.ControllerCallback {
        @Override
        public void onControllerUpdated(@NonNull RoutingController controller) {
            refreshDevices();
        }
    }
}