/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */

#include <assert.h>
#include <string.h>

#include "sysinit/sysinit.h"
#include "syscfg/syscfg.h"
#include "host/ble_hs.h"
#include "host/ble_gap.h"
#include "services/ans/ble_svc_ans.h"

/* Max length of new alert info string */
#define BLE_SVC_ANS_INFO_STR_MAX_LEN        18
/* Max length of a new alert notification, max string length + 2 bytes
 * for category ID and count. */
#define BLE_SVC_ANS_NEW_ALERT_MAX_LEN   (BLE_SVC_ANS_INFO_STR_MAX_LEN + 2)

/* Supported categories bitmasks */
static uint8_t ble_svc_ans_new_alert_cat;
static uint8_t ble_svc_ans_unr_alert_cat;

/* Characteristic values */
static uint8_t ble_svc_ans_new_alert_val[BLE_SVC_ANS_NEW_ALERT_MAX_LEN];
static uint16_t ble_svc_ans_new_alert_val_len;
static uint8_t ble_svc_ans_unr_alert_stat[2];
static uint8_t ble_svc_ans_alert_not_ctrl_pt[2];

/* Alert counts, one value for each category */
static uint8_t ble_svc_ans_new_alert_cnt[BLE_SVC_ANS_CAT_NUM];
static uint8_t ble_svc_ans_unr_alert_cnt[BLE_SVC_ANS_CAT_NUM];

/* Characteristic value handles */
static uint16_t ble_svc_ans_new_alert_val_handle;
static uint16_t ble_svc_ans_unr_alert_val_handle;

/* Connection handle
 *
 * TODO: In order to support multiple connections we would need to save
 *       the handles for every connection, not just the most recent. Then
 *       we would need to notify each connection when needed.
 * */
static uint16_t ble_svc_ans_conn_handle;

/* Access function */
static int
ble_svc_ans_access(uint16_t conn_handle, uint16_t attr_handle,
                   struct ble_gatt_access_ctxt *ctxt, void *arg);

/* Notify new alert */
static int
ble_svc_ans_new_alert_notify(uint8_t cat_id, const char * info_str);

/* Notify unread alert */
static int
ble_svc_ans_unr_alert_notify(uint8_t cat_id);

/* Save written value to local characteristic value */
static int
ble_svc_ans_chr_write(struct os_mbuf *om, uint16_t min_len, uint16_t max_len,
                      void *dst, uint16_t *len);

static const struct ble_gatt_svc_def ble_svc_ans_defs[] = {
    {
        /*** Alert Notification Service. */
        .type = BLE_GATT_SVC_TYPE_PRIMARY,
        .uuid = BLE_UUID16_DECLARE(BLE_SVC_ANS_UUID16),
        .characteristics = (struct ble_gatt_chr_def[]) { {
            /** Supported New Alert Catagory
             *
             * This characteristic exposes what categories of new
             * alert are supported in the server.
             */
            .uuid = BLE_UUID16_DECLARE(BLE_SVC_ANS_CHR_UUID16_SUP_NEW_ALERT_CAT),
            .access_cb = ble_svc_ans_access,
            .flags = BLE_GATT_CHR_F_READ,
        }, {
            /** New Alert
             *
             * This characteristic exposes information about
             * the count of new alerts (for a given category).
             */
            .uuid = BLE_UUID16_DECLARE(BLE_SVC_ANS_CHR_UUID16_NEW_ALERT),
            .access_cb = ble_svc_ans_access,
            .val_handle = &ble_svc_ans_new_alert_val_handle,
            .flags = BLE_GATT_CHR_F_NOTIFY,
        }, {
            /** Supported Unread Alert Catagory
             *
             * This characteristic exposes what categories of
             * unread alert are supported in the server.
             */
            .uuid = BLE_UUID16_DECLARE(BLE_SVC_ANS_CHR_UUID16_SUP_UNR_ALERT_CAT),
            .access_cb = ble_svc_ans_access,
            .flags = BLE_GATT_CHR_F_READ,
        }, {
            /** Unread Alert Status
             *
             * This characteristic exposes the count of unread
             * alert events existing in the server.
             */
            .uuid = BLE_UUID16_DECLARE(BLE_SVC_ANS_CHR_UUID16_UNR_ALERT_STAT),
            .access_cb = ble_svc_ans_access,
            .val_handle = &ble_svc_ans_unr_alert_val_handle,
            .flags = BLE_GATT_CHR_F_NOTIFY,
        }, {
            /** Alert Notification Control Point
             *
             * This characteristic allows the peer device to
             * enable/disable the alert notification of new alert
             * and unread event more selectively than can be done
             * by setting or clearing the notification bit in the
             * Client Characteristic Configuration for each alert
             * characteristic.
             */
            .uuid = BLE_UUID16_DECLARE(BLE_SVC_ANS_CHR_UUID16_ALERT_NOT_CTRL_PT),
            .access_cb = ble_svc_ans_access,
            .flags = BLE_GATT_CHR_F_WRITE,
        }, {
            0, /* No more characteristics in this service. */
        } },
    },

    {
        0, /* No more services. */
    },
};

/**
 * ANS access function
 */
static int
ble_svc_ans_access(uint16_t conn_handle, uint16_t attr_handle,
                          struct ble_gatt_access_ctxt *ctxt,
                          void *arg)
{
    uint16_t uuid16;
    int rc;

    /* ANS Control point command and catagory variables */
    uint8_t cmd_id;
    uint8_t cat_id;
    uint8_t cat_bit_mask;
    int i;

    uuid16 = ble_uuid_u16(ctxt->chr->uuid);
    assert(uuid16 != 0);

    switch (uuid16) {
    case BLE_SVC_ANS_CHR_UUID16_SUP_NEW_ALERT_CAT:
        assert(ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR);
        rc = os_mbuf_append(ctxt->om, &ble_svc_ans_new_alert_cat,
                            sizeof ble_svc_ans_new_alert_cat);
        return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;

    case BLE_SVC_ANS_CHR_UUID16_NEW_ALERT:
        if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
            rc = ble_svc_ans_chr_write(ctxt->om, 0,
                                       sizeof ble_svc_ans_new_alert_val,
                                       ble_svc_ans_new_alert_val,
                                       &ble_svc_ans_new_alert_val_len);
            return rc;

        } else if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) {
            rc = os_mbuf_append(ctxt->om, &ble_svc_ans_new_alert_val,
                                sizeof ble_svc_ans_new_alert_val);
            return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
        }

        assert(0);
        return BLE_ATT_ERR_UNLIKELY;
    case BLE_SVC_ANS_CHR_UUID16_SUP_UNR_ALERT_CAT:
        assert(ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR);
        rc = os_mbuf_append(ctxt->om, &ble_svc_ans_unr_alert_cat,
                            sizeof ble_svc_ans_unr_alert_cat);
        return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;

    case BLE_SVC_ANS_CHR_UUID16_UNR_ALERT_STAT:
        if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
            rc = ble_svc_ans_chr_write(ctxt->om,
                                       sizeof ble_svc_ans_unr_alert_stat,
                                       sizeof ble_svc_ans_unr_alert_stat,
                                       &ble_svc_ans_unr_alert_stat,
                                       NULL);
            return rc;
        } else {
            rc = os_mbuf_append(ctxt->om, &ble_svc_ans_unr_alert_stat,
                                sizeof ble_svc_ans_unr_alert_stat);
            return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
        }

    case BLE_SVC_ANS_CHR_UUID16_ALERT_NOT_CTRL_PT:
        if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
            rc = ble_svc_ans_chr_write(ctxt->om,
                                       sizeof ble_svc_ans_alert_not_ctrl_pt,
                                       sizeof ble_svc_ans_alert_not_ctrl_pt,
                                       &ble_svc_ans_alert_not_ctrl_pt,
                                       NULL);
            if (rc != 0) {
                return rc;
            }

            /* Get command ID and category ID */
            cmd_id = ble_svc_ans_alert_not_ctrl_pt[0];
            cat_id = ble_svc_ans_alert_not_ctrl_pt[1];


            /* Set cat_bit_mask to the appropriate bitmask based on cat_id */
            if (cat_id < BLE_SVC_ANS_CAT_NUM) {
                cat_bit_mask = (1 << cat_id);
            } else if (cat_id == 0xff) {
                cat_bit_mask = cat_id;
            } else {
                /* invalid category ID */
                return BLE_ATT_ERR_UNLIKELY;
            }

            switch (cmd_id) {
            case BLE_SVC_ANS_CMD_EN_NEW_ALERT_CAT:
                ble_svc_ans_new_alert_cat |= cat_bit_mask;
                break;
            case BLE_SVC_ANS_CMD_EN_UNR_ALERT_CAT:
                ble_svc_ans_unr_alert_cat |= cat_bit_mask;
                break;
            case BLE_SVC_ANS_CMD_DIS_NEW_ALERT_CAT:
                ble_svc_ans_new_alert_cat &= ~cat_bit_mask;
                break;
            case BLE_SVC_ANS_CMD_DIS_UNR_ALERT_CAT:
                ble_svc_ans_unr_alert_cat &= ~cat_bit_mask;
                break;
            case BLE_SVC_ANS_CMD_NOT_NEW_ALERT_IMMEDIATE:
                if (cat_id == 0xff) {
                    /* If cat_id is 0xff, notify on all enabled categories */
                    for (i = BLE_SVC_ANS_CAT_NUM - 1; i > 0; --i) {
                        if ((ble_svc_ans_new_alert_cat >> i) & 0x01) {
                            ble_svc_ans_new_alert_notify(i, NULL);
                        }
                    }
                } else {
                    ble_svc_ans_new_alert_notify(cat_id, NULL);
                }
                break;
            case BLE_SVC_ANS_CMD_NOT_UNR_ALERT_IMMEDIATE:
                if (cat_id == 0xff) {
                    /* If cat_id is 0xff, notify on all enabled categories */
                    for (i = BLE_SVC_ANS_CAT_NUM - 1; i > 0; --i) {
                        if ((ble_svc_ans_unr_alert_cat >> i) & 0x01) {
                            ble_svc_ans_unr_alert_notify(i);
                        }
                    }
                } else {
                    ble_svc_ans_unr_alert_notify(cat_id);
                }
                break;
            default:
                return BLE_SVC_ANS_ERR_CMD_NOT_SUPPORTED;
            }
            return 0;
        } else {
            rc = BLE_ATT_ERR_UNLIKELY;
        }
        return rc;

    default:
        assert(0);
        return BLE_ATT_ERR_UNLIKELY;
    }
}

/**
 * This function must be called with the connection handlewhen a gap
 * connect event is received in order to send notifications to the
 * client.
 *
 * @params conn_handle          The connection handle for the current
 *                                  connection.
 */
void
ble_svc_ans_on_gap_connect(uint16_t conn_handle)
{
    ble_svc_ans_conn_handle = conn_handle;
}

/**
 * Adds a new alert to the given category then notifies the client
 * if the given category is valid and enabled.
 *
 * @param cat_flag              The id for the category which should
 *                                  should be incremented and notified
 * @param info_str              The info string to be sent to the client
 *                                  with the notification.
 *
 * @return 0 on success, non-zero error code otherwise.
 */
int
ble_svc_ans_new_alert_add(uint8_t cat_id, const char * info_str)
{
    uint8_t cat_bit_mask;

    if (cat_id < BLE_SVC_ANS_CAT_NUM) {
        cat_bit_mask = (1 << cat_id);
    } else {
        return BLE_HS_EINVAL;
    }

    if ((cat_bit_mask & ble_svc_ans_new_alert_cat) == 0) {
        return BLE_HS_EINVAL;
    }

    ble_svc_ans_new_alert_cnt[cat_id] += 1;
    return ble_svc_ans_new_alert_notify(cat_id, info_str);
}

/**
 * Adds an unread alert to the given category then notifies the client
 * if the given category is valid and enabled.
 *
 * @param cat_flag              The flag for the category which should
 *                                  should be incremented and notified
 *
 * @return 0 on success, non-zero error code otherwise.
 */
int
ble_svc_ans_unr_alert_add(uint8_t cat_id)
{
    uint8_t cat_bit_mask;

    if (cat_id < BLE_SVC_ANS_CAT_NUM) {
        cat_bit_mask = 1 << cat_id;
    } else {
        return BLE_HS_EINVAL;
    }

    if ((cat_bit_mask & ble_svc_ans_unr_alert_cat) == 0) {
        return BLE_HS_EINVAL;
    }

    ble_svc_ans_unr_alert_cnt[cat_id] += 1;
    return ble_svc_ans_unr_alert_notify(cat_id);
}

/**
 * Send a new alert notification to the given category with the
 * given info string.
 *
 * @param cat_id                The ID of the category to send the
 *                                  notification to.
 * @param info_str              The info string to send with the
 *                                  notification
 *
 * @return 0 on success, non-zero error code otherwise.
 */
static int
ble_svc_ans_new_alert_notify(uint8_t cat_id, const char * info_str)
{
    int info_str_len;

    /* Clear notification to remove old infomation that may persist */
    memset(&ble_svc_ans_new_alert_val, '\0',
           BLE_SVC_ANS_NEW_ALERT_MAX_LEN);

    /* Set ID and count values */
    ble_svc_ans_new_alert_val[0] = cat_id;
    ble_svc_ans_new_alert_val[1] = ble_svc_ans_new_alert_cnt[cat_id];

    if (info_str) {
        info_str_len = strlen(info_str);
        if (info_str_len > BLE_SVC_ANS_INFO_STR_MAX_LEN) {
            /* If info_str is longer than the max string length only
             * write up to the maximum length */
            memcpy(&ble_svc_ans_new_alert_val[2], info_str,
                   BLE_SVC_ANS_INFO_STR_MAX_LEN);
        } else {
            memcpy(&ble_svc_ans_new_alert_val[2], info_str, info_str_len);
        }
    }
    return ble_gatts_notify(ble_svc_ans_conn_handle,
                            ble_svc_ans_new_alert_val_handle);
}

/**
 * Send an unread alert notification to the given category.
 *
 * @param cat_id                The ID of the category to send the
 *                                  notificaiton to.
 *
 * @return 0 on success, non-zer0 error code otherwise.
 */
static int
ble_svc_ans_unr_alert_notify(uint8_t cat_id)
{
    ble_svc_ans_unr_alert_stat[0] = cat_id;
    ble_svc_ans_unr_alert_stat[1] = ble_svc_ans_unr_alert_cnt[cat_id];
    return ble_gatts_notify(ble_svc_ans_conn_handle,
                            ble_svc_ans_unr_alert_val_handle);
}

/**
 * Writes the received value from a characteristic write to
 * the given destination.
 */
static int
ble_svc_ans_chr_write(struct os_mbuf *om, uint16_t min_len,
                      uint16_t max_len, void *dst,
                      uint16_t *len)
{
    uint16_t om_len;
    int rc;

    om_len = OS_MBUF_PKTLEN(om);
    if (om_len < min_len || om_len > max_len) {
        return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
    }

    rc = ble_hs_mbuf_to_flat(om, dst, max_len, len);
    if (rc != 0) {
        return BLE_ATT_ERR_UNLIKELY;
    }

    return 0;
}

/**
 * Initialize the ANS with initial values for enabled categories
 * for new and unread alert characteristics. Bitwise or the
 * catagory bitmasks to enable multiple catagories.
 *
 * XXX: We should technically be able to change the new alert and
 *      unread alert catagories when we have no active connections.
 */
void
ble_svc_ans_init(void)
{
    int rc;

    /* Ensure this function only gets called by sysinit. */
    SYSINIT_ASSERT_ACTIVE();

    rc = ble_gatts_count_cfg(ble_svc_ans_defs);
    SYSINIT_PANIC_ASSERT(rc == 0);

    rc = ble_gatts_add_svcs(ble_svc_ans_defs);
    SYSINIT_PANIC_ASSERT(rc == 0);
    (void)rc;

    ble_svc_ans_new_alert_cat = MYNEWT_VAL(BLE_SVC_ANS_NEW_ALERT_CAT);
    ble_svc_ans_unr_alert_cat = MYNEWT_VAL(BLE_SVC_ANS_UNR_ALERT_CAT);
}
