/*
 * Copyright (C) 2018 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.wifi.util;

import android.net.wifi.WifiConfiguration.NetworkSelectionStatus;
import android.util.SparseIntArray;

import com.android.server.wifi.WifiBlocklistMonitor;
import com.android.server.wifi.proto.nano.WifiMetricsProto.NetworkDisableReason;

/**
 * Utilities for Metrics collections.
 */
public class MetricsUtils {
    /**
     * A generic bucket containing a start, end, and count. The utility classes will convert to
     * such a generic bucket which can then be copied into the specific bucket of the proto.
     */
    public static class GenericBucket {
        public long start;
        public long end;
        public int count;
    }

    /**
     * Specifies a ~log histogram consisting of two levels of buckets - a set of N big buckets:
     *
     * Buckets starts at: B + P * M^i, where i=0, ... , N-1 (N big buckets)
     * Each big bucket is divided into S sub-buckets
     *
     * Each (big) bucket is M times bigger than the previous one.
     *
     * The buckets are then:
     * #0: B + P * M^0 with S buckets each of width (P*M^1-P*M^0)/S
     * #1: B + P * M^1 with S buckets each of width (P*M^2-P*M^1)/S
     * ...
     * #N-1: B + P * M^(N-1) with S buckets each of width (P*M^N-P*M^(N-1))/S
     */
    public static class LogHistParms {
        public LogHistParms(int b, int p, int m, int s, int n) {
            this.b = b;
            this.p = p;
            this.m = m;
            this.s = s;
            this.n = n;

            // derived values
            mLog = Math.log(m);
            bb = new double[n];
            sbw = new double[n];
            bb[0] = b + p;
            sbw[0] = p * (m - 1.0) / (double) s;
            for (int i = 1; i < n; ++i) {
                bb[i] = m * (bb[i - 1] - b) + b;
                sbw[i] = m * sbw[i - 1];
            }
        }

        // spec
        public int b;
        public int p;
        public int m;
        public int s;
        public int n;

        // derived
        public double mLog;
        public double[] bb; // bucket base
        public double[] sbw; // sub-bucket width
    }

    /**
     * Adds the input value to the log histogram based on the histogram parameters.
     */
    public static int addValueToLogHistogram(long x, SparseIntArray histogram, LogHistParms hp) {
        double logArg = (double) (x - hp.b) / (double) hp.p;
        int bigBucketIndex = -1;
        if (logArg > 0) {
            bigBucketIndex = (int) (Math.log(logArg) / hp.mLog);
        }
        int subBucketIndex;
        if (bigBucketIndex < 0) {
            bigBucketIndex = 0;
            subBucketIndex = 0;
        } else if (bigBucketIndex >= hp.n) {
            bigBucketIndex = hp.n - 1;
            subBucketIndex = hp.s - 1;
        } else {
            subBucketIndex = (int) ((x - hp.bb[bigBucketIndex]) / hp.sbw[bigBucketIndex]);
            if (subBucketIndex >= hp.s) { // probably a rounding error so move to next big bucket
                bigBucketIndex++;
                if (bigBucketIndex >= hp.n) {
                    bigBucketIndex = hp.n - 1;
                    subBucketIndex = hp.s - 1;
                } else {
                    subBucketIndex = (int) ((x - hp.bb[bigBucketIndex]) / hp.sbw[bigBucketIndex]);
                }
            }
        }
        int key = bigBucketIndex * hp.s + subBucketIndex;

        // note that get() returns 0 if index not there already
        int newValue = histogram.get(key) + 1;
        histogram.put(key, newValue);

        return newValue;
    }

    /**
     * Converts the log histogram (with the specified histogram parameters) to an array of generic
     * histogram buckets.
     */
    public static GenericBucket[] logHistogramToGenericBuckets(SparseIntArray histogram,
            LogHistParms hp) {
        GenericBucket[] protoArray = new GenericBucket[histogram.size()];
        for (int i = 0; i < histogram.size(); ++i) {
            int key = histogram.keyAt(i);

            protoArray[i] = new GenericBucket();
            protoArray[i].start = (long) (hp.bb[key / hp.s] + hp.sbw[key / hp.s] * (key % hp.s));
            protoArray[i].end = (long) (protoArray[i].start + hp.sbw[key / hp.s]);
            protoArray[i].count = histogram.valueAt(i);
        }

        return protoArray;
    }

    /**
     * Adds the input value to the histogram based on the lineaer histogram parameters.
     *
     * The 'int[] hp' contains a list of bucket limits. The number of buckets is hp.length() + 1
     * where buckets are:
     * - < hp[0]
     * - [hp[0], hp[1])
     * ...
     * - >= hp[hp.length() - 1]
     */
    public static int addValueToLinearHistogram(int x, SparseIntArray histogram, int[] hp) {
        int bucket = 0;
        for (int limit : hp) {
            if (x >= limit) {
                bucket++;
                continue;
            }
            break;
        }

        // note that get() returns 0 if index not there already
        int newValue = histogram.get(bucket) + 1;
        histogram.put(bucket, newValue);

        return newValue;
    }

    /**
     * Converts the histogram (with the specified linear histogram parameters) to an array of
     * generic histogram buckets.
     */
    public static GenericBucket[] linearHistogramToGenericBuckets(SparseIntArray histogram,
            int[] linearHistParams) {
        GenericBucket[] protoArray = new GenericBucket[histogram.size()];
        for (int i = 0; i < histogram.size(); ++i) {
            int bucket = histogram.keyAt(i);

            protoArray[i] = new GenericBucket();
            if (bucket == 0) {
                protoArray[i].start = Integer.MIN_VALUE;
                protoArray[i].end = linearHistParams[0];
            } else if (bucket != linearHistParams.length) {
                protoArray[i].start = linearHistParams[bucket - 1];
                protoArray[i].end = linearHistParams[bucket];
            } else {
                protoArray[i].start = linearHistParams[linearHistParams.length - 1];
                protoArray[i].end = Integer.MAX_VALUE;
            }
            protoArray[i].count = histogram.valueAt(i);
        }

        return protoArray;
    }

    /**
     * Converts NetworkSelectionStatus.NetworkSelectionDisableReason to
     * WifiMetricsProto.NetworkDisableReason.DisableReason
     */
    public static int convertNetworkSelectionDisableReasonToWifiProtoEnum(int reason) {
        switch (reason) {
            case NetworkSelectionStatus.DISABLED_ASSOCIATION_REJECTION:
                return NetworkDisableReason.REASON_ASSOCIATION_REJECTION;
            case NetworkSelectionStatus.DISABLED_AUTHENTICATION_FAILURE:
                return NetworkDisableReason.REASON_AUTHENTICATION_FAILURE;
            case NetworkSelectionStatus.DISABLED_DHCP_FAILURE:
                return NetworkDisableReason.REASON_DHCP_FAILURE;
            case NetworkSelectionStatus.DISABLED_NO_INTERNET_TEMPORARY:
            case NetworkSelectionStatus.DISABLED_NO_INTERNET_PERMANENT:
                return NetworkDisableReason.REASON_NETWORK_VALIDATION_FAILURE;
            case NetworkSelectionStatus.DISABLED_AUTHENTICATION_NO_CREDENTIALS:
                return NetworkDisableReason.REASON_AUTHENTICATION_NO_CREDENTIALS;
            case NetworkSelectionStatus.DISABLED_BY_WRONG_PASSWORD:
                return NetworkDisableReason.REASON_WRONG_PASSWORD;
            case NetworkSelectionStatus.DISABLED_AUTHENTICATION_NO_SUBSCRIPTION:
                return NetworkDisableReason.REASON_AUTHENTICATION_NO_SUBSCRIPTION;
            default:
                return NetworkDisableReason.REASON_UNKNOWN;
        }
    }

    /**
     * Converts WifiBlocklistMonitor.FailureReason to
     * WifiMetricsProto.NetworkDisableReason.DisableReason
     */
    public static int convertBssidBlocklistReasonToWifiProtoEnum(int reason) {
        switch (reason) {
            case WifiBlocklistMonitor.REASON_AP_UNABLE_TO_HANDLE_NEW_STA:
                return NetworkDisableReason.REASON_AP_UNABLE_TO_HANDLE_NEW_STA;
            case WifiBlocklistMonitor.REASON_NETWORK_VALIDATION_FAILURE:
                return NetworkDisableReason.REASON_NETWORK_VALIDATION_FAILURE;
            case WifiBlocklistMonitor.REASON_WRONG_PASSWORD:
                return NetworkDisableReason.REASON_WRONG_PASSWORD;
            case WifiBlocklistMonitor.REASON_EAP_FAILURE:
                return NetworkDisableReason.REASON_EAP_FAILURE;
            case WifiBlocklistMonitor.REASON_ASSOCIATION_REJECTION:
                return NetworkDisableReason.REASON_ASSOCIATION_REJECTION;
            case WifiBlocklistMonitor.REASON_ASSOCIATION_TIMEOUT:
                return NetworkDisableReason.REASON_ASSOCIATION_TIMEOUT;
            case WifiBlocklistMonitor.REASON_AUTHENTICATION_FAILURE:
                return NetworkDisableReason.REASON_AUTHENTICATION_FAILURE;
            case WifiBlocklistMonitor.REASON_DHCP_FAILURE:
                return NetworkDisableReason.REASON_DHCP_FAILURE;
            case WifiBlocklistMonitor.REASON_ABNORMAL_DISCONNECT:
                return NetworkDisableReason.REASON_ABNORMAL_DISCONNECT;
            case WifiBlocklistMonitor.REASON_FRAMEWORK_DISCONNECT_MBO_OCE:
                return NetworkDisableReason.REASON_FRAMEWORK_DISCONNECT_MBO_OCE;
            case WifiBlocklistMonitor.REASON_FRAMEWORK_DISCONNECT_FAST_RECONNECT:
                return NetworkDisableReason.REASON_FRAMEWORK_DISCONNECT_FAST_RECONNECT;
            case WifiBlocklistMonitor.REASON_FRAMEWORK_DISCONNECT_CONNECTED_SCORE:
                return NetworkDisableReason.REASON_FRAMEWORK_DISCONNECT_CONNECTED_SCORE;
            default:
                return NetworkDisableReason.REASON_UNKNOWN;
        }
    }
}
