/******************************************************************************
 *
 *  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 <bluetooth/log.h>

#include <cstddef>
#include <cstdint>
#include <cstring>

#include "bta/hf_client/bta_hf_client_int.h"
#include "bta_hf_client_api.h"
#include "bta_sys.h"
#include "btm_api_types.h"
#include "device/include/esco_parameters.h"
#include "hci_error_code.h"
#include "osi/include/allocator.h"
#include "stack/include/bt_hdr.h"
#include "stack/include/btm_client_interface.h"
#include "stack/include/btm_status.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)

using namespace bluetooth;

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;

  log::verbose("");

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

    log::verbose("idx 0x{:04x}, status:0x{:x}", client_cb->sco_idx, status);

    if (status == tBTM_STATUS::BTM_CMD_STARTED) {
      removed_started = true;
    } else if ((status == tBTM_STATUS::BTM_SUCCESS) || (status == tBTM_STATUS::BTM_UNKNOWN_ADDR)) {
      /* If no connection reset the SCO handle */
      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;
  tHCI_STATUS hci_status = HCI_SUCCESS;

  log::verbose("");

  if (client_cb->sco_state == BTA_HF_CLIENT_SCO_LISTEN_ST) {
    if (p_data->link_type == BTM_LINK_TYPE_SCO) {
      // SCO
      resp = esco_parameters_for_codec(SCO_CODEC_CVSD_D1, true);
    } else if (client_cb->negotiated_codec == BTM_SCO_CODEC_LC3) {
      // eSCO LC3, HFP 1.9
      resp = esco_parameters_for_codec(ESCO_CODEC_LC3_T2, true);
    } else if (client_cb->negotiated_codec == BTM_SCO_CODEC_MSBC) {
      // eSCO mSBC
      resp = esco_parameters_for_codec(ESCO_CODEC_MSBC_T2, true);
    } else if (bta_hf_client_cb_arr.features & BTA_HF_CLIENT_FEAT_ESCO_S4) {
      // eSCO CVSD, HFP 1.7 requires S4
      resp = esco_parameters_for_codec(ESCO_CODEC_CVSD_S4, true);
    } else {
      // eSCO CVSD, S3 is preferred by default(before HFP 1.7)
      resp = esco_parameters_for_codec(ESCO_CODEC_CVSD_S3, true);
    }

    /* 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;
  }

  get_btm_client_interface().sco.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) {
  log::verbose("{}", 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) {
    log::error("wrong SCO handle to control block {}", 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) {
  log::verbose("{}", sco_idx);

  tBTA_HF_CLIENT_CB* client_cb = bta_hf_client_find_cb_by_sco_handle(sco_idx);
  if (client_cb == NULL) {
    log::error("wrong SCO handle to control block {}", sco_idx);
    return;
  }

  BT_HDR_RIGID* p_buf = (BT_HDR_RIGID*)osi_malloc(sizeof(BT_HDR_RIGID));
  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) {
  log::verbose("sco_idx {}", sco_idx);

  tBTA_HF_CLIENT_CB* client_cb = bta_hf_client_find_cb_by_sco_handle(sco_idx);
  if (client_cb == NULL) {
    log::error("wrong handle to control block {}", sco_idx);
    return;
  }

  BT_HDR_RIGID* p_buf = (BT_HDR_RIGID*)osi_malloc(sizeof(BT_HDR_RIGID));
  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;

  log::verbose("{}", is_orig);

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

  // codec parameters
  enh_esco_params_t params;
  // Since HF device is not expected to receive AT+BAC send +BCS command,
  // codec support of the connected AG device will be unknown,
  // so HF device will always establish only CVSD connection.
  if ((bta_hf_client_cb_arr.features & BTA_HF_CLIENT_FEAT_ESCO_S4) &&
      (client_cb->peer_features & BTA_HF_CLIENT_PEER_ESCO_S4)) {
    // eSCO CVSD, HFP 1.7 requires S4
    params = esco_parameters_for_codec(ESCO_CODEC_CVSD_S4, true);
  } else {
    // eSCO CVSD, S3 is preferred by default(before HFP 1.7)
    params = esco_parameters_for_codec(ESCO_CODEC_CVSD_S3, true);
  }

  /* if initiating set current scb and peer bd addr */
  if (is_orig) {
    if (get_btm_client_interface().sco.BTM_SetEScoMode(&params) != tBTM_STATUS::BTM_SUCCESS) {
      log::warn("Unable to set ESCO mode");
    }
    /* tell sys to stop av if any */
    bta_sys_sco_use(BTA_ID_HS, 1, client_cb->peer_addr);
  }

  status = get_btm_client_interface().sco.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 == tBTM_STATUS::BTM_CMD_STARTED && !is_orig) {
    if (get_btm_client_interface().sco.BTM_RegForEScoEvts(
                client_cb->sco_idx, bta_hf_client_esco_connreq_cback) == tBTM_STATUS::BTM_SUCCESS) {
      log::verbose("SCO registration success");
    }
  }

  log::verbose("orig {}, inx 0x{:04x}, status 0x{:x}, pkt types 0x{:04x}", 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) {
  log::verbose("before state: {} event: {}", 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:
          log::warn("BTA_HF_CLIENT_SCO_SHUTDOWN_ST: Ignoring event {}", 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);
          break;

        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:
          /* 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:
          log::warn("BTA_HF_CLIENT_SCO_LISTEN_ST: Ignoring event {}", 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:
          log::warn("BTA_HF_CLIENT_SCO_OPENING_ST: Ignoring event {}", 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:
          log::warn("BTA_HF_CLIENT_SCO_OPEN_CL_ST: Ignoring event {}", 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:
          log::warn("BTA_HF_CLIENT_SCO_OPEN_ST: Ignoring event {}", 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:
          log::warn("BTA_HF_CLIENT_SCO_CLOSING_ST: Ignoring event {}", 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:
          log::warn("BTA_HF_CLIENT_SCO_CLOSE_OP_ST: Ignoring event {}", 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:
          log::warn("BTA_HF_CLIENT_SCO_SHUTTING_ST: Ignoring event {}", event);
          break;
      }
      break;

    default:
      break;
  }

  log::verbose("after state: {}", 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) {
  log::verbose("");

  tBTA_HF_CLIENT_CB* client_cb = bta_hf_client_find_cb_by_handle(p_data->hdr.layer_specific);
  if (client_cb == NULL) {
    log::error("wrong handle to control block {}", 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) {
  log::verbose("");

  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) {
  log::verbose("");

  tBTA_HF_CLIENT_CB* client_cb = bta_hf_client_find_cb_by_handle(p_data->hdr.layer_specific);
  if (client_cb == NULL) {
    log::error("wrong handle to control block {}", 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_LC3) {
    bta_hf_client_cback_sco(client_cb, BTA_HF_CLIENT_AUDIO_LC3_OPEN_EVT);
  } else 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) {
  log::verbose("");

  tBTA_HF_CLIENT_CB* client_cb = bta_hf_client_find_cb_by_handle(p_data->hdr.layer_specific);
  if (client_cb == NULL) {
    log::error("wrong handle to control block {}", 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) {
  log::verbose("");

  tBTA_HF_CLIENT_CB* client_cb = bta_hf_client_find_cb_by_handle(p_data->hdr.layer_specific);
  if (client_cb == NULL) {
    log::error("wrong handle to control block {}", 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) {
    log::error("wrong handle to control block {}", p_data->hdr.layer_specific);
    return;
  }

  log::verbose("sco_idx 0x{:x}", 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);
  }
}
