/*
 * Copyright (C) 2021 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.uicc;

import android.annotation.NonNull;
import android.content.Context;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
import android.telephony.SubscriptionInfo;
import android.util.IndentingPrintWriter;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.CommandsInterface;
import com.android.internal.telephony.IccLogicalChannelRequest;
import com.android.internal.telephony.TelephonyComponentFactory;
import com.android.internal.telephony.flags.FeatureFlags;
import com.android.internal.telephony.flags.FeatureFlagsImpl;
import com.android.internal.telephony.flags.Flags;
import com.android.telephony.Rlog;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;

public class UiccPort {
    protected static final String LOG_TAG = "UiccPort";
    protected static final boolean DBG = true;
    private static @NonNull FeatureFlags sFlags = new FeatureFlagsImpl();

    // The lock object is created by UiccSlot that owns this UiccCard - this is to share the lock
    // between UiccSlot, UiccCard, EuiccCard, UiccPort, EuiccPort and UiccProfile for now.
    protected final Object mLock;

    private String mIccid;
    protected String mCardId;
    private Context mContext;
    private CommandsInterface mCi;
    private UiccProfile mUiccProfile;

    private final int mPhoneId;
    private int mPortIdx;
    private int mPhysicalSlotIndex;

    // The list of the opened logical channel record.
    // The channels will be closed by us when detecting client died without closing them in advance.
    // The same lock should be used to protect both access of the list and the individual record.
    @GuardedBy("mOpenChannelRecords")
    private final List<OpenLogicalChannelRecord> mOpenChannelRecords = new ArrayList<>();

    public UiccPort(Context c, CommandsInterface ci, IccCardStatus ics, int phoneId, Object lock,
            UiccCard uiccCard) {
        if (DBG) log("Creating");
        mPhoneId = phoneId;
        mLock = lock;
        update(c, ci, ics, uiccCard);
    }

    /**
     * Update port. The main trigger for this is a change in the ICC Card status.
     */
    public void update(Context c, CommandsInterface ci, IccCardStatus ics, UiccCard uiccCard) {
        synchronized (mLock) {
            mContext = c;
            mCi = ci;
            mIccid = ics.iccid;
            mPortIdx = ics.mSlotPortMapping.mPortIndex;
            mPhysicalSlotIndex = ics.mSlotPortMapping.mPhysicalSlotIndex;
            if (mUiccProfile == null) {
                mUiccProfile = TelephonyComponentFactory.getInstance()
                        .inject(UiccProfile.class.getName()).makeUiccProfile(
                                mContext, mCi, ics, mPhoneId, uiccCard, mLock, sFlags);
            } else {
                mUiccProfile.update(mContext, mCi, ics);
            }
        }
    }

    /**
     * Dispose the port and its related Uicc profiles.
     */
    public void dispose() {
        synchronized (mLock) {
            if (DBG) log("Disposing Port");
            if (mUiccProfile != null) {
                mUiccProfile.dispose();
            }
            mUiccProfile = null;
        }
        cleanupOpenLogicalChannelRecordsIfNeeded();
    }

    @Override
    protected void finalize() {
        if (DBG) log("UiccPort finalized");
        cleanupOpenLogicalChannelRecordsIfNeeded();
    }

    /**
     * @deprecated Please use
     * {@link UiccProfile#isApplicationOnIcc(IccCardApplicationStatus.AppType)} instead.
     */
    @Deprecated
    public boolean isApplicationOnIcc(IccCardApplicationStatus.AppType type) {
        synchronized (mLock) {
            if (mUiccProfile != null) {
                return mUiccProfile.isApplicationOnIcc(type);
            } else {
                return false;
            }
        }
    }

    /**
     * @deprecated Please use {@link UiccProfile#getUniversalPinState()} instead.
     */
    @Deprecated
    public IccCardStatus.PinState getUniversalPinState() {
        synchronized (mLock) {
            if (mUiccProfile != null) {
                return mUiccProfile.getUniversalPinState();
            } else {
                return IccCardStatus.PinState.PINSTATE_UNKNOWN;
            }
        }
    }

    /**
     * @deprecated Please use {@link UiccProfile#getApplication(int)} instead.
     */
    @Deprecated
    public UiccCardApplication getApplication(int family) {
        synchronized (mLock) {
            if (mUiccProfile != null) {
                return mUiccProfile.getApplication(family);
            } else {
                return null;
            }
        }
    }

    /**
     * @deprecated Please use {@link UiccProfile#getApplicationIndex(int)} instead.
     */
    @Deprecated
    public UiccCardApplication getApplicationIndex(int index) {
        synchronized (mLock) {
            if (mUiccProfile != null) {
                return mUiccProfile.getApplicationIndex(index);
            } else {
                return null;
            }
        }
    }

    /**
     * Returns the SIM application of the specified type.
     *
     * @param type ICC application type
     * (@see com.android.internal.telephony.PhoneConstants#APPTYPE_xxx)
     * @return application corresponding to type or a null if no match found
     *
     * @deprecated Please use {@link UiccProfile#getApplicationByType(int)} instead.
     */
    @Deprecated
    public UiccCardApplication getApplicationByType(int type) {
        synchronized (mLock) {
            if (mUiccProfile != null) {
                return mUiccProfile.getApplicationByType(type);
            } else {
                return null;
            }
        }
    }

    /**
     * Resets the application with the input AID. Returns true if any changes were made.
     *
     * A null aid implies a card level reset - all applications must be reset.
     *
     * @deprecated Please use {@link UiccProfile#resetAppWithAid(String, boolean)} instead.
     */
    @Deprecated
    public boolean resetAppWithAid(String aid, boolean reset) {
        synchronized (mLock) {
            if (mUiccProfile != null) {
                return mUiccProfile.resetAppWithAid(aid, reset);
            } else {
                return false;
            }
        }
    }

    /**
     * Exposes {@link CommandsInterface#iccOpenLogicalChannel}
     * @deprecated Please use
     * {@link UiccProfile#iccOpenLogicalChannel(String, int, Message)} instead.
     */
    @Deprecated
    public void iccOpenLogicalChannel(String AID, int p2, Message response) {
        if (mUiccProfile != null) {
            mUiccProfile.iccOpenLogicalChannel(AID, p2, response);
        } else {
            loge("iccOpenLogicalChannel Failed!");
        }
    }

    /**
     * Exposes {@link CommandsInterface#iccCloseLogicalChannel}
     * @deprecated Please use
     * {@link UiccProfile#iccCloseLogicalChannel(int, boolean, Message)} instead.
     */
    @Deprecated
    public void iccCloseLogicalChannel(int channel, Message response) {
        if (mUiccProfile != null) {
            mUiccProfile.iccCloseLogicalChannel(channel, false /*isEs10*/, response);
        } else {
            loge("iccCloseLogicalChannel Failed!");
        }
    }

    /**
     * Exposes {@link CommandsInterface#iccTransmitApduLogicalChannel}
     * @deprecated Please use {@link
     * UiccProfile#iccTransmitApduLogicalChannel(int, int, int, int, int, int, String,
     * boolean, Message)} instead.
     */
    @Deprecated
    public void iccTransmitApduLogicalChannel(int channel, int cla, int command,
            int p1, int p2, int p3, String data, Message response) {
        if (mUiccProfile != null) {
            mUiccProfile.iccTransmitApduLogicalChannel(channel, cla, command, p1, p2, p3,
                    data, false /*isEs10Command*/, response);
        } else {
            loge("iccTransmitApduLogicalChannel Failed!");
        }
    }

    /**
     * Exposes {@link CommandsInterface#iccTransmitApduBasicChannel}
     * @deprecated Please use
     * {@link UiccProfile#iccTransmitApduBasicChannel(int, int, int, int, int, String, Message)}
     * instead.
     */
    @Deprecated
    public void iccTransmitApduBasicChannel(int cla, int command,
            int p1, int p2, int p3, String data, Message response) {
        if (mUiccProfile != null) {
            mUiccProfile.iccTransmitApduBasicChannel(cla, command, p1, p2, p3, data, response);
        } else {
            loge("iccTransmitApduBasicChannel Failed!");
        }
    }

    /**
     * Exposes {@link CommandsInterface#iccIO}
     * @deprecated Please use
     * {@link UiccProfile#iccExchangeSimIO(int, int, int, int, int, String, Message)} instead.
     */
    @Deprecated
    public void iccExchangeSimIO(int fileID, int command, int p1, int p2, int p3,
            String pathID, Message response) {
        if (mUiccProfile != null) {
            mUiccProfile.iccExchangeSimIO(fileID, command, p1, p2, p3, pathID, response);
        } else {
            loge("iccExchangeSimIO Failed!");
        }
    }

    /**
     * Exposes {@link CommandsInterface#sendEnvelopeWithStatus}
     * @deprecated Please use {@link UiccProfile#sendEnvelopeWithStatus(String, Message)} instead.
     */
    @Deprecated
    public void sendEnvelopeWithStatus(String contents, Message response) {
        if (mUiccProfile != null) {
            mUiccProfile.sendEnvelopeWithStatus(contents, response);
        } else {
            loge("sendEnvelopeWithStatus Failed!");
        }
    }

    /**
     * Returns number of applications on this card
     * @deprecated Please use {@link UiccProfile#getNumApplications()} instead.
     */
    @Deprecated
    public int getNumApplications() {
        if (mUiccProfile != null) {
            return mUiccProfile.getNumApplications();
        } else {
            return 0;
        }
    }

    public int getPhoneId() {
        return mPhoneId;
    }

    public int getPortIdx() {
        return mPortIdx;
    }

    public UiccProfile getUiccProfile() {
        return mUiccProfile;
    }

    /**
     * @deprecated Please use {@link UiccProfile#setOperatorBrandOverride(String)} instead.
     */
    @Deprecated
    public boolean setOperatorBrandOverride(String brand) {
        if (mUiccProfile != null) {
            return mUiccProfile.setOperatorBrandOverride(brand);
        } else {
            return false;
        }
    }

    /**
     * @deprecated Please use {@link UiccProfile#getOperatorBrandOverride()} instead.
     */
    @Deprecated
    public String getOperatorBrandOverride() {
        if (mUiccProfile != null) {
            return mUiccProfile.getOperatorBrandOverride();
        } else {
            return null;
        }
    }

    /**
     * Return the IccId corresponding to the port.
     */
    public String getIccId() {
        if (mIccid != null) {
            return mIccid;
        } else if (mUiccProfile != null) {
            return mUiccProfile.getIccId();
        } else {
            return null;
        }
    }

    private void log(String msg) {
        Rlog.d(LOG_TAG, msg);
    }

    private void loge(String msg) {
        Rlog.e(LOG_TAG, msg);
    }

    public void dump(FileDescriptor fd, PrintWriter printWriter, String[] args) {
        IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, "  ");
        pw.println("UiccPort:");
        pw.increaseIndent();
        pw.println("mPortIdx=" + mPortIdx);
        pw.println("mCi=" + mCi);
        pw.println("mIccid=" + SubscriptionInfo.getPrintableId(mIccid));
        pw.println("mPhoneId=" + mPhoneId);
        pw.println("mPhysicalSlotIndex=" + mPhysicalSlotIndex);
        synchronized (mOpenChannelRecords) {
            pw.println("mOpenChannelRecords=" + mOpenChannelRecords);
        }
        pw.println("mUiccProfile");
        if (mUiccProfile != null) {
            mUiccProfile.dump(fd, pw, args);
        }
    }

    /**
     * Informed that a logical channel has been successfully opened.
     *
     * @param request the original request to open the channel, with channel id attached.
     * @hide
     */
    public void onLogicalChannelOpened(@NonNull IccLogicalChannelRequest request) {
        OpenLogicalChannelRecord record = new OpenLogicalChannelRecord(request);
        try {
            synchronized (mOpenChannelRecords) {
                request.binder.linkToDeath(record, /*flags=*/ 0);
                mOpenChannelRecords.add(record);
            }
            if (DBG) log("onLogicalChannelOpened: monitoring client " + record);
        } catch (RemoteException | NullPointerException ex) {
            loge("IccOpenLogicChannel client has died, clean up manually");
            record.binderDied();
        }
    }

    /**
     * Informed that a logical channel has been successfully closed.
     *
     * @param channelId the channel id of the logical channel that was just closed.
     * @hide
     */
    public void onLogicalChannelClosed(int channelId) {
        OpenLogicalChannelRecord record = getOpenLogicalChannelRecord(channelId);
        synchronized (mOpenChannelRecords) {
            if (record != null && record.mRequest != null && record.mRequest.binder != null) {
                if (DBG) log("onLogicalChannelClosed: stop monitoring client " + record);
                record.mRequest.binder.unlinkToDeath(record, /*flags=*/ 0);
                record.mRequest.binder = null;
                mOpenChannelRecords.remove(record);
            }
        }
    }

    /** Get the OpenLogicalChannelRecord matching the channel id. */
    @VisibleForTesting
    public OpenLogicalChannelRecord getOpenLogicalChannelRecord(int channelId) {
        synchronized (mOpenChannelRecords) {
            for (OpenLogicalChannelRecord channelRecord : mOpenChannelRecords) {
                if (channelRecord.mRequest != null
                        && channelRecord.mRequest.channel == channelId) {
                    return channelRecord;
                }
            }
        }
        return null;
    }

    /**
     * Clean up records when logical channels underneath have been released, in cases like SIM
     * removal or modem reset. The obsoleted records may trigger a redundant release of logical
     * channel that may have been assigned to other client.
     */
    private void cleanupOpenLogicalChannelRecordsIfNeeded() {
        if (Flags.cleanupOpenLogicalChannelRecordOnDispose()) {
            synchronized (mOpenChannelRecords) {
                for (OpenLogicalChannelRecord record : mOpenChannelRecords) {
                    if (DBG) log("Clean up " + record);
                    record.mRequest.binder.unlinkToDeath(record, /*flags=*/ 0);
                    record.mRequest.binder = null;
                }
                mOpenChannelRecords.clear();
            }
        }
    }

    /** Record to keep open logical channel info. */
    @VisibleForTesting
    public class OpenLogicalChannelRecord implements IBinder.DeathRecipient {
        IccLogicalChannelRequest mRequest;

        OpenLogicalChannelRecord(IccLogicalChannelRequest request) {
            this.mRequest = request;
        }

        @Override
        public void binderDied() {
            loge("IccOpenLogicalChannelRecord: client died, close channel in record " + this);
            iccCloseLogicalChannel(mRequest.channel, /* response= */ null);
            onLogicalChannelClosed(mRequest.channel);
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder("OpenLogicalChannelRecord {");
            sb.append(" mRequest=" + mRequest).append("}");
            return sb.toString();
        }
    }
}
