/******************************************************************************
 *
 *  Copyright (c) 2014 The Android Open Source Project
 *  Copyright 2004-2012 Broadcom Corporation
 *
 *  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.
 *
 ******************************************************************************/
#include <string.h>

#include "bt_trace.h"
#include "bt_utils.h"
#include "bta_ag_api.h"
#include "bta_hf_client_int.h"
#include "device/include/esco_parameters.h"
#include "osi/include/osi.h"

#define BTA_HF_CLIENT_NO_EDR_ESCO                                \
  (ESCO_PKT_TYPES_MASK_NO_2_EV3 | ESCO_PKT_TYPES_MASK_NO_3_EV3 | \
   ESCO_PKT_TYPES_MASK_NO_2_EV5 | ESCO_PKT_TYPES_MASK_NO_3_EV5)

enum {
  BTA_HF_CLIENT_SCO_LISTEN_E,
  BTA_HF_CLIENT_SCO_OPEN_E,       /* open request */
  BTA_HF_CLIENT_SCO_CLOSE_E,      /* close request */
  BTA_HF_CLIENT_SCO_SHUTDOWN_E,   /* shutdown request */
  BTA_HF_CLIENT_SCO_CONN_OPEN_E,  /* SCO opened */
  BTA_HF_CLIENT_SCO_CONN_CLOSE_E, /* SCO closed */
};

/*******************************************************************************
 *
 * Function         bta_hf_client_remove_sco
 *
 * Description      Removes the specified SCO from the system.
 *
 * Returns          bool   - true if SCO removal was started
 *
 ******************************************************************************/
static bool bta_hf_client_sco_remove(tBTA_HF_CLIENT_CB* client_cb) {
  bool removed_started = false;
  tBTM_STATUS status;

  APPL_TRACE_DEBUG("%s", __func__);

  if (client_cb->sco_idx != BTM_INVALID_SCO_INDEX) {
    status = BTM_RemoveSco(client_cb->sco_idx);

    APPL_TRACE_DEBUG("%s: idx 0x%04x, status:0x%x", __func__,
                     client_cb->sco_idx, status);

    if (status == BTM_CMD_STARTED) {
      removed_started = true;
    }
    /* If no connection reset the SCO handle */
    else if ((status == BTM_SUCCESS) || (status == BTM_UNKNOWN_ADDR)) {
      client_cb->sco_idx = BTM_INVALID_SCO_INDEX;
    }
  }
  return removed_started;
}

/*******************************************************************************
 *
 * Function         bta_hf_client_cback_sco
 *
 * Description      Call application callback function with SCO event.
 *
 *
 * Returns          void
 *
 ******************************************************************************/
void bta_hf_client_cback_sco(tBTA_HF_CLIENT_CB* client_cb, uint8_t event) {
  tBTA_HF_CLIENT evt;

  memset(&evt, 0, sizeof(evt));
  evt.bd_addr = client_cb->peer_addr;

  /* call app cback */
  bta_hf_client_app_callback(event, &evt);
}

/*******************************************************************************
 *
 * Function         bta_hf_client_sco_conn_rsp
 *
 * Description      Process the SCO connection request
 *
 *
 * Returns          void
 *
 ******************************************************************************/
static void bta_hf_client_sco_conn_rsp(tBTA_HF_CLIENT_CB* client_cb,
                                       tBTM_ESCO_CONN_REQ_EVT_DATA* p_data) {
  enh_esco_params_t resp;
  uint8_t hci_status = HCI_SUCCESS;

  APPL_TRACE_DEBUG("%s", __func__);

  if (client_cb->sco_state == BTA_HF_CLIENT_SCO_LISTEN_ST) {
    if (p_data->link_type == BTM_LINK_TYPE_SCO) {
      resp = esco_parameters_for_codec(ESCO_CODEC_CVSD);
    } else {
      if (client_cb->negotiated_codec == BTA_AG_CODEC_MSBC) {
        resp = esco_parameters_for_codec(ESCO_CODEC_MSBC_T1);
      } else {
        // default codec
        resp = esco_parameters_for_codec(ESCO_CODEC_CVSD);
      }
    }

    /* tell sys to stop av if any */
    bta_sys_sco_use(BTA_ID_HS, 1, client_cb->peer_addr);
  } else {
    hci_status = HCI_ERR_HOST_REJECT_DEVICE;
  }

  BTM_EScoConnRsp(p_data->sco_inx, hci_status, &resp);
}

/*******************************************************************************
 *
 * Function         bta_hf_client_sco_connreq_cback
 *
 * Description      BTM eSCO connection requests and eSCO change requests
 *                  Only the connection requests are processed by BTA.
 *
 * Returns          void
 *
 ******************************************************************************/
static void bta_hf_client_esco_connreq_cback(tBTM_ESCO_EVT event,
                                             tBTM_ESCO_EVT_DATA* p_data) {
  APPL_TRACE_DEBUG("%s: %d", __func__, event);

  tBTA_HF_CLIENT_CB* client_cb =
      bta_hf_client_find_cb_by_sco_handle(p_data->conn_evt.sco_inx);
  if (client_cb == NULL) {
    APPL_TRACE_ERROR("%s: wrong SCO handle to control block %d", __func__,
                     p_data->conn_evt.sco_inx);
    return;
  }

  if (event != BTM_ESCO_CONN_REQ_EVT) {
    return;
  }

  bta_hf_client_sco_conn_rsp(client_cb, &p_data->conn_evt);

  client_cb->sco_state = BTA_HF_CLIENT_SCO_OPENING_ST;
}

/*******************************************************************************
 *
 * Function         bta_hf_client_sco_conn_cback
 *
 * Description      BTM SCO connection callback.
 *
 *
 * Returns          void
 *
 ******************************************************************************/
static void bta_hf_client_sco_conn_cback(uint16_t sco_idx) {
  APPL_TRACE_DEBUG("%s: %d", __func__, sco_idx);

  tBTA_HF_CLIENT_CB* client_cb = bta_hf_client_find_cb_by_sco_handle(sco_idx);
  if (client_cb == NULL) {
    APPL_TRACE_ERROR("%s: wrong SCO handle to control block %d", __func__,
                     sco_idx);
    return;
  }

  BT_HDR* p_buf = (BT_HDR*)osi_malloc(sizeof(BT_HDR));
  p_buf->event = BTA_HF_CLIENT_SCO_OPEN_EVT;
  p_buf->layer_specific = client_cb->handle;
  bta_sys_sendmsg(p_buf);
}

/*******************************************************************************
 *
 * Function         bta_hf_client_sco_disc_cback
 *
 * Description      BTM SCO disconnection callback.
 *
 *
 * Returns          void
 *
 ******************************************************************************/
static void bta_hf_client_sco_disc_cback(uint16_t sco_idx) {
  APPL_TRACE_DEBUG("%s: sco_idx %d", __func__, sco_idx);

  tBTA_HF_CLIENT_CB* client_cb = bta_hf_client_find_cb_by_sco_handle(sco_idx);
  if (client_cb == NULL) {
    APPL_TRACE_ERROR("%s: wrong handle to control block %d", __func__, sco_idx);
    return;
  }

  BT_HDR* p_buf = (BT_HDR*)osi_malloc(sizeof(BT_HDR));
  p_buf->event = BTA_HF_CLIENT_SCO_CLOSE_EVT;
  p_buf->layer_specific = client_cb->handle;
  bta_sys_sendmsg(p_buf);
}

/*******************************************************************************
 *
 * Function         bta_hf_client_create_sco
 *
 * Description
 *
 *
 * Returns          void
 *
 ******************************************************************************/
static void bta_hf_client_sco_create(tBTA_HF_CLIENT_CB* client_cb,
                                     bool is_orig) {
  tBTM_STATUS status;

  APPL_TRACE_DEBUG("%s: %d", __func__, is_orig);

  /* Make sure this SCO handle is not already in use */
  if (client_cb->sco_idx != BTM_INVALID_SCO_INDEX) {
    APPL_TRACE_WARNING("%s: Index 0x%04x already in use", __func__,
                       client_cb->sco_idx);
    return;
  }

  enh_esco_params_t params = esco_parameters_for_codec(ESCO_CODEC_CVSD);

  /* if initiating set current scb and peer bd addr */
  if (is_orig) {
    BTM_SetEScoMode(&params);
    /* tell sys to stop av if any */
    bta_sys_sco_use(BTA_ID_HS, 1, client_cb->peer_addr);
  }

  status = BTM_CreateSco(&client_cb->peer_addr, is_orig, params.packet_types,
                         &client_cb->sco_idx, bta_hf_client_sco_conn_cback,
                         bta_hf_client_sco_disc_cback);
  if (status == BTM_CMD_STARTED && !is_orig) {
    if (!BTM_RegForEScoEvts(client_cb->sco_idx,
                            bta_hf_client_esco_connreq_cback))
      APPL_TRACE_DEBUG("%s: SCO registration success", __func__);
  }

  APPL_TRACE_API("%s: orig %d, inx 0x%04x, status 0x%x, pkt types 0x%04x",
                 __func__, is_orig, client_cb->sco_idx, status,
                 params.packet_types);
}

/*******************************************************************************
 *
 * Function         bta_hf_client_sco_event
 *
 * Description      Handle SCO events
 *
 *
 * Returns          void
 *
 ******************************************************************************/
static void bta_hf_client_sco_event(tBTA_HF_CLIENT_CB* client_cb,
                                    uint8_t event) {
  APPL_TRACE_DEBUG("%s: before state: %d event: %d", __func__,
                   client_cb->sco_state, event);

  switch (client_cb->sco_state) {
    case BTA_HF_CLIENT_SCO_SHUTDOWN_ST:
      switch (event) {
        // For WBS we only listen to SCO requests. Even for outgoing SCO
        // requests we first do a AT+BCC and wait for remote to initiate SCO
        case BTA_HF_CLIENT_SCO_LISTEN_E:
          /* create SCO listen connection */
          bta_hf_client_sco_create(client_cb, false);
          client_cb->sco_state = BTA_HF_CLIENT_SCO_LISTEN_ST;
          break;

        // For non WBS cases and enabling outgoing SCO requests we need to force
        // open a SCO channel
        case BTA_HF_CLIENT_SCO_OPEN_E:
          /* remove listening connection */
          bta_hf_client_sco_remove(client_cb);

          /* create SCO connection to peer */
          bta_hf_client_sco_create(client_cb, true);
          client_cb->sco_state = BTA_HF_CLIENT_SCO_OPENING_ST;
          break;

        default:
          APPL_TRACE_WARNING("BTA_HF_CLIENT_SCO_SHUTDOWN_ST: Ignoring event %d",
                             event);
          break;
      }
      break;

    case BTA_HF_CLIENT_SCO_LISTEN_ST:
      switch (event) {
        case BTA_HF_CLIENT_SCO_LISTEN_E:
          /* create SCO listen connection */
          bta_hf_client_sco_create(client_cb, false);

        case BTA_HF_CLIENT_SCO_OPEN_E:
          /* remove listening connection */
          bta_hf_client_sco_remove(client_cb);

          /* create SCO connection to peer */
          bta_hf_client_sco_create(client_cb, true);
          client_cb->sco_state = BTA_HF_CLIENT_SCO_OPENING_ST;
          break;

        case BTA_HF_CLIENT_SCO_SHUTDOWN_E:
        case BTA_HF_CLIENT_SCO_CLOSE_E:
          /* remove listening connection */
          bta_hf_client_sco_remove(client_cb);

          client_cb->sco_state = BTA_HF_CLIENT_SCO_SHUTDOWN_ST;
          break;

        case BTA_HF_CLIENT_SCO_CONN_CLOSE_E:
          /* SCO failed; create SCO listen connection */
          bta_hf_client_sco_create(client_cb, false);
          client_cb->sco_state = BTA_HF_CLIENT_SCO_LISTEN_ST;
          break;

        default:
          APPL_TRACE_WARNING(
              "%s: BTA_HF_CLIENT_SCO_LISTEN_ST: Ignoring event %d", __func__,
              event);
          break;
      }
      break;

    case BTA_HF_CLIENT_SCO_OPENING_ST:
      switch (event) {
        case BTA_HF_CLIENT_SCO_CLOSE_E:
          client_cb->sco_state = BTA_HF_CLIENT_SCO_OPEN_CL_ST;
          break;

        case BTA_HF_CLIENT_SCO_SHUTDOWN_E:
          client_cb->sco_state = BTA_HF_CLIENT_SCO_SHUTTING_ST;
          break;

        case BTA_HF_CLIENT_SCO_CONN_OPEN_E:
          client_cb->sco_state = BTA_HF_CLIENT_SCO_OPEN_ST;
          break;

        case BTA_HF_CLIENT_SCO_CONN_CLOSE_E:
          /* SCO failed; create SCO listen connection */
          bta_hf_client_sco_create(client_cb, false);
          client_cb->sco_state = BTA_HF_CLIENT_SCO_LISTEN_ST;
          break;

        default:
          APPL_TRACE_WARNING("BTA_HF_CLIENT_SCO_OPENING_ST: Ignoring event %d",
                             event);
          break;
      }
      break;

    case BTA_HF_CLIENT_SCO_OPEN_CL_ST:
      switch (event) {
        case BTA_HF_CLIENT_SCO_OPEN_E:
          client_cb->sco_state = BTA_HF_CLIENT_SCO_OPENING_ST;
          break;

        case BTA_HF_CLIENT_SCO_SHUTDOWN_E:
          client_cb->sco_state = BTA_HF_CLIENT_SCO_SHUTTING_ST;
          break;

        case BTA_HF_CLIENT_SCO_CONN_OPEN_E:
          /* close SCO connection */
          bta_hf_client_sco_remove(client_cb);

          client_cb->sco_state = BTA_HF_CLIENT_SCO_CLOSING_ST;
          break;

        case BTA_HF_CLIENT_SCO_CONN_CLOSE_E:
          /* SCO failed; create SCO listen connection */

          client_cb->sco_state = BTA_HF_CLIENT_SCO_LISTEN_ST;
          break;

        default:
          APPL_TRACE_WARNING("BTA_HF_CLIENT_SCO_OPEN_CL_ST: Ignoring event %d",
                             event);
          break;
      }
      break;

    case BTA_HF_CLIENT_SCO_OPEN_ST:
      switch (event) {
        case BTA_HF_CLIENT_SCO_CLOSE_E:
          if (bta_hf_client_sco_remove(client_cb)) {
            client_cb->sco_state = BTA_HF_CLIENT_SCO_CLOSING_ST;
          }
          break;

        case BTA_HF_CLIENT_SCO_SHUTDOWN_E:
          /* remove listening connection */
          bta_hf_client_sco_remove(client_cb);

          client_cb->sco_state = BTA_HF_CLIENT_SCO_SHUTTING_ST;
          break;

        case BTA_HF_CLIENT_SCO_CONN_CLOSE_E:
          /* peer closed SCO */
          bta_hf_client_sco_create(client_cb, false);
          client_cb->sco_state = BTA_HF_CLIENT_SCO_LISTEN_ST;
          break;

        default:
          APPL_TRACE_WARNING("BTA_HF_CLIENT_SCO_OPEN_ST: Ignoring event %d",
                             event);
          break;
      }
      break;

    case BTA_HF_CLIENT_SCO_CLOSING_ST:
      switch (event) {
        case BTA_HF_CLIENT_SCO_OPEN_E:
          client_cb->sco_state = BTA_HF_CLIENT_SCO_CLOSE_OP_ST;
          break;

        case BTA_HF_CLIENT_SCO_SHUTDOWN_E:
          client_cb->sco_state = BTA_HF_CLIENT_SCO_SHUTTING_ST;
          break;

        case BTA_HF_CLIENT_SCO_CONN_CLOSE_E:
          /* peer closed sco; create SCO listen connection */
          bta_hf_client_sco_create(client_cb, false);
          client_cb->sco_state = BTA_HF_CLIENT_SCO_LISTEN_ST;
          break;

        default:
          APPL_TRACE_WARNING("BTA_HF_CLIENT_SCO_CLOSING_ST: Ignoring event %d",
                             event);
          break;
      }
      break;

    case BTA_HF_CLIENT_SCO_CLOSE_OP_ST:
      switch (event) {
        case BTA_HF_CLIENT_SCO_CLOSE_E:
          client_cb->sco_state = BTA_HF_CLIENT_SCO_CLOSING_ST;
          break;

        case BTA_HF_CLIENT_SCO_SHUTDOWN_E:
          client_cb->sco_state = BTA_HF_CLIENT_SCO_SHUTTING_ST;
          break;

        case BTA_HF_CLIENT_SCO_CONN_CLOSE_E:
          /* open SCO connection */
          bta_hf_client_sco_create(client_cb, true);
          client_cb->sco_state = BTA_HF_CLIENT_SCO_OPENING_ST;
          break;

        default:
          APPL_TRACE_WARNING("BTA_HF_CLIENT_SCO_CLOSE_OP_ST: Ignoring event %d",
                             event);
          break;
      }
      break;

    case BTA_HF_CLIENT_SCO_SHUTTING_ST:
      switch (event) {
        case BTA_HF_CLIENT_SCO_CONN_OPEN_E:
          /* close SCO connection; wait for conn close event */
          bta_hf_client_sco_remove(client_cb);
          break;

        case BTA_HF_CLIENT_SCO_CONN_CLOSE_E:
          client_cb->sco_state = BTA_HF_CLIENT_SCO_SHUTDOWN_ST;
          break;

        case BTA_HF_CLIENT_SCO_SHUTDOWN_E:
          client_cb->sco_state = BTA_HF_CLIENT_SCO_SHUTDOWN_ST;
          break;

        default:
          APPL_TRACE_WARNING("BTA_HF_CLIENT_SCO_SHUTTING_ST: Ignoring event %d",
                             event);
          break;
      }
      break;

    default:
      break;
  }

  APPL_TRACE_DEBUG("%s: after state: %d", __func__, client_cb->sco_state);
}

/*******************************************************************************
 *
 * Function         bta_hf_client_sco_listen
 *
 * Description      Initialize SCO listener
 *
 *
 * Returns          void
 *
 ******************************************************************************/
void bta_hf_client_sco_listen(tBTA_HF_CLIENT_DATA* p_data) {
  APPL_TRACE_DEBUG("%s", __func__);

  tBTA_HF_CLIENT_CB* client_cb =
      bta_hf_client_find_cb_by_handle(p_data->hdr.layer_specific);
  if (client_cb == NULL) {
    APPL_TRACE_ERROR("%s: wrong handle to control block %d", __func__,
                     p_data->hdr.layer_specific);
    return;
  }

  bta_hf_client_sco_event(client_cb, BTA_HF_CLIENT_SCO_LISTEN_E);
}

/*******************************************************************************
 *
 * Function         bta_hf_client_sco_shutdown
 *
 * Description
 *
 *
 * Returns          void
 *
 ******************************************************************************/
void bta_hf_client_sco_shutdown(tBTA_HF_CLIENT_CB* client_cb) {
  APPL_TRACE_DEBUG("%s", __func__);

  bta_hf_client_sco_event(client_cb, BTA_HF_CLIENT_SCO_SHUTDOWN_E);
}

/*******************************************************************************
 *
 * Function         bta_hf_client_sco_conn_open
 *
 * Description
 *
 *
 * Returns          void
 *
 ******************************************************************************/
void bta_hf_client_sco_conn_open(tBTA_HF_CLIENT_DATA* p_data) {
  APPL_TRACE_DEBUG("%s", __func__);

  tBTA_HF_CLIENT_CB* client_cb =
      bta_hf_client_find_cb_by_handle(p_data->hdr.layer_specific);
  if (client_cb == NULL) {
    APPL_TRACE_ERROR("%s: wrong handle to control block %d", __func__,
                     p_data->hdr.layer_specific);
    return;
  }

  bta_hf_client_sco_event(client_cb, BTA_HF_CLIENT_SCO_CONN_OPEN_E);

  bta_sys_sco_open(BTA_ID_HS, 1, client_cb->peer_addr);

  if (client_cb->negotiated_codec == BTM_SCO_CODEC_MSBC) {
    bta_hf_client_cback_sco(client_cb, BTA_HF_CLIENT_AUDIO_MSBC_OPEN_EVT);
  } else {
    bta_hf_client_cback_sco(client_cb, BTA_HF_CLIENT_AUDIO_OPEN_EVT);
  }
}

/*******************************************************************************
 *
 * Function         bta_hf_client_sco_conn_close
 *
 * Description
 *
 *
 * Returns          void
 *
 ******************************************************************************/
void bta_hf_client_sco_conn_close(tBTA_HF_CLIENT_DATA* p_data) {
  APPL_TRACE_DEBUG("%s", __func__);

  tBTA_HF_CLIENT_CB* client_cb =
      bta_hf_client_find_cb_by_handle(p_data->hdr.layer_specific);
  if (client_cb == NULL) {
    APPL_TRACE_ERROR("%s: wrong handle to control block %d", __func__,
                     p_data->hdr.layer_specific);
    return;
  }

  /* clear current scb */
  client_cb->sco_idx = BTM_INVALID_SCO_INDEX;

  bta_hf_client_sco_event(client_cb, BTA_HF_CLIENT_SCO_CONN_CLOSE_E);

  bta_sys_sco_close(BTA_ID_HS, 1, client_cb->peer_addr);

  bta_sys_sco_unuse(BTA_ID_HS, 1, client_cb->peer_addr);

  /* call app callback */
  bta_hf_client_cback_sco(client_cb, BTA_HF_CLIENT_AUDIO_CLOSE_EVT);

  if (client_cb->sco_close_rfc) {
    client_cb->sco_close_rfc = false;
    bta_hf_client_rfc_do_close(p_data);
  }
}

/*******************************************************************************
 *
 * Function         bta_hf_client_sco_open
 *
 * Description
 *
 *
 * Returns          void
 *
 ******************************************************************************/
void bta_hf_client_sco_open(tBTA_HF_CLIENT_DATA* p_data) {
  APPL_TRACE_DEBUG("%s", __func__);

  tBTA_HF_CLIENT_CB* client_cb =
      bta_hf_client_find_cb_by_handle(p_data->hdr.layer_specific);
  if (client_cb == NULL) {
    APPL_TRACE_ERROR("%s: wrong handle to control block %d", __func__,
                     p_data->hdr.layer_specific);
    return;
  }

  bta_hf_client_sco_event(client_cb, BTA_HF_CLIENT_SCO_OPEN_E);
}

/*******************************************************************************
 *
 * Function         bta_hf_client_sco_close
 *
 * Description
 *
 *
 * Returns          void
 *
 ******************************************************************************/
void bta_hf_client_sco_close(tBTA_HF_CLIENT_DATA* p_data) {
  tBTA_HF_CLIENT_CB* client_cb =
      bta_hf_client_find_cb_by_handle(p_data->hdr.layer_specific);
  if (client_cb == NULL) {
    APPL_TRACE_ERROR("%s: wrong handle to control block %d", __func__,
                     p_data->hdr.layer_specific);
    return;
  }

  APPL_TRACE_DEBUG("%s: sco_idx 0x%x", __func__, client_cb->sco_idx);

  if (client_cb->sco_idx != BTM_INVALID_SCO_INDEX) {
    bta_hf_client_sco_event(client_cb, BTA_HF_CLIENT_SCO_CLOSE_E);
  }
}