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

import android.annotation.NonNull;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Binder;
import android.os.SystemClock;
import android.os.UserHandle;
import android.service.notification.StatusBarNotification;
import android.util.ArrayMap;
import android.util.IntArray;
import android.util.Log;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto;
import com.android.server.pm.PackageManagerService;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
 * NotificationManagerService helper for handling snoozed notifications.
 */
public class SnoozeHelper {
    private static final String TAG = "SnoozeHelper";
    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
    private static final String INDENT = "    ";

    private static final String REPOST_ACTION = SnoozeHelper.class.getSimpleName() + ".EVALUATE";
    private static final int REQUEST_CODE_REPOST = 1;
    private static final String REPOST_SCHEME = "repost";
    private static final String EXTRA_KEY = "key";
    private static final String EXTRA_USER_ID = "userId";

    private final Context mContext;
    private AlarmManager mAm;
    private final ManagedServices.UserProfiles mUserProfiles;

    // User id : package name : notification key : record.
    private ArrayMap<Integer, ArrayMap<String, ArrayMap<String, NotificationRecord>>>
            mSnoozedNotifications = new ArrayMap<>();
    // notification key : package.
    private ArrayMap<String, String> mPackages = new ArrayMap<>();
    // key : userId
    private ArrayMap<String, Integer> mUsers = new ArrayMap<>();
    private Callback mCallback;

    public SnoozeHelper(Context context, Callback callback,
            ManagedServices.UserProfiles userProfiles) {
        mContext = context;
        IntentFilter filter = new IntentFilter(REPOST_ACTION);
        filter.addDataScheme(REPOST_SCHEME);
        mContext.registerReceiver(mBroadcastReceiver, filter);
        mAm = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
        mCallback = callback;
        mUserProfiles = userProfiles;
    }

    protected boolean isSnoozed(int userId, String pkg, String key) {
        return mSnoozedNotifications.containsKey(userId)
                && mSnoozedNotifications.get(userId).containsKey(pkg)
                && mSnoozedNotifications.get(userId).get(pkg).containsKey(key);
    }

    protected Collection<NotificationRecord> getSnoozed(int userId, String pkg) {
        if (mSnoozedNotifications.containsKey(userId)
                && mSnoozedNotifications.get(userId).containsKey(pkg)) {
            return mSnoozedNotifications.get(userId).get(pkg).values();
        }
        return Collections.EMPTY_LIST;
    }

    protected @NonNull List<NotificationRecord> getSnoozed() {
        List<NotificationRecord> snoozedForUser = new ArrayList<>();
        IntArray userIds = mUserProfiles.getCurrentProfileIds();
        if (userIds != null) {
            final int N = userIds.size();
            for (int i = 0; i < N; i++) {
                final ArrayMap<String, ArrayMap<String, NotificationRecord>> snoozedPkgs =
                        mSnoozedNotifications.get(userIds.get(i));
                if (snoozedPkgs != null) {
                    final int M = snoozedPkgs.size();
                    for (int j = 0; j < M; j++) {
                        final ArrayMap<String, NotificationRecord> records = snoozedPkgs.valueAt(j);
                        if (records != null) {
                            snoozedForUser.addAll(records.values());
                        }
                    }
                }
            }
        }
        return snoozedForUser;
    }

    /**
     * Snoozes a notification and schedules an alarm to repost at that time.
     */
    protected void snooze(NotificationRecord record, long duration) {
        snooze(record);
        scheduleRepost(record.sbn.getPackageName(), record.getKey(), record.getUserId(), duration);
    }

    /**
     * Records a snoozed notification.
     */
    protected void snooze(NotificationRecord record) {
        int userId = record.getUser().getIdentifier();
        if (DEBUG) {
            Slog.d(TAG, "Snoozing " + record.getKey());
        }
        ArrayMap<String, ArrayMap<String, NotificationRecord>> records =
                mSnoozedNotifications.get(userId);
        if (records == null) {
            records = new ArrayMap<>();
        }
        ArrayMap<String, NotificationRecord> pkgRecords = records.get(record.sbn.getPackageName());
        if (pkgRecords == null) {
            pkgRecords = new ArrayMap<>();
        }
        pkgRecords.put(record.getKey(), record);
        records.put(record.sbn.getPackageName(), pkgRecords);
        mSnoozedNotifications.put(userId, records);
        mPackages.put(record.getKey(), record.sbn.getPackageName());
        mUsers.put(record.getKey(), userId);
    }

    protected boolean cancel(int userId, String pkg, String tag, int id) {
        if (mSnoozedNotifications.containsKey(userId)) {
            ArrayMap<String, NotificationRecord> recordsForPkg =
                    mSnoozedNotifications.get(userId).get(pkg);
            if (recordsForPkg != null) {
                final Set<Map.Entry<String, NotificationRecord>> records = recordsForPkg.entrySet();
                for (Map.Entry<String, NotificationRecord> record : records) {
                    final StatusBarNotification sbn = record.getValue().sbn;
                    if (Objects.equals(sbn.getTag(), tag) && sbn.getId() == id) {
                        record.getValue().isCanceled = true;
                        return true;
                    }
                }
            }
        }
        return false;
    }

    protected boolean cancel(int userId, boolean includeCurrentProfiles) {
        int[] userIds = {userId};
        if (includeCurrentProfiles) {
            userIds = mUserProfiles.getCurrentProfileIds().toArray();
        }
        final int N = userIds.length;
        for (int i = 0; i < N; i++) {
            final ArrayMap<String, ArrayMap<String, NotificationRecord>> snoozedPkgs =
                    mSnoozedNotifications.get(userIds[i]);
            if (snoozedPkgs != null) {
                final int M = snoozedPkgs.size();
                for (int j = 0; j < M; j++) {
                    final ArrayMap<String, NotificationRecord> records = snoozedPkgs.valueAt(j);
                    if (records != null) {
                        int P = records.size();
                        for (int k = 0; k < P; k++) {
                            records.valueAt(k).isCanceled = true;
                        }
                    }
                }
                return true;
            }
        }
        return false;
    }

    protected boolean cancel(int userId, String pkg) {
        if (mSnoozedNotifications.containsKey(userId)) {
            if (mSnoozedNotifications.get(userId).containsKey(pkg)) {
                ArrayMap<String, NotificationRecord> records =
                        mSnoozedNotifications.get(userId).get(pkg);
                int N = records.size();
                for (int i = 0; i < N; i++) {
                    records.valueAt(i).isCanceled = true;
                }
                return true;
            }
        }
        return false;
    }

    /**
     * Updates the notification record so the most up to date information is shown on re-post.
     */
    protected void update(int userId, NotificationRecord record) {
        ArrayMap<String, ArrayMap<String, NotificationRecord>> records =
                mSnoozedNotifications.get(userId);
        if (records == null) {
            return;
        }
        ArrayMap<String, NotificationRecord> pkgRecords = records.get(record.sbn.getPackageName());
        if (pkgRecords == null) {
            return;
        }
        NotificationRecord existing = pkgRecords.get(record.getKey());
        pkgRecords.put(record.getKey(), record);
    }

    protected void repost(String key) {
        Integer userId = mUsers.get(key);
        if (userId != null) {
            repost(key, userId);
        }
    }

    protected void repost(String key, int userId) {
        final String pkg = mPackages.remove(key);
        ArrayMap<String, ArrayMap<String, NotificationRecord>> records =
                mSnoozedNotifications.get(userId);
        if (records == null) {
            return;
        }
        ArrayMap<String, NotificationRecord> pkgRecords = records.get(pkg);
        if (pkgRecords == null) {
            return;
        }
        final NotificationRecord record = pkgRecords.remove(key);
        mPackages.remove(key);
        mUsers.remove(key);

        if (record != null && !record.isCanceled) {
            MetricsLogger.action(record.getLogMaker()
                    .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED)
                    .setType(MetricsProto.MetricsEvent.TYPE_OPEN));
            mCallback.repost(userId, record);
        }
    }

    protected void repostGroupSummary(String pkg, int userId, String groupKey) {
        if (mSnoozedNotifications.containsKey(userId)) {
            ArrayMap<String, ArrayMap<String, NotificationRecord>> keysByPackage
                    = mSnoozedNotifications.get(userId);

            if (keysByPackage != null && keysByPackage.containsKey(pkg)) {
                ArrayMap<String, NotificationRecord> recordsByKey = keysByPackage.get(pkg);

                if (recordsByKey != null) {
                    String groupSummaryKey = null;
                    int N = recordsByKey.size();
                    for (int i = 0; i < N; i++) {
                        final NotificationRecord potentialGroupSummary = recordsByKey.valueAt(i);
                        if (potentialGroupSummary.sbn.isGroup()
                                && potentialGroupSummary.getNotification().isGroupSummary()
                                && groupKey.equals(potentialGroupSummary.getGroupKey())) {
                            groupSummaryKey = potentialGroupSummary.getKey();
                            break;
                        }
                    }

                    if (groupSummaryKey != null) {
                        NotificationRecord record = recordsByKey.remove(groupSummaryKey);
                        mPackages.remove(groupSummaryKey);
                        mUsers.remove(groupSummaryKey);

                        if (record != null && !record.isCanceled) {
                            MetricsLogger.action(record.getLogMaker()
                                    .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED)
                                    .setType(MetricsProto.MetricsEvent.TYPE_OPEN));
                            mCallback.repost(userId, record);
                        }
                    }
                }
            }
        }
    }

    protected void clearData(int userId, String pkg) {
        ArrayMap<String, ArrayMap<String, NotificationRecord>> records =
                mSnoozedNotifications.get(userId);
        if (records == null) {
            return;
        }
        ArrayMap<String, NotificationRecord> pkgRecords = records.get(pkg);
        if (pkgRecords == null) {
            return;
        }
        for (int i = pkgRecords.size() - 1; i >= 0; i--) {
            final NotificationRecord r = pkgRecords.removeAt(i);
            if (r != null) {
                mPackages.remove(r.getKey());
                mUsers.remove(r.getKey());
                final PendingIntent pi = createPendingIntent(pkg, r.getKey(), userId);
                mAm.cancel(pi);
                MetricsLogger.action(r.getLogMaker()
                        .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED)
                        .setType(MetricsProto.MetricsEvent.TYPE_DISMISS));
            }
        }
    }

    private PendingIntent createPendingIntent(String pkg, String key, int userId) {
        return PendingIntent.getBroadcast(mContext,
                REQUEST_CODE_REPOST,
                new Intent(REPOST_ACTION)
                        .setPackage(PackageManagerService.PLATFORM_PACKAGE_NAME)
                        .setData(new Uri.Builder().scheme(REPOST_SCHEME).appendPath(key).build())
                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
                        .putExtra(EXTRA_KEY, key)
                        .putExtra(EXTRA_USER_ID, userId),
                PendingIntent.FLAG_UPDATE_CURRENT);
    }

    private void scheduleRepost(String pkg, String key, int userId, long duration) {
        long identity = Binder.clearCallingIdentity();
        try {
            final PendingIntent pi = createPendingIntent(pkg, key, userId);
            mAm.cancel(pi);
            long time = SystemClock.elapsedRealtime() + duration;
            if (DEBUG) Slog.d(TAG, "Scheduling evaluate for " + new Date(time));
            mAm.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, time, pi);
        } finally {
            Binder.restoreCallingIdentity(identity);
        }
    }

    public void dump(PrintWriter pw, NotificationManagerService.DumpFilter filter) {
        pw.println("\n  Snoozed notifications:");
        for (int userId : mSnoozedNotifications.keySet()) {
            pw.print(INDENT);
            pw.println("user: " + userId);
            ArrayMap<String, ArrayMap<String, NotificationRecord>> snoozedPkgs =
                    mSnoozedNotifications.get(userId);
            for (String pkg : snoozedPkgs.keySet()) {
                pw.print(INDENT);
                pw.print(INDENT);
                pw.println("package: " + pkg);
                Set<String> snoozedKeys = snoozedPkgs.get(pkg).keySet();
                for (String key : snoozedKeys) {
                    pw.print(INDENT);
                    pw.print(INDENT);
                    pw.print(INDENT);
                    pw.println(key);
                }
            }
        }
    }

    protected void writeXml(XmlSerializer out, boolean forBackup) throws IOException {

    }

    public void readXml(XmlPullParser parser, boolean forRestore)
            throws XmlPullParserException, IOException {

    }

    @VisibleForTesting
    void setAlarmManager(AlarmManager am) {
        mAm = am;
    }

    protected interface Callback {
        void repost(int userId, NotificationRecord r);
    }

    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (DEBUG) {
                Slog.d(TAG, "Reposting notification");
            }
            if (REPOST_ACTION.equals(intent.getAction())) {
                repost(intent.getStringExtra(EXTRA_KEY), intent.getIntExtra(EXTRA_USER_ID,
                        UserHandle.USER_SYSTEM));
            }
        }
    };
}
