/*
 * 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.networkstack.metrics;

import android.net.wifi.WifiInfo;

import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import com.android.internal.util.HexDump;
import com.android.net.module.util.CollectionUtils;
import com.android.server.connectivity.nano.CellularData;
import com.android.server.connectivity.nano.DataStallEventProto;
import com.android.server.connectivity.nano.DnsEvent;
import com.android.server.connectivity.nano.WifiData;

import com.google.protobuf.nano.MessageNano;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

/**
 * Class to record the stats of detection level information for data stall.
 *
 * @hide
 */
public final class DataStallDetectionStats {
    public static final int UNKNOWN_SIGNAL_STRENGTH = -1;
    // Default value of TCP signals.
    @VisibleForTesting
    public static final int UNSPECIFIED_TCP_FAIL_RATE = -1;
    @VisibleForTesting
    public static final int UNSPECIFIED_TCP_PACKETS_COUNT = -1;
    @VisibleForTesting
    @NonNull
    public final byte[] mCellularInfo;
    @VisibleForTesting
    @NonNull
    public final byte[] mWifiInfo;
    @NonNull
    public final byte[] mDns;
    @VisibleForTesting
    public final int mEvaluationType;
    @VisibleForTesting
    public final int mNetworkType;
    // The TCP packets fail rate percentage from the latest tcp polling. -1 means the TCP signal is
    // not known or not supported on the SDK version of this device.
    @VisibleForTesting @IntRange(from = -1, to = 100)
    public final int mTcpFailRate;
    // Number of packets sent since the last received packet.
    @VisibleForTesting
    public final int mTcpSentSinceLastRecv;

    public DataStallDetectionStats(@Nullable byte[] cell, @Nullable byte[] wifi,
                @NonNull int[] returnCode, @NonNull long[] dnsTime, int evalType, int netType,
                int failRate, int sentSinceLastRecv) {
        mCellularInfo = emptyCellDataIfNull(cell);
        mWifiInfo = emptyWifiInfoIfNull(wifi);

        DnsEvent dns = new DnsEvent();
        dns.dnsReturnCode = returnCode;
        dns.dnsTime = dnsTime;
        mDns = MessageNano.toByteArray(dns);
        mEvaluationType = evalType;
        mNetworkType = netType;
        mTcpFailRate = failRate;
        mTcpSentSinceLastRecv = sentSinceLastRecv;
    }

    /**
     * Because metrics data must contain data for each field even if it's not supported or not
     * available, generate a byte array representing an empty {@link CellularData} if the
     * {@link CellularData} is unavailable.
     *
     * @param cell a byte array representing current {@link CellularData} of {@code this}
     * @return a byte array of a {@link CellularData}.
     */
    @VisibleForTesting
    public static byte[] emptyCellDataIfNull(@Nullable byte[] cell) {
        if (cell != null) return cell;

        CellularData data  = new CellularData();
        data.ratType = DataStallEventProto.RADIO_TECHNOLOGY_UNKNOWN;
        data.networkMccmnc = "";
        data.simMccmnc = "";
        data.signalStrength = UNKNOWN_SIGNAL_STRENGTH;
        return MessageNano.toByteArray(data);
    }

    /**
     * Because metrics data must contain data for each field even if it's not supported or not
     * available, generate a byte array representing an empty {@link WifiData} if the
     * {@link WiFiData} is unavailable.
     *
     * @param wifi a byte array representing current {@link WiFiData} of {@code this}.
     * @return a byte array of a {@link WiFiData}.
     */
    @VisibleForTesting
    public static byte[] emptyWifiInfoIfNull(@Nullable byte[] wifi) {
        if (wifi != null) return wifi;

        WifiData data = new WifiData();
        data.wifiBand = DataStallEventProto.AP_BAND_UNKNOWN;
        data.signalStrength = UNKNOWN_SIGNAL_STRENGTH;
        return MessageNano.toByteArray(data);
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("type: ").append(mNetworkType)
          .append(", evaluation type: ")
          .append(mEvaluationType)
          .append(", wifi info: ")
          .append(HexDump.toHexString(mWifiInfo))
          .append(", cell info: ")
          .append(HexDump.toHexString(mCellularInfo))
          .append(", dns: ")
          .append(HexDump.toHexString(mDns))
          .append(", tcp fail rate: ")
          .append(mTcpFailRate)
          .append(", tcp received: ")
          .append(mTcpSentSinceLastRecv);
        return sb.toString();
    }

    @Override
    public boolean equals(@Nullable final Object o) {
        if (!(o instanceof DataStallDetectionStats)) return false;
        final DataStallDetectionStats other = (DataStallDetectionStats) o;
        return (mNetworkType == other.mNetworkType)
            && (mEvaluationType == other.mEvaluationType)
            && Arrays.equals(mWifiInfo, other.mWifiInfo)
            && Arrays.equals(mCellularInfo, other.mCellularInfo)
            && Arrays.equals(mDns, other.mDns)
            && (mTcpFailRate == other.mTcpFailRate)
            && (mTcpSentSinceLastRecv == other.mTcpSentSinceLastRecv);
    }

    @Override
    public int hashCode() {
        return Objects.hash(mNetworkType, mEvaluationType, Arrays.hashCode(mWifiInfo),
                Arrays.hashCode(mCellularInfo), Arrays.hashCode(mDns), mTcpFailRate,
                mTcpSentSinceLastRecv);
    }

    /**
     * Utility to create an instance of {@Link DataStallDetectionStats}
     *
     * @hide
     */
    public static class Builder {
        @Nullable
        private byte[] mCellularInfo;
        @Nullable
        private byte[] mWifiInfo;
        @NonNull
        private final List<Integer> mDnsReturnCode = new ArrayList<Integer>();
        @NonNull
        private final List<Long> mDnsTimeStamp = new ArrayList<Long>();
        private int mEvaluationType;
        private int mNetworkType;
        private int mTcpFailRate = UNSPECIFIED_TCP_FAIL_RATE;
        private int mTcpSentSinceLastRecv = UNSPECIFIED_TCP_PACKETS_COUNT;

        /**
         * Add a dns event into Builder.
         *
         * @param code the return code of the dns event.
         * @param timeMs the elapsedRealtime in ms that the the dns event was received from netd.
         * @return {@code this} {@link Builder} instance.
         */
        public Builder addDnsEvent(int code, long timeMs) {
            mDnsReturnCode.add(code);
            mDnsTimeStamp.add(timeMs);
            return this;
        }

        /**
         * Set the data stall evaluation type into Builder.
         *
         * @param type the signal type causing a data stall to be suspected.
         * @return {@code this} {@link Builder} instance.
         */
        public Builder setEvaluationType(int type) {
            mEvaluationType = type;
            return this;
        }

        /**
         * Set the network type into Builder.
         *
         * @param type the network type of the logged network.
         * @return {@code this} {@link Builder} instance.
         */
        public Builder setNetworkType(int type) {
            mNetworkType = type;
            return this;
        }

        /**
         * Set the TCP packet fail rate into Builder. The data is included since android R.
         *
         * @param rate the TCP packet fail rate of the logged network. The default value is
         *               {@code UNSPECIFIED_TCP_FAIL_RATE}, which means the TCP signal is not known
         *               or not supported on the SDK version of this device.
         * @return {@code this} {@link Builder} instance.
         */
        public Builder setTcpFailRate(@IntRange(from = -1, to = 100) int rate) {
            mTcpFailRate = rate;
            return this;
        }

        /**
         * Set the number of TCP packets sent since the last received packet into Builder. The data
         * starts to be included since android R.
         *
         * @param count the number of packets sent since the last received packet of the logged
         *              network. Keep it unset as default value or set to
         *              {@code UNSPECIFIED_TCP_PACKETS_COUNT} if the tcp signal is unsupported with
         *              current device android sdk version or the packets count is unknown.
         * @return {@code this} {@link Builder} instance.
         */
        public Builder setTcpSentSinceLastRecv(int count) {
            mTcpSentSinceLastRecv = count;
            return this;
        }

        /**
         * Set the wifi data into Builder.
         *
         * @param info a {@link WifiInfo} of the connected wifi network.
         * @return {@code this} {@link Builder} instance.
         */
        public Builder setWiFiData(@Nullable final WifiInfo info) {
            WifiData data = new WifiData();
            data.wifiBand = getWifiBand(info);
            data.signalStrength = (info != null) ? info.getRssi() : UNKNOWN_SIGNAL_STRENGTH;
            mWifiInfo = MessageNano.toByteArray(data);
            return this;
        }

        private static int getWifiBand(@Nullable final WifiInfo info) {
            if (info == null) return DataStallEventProto.AP_BAND_UNKNOWN;

            int freq = info.getFrequency();
            // Refer to ScanResult.is5GHz(), ScanResult.is24GHz() and ScanResult.is6GHz().
            if (freq >= 5160 && freq <= 5865) {
                return DataStallEventProto.AP_BAND_5GHZ;
            } else if (freq >= 2412 && freq <= 2484) {
                return DataStallEventProto.AP_BAND_2GHZ;
            } else if (freq >= 5945 && freq <= 7105) {
                return DataStallEventProto.AP_BAND_6GHZ;
            } else {
                return DataStallEventProto.AP_BAND_UNKNOWN;
            }
        }

        /**
         * Set the cellular data into Builder.
         *
         * @param radioType the radio technology of the logged cellular network.
         * @param roaming a boolean indicates if logged cellular network is roaming or not.
         * @param networkMccmnc the mccmnc of the camped network.
         * @param simMccmnc the mccmnc of the sim.
         * @return {@code this} {@link Builder} instance.
         */
        public Builder setCellData(int radioType, boolean roaming,
                @NonNull String networkMccmnc, @NonNull String simMccmnc, int ss) {
            CellularData data  = new CellularData();
            data.ratType = radioType;
            data.isRoaming = roaming;
            data.networkMccmnc = networkMccmnc;
            data.simMccmnc = simMccmnc;
            data.signalStrength = ss;
            mCellularInfo = MessageNano.toByteArray(data);
            return this;
        }

        /**
         * Create a new {@Link DataStallDetectionStats}.
         */
        public DataStallDetectionStats build() {
            return new DataStallDetectionStats(mCellularInfo, mWifiInfo,
                    CollectionUtils.toIntArray(mDnsReturnCode),
                    CollectionUtils.toLongArray(mDnsTimeStamp),
                    mEvaluationType, mNetworkType, mTcpFailRate, mTcpSentSinceLastRecv);
        }
    }
}
