/*
 * 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.ims.rcs.uce.presence.publish;

import android.telephony.CarrierConfigManager;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.IndentingPrintWriter;
import android.util.Log;

import com.android.ims.rcs.uce.util.FeatureTags;

import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Parses the Android Carrier Configuration for service-description -> feature tag mappings and
 * tracks the IMS registration to pass in the
 * to determine capabilities for features that the framework does not manage.
 *
 * @see CarrierConfigManager.Ims#KEY_PUBLISH_SERVICE_DESC_FEATURE_TAG_MAP_OVERRIDE_STRING_ARRAY for
 * more information on the format of this key.
 */
public class PublishServiceDescTracker {
    private static final String TAG = "PublishServiceDescTracker";

    /**
     * Map from (service-id, version) to the feature tags required in registration required in order
     * for the RCS feature to be considered "capable".
     * <p>
     * See {@link
     * CarrierConfigManager.Ims#KEY_PUBLISH_SERVICE_DESC_FEATURE_TAG_MAP_OVERRIDE_STRING_ARRAY}
     * for more information on how this can be overridden/extended.
     */
    private static final Map<ServiceDescription, Set<String>> DEFAULT_SERVICE_DESCRIPTION_MAP;
    static {
        ArrayMap<ServiceDescription, Set<String>> map = new ArrayMap<>(23);
        map.put(ServiceDescription.SERVICE_DESCRIPTION_CHAT_IM,
                Collections.singleton(FeatureTags.FEATURE_TAG_CHAT_IM));
        map.put(ServiceDescription.SERVICE_DESCRIPTION_CHAT_SESSION,
                Collections.singleton(FeatureTags.FEATURE_TAG_CHAT_SESSION));
        map.put(ServiceDescription.SERVICE_DESCRIPTION_FT,
                Collections.singleton(FeatureTags.FEATURE_TAG_FILE_TRANSFER));
        map.put(ServiceDescription.SERVICE_DESCRIPTION_FT_SMS,
                Collections.singleton(FeatureTags.FEATURE_TAG_FILE_TRANSFER_VIA_SMS));
        map.put(ServiceDescription.SERVICE_DESCRIPTION_PRESENCE,
                Collections.singleton(FeatureTags.FEATURE_TAG_PRESENCE));
        // Same service-ID & version for MMTEL, but different description.
        map.put(ServiceDescription.SERVICE_DESCRIPTION_MMTEL_VOICE,
                Collections.singleton(FeatureTags.FEATURE_TAG_MMTEL));
        map.put(ServiceDescription.SERVICE_DESCRIPTION_MMTEL_VOICE_VIDEO, new ArraySet<>(
                Arrays.asList(FeatureTags.FEATURE_TAG_MMTEL, FeatureTags.FEATURE_TAG_VIDEO)));
        map.put(ServiceDescription.SERVICE_DESCRIPTION_GEOPUSH,
                Collections.singleton(FeatureTags.FEATURE_TAG_GEO_PUSH));
        map.put(ServiceDescription.SERVICE_DESCRIPTION_GEOPUSH_SMS,
                Collections.singleton(FeatureTags.FEATURE_TAG_GEO_PUSH_VIA_SMS));
        map.put(ServiceDescription.SERVICE_DESCRIPTION_CALL_COMPOSER,
                Collections.singleton(FeatureTags.FEATURE_TAG_CALL_COMPOSER_ENRICHED_CALLING));
        map.put(ServiceDescription.SERVICE_DESCRIPTION_CALL_COMPOSER_MMTEL,
                Collections.singleton(FeatureTags.FEATURE_TAG_CALL_COMPOSER_VIA_TELEPHONY));
        map.put(ServiceDescription.SERVICE_DESCRIPTION_POST_CALL,
                Collections.singleton(FeatureTags.FEATURE_TAG_POST_CALL));
        map.put(ServiceDescription.SERVICE_DESCRIPTION_SHARED_MAP,
                Collections.singleton(FeatureTags.FEATURE_TAG_SHARED_MAP));
        map.put(ServiceDescription.SERVICE_DESCRIPTION_SHARED_SKETCH,
                Collections.singleton(FeatureTags.FEATURE_TAG_SHARED_SKETCH));
        // A map has one key and one value. And if the same key is used, the value is replaced
        // with a new one.
        // The service description between SERVICE_DESCRIPTION_CHATBOT_SESSION and
        // SERVICE_DESCRIPTION_CHATBOT_SESSION_V1 is the same, but this is for botVersion=#1 .
        map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SESSION, new ArraySet<>(
                Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_SESSION,
                        FeatureTags.FEATURE_TAG_CHATBOT_VERSION_SUPPORTED)));
        // This is the service description for botVersion=#1,#2 .
        map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SESSION_V1, new ArraySet<>(
                Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_SESSION,
                        FeatureTags.FEATURE_TAG_CHATBOT_VERSION_V2_SUPPORTED)));
        map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SESSION_V2, new ArraySet<>(
                Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_SESSION,
                        FeatureTags.FEATURE_TAG_CHATBOT_VERSION_V2_SUPPORTED)));
        // The service description between SERVICE_DESCRIPTION_CHATBOT_SA_SESSION and
        // SERVICE_DESCRIPTION_CHATBOT_SA_SESSION_V1 is the same, but this is for botVersion=#1 .
        map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SA_SESSION, new ArraySet<>(
                Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_STANDALONE_MSG,
                        FeatureTags.FEATURE_TAG_CHATBOT_VERSION_SUPPORTED)));
        // This is the service description for botVersion=#1,#2 .
        map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SA_SESSION_V1, new ArraySet<>(
                Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_STANDALONE_MSG,
                        FeatureTags.FEATURE_TAG_CHATBOT_VERSION_V2_SUPPORTED)));
        map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SA_SESSION_V2, new ArraySet<>(
                Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_STANDALONE_MSG,
                        FeatureTags.FEATURE_TAG_CHATBOT_VERSION_V2_SUPPORTED)));
        map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_ROLE,
                Collections.singleton(FeatureTags.FEATURE_TAG_CHATBOT_ROLE));
        map.put(ServiceDescription.SERVICE_DESCRIPTION_SLM, new ArraySet<>(
                Arrays.asList(FeatureTags.FEATURE_TAG_PAGER_MODE,
                        FeatureTags.FEATURE_TAG_LARGE_MODE,
                        FeatureTags.FEATURE_TAG_DEFERRED_MESSAGING,
                        FeatureTags.FEATURE_TAG_LARGE_PAGER_MODE)));
        map.put(ServiceDescription.SERVICE_DESCRIPTION_SLM_PAGER_LARGE, new ArraySet<>(
                Arrays.asList(FeatureTags.FEATURE_TAG_PAGER_MODE,
                        FeatureTags.FEATURE_TAG_LARGE_MODE)));
        DEFAULT_SERVICE_DESCRIPTION_MAP = Collections.unmodifiableMap(map);
    }

    // Maps from ServiceDescription to the set of feature tags required to consider the feature
    // capable for PUBLISH.
    private final Map<ServiceDescription, Set<String>> mServiceDescriptionFeatureTagMap;
    // Handles cases where multiple ServiceDescriptions match a subset of the same feature tags.
    // This will be used to only include the feature tags where the
    private final Set<ServiceDescription> mServiceDescriptionPartialMatches = new ArraySet<>();
    // The capabilities calculated based off of the last IMS registration.
    private final Set<ServiceDescription> mRegistrationCapabilities = new ArraySet<>();
    // Contains the feature tags used in the last update to IMS registration.
    private Set<String> mRegistrationFeatureTags = new ArraySet<>();

    /**
     * Create a new instance, which incorporates any carrier config overrides of the default
     * mapping.
     */
    public static PublishServiceDescTracker fromCarrierConfig(String[] carrierConfig) {
        Map<ServiceDescription, Set<String>> elements = new ArrayMap<>();
        for (Map.Entry<ServiceDescription, Set<String>> entry :
                DEFAULT_SERVICE_DESCRIPTION_MAP.entrySet()) {

            elements.put(entry.getKey(), entry.getValue().stream()
                    .map(PublishServiceDescTracker::removeInconsistencies)
                    .collect(Collectors.toSet()));
        }
        if (carrierConfig != null) {
            for (String entry : carrierConfig) {
                String[] serviceDesc = entry.split("\\|");
                if (serviceDesc.length < 4) {
                    Log.w(TAG, "fromCarrierConfig: error parsing " + entry);
                    continue;
                }
                elements.put(new ServiceDescription(serviceDesc[0].trim(), serviceDesc[1].trim(),
                        serviceDesc[2].trim()), parseFeatureTags(serviceDesc[3]));
            }
        }
        return new PublishServiceDescTracker(elements);
    }

    /**
     * Parse the feature tags in the string, which will be separated by ";".
     */
    private static Set<String> parseFeatureTags(String featureTags) {
        // First, split feature tags into individual params
        String[] featureTagSplit = featureTags.split(";");
        if (featureTagSplit.length == 0) {
            return Collections.emptySet();
        }
        ArraySet<String> tags = new ArraySet<>(featureTagSplit.length);
        // Add each tag, first trying to remove inconsistencies in string matching that may cause
        // it to fail.
        for (String tag : featureTagSplit) {
            tags.add(removeInconsistencies(tag));
        }
        return tags;
    }

    private PublishServiceDescTracker(Map<ServiceDescription, Set<String>> serviceFeatureTagMap) {
        mServiceDescriptionFeatureTagMap = serviceFeatureTagMap;
        Set<ServiceDescription> keySet = mServiceDescriptionFeatureTagMap.keySet();
        // Go through and collect any ServiceDescriptions that have the same service-id & version
        // (but not the same description) and add them to a "partial match" list.
        for (ServiceDescription c : keySet) {
            mServiceDescriptionPartialMatches.addAll(keySet.stream()
                    .filter(s -> !Objects.equals(s, c) && isSimilar(c , s))
                    .collect(Collectors.toList()));
        }
    }

    /**
     * Update the IMS registration associated with this tracker.
     * @param imsRegistration A List of feature tags that were associated with the last IMS
     *                        registration.
     */
    public void updateImsRegistration(Set<String> imsRegistration) {
        Set<String> sanitizedTags = imsRegistration.stream()
                // Ensure formatting passed in is the same as format stored here.
                .map(PublishServiceDescTracker::parseFeatureTags)
                // Each entry should only contain one feature tag.
                .map(s -> s.iterator().next()).collect(Collectors.toSet());
        // For aliased service descriptions (service-id && version is the same, but desc is
        // different), Keep a "score" of the number of feature tags that the service description
        // has associated with it. If another is found with a higher score, replace this one.
        Map<ServiceDescription, Integer> aliasedServiceDescScore = new ArrayMap<>();
        synchronized (mRegistrationCapabilities) {
            mRegistrationFeatureTags = imsRegistration;
            mRegistrationCapabilities.clear();
            for (Map.Entry<ServiceDescription, Set<String>> desc :
                    mServiceDescriptionFeatureTagMap.entrySet()) {
                boolean found = true;
                for (String tag : desc.getValue()) {
                    if (!sanitizedTags.contains(tag)) {
                        found = false;
                        break;
                    }
                }
                if (found) {
                    // There may be ambiguity with multiple entries having the same service-id &&
                    // version, but not the same description. In this case, we need to find any
                    // other entries with the same id & version and replace it with the new entry
                    // if it matches more "completely", i.e. match "mmtel;video" over "mmtel" if the
                    // registration set includes "mmtel;video". Skip putting that in for now and
                    // instead track the match with the most feature tags associated with it that
                    // are all found in the IMS registration.
                    if (mServiceDescriptionPartialMatches.contains(desc.getKey())) {
                        ServiceDescription aliasedDesc = aliasedServiceDescScore.keySet().stream()
                                .filter(s -> isSimilar(s, desc.getKey()))
                                .findFirst().orElse(null);
                        if (aliasedDesc != null) {
                            Integer prevEntrySize = aliasedServiceDescScore.get(aliasedDesc);
                            if (prevEntrySize != null
                                    // Overrides are added below the original map, so prefer those.
                                    && (prevEntrySize <= desc.getValue().size())) {
                                aliasedServiceDescScore.remove(aliasedDesc);
                                aliasedServiceDescScore.put(desc.getKey(), desc.getValue().size());
                            }
                        } else {
                            aliasedServiceDescScore.put(desc.getKey(), desc.getValue().size());
                        }
                    } else {
                        mRegistrationCapabilities.add(desc.getKey());
                    }
                }
            }
            // Collect the highest "scored" ServiceDescriptions and add themto registration caps.
            mRegistrationCapabilities.addAll(aliasedServiceDescScore.keySet());
        }
    }

    /**
     * @return A copy of the service-description pairs (service-id, version) that are associated
     * with the last IMS registration update in {@link #updateImsRegistration(Set)}
     */
    public Set<ServiceDescription> copyRegistrationCapabilities() {
        synchronized (mRegistrationCapabilities) {
            return new ArraySet<>(mRegistrationCapabilities);
        }
    }

    /**
     * @return A copy of the last update to the IMS feature tags via {@link #updateImsRegistration}.
     */
    public Set<String> copyRegistrationFeatureTags() {
        synchronized (mRegistrationCapabilities) {
            return new ArraySet<>(mRegistrationFeatureTags);
        }
    }

    /**
     * Dumps the current state of this tracker.
     */
    public void dump(PrintWriter printWriter) {
        IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, "  ");
        pw.println("PublishServiceDescTracker");
        pw.increaseIndent();

        pw.println("ServiceDescription -> Feature Tag Map:");
        pw.increaseIndent();
        for (Map.Entry<ServiceDescription, Set<String>> entry :
                mServiceDescriptionFeatureTagMap.entrySet()) {
            pw.print(entry.getKey());
            pw.print("->");
            pw.println(entry.getValue());
        }
        pw.println();
        pw.decreaseIndent();

        if (!mServiceDescriptionPartialMatches.isEmpty()) {
            pw.println("Similar ServiceDescriptions:");
            pw.increaseIndent();
            for (ServiceDescription entry : mServiceDescriptionPartialMatches) {
                pw.println(entry);
            }
            pw.decreaseIndent();
        } else {
            pw.println("No Similar ServiceDescriptions:");
        }
        pw.println();

        pw.println("Last IMS registration update:");
        pw.increaseIndent();
        for (String entry : mRegistrationFeatureTags) {
            pw.println(entry);
        }
        pw.println();
        pw.decreaseIndent();

        pw.println("Capabilities:");
        pw.increaseIndent();
        for (ServiceDescription entry : mRegistrationCapabilities) {
            pw.println(entry);
        }
        pw.println();
        pw.decreaseIndent();

        pw.decreaseIndent();
    }

    /**
     * Test if two ServiceDescriptions are similar, meaning service-id && version are equal.
     */
    private static boolean isSimilar(ServiceDescription a, ServiceDescription b) {
        return (a.serviceId.equals(b.serviceId) && a.version.equals(b.version));
    }

    /**
     * Remove any formatting inconsistencies that could make string matching difficult.
     */
    private static String removeInconsistencies(String tag) {
        tag = tag.toLowerCase();
        tag = tag.replaceAll("\\s+", "");
        return tag;
    }
}
