/*****************************************************************************

            (c) Cambridge Silicon Radio Limited 2012
            All rights reserved and confidential information of CSR

            Refer to LICENSE.txt included with this source for details
            on the license terms.

*****************************************************************************/

/*
 * ---------------------------------------------------------------------------
 *  FILE:     csr_wifi_hip_ta_sampling.c
 *
 *  PURPOSE:
 *      The traffic analysis sampling module.
 *      This gathers data which is sent to the SME and used to analyse
 *      the traffic behaviour.
 *
 * Provides:
 *      unifi_ta_sampling_init - Initialise the internal state
 *      unifi_ta_sample        - Sampling function, call this for every data packet
 *
 * Calls these external functions which must be provided:
 *      unifi_ta_indicate_sampling - Pass sample data to the SME.
 *      unifi_ta_indicate_protocol - Report certain data packet types to the SME.
 * ---------------------------------------------------------------------------
 */

#include "csr_wifi_hip_card_sdio.h"

/* Maximum number of Tx frames we store each CYCLE_1, for detecting period */
#define TA_MAX_INTERVALS_IN_C1          100

/* Number of intervals in CYCLE_1 (one second), for detecting periodic */
/* Must match size of unifi_TrafficStats.intervals - 1 */
#define TA_INTERVALS_NUM               10

/* Step (in msecs) between intervals, for detecting periodic */
/* We are only interested in periods up to 100ms, i.e. between beacons */
/* This is correct for TA_INTERVALS_NUM=10 */
#define TA_INTERVALS_STEP               10


enum ta_frame_identity
{
    TA_FRAME_UNKNOWN,
    TA_FRAME_ETHERNET_UNINTERESTING,
    TA_FRAME_ETHERNET_INTERESTING
};


#define TA_ETHERNET_TYPE_OFFSET     6
#define TA_LLC_HEADER_SIZE          8
#define TA_IP_TYPE_OFFSET           17
#define TA_UDP_SOURCE_PORT_OFFSET   28
#define TA_UDP_DEST_PORT_OFFSET     (TA_UDP_SOURCE_PORT_OFFSET + 2)
#define TA_BOOTP_CLIENT_MAC_ADDR_OFFSET 64
#define TA_DHCP_MESSAGE_TYPE_OFFSET 278
#define TA_DHCP_MESSAGE_TYPE_ACK    0x05
#define TA_PROTO_TYPE_IP            0x0800
#define TA_PROTO_TYPE_EAP           0x888E
#define TA_PROTO_TYPE_WAI           0x8864
#define TA_PROTO_TYPE_ARP           0x0806
#define TA_IP_TYPE_TCP              0x06
#define TA_IP_TYPE_UDP              0x11
#define TA_UDP_PORT_BOOTPC          0x0044
#define TA_UDP_PORT_BOOTPS          0x0043
#define TA_EAPOL_TYPE_OFFSET        9
#define TA_EAPOL_TYPE_START         0x01

#define snap_802_2                  0xAAAA0300
#define oui_rfc1042                 0x00000000
#define oui_8021h                   0x0000f800
static const u8 aironet_snap[5] = { 0x00, 0x40, 0x96, 0x00, 0x00 };


/*
 * ---------------------------------------------------------------------------
 *  ta_detect_protocol
 *
 *      Internal only.
 *      Detects a specific protocol in a frame and indicates a TA event.
 *
 *  Arguments:
 *      ta              The pointer to the TA module.
 *      direction       The direction of the frame (tx or rx).
 *      data            Pointer to the structure that contains the data.
 *
 *  Returns:
 *      None
 * ---------------------------------------------------------------------------
 */
static enum ta_frame_identity ta_detect_protocol(card_t *card, CsrWifiRouterCtrlProtocolDirection direction,
                                                 const bulk_data_desc_t *data,
                                                 const u8 *saddr,
                                                 const u8 *sta_macaddr)
{
    ta_data_t *tad = &card->ta_sampling;
    u16 proto;
    u16 source_port, dest_port;
    CsrWifiMacAddress srcAddress;
    u32 snap_hdr, oui_hdr;

    if (data->data_length < TA_LLC_HEADER_SIZE)
    {
        return TA_FRAME_UNKNOWN;
    }

    snap_hdr = (((u32)data->os_data_ptr[0]) << 24) |
               (((u32)data->os_data_ptr[1]) << 16) |
               (((u32)data->os_data_ptr[2]) << 8);
    if (snap_hdr != snap_802_2)
    {
        return TA_FRAME_UNKNOWN;
    }

    if (tad->packet_filter & CSR_WIFI_ROUTER_CTRL_TRAFFIC_PACKET_TYPE_CUSTOM)
    {
        /*
         * Here we would use the custom filter to detect interesting frames.
         */
    }

    oui_hdr = (((u32)data->os_data_ptr[3]) << 24) |
              (((u32)data->os_data_ptr[4]) << 16) |
              (((u32)data->os_data_ptr[5]) << 8);
    if ((oui_hdr == oui_rfc1042) || (oui_hdr == oui_8021h))
    {
        proto = (data->os_data_ptr[TA_ETHERNET_TYPE_OFFSET] * 256) +
                data->os_data_ptr[TA_ETHERNET_TYPE_OFFSET + 1];

        /* The only interesting IP frames are the DHCP */
        if (proto == TA_PROTO_TYPE_IP)
        {
            if (data->data_length > TA_IP_TYPE_OFFSET)
            {
                if (tad->packet_filter & CSR_WIFI_ROUTER_CTRL_TRAFFIC_PACKET_TYPE_CUSTOM)
                {
                    ta_l4stats_t *ta_l4stats = &tad->ta_l4stats;
                    u8 l4proto = data->os_data_ptr[TA_IP_TYPE_OFFSET];

                    if (l4proto == TA_IP_TYPE_TCP)
                    {
                        if (direction == CSR_WIFI_ROUTER_CTRL_PROTOCOL_DIRECTION_TX)
                        {
                            ta_l4stats->txTcpBytesCount += data->data_length;
                        }
                        else
                        {
                            ta_l4stats->rxTcpBytesCount += data->data_length;
                        }
                    }
                    else if (l4proto == TA_IP_TYPE_UDP)
                    {
                        if (direction == CSR_WIFI_ROUTER_CTRL_PROTOCOL_DIRECTION_TX)
                        {
                            ta_l4stats->txUdpBytesCount += data->data_length;
                        }
                        else
                        {
                            ta_l4stats->rxUdpBytesCount += data->data_length;
                        }
                    }
                }

                /* detect DHCP frames */
                if (tad->packet_filter & CSR_WIFI_ROUTER_CTRL_TRAFFIC_PACKET_TYPE_DHCP)
                {
                    /* DHCP frames are UDP frames with BOOTP ports */
                    if (data->os_data_ptr[TA_IP_TYPE_OFFSET] == TA_IP_TYPE_UDP)
                    {
                        if (data->data_length > TA_UDP_DEST_PORT_OFFSET)
                        {
                            source_port = (data->os_data_ptr[TA_UDP_SOURCE_PORT_OFFSET] * 256) +
                                          data->os_data_ptr[TA_UDP_SOURCE_PORT_OFFSET + 1];
                            dest_port = (data->os_data_ptr[TA_UDP_DEST_PORT_OFFSET] * 256) +
                                        data->os_data_ptr[TA_UDP_DEST_PORT_OFFSET + 1];

                            if (((source_port == TA_UDP_PORT_BOOTPC) && (dest_port == TA_UDP_PORT_BOOTPS)) ||
                                ((source_port == TA_UDP_PORT_BOOTPS) && (dest_port == TA_UDP_PORT_BOOTPC)))
                            {
                                /* The DHCP should have at least a message type (request, ack, nack, etc) */
                                if (data->data_length > TA_DHCP_MESSAGE_TYPE_OFFSET + 6)
                                {
                                    UNIFI_MAC_ADDRESS_COPY(srcAddress.a, saddr);

                                    if (direction == CSR_WIFI_ROUTER_CTRL_PROTOCOL_DIRECTION_TX)
                                    {
                                        unifi_ta_indicate_protocol(card->ospriv,
                                                                   CSR_WIFI_ROUTER_CTRL_TRAFFIC_PACKET_TYPE_DHCP,
                                                                   direction,
                                                                   &srcAddress);
                                        return TA_FRAME_ETHERNET_UNINTERESTING;
                                    }

                                    /* DHCPACK is a special indication */
                                    if (UNIFI_MAC_ADDRESS_CMP(data->os_data_ptr + TA_BOOTP_CLIENT_MAC_ADDR_OFFSET, sta_macaddr) == TRUE)
                                    {
                                        if (data->os_data_ptr[TA_DHCP_MESSAGE_TYPE_OFFSET] == TA_DHCP_MESSAGE_TYPE_ACK)
                                        {
                                            unifi_ta_indicate_protocol(card->ospriv,
                                                                       CSR_WIFI_ROUTER_CTRL_TRAFFIC_PACKET_TYPE_DHCP_ACK,
                                                                       direction,
                                                                       &srcAddress);
                                        }
                                        else
                                        {
                                            unifi_ta_indicate_protocol(card->ospriv,
                                                                       CSR_WIFI_ROUTER_CTRL_TRAFFIC_PACKET_TYPE_DHCP,
                                                                       direction,
                                                                       &srcAddress);
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }

            return TA_FRAME_ETHERNET_INTERESTING;
        }

        /* detect protocol type EAPOL or WAI (treated as equivalent here) */
        if (tad->packet_filter & CSR_WIFI_ROUTER_CTRL_TRAFFIC_PACKET_TYPE_EAPOL)
        {
            if (TA_PROTO_TYPE_EAP == proto || TA_PROTO_TYPE_WAI == proto)
            {
                if ((TA_PROTO_TYPE_WAI == proto) || (direction != CSR_WIFI_ROUTER_CTRL_PROTOCOL_DIRECTION_TX) ||
                    (data->os_data_ptr[TA_EAPOL_TYPE_OFFSET] == TA_EAPOL_TYPE_START))
                {
                    UNIFI_MAC_ADDRESS_COPY(srcAddress.a, saddr);
                    unifi_ta_indicate_protocol(card->ospriv,
                                               CSR_WIFI_ROUTER_CTRL_TRAFFIC_PACKET_TYPE_EAPOL,
                                               direction, &srcAddress);
                }
                return TA_FRAME_ETHERNET_UNINTERESTING;
            }
        }

        /* detect protocol type 0x0806 (ARP) */
        if (tad->packet_filter & CSR_WIFI_ROUTER_CTRL_TRAFFIC_PACKET_TYPE_ARP)
        {
            if (proto == TA_PROTO_TYPE_ARP)
            {
                UNIFI_MAC_ADDRESS_COPY(srcAddress.a, saddr);
                unifi_ta_indicate_protocol(card->ospriv,
                                           CSR_WIFI_ROUTER_CTRL_TRAFFIC_PACKET_TYPE_ARP,
                                           direction, &srcAddress);
                return TA_FRAME_ETHERNET_UNINTERESTING;
            }
        }

        return TA_FRAME_ETHERNET_INTERESTING;
    }
    else if (tad->packet_filter & CSR_WIFI_ROUTER_CTRL_TRAFFIC_PACKET_TYPE_AIRONET)
    {
        /* detect Aironet frames */
        if (!memcmp(data->os_data_ptr + 3, aironet_snap, 5))
        {
            UNIFI_MAC_ADDRESS_COPY(srcAddress.a, saddr);
            unifi_ta_indicate_protocol(card->ospriv, CSR_WIFI_ROUTER_CTRL_TRAFFIC_PACKET_TYPE_AIRONET,
                                       direction, &srcAddress);
        }
    }

    return TA_FRAME_ETHERNET_UNINTERESTING;
} /* ta_detect_protocol() */


static void tas_reset_data(ta_data_t *tad)
{
    s16 i;

    for (i = 0; i < (TA_INTERVALS_NUM + 1); i++)
    {
        tad->stats.intervals[i] = 0;
    }

    tad->stats.rxFramesNum = 0;
    tad->stats.txFramesNum = 0;
    tad->stats.rxBytesCount = 0;
    tad->stats.txBytesCount = 0;
    tad->stats.rxMeanRate = 0;

    tad->rx_sum_rate = 0;

    tad->ta_l4stats.rxTcpBytesCount = 0;
    tad->ta_l4stats.txTcpBytesCount = 0;
    tad->ta_l4stats.rxUdpBytesCount = 0;
    tad->ta_l4stats.txUdpBytesCount = 0;
} /* tas_reset_data() */


/*
 * ---------------------------------------------------------------------------
 *  API.
 *  unifi_ta_sampling_init
 *
 *      (Re)Initialise the Traffic Analysis sampling module.
 *      Resets the counters and timestamps.
 *
 *  Arguments:
 *      tad             Pointer to a ta_data_t structure containing the
 *                      context for this device instance.
 *      drv_priv        An opaque pointer that the TA sampling module will
 *                      pass in call-outs.
 *
 *  Returns:
 *      None.
 * ---------------------------------------------------------------------------
 */
void unifi_ta_sampling_init(card_t *card)
{
    (void)unifi_ta_configure(card, CSR_WIFI_ROUTER_CTRL_TRAFFIC_CONFIG_TYPE_RESET, NULL);

    card->ta_sampling.packet_filter = CSR_WIFI_ROUTER_CTRL_TRAFFIC_PACKET_TYPE_NONE;
    card->ta_sampling.traffic_type = CSR_WIFI_ROUTER_CTRL_TRAFFIC_TYPE_OCCASIONAL;
} /* unifi_ta_sampling_init() */


/*
 * ---------------------------------------------------------------------------
 *  API.
 *  unifi_ta_sample
 *
 *      Sample a data frame for the TA module.
 *      This function stores all the useful information it can extract from
 *      the frame and detects any specific protocols.
 *
 *  Arguments:
 *      tad             The pointer to the TA sampling context struct.
 *      direction       The direction of the frame (rx, tx)
 *      data            Pointer to the frame data
 *      saddr           Source MAC address of frame.
 *      timestamp       Time (in msecs) that the frame was received.
 *      rate            Reported data rate for the rx frame (0 for tx frames)
 *
 *  Returns:
 *      None
 * ---------------------------------------------------------------------------
 */
void unifi_ta_sample(card_t                            *card,
                     CsrWifiRouterCtrlProtocolDirection direction,
                     const bulk_data_desc_t            *data,
                     const u8                    *saddr,
                     const u8                    *sta_macaddr,
                     u32                          timestamp,
                     u16                          rate)
{
    ta_data_t *tad = &card->ta_sampling;
    enum ta_frame_identity identity;
    u32 time_delta;



    /* Step1: Check for specific frames */
    if (tad->packet_filter != CSR_WIFI_ROUTER_CTRL_TRAFFIC_PACKET_TYPE_NONE)
    {
        identity = ta_detect_protocol(card, direction, data, saddr, sta_macaddr);
    }
    else
    {
        identity = TA_FRAME_ETHERNET_INTERESTING;
    }


    /* Step2: Update the information in the current record */
    if (direction == CSR_WIFI_ROUTER_CTRL_PROTOCOL_DIRECTION_RX)
    {
        /* Update the Rx packet count and the throughput count */
        tad->stats.rxFramesNum++;
        tad->stats.rxBytesCount += data->data_length;

        /* Accumulate packet Rx rates for later averaging */
        tad->rx_sum_rate += rate;
    }
    else
    {
        if (identity == TA_FRAME_ETHERNET_INTERESTING)
        {
            /*
             * Store the period between the last and the current frame.
             * There is not point storing more than TA_MAX_INTERVALS_IN_C1 periods,
             * the traffic will be bursty or continuous.
             */
            if (tad->stats.txFramesNum < TA_MAX_INTERVALS_IN_C1)
            {
                u32 interval;
                u32 index_in_intervals;

                interval = timestamp - tad->tx_last_ts;
                tad->tx_last_ts = timestamp;
                index_in_intervals = (interval + TA_INTERVALS_STEP / 2 - 1) / TA_INTERVALS_STEP;

                /* If the interval is interesting, update the t1_intervals count */
                if (index_in_intervals <= TA_INTERVALS_NUM)
                {
                    unifi_trace(card->ospriv, UDBG5,
                                "unifi_ta_sample: TX interval=%d index=%d\n",
                                interval, index_in_intervals);
                    tad->stats.intervals[index_in_intervals]++;
                }
            }
        }

        /* Update the Tx packet count... */
        tad->stats.txFramesNum++;
        /* ... and the number of bytes for throughput. */
        tad->stats.txBytesCount += data->data_length;
    }

    /*
     * If more than one second has elapsed since the last report, send
     * another one.
     */
    /* Unsigned subtraction handles wrap-around from 0xFFFFFFFF to 0 */
    time_delta = timestamp - tad->last_indication_time;
    if (time_delta >= 1000)
    {
        /*
         * rxFramesNum can be flashed in tas_reset_data() by another thread.
         * Use a temp to avoid division by zero.
         */
        u32 temp_rxFramesNum;
        temp_rxFramesNum = tad->stats.rxFramesNum;

        /* Calculate this interval's mean frame Rx rate from the sum */
        if (temp_rxFramesNum)
        {
            tad->stats.rxMeanRate = tad->rx_sum_rate / temp_rxFramesNum;
        }
        unifi_trace(card->ospriv, UDBG5,
                    "unifi_ta_sample: RX fr=%lu, r=%u, sum=%lu, av=%lu\n",
                    tad->stats.rxFramesNum, rate,
                    tad->rx_sum_rate, tad->stats.rxMeanRate);

        /*
         * Send the information collected in the stats struct
         * to the SME and reset the counters.
         */
        if (tad->packet_filter & CSR_WIFI_ROUTER_CTRL_TRAFFIC_PACKET_TYPE_CUSTOM)
        {
            u32 rxTcpThroughput = tad->ta_l4stats.rxTcpBytesCount / time_delta;
            u32 txTcpThroughput = tad->ta_l4stats.txTcpBytesCount / time_delta;
            u32 rxUdpThroughput = tad->ta_l4stats.rxUdpBytesCount / time_delta;
            u32 txUdpThroughput = tad->ta_l4stats.txUdpBytesCount / time_delta;

            unifi_ta_indicate_l4stats(card->ospriv,
                                      rxTcpThroughput,
                                      txTcpThroughput,
                                      rxUdpThroughput,
                                      txUdpThroughput
                                      );
        }
        unifi_ta_indicate_sampling(card->ospriv, &tad->stats);
        tas_reset_data(tad);
        tad->last_indication_time = timestamp;
    }
} /* unifi_ta_sample() */


/*
 * ---------------------------------------------------------------------------
 *  External API.
 *  unifi_ta_configure
 *
 *      Configures the TA module parameters.
 *
 *  Arguments:
 *      ta              The pointer to the TA module.
 *      config_type     The type of the configuration request
 *      config          Pointer to the configuration parameters.
 *
 *  Returns:
 *      CSR_RESULT_SUCCESS on success, CSR error code otherwise
 * ---------------------------------------------------------------------------
 */
CsrResult unifi_ta_configure(card_t                               *card,
                             CsrWifiRouterCtrlTrafficConfigType    config_type,
                             const CsrWifiRouterCtrlTrafficConfig *config)
{
    ta_data_t *tad = &card->ta_sampling;

    /* Reinitialise our data when we are reset */
    if (config_type == CSR_WIFI_ROUTER_CTRL_TRAFFIC_CONFIG_TYPE_RESET)
    {
        /* Reset the stats to zero */
        tas_reset_data(tad);

        /* Reset the timer variables */
        tad->tx_last_ts = 0;
        tad->last_indication_time = 0;

        return CSR_RESULT_SUCCESS;
    }

    if (config_type == CSR_WIFI_ROUTER_CTRL_TRAFFIC_CONFIG_TYPE_FILTER)
    {
        tad->packet_filter = config->packetFilter;

        if (tad->packet_filter & CSR_WIFI_ROUTER_CTRL_TRAFFIC_PACKET_TYPE_CUSTOM)
        {
            tad->custom_filter = config->customFilter;
        }

        return CSR_RESULT_SUCCESS;
    }

    return CSR_RESULT_SUCCESS;
} /* unifi_ta_configure() */


/*
 * ---------------------------------------------------------------------------
 *  External API.
 *  unifi_ta_classification
 *
 *      Configures the current TA classification.
 *
 *  Arguments:
 *      ta              The pointer to the TA module.
 *      traffic_type    The classification type
 *      period          The traffic period if the type is periodic
 *
 *  Returns:
 *      None
 * ---------------------------------------------------------------------------
 */
void unifi_ta_classification(card_t                      *card,
                             CsrWifiRouterCtrlTrafficType traffic_type,
                             u16                    period)
{
    unifi_trace(card->ospriv, UDBG3,
                "Changed current ta classification to: %d\n", traffic_type);

    card->ta_sampling.traffic_type = traffic_type;
}


