/*
 * Copyright (c) 2019 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.ims;

import android.content.Context;
import android.net.Uri;
import android.os.IBinder;
import android.os.PersistableBundle;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.telephony.BinderCacheManager;
import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyFrameworkInitializer;
import android.telephony.ims.ImsException;
import android.telephony.ims.ImsService;
import android.telephony.ims.RcsUceAdapter.StackPublishTriggerType;
import android.telephony.ims.RegistrationManager;
import android.telephony.ims.aidl.ICapabilityExchangeEventListener;
import android.telephony.ims.aidl.IImsCapabilityCallback;
import android.telephony.ims.aidl.IImsConfig;
import android.telephony.ims.aidl.IImsRcsController;
import android.telephony.ims.aidl.IImsRcsFeature;
import android.telephony.ims.aidl.IImsRegistration;
import android.telephony.ims.aidl.IImsRegistrationCallback;
import android.telephony.ims.aidl.IOptionsRequestCallback;
import android.telephony.ims.aidl.IOptionsResponseCallback;
import android.telephony.ims.aidl.IPublishResponseCallback;
import android.telephony.ims.aidl.ISipTransport;
import android.telephony.ims.aidl.ISubscribeResponseCallback;
import android.telephony.ims.feature.CapabilityChangeRequest;
import android.telephony.ims.feature.ImsFeature;
import android.telephony.ims.feature.RcsFeature;
import android.telephony.ims.feature.RcsFeature.RcsImsCapabilities;
import android.telephony.ims.stub.ImsRegistrationImplBase;
import android.util.Log;

import com.android.ims.internal.IImsServiceFeatureCallback;
import com.android.internal.annotations.VisibleForTesting;
import com.android.telephony.Rlog;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;

/**
 * Encapsulates all logic related to the RcsFeature:
 * - Updating RcsFeature capabilities.
 * - Registering/Unregistering availability/registration callbacks.
 * - Querying Registration and Capability information.
 */
public class RcsFeatureManager implements FeatureUpdates {
    private static final String TAG = "RcsFeatureManager";
    private static boolean DBG = true;

    private static final int CAPABILITY_OPTIONS = RcsImsCapabilities.CAPABILITY_TYPE_OPTIONS_UCE;
    private static final int CAPABILITY_PRESENCE = RcsImsCapabilities.CAPABILITY_TYPE_PRESENCE_UCE;

    /**
     * The capability exchange event callbacks from the RcsFeature.
     */
    public interface CapabilityExchangeEventCallback {
        /**
         * Triggered by RcsFeature to publish the device's capabilities to the network.
         */
        void onRequestPublishCapabilities(@StackPublishTriggerType int publishTriggerType);

        /**
         * Notify that the devices is unpublished.
         */
        void onUnpublish();

        /**
         * Receive a capabilities request from the remote client.
         */
        void onRemoteCapabilityRequest(Uri contactUri,
                List<String> remoteCapabilities, IOptionsRequestCallback cb);
    }

    /*
     * Setup the listener to listen to the requests and updates from ImsService.
     */
    private ICapabilityExchangeEventListener mCapabilityEventListener =
            new ICapabilityExchangeEventListener.Stub() {
                @Override
                public void onRequestPublishCapabilities(@StackPublishTriggerType int type) {
                    mCapabilityEventCallback.forEach(
                            callback -> callback.onRequestPublishCapabilities(type));
                }

                @Override
                public void onUnpublish() {
                    mCapabilityEventCallback.forEach(callback -> callback.onUnpublish());
                }

                @Override
                public void onRemoteCapabilityRequest(Uri contactUri,
                        List<String> remoteCapabilities, IOptionsRequestCallback cb) {
                    mCapabilityEventCallback.forEach(
                            callback -> callback.onRemoteCapabilityRequest(
                                    contactUri, remoteCapabilities, cb));
                }
            };

    private final int mSlotId;
    private final Context mContext;
    private final Set<CapabilityExchangeEventCallback> mCapabilityEventCallback
            = new CopyOnWriteArraySet<>();
    private final BinderCacheManager<IImsRcsController> mBinderCache
            = new BinderCacheManager<>(RcsFeatureManager::getIImsRcsControllerInterface);

    @VisibleForTesting
    public RcsFeatureConnection mRcsFeatureConnection;

    /**
     * Use to obtain a FeatureConnector, which will maintain a consistent listener to the
     * RcsFeature attached to the specified slotId. If the RcsFeature changes (due to things like
     * SIM swap), a new RcsFeatureManager will be delivered to this Listener.
     * @param context The Context this connector should use.
     * @param slotId The slotId associated with the Listener and requested RcsFeature
     * @param listener The listener, which will be used to generate RcsFeatureManager instances.
     * @param executor The executor that the Listener callbacks will be called on.
     * @param logPrefix The prefix used in logging of the FeatureConnector for notable events.
     * @return A FeatureConnector, which will start delivering RcsFeatureManagers as the underlying
     * RcsFeature instances become available to the platform.
     * @see {@link FeatureConnector#connect()}.
     */
    public static FeatureConnector<RcsFeatureManager> getConnector(Context context, int slotId,
            FeatureConnector.Listener<RcsFeatureManager> listener, Executor executor,
            String logPrefix) {
        ArrayList<Integer> filter = new ArrayList<>();
        filter.add(ImsFeature.STATE_READY);
        return new FeatureConnector<>(context, slotId, RcsFeatureManager::new, logPrefix, filter,
                listener, executor);
    }

    /**
     * Use {@link #getConnector} to get an instance of this class.
     */
    private RcsFeatureManager(Context context, int slotId) {
        mContext = context;
        mSlotId = slotId;
    }

    /**
     * Opens a persistent connection to the RcsFeature. This must be called before the RcsFeature
     * can be used to communicate.
     */
    public void openConnection() throws android.telephony.ims.ImsException {
        try {
            mRcsFeatureConnection.setCapabilityExchangeEventListener(mCapabilityEventListener);
        } catch (RemoteException e){
            throw new android.telephony.ims.ImsException("Service is not available.",
                    android.telephony.ims.ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
        }
    }

    /**
     * Closes the persistent connection to the RcsFeature. This must be called when this manager
     * wishes to no longer be used to communicate with the RcsFeature.
     */
    public void releaseConnection() {
        try {
            mRcsFeatureConnection.setCapabilityExchangeEventListener(null);
        } catch (RemoteException e){
            // Connection may not be available at this point.
        }
        mRcsFeatureConnection.close();
        mCapabilityEventCallback.clear();
    }

    /**
     * Adds a callback for {@link CapabilityExchangeEventCallback}.
     * Note: These callbacks will be sent on the binder thread used to notify the callback.
     */
    public void addCapabilityEventCallback(CapabilityExchangeEventCallback listener) {
        mCapabilityEventCallback.add(listener);
    }

    /**
     * Removes an existing {@link CapabilityExchangeEventCallback}.
     */
    public void removeCapabilityEventCallback(CapabilityExchangeEventCallback listener) {
        mCapabilityEventCallback.remove(listener);
    }

    /**
     * Update the capabilities for this RcsFeature.
     */
    public void updateCapabilities(int newSubId) throws android.telephony.ims.ImsException {
        boolean optionsSupport = isOptionsSupported(newSubId);
        boolean presenceSupported = isPresenceSupported(newSubId);

        logi("Update capabilities for slot " + mSlotId + " and sub " + newSubId + ": options="
                + optionsSupport+ ", presence=" + presenceSupported);

        if (optionsSupport || presenceSupported) {
            CapabilityChangeRequest request = new CapabilityChangeRequest();
            if (optionsSupport) {
                addRcsUceCapability(request, CAPABILITY_OPTIONS);
            }
            if (presenceSupported) {
                addRcsUceCapability(request, CAPABILITY_PRESENCE);
            }
            sendCapabilityChangeRequest(request);
        } else {
            disableAllRcsUceCapabilities();
        }
    }

    /**
     * Add a {@link RegistrationManager.RegistrationCallback} callback that gets called when IMS
     * registration has changed for a specific subscription.
     */
    public void registerImsRegistrationCallback(int subId, IImsRegistrationCallback callback)
            throws android.telephony.ims.ImsException {
        try {
            mRcsFeatureConnection.addCallbackForSubscription(subId, callback);
        } catch (IllegalStateException e) {
            loge("registerImsRegistrationCallback error: ", e);
            throw new android.telephony.ims.ImsException("Can not register callback",
                    android.telephony.ims.ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
        }
    }

    /**
     * Add a {@link RegistrationManager.RegistrationCallback} callback that gets called when IMS
     * registration has changed, independent of the subscription it is currently on.
     */
    public void registerImsRegistrationCallback(IImsRegistrationCallback callback)
            throws android.telephony.ims.ImsException {
        try {
            mRcsFeatureConnection.addCallback(callback);
        } catch (IllegalStateException e) {
            loge("registerImsRegistrationCallback error: ", e);
            throw new android.telephony.ims.ImsException("Can not register callback",
                    android.telephony.ims.ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
        }
    }

    /**
     * Removes a previously registered {@link RegistrationManager.RegistrationCallback} callback
     * that is associated with a specific subscription.
     */
    public void unregisterImsRegistrationCallback(int subId, IImsRegistrationCallback callback) {
        mRcsFeatureConnection.removeCallbackForSubscription(subId, callback);
    }

    /**
     * Removes a previously registered {@link RegistrationManager.RegistrationCallback} callback
     * that was not associated with a subscription.
     */
    public void unregisterImsRegistrationCallback(IImsRegistrationCallback callback) {
        mRcsFeatureConnection.removeCallback(callback);
    }

    /**
     * Get the IMS RCS registration technology for this Phone,
     * defined in {@link ImsRegistrationImplBase}.
     */
    public void getImsRegistrationTech(Consumer<Integer> callback) {
        try {
            int tech = mRcsFeatureConnection.getRegistrationTech();
            callback.accept(tech);
        } catch (RemoteException e) {
            loge("getImsRegistrationTech error: ", e);
            callback.accept(ImsRegistrationImplBase.REGISTRATION_TECH_NONE);
        }
    }

    /**
     * Register an ImsCapabilityCallback with RCS service, which will provide RCS availability
     * updates.
     */
    public void registerRcsAvailabilityCallback(int subId, IImsCapabilityCallback callback)
            throws android.telephony.ims.ImsException {
        try {
            mRcsFeatureConnection.addCallbackForSubscription(subId, callback);
        } catch (IllegalStateException e) {
            loge("registerRcsAvailabilityCallback: ", e);
            throw new android.telephony.ims.ImsException("Can not register callback",
                    android.telephony.ims.ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
        }
    }

    /**
     * Remove an registered ImsCapabilityCallback from RCS service.
     */
    public void unregisterRcsAvailabilityCallback(int subId, IImsCapabilityCallback callback) {
            mRcsFeatureConnection.removeCallbackForSubscription(subId, callback);
    }

    public boolean isImsServiceCapable(@ImsService.ImsServiceCapability long capabilities)
            throws ImsException {
        try {
            return mRcsFeatureConnection.isCapable(capabilities);
        } catch (RemoteException e) {
            throw new ImsException(e.getMessage(), ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
        }
    }

    /**
     * @return The SipTransport interface if it exists or {@code null} if it does not exist due to
     * the ImsService not supporting it.
     */
    public ISipTransport getSipTransport() throws ImsException {
        if (!isImsServiceCapable(ImsService.CAPABILITY_SIP_DELEGATE_CREATION)) {
            return null;
        }
        return mRcsFeatureConnection.getSipTransport();
    }

    public IImsRegistration getImsRegistration() {
        return mRcsFeatureConnection.getRegistration();
    }

    /**
     * Query for the specific capability.
     */
    public boolean isCapable(
            @RcsImsCapabilities.RcsImsCapabilityFlag int capability,
            @ImsRegistrationImplBase.ImsRegistrationTech int radioTech)
            throws android.telephony.ims.ImsException {
        CountDownLatch latch = new CountDownLatch(1);
        AtomicReference<Boolean> capableRef = new AtomicReference<>();

        IImsCapabilityCallback callback = new IImsCapabilityCallback.Stub() {
            @Override
            public void onQueryCapabilityConfiguration(
                    int resultCapability, int resultRadioTech, boolean enabled) {
                if ((capability != resultCapability) || (radioTech != resultRadioTech)) {
                    return;
                }
                if (DBG) log("capable result:capability=" + capability + ", enabled=" + enabled);
                capableRef.set(enabled);
                latch.countDown();
            }

            @Override
            public void onCapabilitiesStatusChanged(int config) {
                // Don't handle it
            }

            @Override
            public void onChangeCapabilityConfigurationError(int capability, int radioTech,
                    int reason) {
                // Don't handle it
            }
        };

        try {
            if (DBG) log("Query capability: " + capability + ", radioTech=" + radioTech);
            mRcsFeatureConnection.queryCapabilityConfiguration(capability, radioTech, callback);
            return awaitResult(latch, capableRef);
        } catch (RemoteException e) {
            loge("isCapable error: ", e);
            throw new android.telephony.ims.ImsException("Can not determine capabilities",
                    android.telephony.ims.ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
        }
    }

    private static <T> T awaitResult(CountDownLatch latch, AtomicReference<T> resultRef) {
        try {
            latch.await();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return resultRef.get();
    }

    /**
     * Query the availability of an IMS RCS capability.
     */
    public boolean isAvailable(@RcsImsCapabilities.RcsImsCapabilityFlag int capability,
            @ImsRegistrationImplBase.ImsRegistrationTech int radioTech)
            throws android.telephony.ims.ImsException {
        try {
            if (mRcsFeatureConnection.getRegistrationTech() != radioTech) {
                return false;
            }
            int currentStatus = mRcsFeatureConnection.queryCapabilityStatus();
            return new RcsImsCapabilities(currentStatus).isCapable(capability);
        } catch (RemoteException e) {
            loge("isAvailable error: ", e);
            throw new android.telephony.ims.ImsException("Can not determine availability",
                    android.telephony.ims.ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
        }
    }

    /**
     * Add UCE capabilities with given type.
     * @param capability the specific RCS UCE capability wants to enable
     */
    public void addRcsUceCapability(CapabilityChangeRequest request,
            @RcsImsCapabilities.RcsImsCapabilityFlag int capability) {
        request.addCapabilitiesToEnableForTech(capability,
                ImsRegistrationImplBase.REGISTRATION_TECH_NR);
        request.addCapabilitiesToEnableForTech(capability,
                ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
        request.addCapabilitiesToEnableForTech(capability,
                ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN);
    }

    public void requestPublication(String pidfXml, IPublishResponseCallback responseCallback)
            throws RemoteException {
        mRcsFeatureConnection.requestPublication(pidfXml, responseCallback);
    }

    public void requestCapabilities(List<Uri> uris, ISubscribeResponseCallback c)
            throws RemoteException {
        mRcsFeatureConnection.requestCapabilities(uris, c);
    }

    public void sendOptionsCapabilityRequest(Uri contactUri, List<String> myCapabilities,
            IOptionsResponseCallback callback) throws RemoteException {
        mRcsFeatureConnection.sendOptionsCapabilityRequest(contactUri, myCapabilities, callback);
    }

    /**
     * Disable all of the UCE capabilities.
     */
    private void disableAllRcsUceCapabilities() throws android.telephony.ims.ImsException {
        final int techNr = ImsRegistrationImplBase.REGISTRATION_TECH_NR;
        final int techLte = ImsRegistrationImplBase.REGISTRATION_TECH_LTE;
        final int techIWlan = ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN;
        CapabilityChangeRequest request = new CapabilityChangeRequest();
        request.addCapabilitiesToDisableForTech(CAPABILITY_OPTIONS, techNr);
        request.addCapabilitiesToDisableForTech(CAPABILITY_OPTIONS, techLte);
        request.addCapabilitiesToDisableForTech(CAPABILITY_OPTIONS, techIWlan);
        request.addCapabilitiesToDisableForTech(CAPABILITY_PRESENCE, techNr);
        request.addCapabilitiesToDisableForTech(CAPABILITY_PRESENCE, techLte);
        request.addCapabilitiesToDisableForTech(CAPABILITY_PRESENCE, techIWlan);
        sendCapabilityChangeRequest(request);
    }

    private void sendCapabilityChangeRequest(CapabilityChangeRequest request)
            throws android.telephony.ims.ImsException {
        try {
            if (DBG) log("sendCapabilityChangeRequest: " + request);
            mRcsFeatureConnection.changeEnabledCapabilities(request, null);
        } catch (RemoteException e) {
            throw new android.telephony.ims.ImsException("Can not connect to service",
                    android.telephony.ims.ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
        }
    }

    private boolean isOptionsSupported(int subId) {
        return isCapabilityTypeSupported(mContext, subId, CAPABILITY_OPTIONS);
    }

    private boolean isPresenceSupported(int subId) {
        return isCapabilityTypeSupported(mContext, subId, CAPABILITY_PRESENCE);
    }

    /*
     * Check if the given type of capability is supported.
     */
    private static boolean isCapabilityTypeSupported(
        Context context, int subId, int capabilityType) {

        if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
            Log.e(TAG, "isCapabilityTypeSupported: Invalid subId=" + subId);
            return false;
        }

        CarrierConfigManager configManager =
            (CarrierConfigManager) context.getSystemService(Context.CARRIER_CONFIG_SERVICE);
        if (configManager == null) {
            Log.e(TAG, "isCapabilityTypeSupported: CarrierConfigManager is null, " + subId);
            return false;
        }

        PersistableBundle b = configManager.getConfigForSubId(subId);
        if (b == null) {
            Log.e(TAG, "isCapabilityTypeSupported: PersistableBundle is null, " + subId);
            return false;
        }

        if (capabilityType == CAPABILITY_OPTIONS) {
            return b.getBoolean(CarrierConfigManager.KEY_USE_RCS_SIP_OPTIONS_BOOL, false);
        } else if (capabilityType == CAPABILITY_PRESENCE) {
            return b.getBoolean(CarrierConfigManager.Ims.KEY_ENABLE_PRESENCE_PUBLISH_BOOL, false);
        }
        return false;
    }

    @Override
    public void registerFeatureCallback(int slotId, IImsServiceFeatureCallback cb) {
        IImsRcsController controller = mBinderCache.listenOnBinder(cb, () -> {
            try {
                cb.imsFeatureRemoved(
                        FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE);
            } catch (RemoteException ignore) {} // This is local.
        });

        try {
            if (controller == null) {
                Log.e(TAG, "registerRcsFeatureListener: IImsRcsController is null");
                cb.imsFeatureRemoved(FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE);
                return;
            }
            controller.registerRcsFeatureCallback(slotId, cb);
        } catch (ServiceSpecificException e) {
            try {
                switch (e.errorCode) {
                    case ImsException.CODE_ERROR_UNSUPPORTED_OPERATION:
                        cb.imsFeatureRemoved(FeatureConnector.UNAVAILABLE_REASON_IMS_UNSUPPORTED);
                        break;
                    default: {
                        cb.imsFeatureRemoved(
                                FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE);
                    }
                }
            } catch (RemoteException ignore) {} // Already dead anyway if this happens.
        } catch (RemoteException e) {
            try {
                cb.imsFeatureRemoved(FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE);
            } catch (RemoteException ignore) {} // Already dead if this happens.
        }
    }

    @Override
    public void unregisterFeatureCallback(IImsServiceFeatureCallback cb) {
        try {
            IImsRcsController imsRcsController = mBinderCache.removeRunnable(cb);
            if (imsRcsController != null) {
                imsRcsController.unregisterImsFeatureCallback(cb);
            }
        } catch (RemoteException e) {
            // This means that telephony died, so do not worry about it.
            Rlog.e(TAG, "unregisterImsFeatureCallback (RCS), RemoteException: "
                    + e.getMessage());
        }
    }

    private IImsRcsController getIImsRcsController() {
        return mBinderCache.getBinder();
    }

    private static IImsRcsController getIImsRcsControllerInterface() {
        IBinder binder = TelephonyFrameworkInitializer
                .getTelephonyServiceManager()
                .getTelephonyImsServiceRegisterer()
                .get();
        IImsRcsController c = IImsRcsController.Stub.asInterface(binder);
        return c;
    }

    @Override
    public void associate(ImsFeatureContainer c) {
        IImsRcsFeature f = IImsRcsFeature.Stub.asInterface(c.imsFeature);
        mRcsFeatureConnection = new RcsFeatureConnection(mContext, mSlotId, f, c.imsConfig,
                c.imsRegistration, c.sipTransport);
    }

    @Override
    public void invalidate() {
        mRcsFeatureConnection.onRemovedOrDied();
    }

    @Override
    public void updateFeatureState(int state) {
        mRcsFeatureConnection.updateFeatureState(state);
    }

    @Override
    public void updateFeatureCapabilities(long capabilities) {
        mRcsFeatureConnection.updateFeatureCapabilities(capabilities);
    }

    /**
     * Testing interface used to mock SubscriptionManager in testing
     * @hide
     */
    @VisibleForTesting
    public interface SubscriptionManagerProxy {
        /**
         * Mock-able interface for {@link SubscriptionManager#getSubId(int)} used for testing.
         */
        int getSubId(int slotId);
    }

    public IImsConfig getConfig() {
        return mRcsFeatureConnection.getConfig();
    }

    private static SubscriptionManagerProxy sSubscriptionManagerProxy
            = slotId -> {
                int[] subIds = SubscriptionManager.getSubId(slotId);
                if (subIds != null) {
                    return subIds[0];
                }
                return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
            };

    /**
     * Testing function used to mock SubscriptionManager in testing
     * @hide
     */
    @VisibleForTesting
    public static void setSubscriptionManager(SubscriptionManagerProxy proxy) {
        sSubscriptionManagerProxy = proxy;
    }

    private void log(String s) {
        Rlog.d(TAG + " [" + mSlotId + "]", s);
    }

    private void logi(String s) {
        Rlog.i(TAG + " [" + mSlotId + "]", s);
    }

    private void loge(String s) {
        Rlog.e(TAG + " [" + mSlotId + "]", s);
    }

    private void loge(String s, Throwable t) {
        Rlog.e(TAG + " [" + mSlotId + "]", s, t);
    }
}
