/*
 * Copyright (C) 2020 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.settings.network;

import android.annotation.IntDef;
import android.content.Context;
import android.provider.Settings;
import android.telephony.TelephonyManager;
import android.telephony.UiccSlotInfo;
import android.util.Log;

import com.android.settingslib.utils.ThreadUtils;

import com.google.common.collect.ImmutableList;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class UiccSlotUtil {

    private static final String TAG = "UiccSlotUtil";

    private static final long DEFAULT_WAIT_AFTER_SWITCH_TIMEOUT_MILLIS = 25 * 1000L;
    ;

    public static final int INVALID_PHYSICAL_SLOT_ID = -1;

    /**
     * Mode for switching to eSIM slot which decides whether there is cleanup process, e.g.
     * disabling test profile, after eSIM slot is activated and whether we will wait it finished.
     */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({
        SwitchingEsimMode.NO_CLEANUP,
        SwitchingEsimMode.ASYNC_CLEANUP,
        SwitchingEsimMode.SYNC_CLEANUP
    })
    public @interface SwitchingEsimMode {
        /** No cleanup process after switching to eSIM slot */
        int NO_CLEANUP = 0;
        /** Has cleanup process, but we will not wait it finished. */
        int ASYNC_CLEANUP = 1;
        /** Has cleanup process and we will wait until it's finished */
        int SYNC_CLEANUP = 2;
    }

    /**
     * Returns an immutable list of all UICC slots. If TelephonyManager#getUiccSlotsInfo returns, it
     * returns an empty list instead.
     */
    public static ImmutableList<UiccSlotInfo> getSlotInfos(TelephonyManager telMgr) {
        UiccSlotInfo[] slotInfos = telMgr.getUiccSlotsInfo();
        if (slotInfos == null) {
            return ImmutableList.of();
        }
        return ImmutableList.copyOf(slotInfos);
    }

    /**
     * Switches to the removable slot. It waits for SIM_STATE_LOADED after switch. If slotId is
     * INVALID_PHYSICAL_SLOT_ID, the method will use the first detected inactive removable slot.
     *
     * @param slotId the physical removable slot id.
     * @param context the application context.
     * @throws UiccSlotsException if there is an error.
     */
    public static synchronized void switchToRemovableSlot(int slotId, Context context)
            throws UiccSlotsException {
        if (ThreadUtils.isMainThread()) {
            throw new IllegalThreadStateException(
                    "Do not call switchToRemovableSlot on the main thread.");
        }
        TelephonyManager telMgr = context.getSystemService(TelephonyManager.class);
        if (telMgr.isMultiSimEnabled()) {
            // If this device supports multiple active slots, don't mess with TelephonyManager.
            Log.i(TAG, "Multiple active slots supported. Not calling switchSlots.");
            return;
        }
        UiccSlotInfo[] slots = telMgr.getUiccSlotsInfo();
        if (slotId == INVALID_PHYSICAL_SLOT_ID) {
            for (int i = 0; i < slots.length; i++) {
                if (slots[i].isRemovable()
                        && !slots[i].getPorts().stream().findFirst().get().isActive()
                        && slots[i].getCardStateInfo() != UiccSlotInfo.CARD_STATE_INFO_ERROR
                        && slots[i].getCardStateInfo() != UiccSlotInfo.CARD_STATE_INFO_RESTRICTED) {
                    performSwitchToRemovableSlot(i, context);
                    return;
                }
            }
        } else {
            if (slotId >= slots.length || !slots[slotId].isRemovable()) {
                throw new UiccSlotsException("The given slotId is not a removable slot: " + slotId);
            }
            if (!slots[slotId].getPorts().stream().findFirst().get().isActive()) {
                performSwitchToRemovableSlot(slotId, context);
            }
        }
    }

    private static void performSwitchToRemovableSlot(int slotId, Context context)
            throws UiccSlotsException {
        CarrierConfigChangedReceiver receiver = null;
        long waitingTimeMillis =
                Settings.Global.getLong(
                        context.getContentResolver(),
                        Settings.Global.EUICC_SWITCH_SLOT_TIMEOUT_MILLIS,
                        DEFAULT_WAIT_AFTER_SWITCH_TIMEOUT_MILLIS);
        try {
            CountDownLatch latch = new CountDownLatch(1);
            receiver = new CarrierConfigChangedReceiver(latch);
            receiver.registerOn(context);
            switchSlots(context, slotId);
            latch.await(waitingTimeMillis, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            Log.e(TAG, "Failed switching to physical slot.", e);
        } finally {
            if (receiver != null) {
                context.unregisterReceiver(receiver);
            }
        }
    }

    /**
     * Changes the logical slot to physical slot mapping. OEM should override this to provide
     * device-specific implementation if the device supports switching slots.
     *
     * @param context the application context.
     * @param physicalSlots List of physical slot ids in the order of logical slots.
     */
    private static void switchSlots(Context context, int... physicalSlots)
            throws UiccSlotsException {
        TelephonyManager telMgr = context.getSystemService(TelephonyManager.class);
        if (telMgr.isMultiSimEnabled()) {
            // If this device supports multiple active slots, don't mess with TelephonyManager.
            Log.i(TAG, "Multiple active slots supported. Not calling switchSlots.");
            return;
        }
        if (!telMgr.switchSlots(physicalSlots)) {
            throw new UiccSlotsException("Failed to switch slots");
        }
    }
}
