/*
 * Copyright (C) 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.car.settings.datausage;

import static android.app.usage.NetworkStats.Bucket.UID_REMOVED;
import static android.app.usage.NetworkStats.Bucket.UID_TETHERING;

import android.annotation.NonNull;
import android.app.usage.NetworkStats;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.content.pm.UserInfo;
import android.net.NetworkTemplate;
import android.os.UserHandle;
import android.util.SparseArray;

import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceGroup;

import com.android.car.settings.R;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
import com.android.car.settings.common.ProgressBarPreference;
import com.android.car.settings.profiles.ProfileHelper;
import com.android.settingslib.AppItem;
import com.android.settingslib.net.UidDetail;
import com.android.settingslib.net.UidDetailProvider;
import com.android.settingslib.utils.ThreadUtils;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

import javax.annotation.Nullable;

/**
 * Controller that adds all the applications using the data sorted by the amount of data used. The
 * first application that used most amount of data will be at the top with progress 100 percentage.
 * All other progress are calculated relatively.
 */
public class AppDataUsagePreferenceController extends
        PreferenceController<PreferenceGroup> implements AppsNetworkStatsManager.Callback {

    private final UidDetailProvider mUidDetailProvider;
    private NetworkTemplate mNetworkTemplate;

    public AppDataUsagePreferenceController(Context context, String preferenceKey,
            FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
        this(context, preferenceKey, fragmentController, uxRestrictions,
                new UidDetailProvider(context));
    }

    @VisibleForTesting
    AppDataUsagePreferenceController(Context context, String preferenceKey,
            FragmentController fragmentController, CarUxRestrictions uxRestrictions,
            UidDetailProvider uidDetailProvider) {
        super(context, preferenceKey, fragmentController, uxRestrictions);
        mUidDetailProvider = uidDetailProvider;
    }

    @VisibleForTesting
    boolean hasNextBucket(@NonNull NetworkStats stats) {
        return stats.hasNextBucket();
    }

    @NonNull
    @VisibleForTesting
    NetworkStats.Bucket getNextBucket(@NonNull NetworkStats stats) {
        NetworkStats.Bucket bucket = new NetworkStats.Bucket();
        stats.getNextBucket(bucket);
        return bucket;
    }

    @Override
    protected Class<PreferenceGroup> getPreferenceType() {
        return PreferenceGroup.class;
    }

    @Override
    public void onDataLoaded(@Nullable NetworkStats stats, @Nullable int[] restrictedUids) {
        List<AppItem> items = new ArrayList<>();
        long largest = 0;

        List<UserInfo> profiles = ProfileHelper.getInstance(getContext()).getAllProfiles();
        SparseArray<AppItem> knownItems = new SparseArray<>();

        if (stats != null) {
            while (hasNextBucket(stats)) {
                NetworkStats.Bucket entry = getNextBucket(stats);
                long size = aggregateDataUsage(knownItems, items, entry, profiles);
                largest = Math.max(size, largest);
            }
        }

        updateRestrictedState(restrictedUids, knownItems, items, profiles);
        sortAndAddPreferences(items, largest);
    }

    /** Sets the {@link NetworkTemplate}  */
    public void setNetworkTemplate(NetworkTemplate networkTemplate) {
        mNetworkTemplate = networkTemplate;
    }

    private long aggregateDataUsage(SparseArray<AppItem> knownItems, List<AppItem> items,
            NetworkStats.Bucket entry, List<UserInfo> profiles) {
        int currentUserId = UserHandle.myUserId();

        // Decide how to collapse items together.
        int uid = entry.getUid();

        int collapseKey;
        int category;
        int userId = UserHandle.getUserId(uid);

        if (isUidValid(uid)) {
            collapseKey = uid;
            category = AppItem.CATEGORY_APP;
            return accumulate(collapseKey, knownItems, entry, category, items);
        }

        if (!UserHandle.isApp(uid)) {
            collapseKey = android.os.Process.SYSTEM_UID;
            category = AppItem.CATEGORY_APP;
            return accumulate(collapseKey, knownItems, entry, category, items);
        }

        if (profileContainsUserId(profiles, userId) && userId == currentUserId) {
            // Add to app item.
            collapseKey = uid;
            category = AppItem.CATEGORY_APP;
            return accumulate(collapseKey, knownItems, entry, category, items);
        }

        if (profileContainsUserId(profiles, userId) && userId != currentUserId) {
            // Add to a managed user item.
            int managedKey = UidDetailProvider.buildKeyForUser(userId);
            long usersLargest = accumulate(managedKey, knownItems, entry, AppItem.CATEGORY_USER,
                    items);
            collapseKey = uid;
            category = AppItem.CATEGORY_APP;
            long appLargest = accumulate(collapseKey, knownItems, entry, category, items);
            return Math.max(usersLargest, appLargest);
        }

        // If it is a removed user add it to the removed users' key.
        Optional<UserInfo> info = profiles.stream().filter(
                userInfo -> userInfo.id == userId).findFirst();
        if (!info.isPresent()) {
            collapseKey = UID_REMOVED;
            category = AppItem.CATEGORY_APP;
        } else {
            // Add to other user item.
            collapseKey = UidDetailProvider.buildKeyForUser(userId);
            category = AppItem.CATEGORY_USER;
        }

        return accumulate(collapseKey, knownItems, entry, category, items);
    }

    /**
     * UID does not belong to a regular app and maybe belongs to a removed application or
     * application using for tethering traffic.
     */
    private boolean isUidValid(int uid) {
        return !UserHandle.isApp(uid) && (uid == UID_REMOVED || uid == UID_TETHERING);
    }

    private boolean profileContainsUserId(List<UserInfo> profiles, int userId) {
        return profiles.stream().anyMatch(userInfo -> userInfo.id == userId);
    }

    private void updateRestrictedState(@Nullable int[] restrictedUids,
            SparseArray<AppItem> knownItems, List<AppItem> items, List<UserInfo> profiles) {
        if (restrictedUids == null) {
            return;
        }

        for (int i = 0; i < restrictedUids.length; ++i) {
            int uid = restrictedUids[i];
            // Only splice in restricted state for current user or managed users.
            if (!profileContainsUserId(profiles, uid)) {
                continue;
            }

            AppItem item = knownItems.get(uid);
            if (item == null) {
                item = new AppItem(uid);
                item.total = -1;
                items.add(item);
                knownItems.put(item.key, item);
            }
            item.restricted = true;
        }
    }

    private void sortAndAddPreferences(List<AppItem> items, long largest) {
        getPreference().removeAll();
        Collections.sort(items);
        for (int i = 0; i < items.size(); i++) {
            int percentTotal = largest != 0 ? (int) (items.get(i).total * 100 / largest) : 0;
            AppDataUsagePreference preference = new AppDataUsagePreference(getContext(),
                    items.get(i), percentTotal, mUidDetailProvider);
            getPreference().addPreference(preference);
        }
    }

    /**
     * Accumulate data usage of a network stats entry for the item mapped by the collapse key.
     * Creates the item if needed.
     *
     * @param collapseKey  the collapse key used to map the item.
     * @param knownItems   collection of known (already existing) items.
     * @param entry        the network stats entry to extract data usage from.
     * @param itemCategory the item is categorized on the list view by this category. Must be
     */
    private static long accumulate(int collapseKey, SparseArray<AppItem> knownItems,
            NetworkStats.Bucket entry, int itemCategory, List<AppItem> items) {
        int uid = entry.getUid();
        AppItem item = knownItems.get(collapseKey);
        if (item == null) {
            item = new AppItem(collapseKey);
            item.category = itemCategory;
            items.add(item);
            knownItems.put(item.key, item);
        }
        item.addUid(uid);
        item.total += entry.getRxBytes() + entry.getTxBytes();
        return item.total;
    }

    private class AppDataUsagePreference extends ProgressBarPreference {

        private final AppItem mItem;
        private final int mPercent;
        private UidDetail mDetail;

        AppDataUsagePreference(Context context, AppItem item, int percent,
                UidDetailProvider provider) {
            super(context);
            mItem = item;
            mPercent = percent;
            setLayoutResource(R.layout.progress_bar_preference);
            setKey(String.valueOf(item.key));
            if (item.restricted && item.total <= 0) {
                setSummary(R.string.data_usage_app_restricted);
            } else {
                CharSequence s = DataUsageUtils.bytesToIecUnits(context, item.total);
                setSummary(s);
            }
            mDetail = provider.getUidDetail(item.key, /* blocking= */ false);
            if (mDetail != null) {
                setAppInfo();
                setOnClickListener();
            } else {
                ThreadUtils.postOnBackgroundThread(() -> {
                    mDetail = provider.getUidDetail(mItem.key, /* blocking= */ true);
                    ThreadUtils.postOnMainThread(() -> {
                        setAppInfo();
                        setOnClickListener();
                    });
                });
            }
        }

        private void setAppInfo() {
            if (mDetail != null) {
                setIcon(mDetail.icon);
                setTitle(mDetail.label);
                setProgress(mPercent);
            } else {
                setIcon(null);
                setTitle(null);
            }
        }

        private void setOnClickListener() {
            if (mDetail != null && mNetworkTemplate != null) {
                setOnPreferenceClickListener(p -> {
                    getFragmentController().launchFragment(
                            AppSpecificDataUsageFragment.getInstance(mItem, mNetworkTemplate));
                    return true;
                });
            }
        }
    }
}
