/*
 * Copyright (C) 2023 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.service.notification;

import android.annotation.IntDef;
import android.annotation.Nullable;
import android.app.Flags;
import android.util.ArrayMap;
import android.util.ArraySet;

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

/**
 * ZenModeDiff is a utility class meant to encapsulate the diff between ZenModeConfigs and their
 * subcomponents (automatic and manual ZenRules).
 *
 * <p>Note that this class is intended to detect <em>meaningful</em> differences, so objects that
 * are not identical (as per their {@code equals()} implementation) can still produce an empty diff
 * if only "metadata" fields are updated.
 *
 * @hide
 */
public class ZenModeDiff {
    /**
     * Enum representing whether the existence of a config or rule has changed (added or removed,
     * or "none" meaning there is no change, which may either mean both null, or there exists a
     * diff in fields rather than add/remove).
     */
    @IntDef(value = {
            NONE,
            ADDED,
            REMOVED,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface ExistenceChange{}

    public static final int NONE = 0;
    public static final int ADDED = 1;
    public static final int REMOVED = 2;

    /**
     * Diff class representing an individual field diff.
     * @param <T> The type of the field.
     */
    public static class FieldDiff<T> {
        private final T mFrom;
        private final T mTo;

        /**
         * Constructor to create a FieldDiff object with the given values.
         * @param from from (old) value
         * @param to to (new) value
         */
        public FieldDiff(@Nullable T from, @Nullable T to) {
            mFrom = from;
            mTo = to;
        }

        /**
         * Get the "from" value
         */
        public T from() {
            return mFrom;
        }

        /**
         * Get the "to" value
         */
        public T to() {
            return mTo;
        }

        /**
         * Get the string representation of this field diff, in the form of "from->to".
         */
        @Override
        public String toString() {
            return mFrom + "->" + mTo;
        }

        /**
         * Returns whether this represents an actual diff.
         */
        public boolean hasDiff() {
            // note that Objects.equals handles null values gracefully.
            return !Objects.equals(mFrom, mTo);
        }
    }

    /**
     * Base diff class that contains info about whether something was added, and a set of named
     * fields that changed.
     * Extend for diffs of specific types of objects.
     */
    private abstract static class BaseDiff {
        // Whether the diff was added or removed
        @ExistenceChange private int mExists = NONE;

        // Map from field name to diffs for any standalone fields in the object.
        private ArrayMap<String, FieldDiff> mFields = new ArrayMap<>();

        // Functions for actually diffing objects and string representations have to be implemented
        // by subclasses.

        /**
         * Return whether this diff represents any changes.
         */
        public abstract boolean hasDiff();

        /**
         * Return a string representation of the diff.
         */
        public abstract String toString();

        /**
         * Constructor that takes the two objects meant to be compared. This constructor sets
         * whether there is an existence change (added or removed).
         * @param from previous Object
         * @param to new Object
         */
        BaseDiff(Object from, Object to) {
            if (from == null) {
                if (to != null) {
                    mExists = ADDED;
                }
                // If both are null, there isn't an existence change; callers/inheritors must handle
                // the both null case.
            } else if (to == null) {
                // in this case, we know that from != null
                mExists = REMOVED;
            }

            // Subclasses should implement the actual diffing functionality in their own
            // constructors.
        }

        /**
         * Add a diff for a specific field to the map.
         * @param name field name
         * @param diff FieldDiff object representing the diff
         */
        final void addField(String name, FieldDiff diff) {
            mFields.put(name, diff);
        }

        /**
         * Returns whether this diff represents a config being newly added.
         */
        public final boolean wasAdded() {
            return mExists == ADDED;
        }

        /**
         * Returns whether this diff represents a config being removed.
         */
        public final boolean wasRemoved() {
            return mExists == REMOVED;
        }

        /**
         * Returns whether this diff represents an object being either added or removed.
         */
        public final boolean hasExistenceChange() {
            return mExists != NONE;
        }

        /**
         * Returns whether there are any individual field diffs.
         */
        public final boolean hasFieldDiffs() {
            return mFields.size() > 0;
        }

        /**
         * Returns the diff for the specific named field if it exists
         */
        public final FieldDiff getDiffForField(String name) {
            return mFields.getOrDefault(name, null);
        }

        /**
         * Get the set of all field names with some diff.
         */
        public final Set<String> fieldNamesWithDiff() {
            return mFields.keySet();
        }
    }

    /**
     * Diff class representing a diff between two ZenModeConfigs.
     */
    public static class ConfigDiff extends BaseDiff {
        // Rules. Automatic rule map is keyed by the rule name.
        private final ArrayMap<String, RuleDiff> mAutomaticRulesDiff = new ArrayMap<>();
        private RuleDiff mManualRuleDiff;

        // Field name constants
        public static final String FIELD_USER = "user";
        public static final String FIELD_ALLOW_ALARMS = "allowAlarms";
        public static final String FIELD_ALLOW_MEDIA = "allowMedia";
        public static final String FIELD_ALLOW_SYSTEM = "allowSystem";
        public static final String FIELD_ALLOW_CALLS = "allowCalls";
        public static final String FIELD_ALLOW_REMINDERS = "allowReminders";
        public static final String FIELD_ALLOW_EVENTS = "allowEvents";
        public static final String FIELD_ALLOW_REPEAT_CALLERS = "allowRepeatCallers";
        public static final String FIELD_ALLOW_MESSAGES = "allowMessages";
        public static final String FIELD_ALLOW_CONVERSATIONS = "allowConversations";
        public static final String FIELD_ALLOW_CALLS_FROM = "allowCallsFrom";
        public static final String FIELD_ALLOW_MESSAGES_FROM = "allowMessagesFrom";
        public static final String FIELD_ALLOW_CONVERSATIONS_FROM = "allowConversationsFrom";
        public static final String FIELD_SUPPRESSED_VISUAL_EFFECTS = "suppressedVisualEffects";
        public static final String FIELD_ARE_CHANNELS_BYPASSING_DND = "areChannelsBypassingDnd";
        public static final String FIELD_ALLOW_PRIORITY_CHANNELS = "allowPriorityChannels";
        private static final Set<String> PEOPLE_TYPE_FIELDS =
                Set.of(FIELD_ALLOW_CALLS_FROM, FIELD_ALLOW_MESSAGES_FROM);

        /**
         * Create a diff that contains diffs between the "from" and "to" ZenModeConfigs.
         *
         * @param from previous ZenModeConfig
         * @param to   new ZenModeConfig
         */
        public ConfigDiff(ZenModeConfig from, ZenModeConfig to) {
            super(from, to);
            // If both are null skip
            if (from == null && to == null) {
                return;
            }
            if (hasExistenceChange()) {
                // either added or removed; return here. otherwise (they're not both null) there's
                // field diffs.
                return;
            }

            // Now we compare all the fields, knowing there's a diff and that neither is null
            if (from.user != to.user) {
                addField(FIELD_USER, new FieldDiff<>(from.user, to.user));
            }
            if (from.allowAlarms != to.allowAlarms) {
                addField(FIELD_ALLOW_ALARMS, new FieldDiff<>(from.allowAlarms, to.allowAlarms));
            }
            if (from.allowMedia != to.allowMedia) {
                addField(FIELD_ALLOW_MEDIA, new FieldDiff<>(from.allowMedia, to.allowMedia));
            }
            if (from.allowSystem != to.allowSystem) {
                addField(FIELD_ALLOW_SYSTEM, new FieldDiff<>(from.allowSystem, to.allowSystem));
            }
            if (from.allowCalls != to.allowCalls) {
                addField(FIELD_ALLOW_CALLS, new FieldDiff<>(from.allowCalls, to.allowCalls));
            }
            if (from.allowReminders != to.allowReminders) {
                addField(FIELD_ALLOW_REMINDERS,
                        new FieldDiff<>(from.allowReminders, to.allowReminders));
            }
            if (from.allowEvents != to.allowEvents) {
                addField(FIELD_ALLOW_EVENTS, new FieldDiff<>(from.allowEvents, to.allowEvents));
            }
            if (from.allowRepeatCallers != to.allowRepeatCallers) {
                addField(FIELD_ALLOW_REPEAT_CALLERS,
                        new FieldDiff<>(from.allowRepeatCallers, to.allowRepeatCallers));
            }
            if (from.allowMessages != to.allowMessages) {
                addField(FIELD_ALLOW_MESSAGES,
                        new FieldDiff<>(from.allowMessages, to.allowMessages));
            }
            if (from.allowConversations != to.allowConversations) {
                addField(FIELD_ALLOW_CONVERSATIONS,
                        new FieldDiff<>(from.allowConversations, to.allowConversations));
            }
            if (from.allowCallsFrom != to.allowCallsFrom) {
                addField(FIELD_ALLOW_CALLS_FROM,
                        new FieldDiff<>(from.allowCallsFrom, to.allowCallsFrom));
            }
            if (from.allowMessagesFrom != to.allowMessagesFrom) {
                addField(FIELD_ALLOW_MESSAGES_FROM,
                        new FieldDiff<>(from.allowMessagesFrom, to.allowMessagesFrom));
            }
            if (from.allowConversationsFrom != to.allowConversationsFrom) {
                addField(FIELD_ALLOW_CONVERSATIONS_FROM,
                        new FieldDiff<>(from.allowConversationsFrom, to.allowConversationsFrom));
            }
            if (from.suppressedVisualEffects != to.suppressedVisualEffects) {
                addField(FIELD_SUPPRESSED_VISUAL_EFFECTS,
                        new FieldDiff<>(from.suppressedVisualEffects, to.suppressedVisualEffects));
            }
            if (from.areChannelsBypassingDnd != to.areChannelsBypassingDnd) {
                addField(FIELD_ARE_CHANNELS_BYPASSING_DND,
                        new FieldDiff<>(from.areChannelsBypassingDnd, to.areChannelsBypassingDnd));
            }
            if (Flags.modesApi()) {
                if (from.allowPriorityChannels != to.allowPriorityChannels) {
                    addField(FIELD_ALLOW_PRIORITY_CHANNELS,
                            new FieldDiff<>(from.allowPriorityChannels, to.allowPriorityChannels));
                }
            }

            // Compare automatic and manual rules
            final ArraySet<String> allRules = new ArraySet<>();
            addKeys(allRules, from.automaticRules);
            addKeys(allRules, to.automaticRules);
            final int num = allRules.size();
            for (int i = 0; i < num; i++) {
                final String rule = allRules.valueAt(i);
                final ZenModeConfig.ZenRule
                        fromRule = from.automaticRules != null ? from.automaticRules.get(rule)
                        : null;
                final ZenModeConfig.ZenRule
                        toRule = to.automaticRules != null ? to.automaticRules.get(rule) : null;
                RuleDiff ruleDiff = new RuleDiff(fromRule, toRule);
                if (ruleDiff.hasDiff()) {
                    mAutomaticRulesDiff.put(rule, ruleDiff);
                }
            }
            // If there's no diff this may turn out to be null, but that's also fine
            RuleDiff manualRuleDiff = new RuleDiff(from.manualRule, to.manualRule);
            if (manualRuleDiff.hasDiff()) {
                mManualRuleDiff = manualRuleDiff;
            }
        }

        private static <T> void addKeys(ArraySet<T> set, ArrayMap<T, ?> map) {
            if (map != null) {
                for (int i = 0; i < map.size(); i++) {
                    set.add(map.keyAt(i));
                }
            }
        }

        /**
         * Returns whether this diff object contains any diffs in any field.
         */
        @Override
        public boolean hasDiff() {
            return hasExistenceChange()
                    || hasFieldDiffs()
                    || mManualRuleDiff != null
                    || mAutomaticRulesDiff.size() > 0;
        }

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder("Diff[");
            if (!hasDiff()) {
                sb.append("no changes");
            }

            // If added or deleted, then that's just the end of it
            if (hasExistenceChange()) {
                if (wasAdded()) {
                    sb.append("added");
                } else if (wasRemoved()) {
                    sb.append("removed");
                }
            }

            // Handle top-level field change
            boolean first = true;
            for (String key : fieldNamesWithDiff()) {
                FieldDiff diff = getDiffForField(key);
                if (diff == null) {
                    // this shouldn't happen, but
                    continue;
                }
                if (first) {
                    first = false;
                } else {
                    sb.append(",\n");
                }

                // Some special handling for people- and conversation-type fields for readability
                if (PEOPLE_TYPE_FIELDS.contains(key)) {
                    sb.append(key);
                    sb.append(":");
                    sb.append(ZenModeConfig.sourceToString((int) diff.from()));
                    sb.append("->");
                    sb.append(ZenModeConfig.sourceToString((int) diff.to()));
                } else if (key.equals(FIELD_ALLOW_CONVERSATIONS_FROM)) {
                    sb.append(key);
                    sb.append(":");
                    sb.append(ZenPolicy.conversationTypeToString((int) diff.from()));
                    sb.append("->");
                    sb.append(ZenPolicy.conversationTypeToString((int) diff.to()));
                } else {
                    sb.append(key);
                    sb.append(":");
                    sb.append(diff);
                }
            }

            // manual rule
            if (mManualRuleDiff != null && mManualRuleDiff.hasDiff()) {
                if (first) {
                    first = false;
                } else {
                    sb.append(",\n");
                }
                sb.append("manualRule:");
                sb.append(mManualRuleDiff);
            }

            // automatic rules
            for (String rule : mAutomaticRulesDiff.keySet()) {
                RuleDiff diff = mAutomaticRulesDiff.get(rule);
                if (diff != null && diff.hasDiff()) {
                    if (first) {
                        first = false;
                    } else {
                        sb.append(",\n");
                    }
                    sb.append("automaticRule[");
                    sb.append(rule);
                    sb.append("]:");
                    sb.append(diff);
                }
            }

            return sb.append(']').toString();
        }

        /**
         * Get the diff in manual rule, if it exists.
         */
        public RuleDiff getManualRuleDiff() {
            return mManualRuleDiff;
        }

        /**
         * Get the full map of automatic rule diffs, or null if there are no diffs.
         */
        public ArrayMap<String, RuleDiff> getAllAutomaticRuleDiffs() {
            return (mAutomaticRulesDiff.size() > 0) ? mAutomaticRulesDiff : null;
        }
    }

    /**
     * Diff class representing a change between two ZenRules.
     */
    public static class RuleDiff extends BaseDiff {
        public static final String FIELD_ENABLED = "enabled";
        public static final String FIELD_SNOOZING = "snoozing";
        public static final String FIELD_NAME = "name";
        public static final String FIELD_ZEN_MODE = "zenMode";
        public static final String FIELD_CONDITION_ID = "conditionId";
        public static final String FIELD_CONDITION = "condition";
        public static final String FIELD_COMPONENT = "component";
        public static final String FIELD_CONFIGURATION_ACTIVITY = "configurationActivity";
        public static final String FIELD_ID = "id";
        public static final String FIELD_CREATION_TIME = "creationTime";
        public static final String FIELD_ENABLER = "enabler";
        public static final String FIELD_ZEN_POLICY = "zenPolicy";
        public static final String FIELD_ZEN_DEVICE_EFFECTS = "zenDeviceEffects";
        public static final String FIELD_MODIFIED = "modified";
        public static final String FIELD_PKG = "pkg";
        public static final String FIELD_ALLOW_MANUAL = "allowManualInvocation";
        public static final String FIELD_ICON_RES = "iconResName";
        public static final String FIELD_TRIGGER_DESCRIPTION = "triggerDescription";
        public static final String FIELD_TYPE = "type";
        // NOTE: new field strings must match the variable names in ZenModeConfig.ZenRule

        // Special field to track whether this rule became active or inactive
        FieldDiff<Boolean> mActiveDiff;

        /**
         * Create a RuleDiff representing the difference between two ZenRule objects.
         * @param from previous ZenRule
         * @param to new ZenRule
         * @return The diff between the two given ZenRules
         */
        public RuleDiff(ZenModeConfig.ZenRule from, ZenModeConfig.ZenRule to) {
            super(from, to);
            // Short-circuit the both-null case
            if (from == null && to == null) {
                return;
            }

            // Even if added or removed, there may be a change in whether or not it was active.
            // This only applies to automatic rules.
            boolean fromActive = from != null ? from.isAutomaticActive() : false;
            boolean toActive = to != null ? to.isAutomaticActive() : false;
            if (fromActive != toActive) {
                mActiveDiff = new FieldDiff<>(fromActive, toActive);
            }

            // Return if the diff was added or removed
            if (hasExistenceChange()) {
                return;
            }

            if (from.enabled != to.enabled) {
                addField(FIELD_ENABLED, new FieldDiff<>(from.enabled, to.enabled));
            }
            if (from.snoozing != to.snoozing) {
                addField(FIELD_SNOOZING, new FieldDiff<>(from.snoozing, to.snoozing));
            }
            if (!Objects.equals(from.name, to.name)) {
                addField(FIELD_NAME, new FieldDiff<>(from.name, to.name));
            }
            if (from.zenMode != to.zenMode) {
                addField(FIELD_ZEN_MODE, new FieldDiff<>(from.zenMode, to.zenMode));
            }
            if (!Objects.equals(from.conditionId, to.conditionId)) {
                addField(FIELD_CONDITION_ID, new FieldDiff<>(from.conditionId,
                        to.conditionId));
            }
            if (!Objects.equals(from.condition, to.condition)) {
                addField(FIELD_CONDITION, new FieldDiff<>(from.condition, to.condition));
            }
            if (!Objects.equals(from.component, to.component)) {
                addField(FIELD_COMPONENT, new FieldDiff<>(from.component, to.component));
            }
            if (!Objects.equals(from.configurationActivity, to.configurationActivity)) {
                addField(FIELD_CONFIGURATION_ACTIVITY, new FieldDiff<>(
                        from.configurationActivity, to.configurationActivity));
            }
            if (!Objects.equals(from.id, to.id)) {
                addField(FIELD_ID, new FieldDiff<>(from.id, to.id));
            }
            if (from.creationTime != to.creationTime) {
                addField(FIELD_CREATION_TIME,
                        new FieldDiff<>(from.creationTime, to.creationTime));
            }
            if (!Objects.equals(from.enabler, to.enabler)) {
                addField(FIELD_ENABLER, new FieldDiff<>(from.enabler, to.enabler));
            }
            if (!Objects.equals(from.zenPolicy, to.zenPolicy)) {
                addField(FIELD_ZEN_POLICY, new FieldDiff<>(from.zenPolicy, to.zenPolicy));
            }
            if (from.modified != to.modified) {
                addField(FIELD_MODIFIED, new FieldDiff<>(from.modified, to.modified));
            }
            if (!Objects.equals(from.pkg, to.pkg)) {
                addField(FIELD_PKG, new FieldDiff<>(from.pkg, to.pkg));
            }
            if (android.app.Flags.modesApi()) {
                if (!Objects.equals(from.zenDeviceEffects, to.zenDeviceEffects)) {
                    addField(FIELD_ZEN_DEVICE_EFFECTS,
                            new FieldDiff<>(from.zenDeviceEffects, to.zenDeviceEffects));
                }
                if (!Objects.equals(from.triggerDescription, to.triggerDescription)) {
                    addField(FIELD_TRIGGER_DESCRIPTION,
                            new FieldDiff<>(from.triggerDescription, to.triggerDescription));
                }
                if (from.type != to.type) {
                    addField(FIELD_TYPE, new FieldDiff<>(from.type, to.type));
                }
                if (from.allowManualInvocation != to.allowManualInvocation) {
                    addField(FIELD_ALLOW_MANUAL,
                            new FieldDiff<>(from.allowManualInvocation, to.allowManualInvocation));
                }
                if (!Objects.equals(from.iconResName, to.iconResName)) {
                    addField(FIELD_ICON_RES, new FieldDiff<>(from.iconResName, to.iconResName));
                }
            }
        }

        /**
         * Returns whether this object represents an actual diff.
         */
        @Override
        public boolean hasDiff() {
            return hasExistenceChange() || hasFieldDiffs();
        }

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder("ZenRuleDiff{");
            // If there's no diff, probably we haven't actually let this object continue existing
            // but might as well handle this case.
            if (!hasDiff()) {
                sb.append("no changes");
            }

            // If added or deleted, then that's just the end of it
            if (hasExistenceChange()) {
                if (wasAdded()) {
                    sb.append("added");
                } else if (wasRemoved()) {
                    sb.append("removed");
                }
            }

            // Go through all of the individual fields
            boolean first = true;
            for (String key : fieldNamesWithDiff()) {
                FieldDiff diff = getDiffForField(key);
                if (diff == null) {
                    // this shouldn't happen, but
                    continue;
                }
                if (first) {
                    first = false;
                } else {
                    sb.append(", ");
                }

                sb.append(key);
                sb.append(":");
                sb.append(diff);
            }

            if (becameActive()) {
                if (!first) {
                    sb.append(", ");
                }
                sb.append("(->active)");
            } else if (becameInactive()) {
                if (!first) {
                    sb.append(", ");
                }
                sb.append("(->inactive)");
            }

            return sb.append("}").toString();
        }

        /**
         * Returns whether this diff indicates that this (automatic) rule became active.
         */
        public boolean becameActive() {
            // if the "to" side is true, then it became active
            return mActiveDiff != null && mActiveDiff.to();
        }

        /**
         * Returns whether this diff indicates that this (automatic) rule became inactive.
         */
        public boolean becameInactive() {
            // if the "to" side is false, then it became inactive
            return mActiveDiff != null && !mActiveDiff.to();
        }
    }
}
