/*
 * Copyright (C) 2024 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.internal.telephony;

import android.annotation.NonNull;
import android.content.Context;
import android.os.AsyncResult;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.Message;
import android.telephony.AccessNetworkConstants;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyRegistryManager;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.flags.FeatureFlags;
import com.android.internal.telephony.imsphone.ImsPhone;
import com.android.internal.telephony.subscription.SubscriptionManagerService;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.stream.Collectors;

public class SimultaneousCallingTracker {
    private static SimultaneousCallingTracker sInstance = null;
    private final Context mContext;

    /**
     * A dynamic map of all voice capable {@link Phone} objects mapped to the set of {@link Phone}
     * objects each {@link Phone} has a compatible user association with. To be considered
     * compatible based on user association, both must be associated with the same
     * {@link android.os.UserHandle} or both must be unassociated.
     */
    private Map<Phone, Set<Phone>> mVoiceCapablePhoneMap = new HashMap<>();

    @VisibleForTesting
    public boolean isDeviceSimultaneousCallingCapable = false;
    public Set<Listener> mListeners = new CopyOnWriteArraySet<>();
    private final PhoneConfigurationManager mPhoneConfigurationManager;
    private final Handler mHandler;

    /**
     * A dynamic map of all the Phone IDs mapped to the set of {@link Phone} objects each
     * {@link Phone} supports simultaneous calling (DSDA) with.
     */
    private Map<Integer, Set<Phone>> mSimultaneousCallPhoneSupportMap = new HashMap<>();
    private static final String LOG_TAG = "SimultaneousCallingTracker";
    protected static final int EVENT_SUBSCRIPTION_CHANGED         = 101;
    protected static final int EVENT_PHONE_CAPABILITY_CHANGED     = 102;
    protected static final int EVENT_MULTI_SIM_CONFIG_CHANGED     = 103;
    protected static final int EVENT_DEVICE_CONFIG_CHANGED        = 104;
    protected static final int EVENT_IMS_REGISTRATION_CHANGED     = 105;

    /** Feature flags */
    @NonNull
    private final FeatureFlags mFeatureFlags;

    /**
     * Init method to instantiate the object
     * Should only be called once.
     */
    public static SimultaneousCallingTracker init(Context context,
            @NonNull FeatureFlags featureFlags) {
        if (sInstance == null) {
            sInstance = new SimultaneousCallingTracker(context, featureFlags);
        } else {
            Log.wtf(LOG_TAG, "init() called multiple times!  sInstance = " + sInstance);
        }
        return sInstance;
    }

    /**
     * Constructor.
     * @param context context needed to send broadcast.
     */
    private SimultaneousCallingTracker(Context context, @NonNull FeatureFlags featureFlags) {
        mContext = context;
        mFeatureFlags = featureFlags;
        mHandler = new ConfigManagerHandler();
        mPhoneConfigurationManager = PhoneConfigurationManager.getInstance();
        mPhoneConfigurationManager.addListener(mPhoneConfigurationManagerListener);
        PhoneConfigurationManager.registerForMultiSimConfigChange(mHandler,
                EVENT_MULTI_SIM_CONFIG_CHANGED, null);
        TelephonyRegistryManager telephonyRegistryManager = (TelephonyRegistryManager)
                context.getSystemService(Context.TELEPHONY_REGISTRY_SERVICE);
        telephonyRegistryManager.addOnSubscriptionsChangedListener(
                mSubscriptionsChangedListener, new HandlerExecutor(mHandler));
    }

    /**
     * Static method to get instance.
     */
    public static SimultaneousCallingTracker getInstance() {
        if (sInstance == null) {
            Log.wtf(LOG_TAG, "getInstance null");
        }

        return sInstance;
    }

    /**
     * Handler class to handle callbacks
     */
    private final class ConfigManagerHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            if (!mFeatureFlags.simultaneousCallingIndications()) { return; }
            Log.v(LOG_TAG, "Received EVENT " + msg.what);
            switch (msg.what) {
                case EVENT_PHONE_CAPABILITY_CHANGED -> {
                    checkSimultaneousCallingDeviceCapability();
                }
                case EVENT_SUBSCRIPTION_CHANGED -> {
                    updatePhoneMapAndSimultaneousCallSupportMap();
                }
                case EVENT_MULTI_SIM_CONFIG_CHANGED -> {
                    int activeModemCount = (int) ((AsyncResult) msg.obj).result;
                    if (activeModemCount > 1) {
                        // SSIM --> MSIM: recalculate simultaneous calling supported combinations
                        updatePhoneMapAndSimultaneousCallSupportMap();
                    } else {
                        // MSIM --> SSIM: remove all simultaneous calling supported combinations
                        disableSimultaneousCallingSupport();
                        handleSimultaneousCallingSupportChanged();
                    }
                }
                case EVENT_DEVICE_CONFIG_CHANGED, EVENT_IMS_REGISTRATION_CHANGED -> {
                    updateSimultaneousCallSupportMap();
                }
                default -> Log.i(LOG_TAG, "Received unknown event: " + msg.what);
            }
        }
    }

    /**
     * Listener interface for events related to the {@link SimultaneousCallingTracker}.
     */
    public interface Listener {
        /**
         * Inform Telecom that the simultaneous calling subscription support map may have changed.
         *
         * @param simultaneousCallSubSupportMap Map of all voice capable subscription IDs mapped to
         *                                      a set containing the subscription IDs which that
         *                                      subscription is DSDA compatible with.
         */
        public void onSimultaneousCallingSupportChanged(Map<Integer,
                Set<Integer>> simultaneousCallSubSupportMap);
    }

    /**
     * Base listener implementation.
     */
    public abstract static class ListenerBase implements SimultaneousCallingTracker.Listener {
        @Override
        public void onSimultaneousCallingSupportChanged(Map<Integer,
                Set<Integer>> simultaneousCallSubSupportMap) {}
    }

    /**
     * Assign a listener to be notified of state changes.
     *
     * @param listener A listener.
     */
    public void addListener(Listener listener) {
        if (mFeatureFlags.simultaneousCallingIndications()) {
            mListeners.add(listener);
        }
    }

    /**
     * Removes a listener.
     *
     * @param listener A listener.
     */
    public final void removeListener(Listener listener) {
        if (mFeatureFlags.simultaneousCallingIndications()) {
            mListeners.remove(listener);
        }
    }

    /**
     * Listener for listening to events in the {@link android.telephony.TelephonyRegistryManager}
     */
    private final SubscriptionManager.OnSubscriptionsChangedListener mSubscriptionsChangedListener =
            new SubscriptionManager.OnSubscriptionsChangedListener() {
                @Override
                public void onSubscriptionsChanged() {
                    if (!mHandler.hasMessages(EVENT_SUBSCRIPTION_CHANGED)) {
                        mHandler.sendEmptyMessage(EVENT_SUBSCRIPTION_CHANGED);
                    }
                }
            };

    /**
     * Listener for listening to events in the {@link PhoneConfigurationManager}.
     */
    private final PhoneConfigurationManager.Listener mPhoneConfigurationManagerListener =
            new PhoneConfigurationManager.Listener() {
                @Override
                public void onPhoneCapabilityChanged() {
                    if (!mHandler.hasMessages(EVENT_PHONE_CAPABILITY_CHANGED)) {
                        mHandler.sendEmptyMessage(EVENT_PHONE_CAPABILITY_CHANGED);
                    }
                }
                @Override
                public void onDeviceConfigChanged() {
                    if (!mHandler.hasMessages(EVENT_DEVICE_CONFIG_CHANGED)) {
                        mHandler.sendEmptyMessage(EVENT_DEVICE_CONFIG_CHANGED);
                    }
                }
            };

    private void checkSimultaneousCallingDeviceCapability() {
        if (mPhoneConfigurationManager.getNumberOfModemsWithSimultaneousVoiceConnections() > 1) {
            isDeviceSimultaneousCallingCapable = true;
            mPhoneConfigurationManager.registerForSimultaneousCellularCallingSlotsChanged(
                    this::onSimultaneousCellularCallingSlotsChanged);
        }
    }

    /**
     *
     * @param subId to get the slots supporting simultaneous calling with
     * @return the set of subId's that support simultaneous calling with the param subId
     */
    public Set<Integer> getSubIdsSupportingSimultaneousCalling(int subId) {
        if (!isDeviceSimultaneousCallingCapable) {
            Log.v(LOG_TAG, "Device is not simultaneous calling capable");
            return Collections.emptySet();
        }
        for (int phoneId : mSimultaneousCallPhoneSupportMap.keySet()) {
            if (PhoneFactory.getPhone(phoneId).getSubId() == subId) {
                Set<Integer> subIdsSupportingSimultaneousCalling = new HashSet<>();
                for (Phone phone : mSimultaneousCallPhoneSupportMap.get(phoneId)) {
                    subIdsSupportingSimultaneousCalling.add(phone.getSubId());
                }
                Log.d(LOG_TAG, "getSlotsSupportingSimultaneousCalling for subId=" + subId +
                        "; subIdsSupportingSimultaneousCalling=[" +
                        getStringFromSet(subIdsSupportingSimultaneousCalling) + "].");
                return subIdsSupportingSimultaneousCalling;
            }
        }
        Log.e(LOG_TAG, "getSlotsSupportingSimultaneousCalling: Subscription ID not found in"
                + " the map of voice capable phones.");
        return Collections.emptySet();
    }

    private void updatePhoneMapAndSimultaneousCallSupportMap() {
        if (!isDeviceSimultaneousCallingCapable) {
            Log.d(LOG_TAG, "Ignoring updatePhoneMapAndSimultaneousCallSupportMap since device "
                    + "is not DSDA capable.");
            return;
        }
        unregisterForImsRegistrationChanges(mVoiceCapablePhoneMap);
        mVoiceCapablePhoneMap = generateVoiceCapablePhoneMapBasedOnUserAssociation();
        Log.i(LOG_TAG, "updatePhoneMapAndSimultaneousCallSupportMap: mVoiceCapablePhoneMap.size = "
                + mVoiceCapablePhoneMap.size());
        registerForImsRegistrationChanges(mVoiceCapablePhoneMap);
        updateSimultaneousCallSupportMap();
    }

    private void updateSimultaneousCallSupportMap() {
        if (!isDeviceSimultaneousCallingCapable) {
            Log.d(LOG_TAG, "Ignoring updateSimultaneousCallSupportMap since device is not DSDA"
                    + "capable.");
            return;
        }
        mSimultaneousCallPhoneSupportMap =
                generateSimultaneousCallSupportMap(mVoiceCapablePhoneMap);
        handleSimultaneousCallingSupportChanged();
    }

    /**
     * The simultaneous cellular calling slots have changed.
     * @param slotIds The Set of slotIds that have simultaneous cellular calling.
     */
    private void onSimultaneousCellularCallingSlotsChanged(Set<Integer> slotIds) {
        //Cellular calling slots have changed - regenerate simultaneous calling support map:
        updateSimultaneousCallSupportMap();
    }

    private void disableSimultaneousCallingSupport() {
        if (!isDeviceSimultaneousCallingCapable) {
            Log.d(LOG_TAG, "Ignoring updateSimultaneousCallSupportMap since device is not DSDA"
                    + "capable.");
            return;
        }
        unregisterForImsRegistrationChanges(mVoiceCapablePhoneMap);

        // In Single-SIM mode, simultaneous calling is not supported at all:
        mSimultaneousCallPhoneSupportMap.clear();
        mVoiceCapablePhoneMap.clear();
    }

    /**
     * Registers a listener to receive IMS registration changes for all phones in the phoneMap.
     *
     * @param phoneMap Map of voice capable phones mapped to the set of phones each has a compatible
     *                 user association with.
     */
    private void registerForImsRegistrationChanges(Map<Phone, Set<Phone>> phoneMap) {
        for (Phone phone : phoneMap.keySet()) {
            ImsPhone imsPhone = (ImsPhone) phone.getImsPhone();
            if (imsPhone != null) {
                Log.v(LOG_TAG, "registerForImsRegistrationChanges: registering phoneId = " +
                        phone.getPhoneId());
                imsPhone.registerForImsRegistrationChanges(mHandler,
                        EVENT_IMS_REGISTRATION_CHANGED, null);
            } else {
                Log.v(LOG_TAG, "registerForImsRegistrationChanges: phone not recognized as "
                        + "ImsPhone: phoneId = " + phone.getPhoneId());
            }
        }
    }

    /**
     * Unregisters the listener to stop receiving IMS registration changes for all phones in the
     * phoneMap.
     *
     * @param phoneMap Map of voice capable phones mapped to the set of phones each has a compatible
     *                 user association with.
     */
    private void unregisterForImsRegistrationChanges(Map<Phone, Set<Phone>> phoneMap) {
        for (Phone phone : phoneMap.keySet()) {
            ImsPhone imsPhone = (ImsPhone) phone.getImsPhone();
            if (imsPhone != null) {
                imsPhone.unregisterForImsRegistrationChanges(mHandler);
            }
        }
    }

    /**
     * Generates mVoiceCapablePhoneMap by iterating through {@link PhoneFactory#getPhones()} and
     * checking whether each {@link Phone} corresponds to a valid and voice capable subscription.
     * Maps the voice capable phones to the other voice capable phones that have compatible user
     * associations
     */
    private Map<Phone, Set<Phone>> generateVoiceCapablePhoneMapBasedOnUserAssociation() {
        Map<Phone, Set<Phone>> voiceCapablePhoneMap = new HashMap<>(3);

        // Generate a map of phone slots that corresponds to valid and voice capable subscriptions:
        Phone[] allPhones = PhoneFactory.getPhones();
        for (Phone phone : allPhones) {
            int subId = phone.getSubId();
            SubscriptionInfo subInfo =
                    SubscriptionManagerService.getInstance().getSubscriptionInfo(subId);

            if (mFeatureFlags.dataOnlyCellularService() &&
                    subId > SubscriptionManager.INVALID_SUBSCRIPTION_ID && subInfo != null &&
                    subInfo.getServiceCapabilities()
                            .contains(SubscriptionManager.SERVICE_CAPABILITY_VOICE)) {
                Log.v(LOG_TAG, "generateVoiceCapablePhoneMapBasedOnUserAssociation: adding "
                        + "phoneId = " + phone.getPhoneId());
                voiceCapablePhoneMap.put(phone, new HashSet<>(3));
            }
        }

        Map<Phone, Set<Phone>> userAssociationPhoneMap = new HashMap<>(3);
        // Map the voice capable phones to the others that have compatible user associations:
        for (Phone phone1 : voiceCapablePhoneMap.keySet()) {
            Set<Phone> phone1UserAssociationCompatiblePhones = new HashSet<>(3);
            for (Phone phone2 : voiceCapablePhoneMap.keySet()) {
                if (phone1.getPhoneId() == phone2.getPhoneId()) { continue; }
                if (phonesHaveSameUserAssociation(phone1, phone2)) {
                    phone1UserAssociationCompatiblePhones.add(phone2);
                }
            }
            userAssociationPhoneMap.put(phone1, phone1UserAssociationCompatiblePhones);
        }

        return userAssociationPhoneMap;
    }

    private Map<Integer, Set<Phone>> generateSimultaneousCallSupportMap(
            Map<Phone, Set<Phone>> phoneMap) {
        Map<Integer, Set<Phone>> simultaneousCallSubSupportMap = new HashMap<>(3);

        // Initially populate simultaneousCallSubSupportMap based on the passed in phoneMap:
        for (Phone phone : phoneMap.keySet()) {
            simultaneousCallSubSupportMap.put(phone.getPhoneId(),
                    new HashSet<>(phoneMap.get(phone)));
        }

        // Remove phone combinations that don't support simultaneous calling from the support map:
        for (Phone phone : phoneMap.keySet()) {
            if (phone.isImsRegistered()) {
                if (mPhoneConfigurationManager.isVirtualDsdaEnabled() ||
                        phone.isImsServiceSimultaneousCallingSupportCapable(mContext)) {
                    // Check if the transport types of each phone support simultaneous IMS calling:
                    int phone1TransportType = ((ImsPhone) phone.getImsPhone()).getTransportType();
                    if (phone1TransportType == AccessNetworkConstants.TRANSPORT_TYPE_WLAN) {
                        // The transport type of this phone is WLAN so all combos are supported:
                        continue;
                    }
                    for (Phone phone2 : phoneMap.keySet()) {
                        if (phone.getPhoneId() == phone2.getPhoneId()) { continue; }
                        if (!phonesSupportSimultaneousCallingViaCellularOrWlan(phone, phone2)) {
                            simultaneousCallSubSupportMap.get(phone.getPhoneId()).remove(phone2);
                        }
                    }
                } else {
                    // IMS is registered, vDSDA is disabled, but IMS is not DSDA capable so
                    // clear the map for this phone:
                    simultaneousCallSubSupportMap.get(phone.getPhoneId()).clear();
                }
            } else {
                // Check if this phone supports simultaneous cellular calling with other phones:
                for (Phone phone2 : phoneMap.keySet()) {
                    if (phone.getPhoneId() == phone2.getPhoneId()) { continue; }
                    if (!phonesSupportSimultaneousCallingViaCellularOrWlan(phone, phone2)) {
                        simultaneousCallSubSupportMap.get(phone.getPhoneId()).remove(phone2);
                    }
                }
            }
        }
        Log.v(LOG_TAG, "generateSimultaneousCallSupportMap: returning "
                + "simultaneousCallSubSupportMap = " +
                getStringFromMap(simultaneousCallSubSupportMap));
        return simultaneousCallSubSupportMap;
    }

    /**
     * Determines whether the {@link Phone} instances have compatible user associations. To be
     * considered compatible based on user association, both must be associated with the same
     * {@link android.os.UserHandle} or both must be unassociated.
     */
    private boolean phonesHaveSameUserAssociation(Phone phone1, Phone phone2) {
        return Objects.equals(phone1.getUserHandle(), phone2.getUserHandle());
    }

    private boolean phonesSupportCellularSimultaneousCalling(Phone phone1, Phone phone2) {
        Set<Integer> slotsSupportingSimultaneousCellularCalls =
                mPhoneConfigurationManager.getSlotsSupportingSimultaneousCellularCalls();
        Log.v(LOG_TAG, "phonesSupportCellularSimultaneousCalling: modem returned slots = " +
                getStringFromSet(slotsSupportingSimultaneousCellularCalls));
        if (slotsSupportingSimultaneousCellularCalls.contains(phone1.getPhoneId()) &&
                slotsSupportingSimultaneousCellularCalls.contains(phone2.getPhoneId())) {
            return true;
        };
        return false;
    }

    private boolean phonesSupportSimultaneousCallingViaCellularOrWlan(Phone phone1, Phone phone2) {
        int phone2TransportType =
                ((ImsPhone) phone2.getImsPhone()).getTransportType();
        return phone2TransportType == AccessNetworkConstants.TRANSPORT_TYPE_WLAN ||
                phonesSupportCellularSimultaneousCalling(phone1, phone2);
    }

    private void handleSimultaneousCallingSupportChanged() {
        try {
            Log.v(LOG_TAG, "handleSimultaneousCallingSupportChanged");
            // Convert mSimultaneousCallPhoneSupportMap to a map of each subId to a set of the
            // subIds it supports simultaneous calling with:
            Map<Integer, Set<Integer>> simultaneousCallSubscriptionIdMap = new HashMap<>();
            for (Integer phoneId : mSimultaneousCallPhoneSupportMap.keySet()) {
                Phone phone = PhoneFactory.getPhone(phoneId);
                if (phone == null) {
                    Log.wtf(LOG_TAG, "handleSimultaneousCallingSupportChanged: phoneId=" +
                            phoneId + " not found.");
                    return;
                }
                int subId = phone.getSubId();
                Set<Integer> supportedSubscriptionIds = new HashSet<>(3);
                for (Phone p : mSimultaneousCallPhoneSupportMap.get(phoneId)) {
                    supportedSubscriptionIds.add(p.getSubId());
                }
                simultaneousCallSubscriptionIdMap.put(subId, supportedSubscriptionIds);
            }

            // Notify listeners that simultaneous calling support has changed:
            for (Listener l : mListeners) {
                l.onSimultaneousCallingSupportChanged(simultaneousCallSubscriptionIdMap);
            }
        } catch (Exception e) {
            Log.w(LOG_TAG, "handleSimultaneousCallingSupportChanged: Exception = " + e);
        }
    }

    private String getStringFromMap(Map<Integer, Set<Phone>> phoneMap) {
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<Integer, Set<Phone>> entry : phoneMap.entrySet()) {
            sb.append("Phone ID=");
            sb.append(entry.getKey());
            sb.append(" - Simultaneous calling compatible phone IDs=[");
            sb.append(entry.getValue().stream().map(Phone::getPhoneId).map(String::valueOf)
                    .collect(Collectors.joining(", ")));
            sb.append("]; ");
        }
        return sb.toString();
    }

    private String getStringFromSet(Set<Integer> integerSet) {
        return integerSet.stream().map(String::valueOf).collect(Collectors.joining(","));
    }
}
