/*
 * Copyright 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 android.telephony;

import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.SparseArray;

import com.android.internal.telephony.flags.Flags;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Objects;

/**
 * Provides the barring configuration for a particular service type.
 *
 * Provides indication about the barring of a particular service for use. Certain barring types
 * are only valid for certain technology families. Any service that does not have a barring
 * configuration is unbarred by default.
 */
public final class BarringInfo implements Parcelable {

    /**
     * Barring Service Type
     *
     * @hide
     */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef(prefix = "BARRING_SERVICE_TYPE_", value = {
            BARRING_SERVICE_TYPE_CS_SERVICE,
            BARRING_SERVICE_TYPE_PS_SERVICE,
            BARRING_SERVICE_TYPE_CS_VOICE,
            BARRING_SERVICE_TYPE_MO_SIGNALLING,
            BARRING_SERVICE_TYPE_MO_DATA,
            BARRING_SERVICE_TYPE_CS_FALLBACK,
            BARRING_SERVICE_TYPE_MMTEL_VOICE,
            BARRING_SERVICE_TYPE_MMTEL_VIDEO,
            BARRING_SERVICE_TYPE_EMERGENCY,
            BARRING_SERVICE_TYPE_SMS})
    public @interface BarringServiceType {}

    /* Applicable to UTRAN */
    /** Barring indicator for circuit-switched service; applicable to UTRAN */
    public static final int BARRING_SERVICE_TYPE_CS_SERVICE =
            android.hardware.radio.network.BarringInfo.SERVICE_TYPE_CS_SERVICE;
    /** Barring indicator for packet-switched service; applicable to UTRAN */
    public static final int BARRING_SERVICE_TYPE_PS_SERVICE =
            android.hardware.radio.network.BarringInfo.SERVICE_TYPE_PS_SERVICE;
    /** Barring indicator for circuit-switched voice service; applicable to UTRAN */
    public static final int BARRING_SERVICE_TYPE_CS_VOICE =
            android.hardware.radio.network.BarringInfo.SERVICE_TYPE_CS_VOICE;

    /* Applicable to EUTRAN, NGRAN */
    /** Barring indicator for mobile-originated signalling; applicable to EUTRAN and NGRAN */
    public static final int BARRING_SERVICE_TYPE_MO_SIGNALLING =
            android.hardware.radio.network.BarringInfo.SERVICE_TYPE_MO_SIGNALLING;
    /** Barring indicator for mobile-originated data traffic; applicable to EUTRAN and NGRAN */
    public static final int BARRING_SERVICE_TYPE_MO_DATA =
            android.hardware.radio.network.BarringInfo.SERVICE_TYPE_MO_DATA;
    /** Barring indicator for circuit-switched fallback for voice; applicable to EUTRAN and NGRAN */
    public static final int BARRING_SERVICE_TYPE_CS_FALLBACK =
            android.hardware.radio.network.BarringInfo.SERVICE_TYPE_CS_FALLBACK;
    /** Barring indicator for MMTEL (IMS) voice; applicable to EUTRAN and NGRAN */
    public static final int BARRING_SERVICE_TYPE_MMTEL_VOICE =
            android.hardware.radio.network.BarringInfo.SERVICE_TYPE_MMTEL_VOICE;
    /** Barring indicator for MMTEL (IMS) video; applicable to EUTRAN and NGRAN */
    public static final int BARRING_SERVICE_TYPE_MMTEL_VIDEO =
            android.hardware.radio.network.BarringInfo.SERVICE_TYPE_MMTEL_VIDEO;

    /* Applicable to UTRAN, EUTRAN, NGRAN */
    /** Barring indicator for emergency services; applicable to UTRAN, EUTRAN, and NGRAN */
    public static final int BARRING_SERVICE_TYPE_EMERGENCY =
            android.hardware.radio.network.BarringInfo.SERVICE_TYPE_EMERGENCY;
    /** Barring indicator for SMS sending; applicable to UTRAN, EUTRAN, and NGRAN */
    public static final int BARRING_SERVICE_TYPE_SMS =
            android.hardware.radio.network.BarringInfo.SERVICE_TYPE_SMS;

    //TODO: add barring constants for Operator-Specific barring codes

    /** Describe the current barring configuration of a cell */
    public static final class BarringServiceInfo implements Parcelable {
        /**
         * Barring Type
         * @hide
         */
        @Retention(RetentionPolicy.SOURCE)
        @IntDef(prefix = "BARRING_TYPE_", value =
                    {BARRING_TYPE_NONE,
                    BARRING_TYPE_UNCONDITIONAL,
                    BARRING_TYPE_CONDITIONAL,
                    BARRING_TYPE_UNKNOWN})
        public @interface BarringType {}

        /** Barring is inactive */
        public static final int BARRING_TYPE_NONE =
                android.hardware.radio.network.BarringInfo.BARRING_TYPE_NONE;
        /** The service is barred */
        public static final int BARRING_TYPE_UNCONDITIONAL =
                android.hardware.radio.network.BarringInfo.BARRING_TYPE_UNCONDITIONAL;
        /** The service may be barred based on additional factors */
        public static final int BARRING_TYPE_CONDITIONAL =
                android.hardware.radio.network.BarringInfo.BARRING_TYPE_CONDITIONAL;

        /** If a modem does not report barring info, then the barring type will be UNKNOWN */
        public static final int BARRING_TYPE_UNKNOWN = -1;

        private final @BarringType int mBarringType;

        private final boolean mIsConditionallyBarred;
        private final int mConditionalBarringFactor;
        private final int mConditionalBarringTimeSeconds;

        /** @hide */
        public BarringServiceInfo(@BarringType int type) {
            this(type, false, 0, 0);
        }

        /** @hide */
        @TestApi
        public BarringServiceInfo(@BarringType int barringType, boolean isConditionallyBarred,
                int conditionalBarringFactor, int conditionalBarringTimeSeconds) {
            mBarringType = barringType;
            mIsConditionallyBarred = isConditionallyBarred;
            mConditionalBarringFactor = conditionalBarringFactor;
            mConditionalBarringTimeSeconds = conditionalBarringTimeSeconds;
        }

        public @BarringType int getBarringType() {
            return mBarringType;
        }

        /**
         * @return true if the conditional barring parameters have resulted in the service being
         *         barred; false if the service has either not been evaluated for conditional
         *         barring or has been evaluated and isn't barred.
         */
        public boolean isConditionallyBarred() {
            return mIsConditionallyBarred;
        }

        /**
         * @return the conditional barring factor as a percentage 0-100, which is the probability of
         *         a random device being allowed for a conditionally barred service.
         */
        public int getConditionalBarringFactor() {
            return mConditionalBarringFactor;
        }

        /**
         * @return the conditional barring time seconds, which is the interval between successive
         *         evaluations for conditional barring based on the barring factor.
         */
        @SuppressLint("MethodNameUnits")
        public int getConditionalBarringTimeSeconds() {
            return mConditionalBarringTimeSeconds;
        }

        /**
         * Return whether a service is currently barred based on the BarringInfo
         *
         * @return true if the service is currently being barred, otherwise false
         */
        public boolean isBarred() {
            return mBarringType == BarringServiceInfo.BARRING_TYPE_UNCONDITIONAL
                    || (mBarringType == BarringServiceInfo.BARRING_TYPE_CONDITIONAL
                            && mIsConditionallyBarred);
        }

        @Override
        public int hashCode() {
            return Objects.hash(mBarringType, mIsConditionallyBarred,
                    mConditionalBarringFactor, mConditionalBarringTimeSeconds);
        }

        @Override
        public boolean equals(Object rhs) {
            if (!(rhs instanceof BarringServiceInfo)) return false;

            BarringServiceInfo other = (BarringServiceInfo) rhs;
            return mBarringType == other.mBarringType
                    && mIsConditionallyBarred == other.mIsConditionallyBarred
                    && mConditionalBarringFactor == other.mConditionalBarringFactor
                    && mConditionalBarringTimeSeconds == other.mConditionalBarringTimeSeconds;
        }

        private static String barringTypeToString(@BarringType int barringType) {
            return switch (barringType) {
                case BARRING_TYPE_NONE -> "NONE";
                case BARRING_TYPE_CONDITIONAL -> "CONDITIONAL";
                case BARRING_TYPE_UNCONDITIONAL -> "UNCONDITIONAL";
                case BARRING_TYPE_UNKNOWN -> "UNKNOWN";
                default -> "UNKNOWN(" + barringType + ")";
            };
        }

        @Override
        public String toString() {
            return "BarringServiceInfo {mBarringType=" + barringTypeToString(mBarringType)
                    + ", mIsConditionallyBarred=" + mIsConditionallyBarred
                    + ", mConditionalBarringFactor=" + mConditionalBarringFactor
                    + ", mConditionalBarringTimeSeconds=" + mConditionalBarringTimeSeconds + "}";
        }

        /** @hide */
        public BarringServiceInfo(Parcel p) {
            mBarringType = p.readInt();
            mIsConditionallyBarred = p.readBoolean();
            mConditionalBarringFactor = p.readInt();
            mConditionalBarringTimeSeconds = p.readInt();
        }

        @Override
        public void writeToParcel(@NonNull Parcel dest, int flags) {
            dest.writeInt(mBarringType);
            dest.writeBoolean(mIsConditionallyBarred);
            dest.writeInt(mConditionalBarringFactor);
            dest.writeInt(mConditionalBarringTimeSeconds);
        }

        /* {@inheritDoc} */
        public static final @NonNull Parcelable.Creator<BarringServiceInfo> CREATOR =
                new Parcelable.Creator<BarringServiceInfo>() {
                    @Override
                    public BarringServiceInfo createFromParcel(Parcel source) {
                        return new BarringServiceInfo(source);
                    }

                    @Override
                    public BarringServiceInfo[] newArray(int size) {
                        return new BarringServiceInfo[size];
                    }
                };

        @Override
        public int describeContents() {
            return 0;
        }
    }

    private static final BarringServiceInfo BARRING_SERVICE_INFO_UNKNOWN =
            new BarringServiceInfo(BarringServiceInfo.BARRING_TYPE_UNKNOWN);

    private static final BarringServiceInfo BARRING_SERVICE_INFO_UNBARRED =
            new BarringServiceInfo(BarringServiceInfo.BARRING_TYPE_NONE);

    private CellIdentity mCellIdentity;

    // A SparseArray potentially mapping each BarringService type to a BarringServiceInfo config
    // that describes the current barring status of that particular service.
    private SparseArray<BarringServiceInfo> mBarringServiceInfos;

    /** @hide */
    @SystemApi
    public BarringInfo() {
        mBarringServiceInfos = new SparseArray<>();
    }

    /**
     * Constructor for new BarringInfo instances.
     *
     * @hide
     */
    @TestApi
    public BarringInfo(@Nullable CellIdentity barringCellId,
            @NonNull SparseArray<BarringServiceInfo> barringServiceInfos) {
        mCellIdentity = barringCellId;
        mBarringServiceInfos = barringServiceInfos;
    }

    /**
     * Get the BarringServiceInfo for a specified service.
     *
     * @return a BarringServiceInfo struct describing the current barring status for a service
     */
    public @NonNull BarringServiceInfo getBarringServiceInfo(@BarringServiceType int service) {
        BarringServiceInfo bsi = mBarringServiceInfos.get(service);
        // If barring is reported but not for a particular service, then we report the barring
        // type as UNKNOWN; if the modem reports barring info but doesn't report for a particular
        // service then we can safely assume that the service isn't barred (for instance because
        // that particular service isn't applicable to the current RAN).
        return (bsi != null) ? bsi : mBarringServiceInfos.size() > 0
                ? BARRING_SERVICE_INFO_UNBARRED : BARRING_SERVICE_INFO_UNKNOWN;
    }

    /**
     * Return the cell that reported the BarringInfo.
     *
     * @return the {@link CellIdentity} of the cell that is broadcasting this barring information.
     *
     * @hide
     */
    @SystemApi
    @FlaggedApi(Flags.FLAG_CELL_IDENTITY_FOR_BARRING_INFO)
    public @Nullable CellIdentity getCellIdentity() {
        return mCellIdentity;
    }

    /** @hide */
    @SystemApi
    public @NonNull BarringInfo createLocationInfoSanitizedCopy() {
        // The only thing that would need sanitizing is the CellIdentity
        if (mCellIdentity == null) return this;

        return new BarringInfo(mCellIdentity.sanitizeLocationInfo(), mBarringServiceInfos);
    }

    /** @hide */
    public BarringInfo(Parcel p) {
        mCellIdentity = p.readParcelable(CellIdentity.class.getClassLoader(), android.telephony.CellIdentity.class);
        mBarringServiceInfos = p.readSparseArray(BarringServiceInfo.class.getClassLoader(), android.telephony.BarringInfo.BarringServiceInfo.class);
    }

    @Override
    public void writeToParcel(@NonNull Parcel dest, int flags) {
        dest.writeParcelable(mCellIdentity, flags);
        dest.writeSparseArray(mBarringServiceInfos);
    }

    public static final @NonNull Parcelable.Creator<BarringInfo> CREATOR =
            new Parcelable.Creator<BarringInfo>() {
                @Override
                public BarringInfo createFromParcel(Parcel source) {
                    return new BarringInfo(source);
                }

                @Override
                public BarringInfo[] newArray(int size) {
                    return new BarringInfo[size];
                }
            };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public int hashCode() {
        int hash = mCellIdentity != null ? mCellIdentity.hashCode() : 7;
        for (int i = 0; i < mBarringServiceInfos.size(); i++) {
            hash = hash + 15 * mBarringServiceInfos.keyAt(i);
            hash = hash + 31 * mBarringServiceInfos.valueAt(i).hashCode();
        }
        return hash;
    }

    @Override
    public boolean equals(Object rhs) {
        if (!(rhs instanceof BarringInfo)) return false;

        BarringInfo bi = (BarringInfo) rhs;

        if (hashCode() != bi.hashCode()) return false;

        if (mBarringServiceInfos.size() != bi.mBarringServiceInfos.size()) return false;

        for (int i = 0; i < mBarringServiceInfos.size(); i++) {
            if (mBarringServiceInfos.keyAt(i) != bi.mBarringServiceInfos.keyAt(i)) return false;
            if (!Objects.equals(mBarringServiceInfos.valueAt(i),
                        bi.mBarringServiceInfos.valueAt(i))) {
                return false;
            }
        }
        return true;
    }

    @Override
    public String toString() {
        return "BarringInfo {mCellIdentity=" + mCellIdentity
               + ", mBarringServiceInfos=" + mBarringServiceInfos + "}";
    }
}
