/*
 * 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 com.android.internal.telephony.dataconnection;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.telephony.Annotation.ApnType;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.telephony.data.ApnSetting;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.PhoneConstants;
import com.android.internal.telephony.PhoneFactory;
import com.android.internal.telephony.SubscriptionController;
import com.android.internal.telephony.dataconnection.DataEnabledOverride.OverrideConditions.Condition;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
 * This class represents the rules for overriding data enabled settings in different conditions.
 * When data is disabled by the user, data can still be turned on temporarily when conditions
 * satisfy any rule here.
 */
public class DataEnabledOverride {

    private final Set<OverrideRule> mRules = new HashSet<>();

    /**
     * The rule for allowing data during voice call.
     */
    private static final OverrideRule OVERRIDE_RULE_ALLOW_DATA_DURING_VOICE_CALL =
            new OverrideRule(ApnSetting.TYPE_ALL, OverrideConditions.CONDITION_IN_VOICE_CALL
                    | OverrideConditions.CONDITION_NON_DEFAULT
                    | OverrideConditions.CONDITION_DEFAULT_DATA_ENABLED
                    | OverrideConditions.CONDITION_DSDS_ENABLED);

    /**
     * The rule for always allowing mms. Without adding any condition to the rule, any condition can
     * satisfy this rule for mms.
     */
    private static final OverrideRule OVERRIDE_RULE_ALWAYS_ALLOW_MMS =
            new OverrideRule(ApnSetting.TYPE_MMS, OverrideConditions.CONDITION_UNCONDITIONALLY);

    /**
     * Data enabled override rule
     */
    private static class OverrideRule {
        /**
         * APN type of the rule. The rule is APN type specific. The override is applicable to the
         * specified APN type as well. For now we only support one APN type per rule. Can be
         * expanded to multiple APN types in the future.
         */
        private final @ApnType int mApnType;

        /** The required conditions for overriding */
        private final OverrideConditions mRequiredConditions;

        /**
         * Constructor
         *
         * @param rule The override rule string. For example, {@code mms=nonDefault} or
         * {@code default=voiceCall & nonDefault}
         */
        OverrideRule(@NonNull String rule) {
            String[] tokens = rule.trim().split("\\s*=\\s*");
            if (tokens.length != 2) {
                throw new IllegalArgumentException("Invalid data enabled override rule format: "
                        + rule);
            }

            if (TextUtils.isEmpty(tokens[0])) {
                throw new IllegalArgumentException("APN type can't be empty");
            }

            mApnType = ApnSetting.getApnTypesBitmaskFromString(tokens[0]);
            if (mApnType == ApnSetting.TYPE_NONE) {
                throw new IllegalArgumentException("Invalid APN type. Rule=" + rule);
            }

            mRequiredConditions = new OverrideConditions(tokens[1]);
        }

        /**
         * Constructor
         *
         * @param apnType APN type of the rule
         * @param requiredConditions The required conditions for the rule
         */
        private OverrideRule(int apnType, int requiredConditions) {
            mApnType = apnType;
            mRequiredConditions = new OverrideConditions(requiredConditions);
        }

        /**
         * Check if this rule can be satisfied by the given APN type and provided conditions.
         *
         * @param apnType APN type to check
         * @param providedConditions The provided conditions to check
         * @return {@code true} if satisfied
         */
        boolean isSatisfiedByConditions(@ApnType int apnType, @Condition int providedConditions) {
            return (mApnType == apnType || mApnType == ApnSetting.TYPE_ALL)
                    && mRequiredConditions.allMet(providedConditions);
        }

        @Override
        public String toString() {
            return ApnSetting.getApnTypeString(mApnType) + "=" + mRequiredConditions;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            OverrideRule that = (OverrideRule) o;
            return mApnType == that.mApnType
                    && Objects.equals(mRequiredConditions, that.mRequiredConditions);
        }

        @Override
        public int hashCode() {
            return Objects.hash(mApnType, mRequiredConditions);
        }
    }

    /**
     * Represent the conditions for overriding data enabled settings
     */
    static class OverrideConditions {
        // Possible values for data enabled override condition. Note these flags are bitmasks.
        /** Unconditionally override enabled settings */
        static final int CONDITION_UNCONDITIONALLY = 0;

        /** Enable data only on subscription that is not user selected default data subscription */
        static final int CONDITION_NON_DEFAULT = 1 << 0;

        /** Enable data only when device has ongoing voice call */
        static final int CONDITION_IN_VOICE_CALL = 1 << 1;

        /** Enable data only when default data is on */
        static final int CONDITION_DEFAULT_DATA_ENABLED = 1 << 2;

        /** Enable data only when device is in DSDS mode */
        static final int CONDITION_DSDS_ENABLED = 1 << 3;

        /** Enable data unconditionally in string format */
        static final String CONDITION_UNCONDITIONALLY_STRING = "unconditionally";

        /** Enable data only on subscription that is not default in string format */
        static final String CONDITION_NON_DEFAULT_STRING = "nonDefault";

        /** Enable data only when device has ongoing voice call in string format */
        static final String CONDITION_VOICE_CALL_STRING = "inVoiceCall";

        /** Enable data only when default data is on in string format */
        static final String CONDITION_DEFAULT_DATA_ENABLED_STRING = "DefaultDataOn";

        /** Enable data only when device is in DSDS mode in string format */
        static final String CONDITION_DSDS_ENABLED_STRING = "dsdsEnabled";

        /** @hide */
        @IntDef(flag = true, prefix = { "OVERRIDE_CONDITION_" }, value = {
                CONDITION_NON_DEFAULT,
                CONDITION_IN_VOICE_CALL,
                CONDITION_DEFAULT_DATA_ENABLED,
                CONDITION_DSDS_ENABLED
        })
        @Retention(RetentionPolicy.SOURCE)
        public @interface Condition {}

        private static final Map<Integer, String> OVERRIDE_CONDITION_INT_MAP = new ArrayMap<>();
        private static final Map<String, Integer> OVERRIDE_CONDITION_STRING_MAP = new ArrayMap<>();

        static {
            OVERRIDE_CONDITION_INT_MAP.put(CONDITION_NON_DEFAULT,
                    CONDITION_NON_DEFAULT_STRING);
            OVERRIDE_CONDITION_INT_MAP.put(CONDITION_IN_VOICE_CALL,
                    CONDITION_VOICE_CALL_STRING);
            OVERRIDE_CONDITION_INT_MAP.put(CONDITION_DEFAULT_DATA_ENABLED,
                    CONDITION_DEFAULT_DATA_ENABLED_STRING);
            OVERRIDE_CONDITION_INT_MAP.put(CONDITION_DSDS_ENABLED,
                    CONDITION_DSDS_ENABLED_STRING);

            OVERRIDE_CONDITION_STRING_MAP.put(CONDITION_UNCONDITIONALLY_STRING,
                    CONDITION_UNCONDITIONALLY);
            OVERRIDE_CONDITION_STRING_MAP.put(CONDITION_NON_DEFAULT_STRING,
                    CONDITION_NON_DEFAULT);
            OVERRIDE_CONDITION_STRING_MAP.put(CONDITION_VOICE_CALL_STRING,
                    CONDITION_IN_VOICE_CALL);
            OVERRIDE_CONDITION_STRING_MAP.put(CONDITION_DEFAULT_DATA_ENABLED_STRING,
                    CONDITION_DEFAULT_DATA_ENABLED);
            OVERRIDE_CONDITION_STRING_MAP.put(CONDITION_DSDS_ENABLED_STRING,
                    CONDITION_DSDS_ENABLED);
        }

        private final @Condition int mConditions;

        /**
         * Conditions for overriding data enabled setting
         *
         * @param conditions Conditions in string format
         */
        OverrideConditions(@NonNull String conditions) {
            mConditions = getBitmaskFromString(conditions);
        }

        /**
         * Conditions for overriding data enabled setting
         *
         * @param conditions Conditions in bitmask
         */
        OverrideConditions(@Condition int conditions) {
            mConditions = conditions;
        }

        private static String getStringFromBitmask(@Condition int conditions) {
            if (conditions == CONDITION_UNCONDITIONALLY) {
                return CONDITION_UNCONDITIONALLY_STRING;
            }
            List<String> conditionsStrings = new ArrayList<>();
            for (Integer condition : OVERRIDE_CONDITION_INT_MAP.keySet()) {
                if ((conditions & condition) == condition) {
                    conditionsStrings.add(OVERRIDE_CONDITION_INT_MAP.get(condition));
                }
            }
            return TextUtils.join("&", conditionsStrings);
        }

        private static @Condition int getBitmaskFromString(@NonNull String str) {
            if (TextUtils.isEmpty(str)) {
                throw new IllegalArgumentException("Empty rule string");
            }

            String[] conditionStrings = str.trim().split("\\s*&\\s*");
            int bitmask = 0;

            for (String conditionStr : conditionStrings) {
                if (!TextUtils.isEmpty(conditionStr)) {
                    if (!OVERRIDE_CONDITION_STRING_MAP.containsKey(conditionStr)) {
                        throw new IllegalArgumentException("Invalid conditions: " + str);
                    }
                    bitmask |= OVERRIDE_CONDITION_STRING_MAP.get(conditionStr);
                }
            }

            return bitmask;
        }

        /**
         * Check if provided conditions can meet all conditions in the rule.
         *
         * @param providedConditions The provided conditions
         * @return {@code true} if all conditions are met.
         */
        boolean allMet(@Condition int providedConditions) {
            return (providedConditions & mConditions) == mConditions;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            OverrideConditions that = (OverrideConditions) o;
            return mConditions == that.mConditions;
        }

        @Override
        public int hashCode() {
            return Objects.hash(mConditions);
        }

        @Override
        public String toString() {
            return getStringFromBitmask(mConditions);
        }
    }

    /**
     * Constructor
     *
     * @param rules Data enabled override rules
     */
    public DataEnabledOverride(@NonNull String rules) {
        updateRules(rules);
    }

    /**
     * Update the data enabled override rules.
     *
     * @param newRules New override rules
     */
    @VisibleForTesting
    public void updateRules(@NonNull String newRules) {
        mRules.clear();
        String[] rulesString = newRules.trim().split("\\s*,\\s*");
        for (String rule : rulesString) {
            if (!TextUtils.isEmpty(rule)) {
                mRules.add(new OverrideRule(rule));
            }
        }
    }

    /**
     * Set always allowing MMS
     *
     * @param allow {@code true} if always allowing, otherwise {@code false}.
     */
    public void setAlwaysAllowMms(boolean allow) {
        if (allow) {
            mRules.add(OVERRIDE_RULE_ALWAYS_ALLOW_MMS);
        } else {
            mRules.remove(OVERRIDE_RULE_ALWAYS_ALLOW_MMS);
        }
    }

    /**
     * Set allowing mobile data during voice call. This is used for allowing data on the non-default
     * data SIM. When a voice call is placed on the non-default data SIM on DSDS devices, users will
     * not be able to use mobile data. By calling this API, data will be temporarily enabled on the
     * non-default data SIM during the life cycle of the voice call.
     *
     * @param allow {@code true} if allowing using data during voice call, {@code false} if
     * disallowed.
     */
    public void setDataAllowedInVoiceCall(boolean allow) {
        if (allow) {
            mRules.add(OVERRIDE_RULE_ALLOW_DATA_DURING_VOICE_CALL);
        } else {
            mRules.remove(OVERRIDE_RULE_ALLOW_DATA_DURING_VOICE_CALL);
        }
    }

    /**
     * Check if data is allowed during voice call.
     *
     * @return {@code true} if data is allowed during voice call.
     */
    public boolean isDataAllowedInVoiceCall() {
        return mRules.contains(OVERRIDE_RULE_ALLOW_DATA_DURING_VOICE_CALL);
    }

    private boolean canSatisfyAnyRule(@ApnType int apnType,
                                      @Condition int providedConditions) {
        for (OverrideRule rule : mRules) {
            if (rule.isSatisfiedByConditions(apnType, providedConditions)) {
                return true;
            }
        }
        return false;
    }

    private @Condition int getCurrentConditions(Phone phone) {
        int conditions = 0;

        if (phone != null) {
            // Check if the device is on voice call
            if (phone.getState() != PhoneConstants.State.IDLE) {
                conditions |= OverrideConditions.CONDITION_IN_VOICE_CALL;
            }

            int defaultDataSubId = SubscriptionController.getInstance().getDefaultDataSubId();

            if (phone.getSubId() != defaultDataSubId) {
                conditions |= OverrideConditions.CONDITION_NON_DEFAULT;
            }

            if (defaultDataSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
                int phoneId = SubscriptionController.getInstance().getPhoneId(defaultDataSubId);
                try {
                    Phone defaultDataPhone = PhoneFactory.getPhone(phoneId);
                    if (defaultDataPhone != null && defaultDataPhone.isUserDataEnabled()) {
                        conditions |= OverrideConditions.CONDITION_DEFAULT_DATA_ENABLED;
                    }
                } catch (IllegalStateException e) {
                    //ignore the exception and do not add the condition
                    Log.d("DataEnabledOverride", e.getMessage());
                }
            }

            if (TelephonyManager.from(phone.getContext()).isMultiSimEnabled()) {
                conditions |= OverrideConditions.CONDITION_DSDS_ENABLED;
            }
        }

        return conditions;
    }

    /**
     * Check for given APN type if we should enable data.
     *
     * @param phone Phone object
     * @param apnType APN type
     * @return {@code true} if data should be enabled for the current condition.
     */
    public boolean shouldOverrideDataEnabledSettings(Phone phone, @ApnType int apnType) {
        return canSatisfyAnyRule(apnType, getCurrentConditions(phone));
    }

    /**
     * Get data enabled override rules.
     *
     * @return Get data enabled override rules in string format
     */
    @NonNull
    public String getRules() {
        List<String> ruleStrings = new ArrayList<>();
        for (OverrideRule rule : mRules) {
            ruleStrings.add(rule.toString());
        }
        return TextUtils.join(",", ruleStrings);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        DataEnabledOverride that = (DataEnabledOverride) o;
        return mRules.equals(that.mRules);
    }

    @Override
    public int hashCode() {
        return Objects.hash(mRules);
    }

    @Override
    public String toString() {
        return "DataEnabledOverride: [rules=\"" + getRules() + "\"]";
    }
}
