use crate::{ble, BLEAdvertisedDevice, BLEReturnCode, Signal};
use alloc::{boxed::Box, vec::Vec};
use core::ffi::c_void;

pub struct BLEScan {
  #[allow(clippy::type_complexity)]
  on_result: Option<Box<dyn FnMut(&BLEAdvertisedDevice) + Send + Sync>>,
  on_completed: Option<Box<dyn FnMut() + Send + Sync>>,
  scan_params: esp_idf_sys::ble_gap_disc_params,
  stopped: bool,
  scan_results: Vec<BLEAdvertisedDevice>,
  signal: Signal<()>,
}

impl BLEScan {
  pub(crate) fn new() -> Self {
    let mut ret = Self {
      on_result: None,
      on_completed: None,
      scan_params: esp_idf_sys::ble_gap_disc_params {
        itvl: 0,
        window: 0,
        filter_policy: esp_idf_sys::BLE_HCI_SCAN_FILT_NO_WL as _,
        _bitfield_align_1: [0; 0],
        _bitfield_1: esp_idf_sys::__BindgenBitfieldUnit::new([0; 1]),
      },
      stopped: true,
      scan_results: Vec::new(),
      signal: Signal::new(),
    };
    ret.limited(false);
    ret.filter_duplicates(true);
    ret.active_scan(false).interval(100).window(100);
    ret
  }

  pub fn active_scan(&mut self, active: bool) -> &mut Self {
    self.scan_params.set_passive((!active) as _);
    self
  }

  pub fn filter_duplicates(&mut self, val: bool) -> &mut Self {
    self.scan_params.set_filter_duplicates(val as _);
    self
  }

  /// Set whether or not the BLE controller only report scan results
  /// from devices advertising in limited discovery mode, i.e. directed advertising.
  pub fn limited(&mut self, val: bool) -> &mut Self {
    self.scan_params.set_limited(val as _);
    self
  }

  /// Set the interval to scan.
  pub fn interval(&mut self, interval_msecs: u16) -> &mut Self {
    self.scan_params.itvl = ((interval_msecs as f32) / 0.625) as u16;
    self
  }

  /// Set the window to actively scan.
  pub fn window(&mut self, window_msecs: u16) -> &mut Self {
    self.scan_params.window = ((window_msecs as f32) / 0.625) as u16;
    self
  }

  pub fn on_result(
    &mut self,
    callback: impl FnMut(&BLEAdvertisedDevice) + Send + Sync + 'static,
  ) -> &mut Self {
    self.on_result = Some(Box::new(callback));
    self
  }

  pub fn on_completed(&mut self, callback: impl FnMut() + Send + Sync + 'static) -> &mut Self {
    self.on_completed = Some(Box::new(callback));
    self
  }

  pub async fn start(&mut self, duration_ms: i32) -> Result<(), BLEReturnCode> {
    unsafe {
      ble!(esp_idf_sys::ble_gap_disc(
        crate::ble_device::OWN_ADDR_TYPE,
        duration_ms,
        &self.scan_params,
        Some(Self::handle_gap_event),
        self as *mut Self as _,
      ))?;
    }
    self.stopped = false;

    self.signal.wait().await;
    Ok(())
  }

  pub fn stop(&mut self) -> Result<(), BLEReturnCode> {
    self.stopped = true;
    let rc = unsafe { esp_idf_sys::ble_gap_disc_cancel() };
    if rc != 0 && rc != (esp_idf_sys::BLE_HS_EALREADY as _) {
      return BLEReturnCode::convert(rc as _);
    }

    if let Some(callback) = self.on_completed.as_mut() {
      callback();
    }
    self.signal.signal(());

    Ok(())
  }

  pub fn get_results(&mut self) -> core::slice::Iter<'_, BLEAdvertisedDevice> {
    self.scan_results.iter()
  }

  pub fn clear_results(&mut self) {
    self.scan_results.clear();
  }

  extern "C" fn handle_gap_event(event: *mut esp_idf_sys::ble_gap_event, arg: *mut c_void) -> i32 {
    let event = unsafe { &*event };
    let scan = unsafe { &mut *(arg as *mut Self) };

    match event.type_ as u32 {
      esp_idf_sys::BLE_GAP_EVENT_EXT_DISC | esp_idf_sys::BLE_GAP_EVENT_DISC => {
        let disc = unsafe { &event.__bindgen_anon_1.disc };

        let mut advertised_device = scan
          .scan_results
          .iter_mut()
          .find(|x| x.addr().value.val == disc.addr.val);

        if advertised_device.is_none() {
          if disc.event_type != esp_idf_sys::BLE_HCI_ADV_RPT_EVTYPE_SCAN_RSP as _ {
            let device = BLEAdvertisedDevice::new(disc);
            scan.scan_results.push(device);
            advertised_device = scan.scan_results.last_mut();
          } else {
            return 0;
          }
        }

        let advertised_device = advertised_device.unwrap();

        let data = unsafe { core::slice::from_raw_parts(disc.data, disc.length_data as _) };
        ::log::debug!("DATA: {:X?}", data);
        advertised_device.parse_advertisement(data);

        if let Some(callback) = scan.on_result.as_mut() {
          if scan.scan_params.passive() != 0
            || (advertised_device.adv_type() != esp_idf_sys::BLE_HCI_ADV_TYPE_ADV_IND as _
              && advertised_device.adv_type() != esp_idf_sys::BLE_HCI_ADV_TYPE_ADV_IND as _)
            || disc.event_type == esp_idf_sys::BLE_HCI_ADV_RPT_EVTYPE_SCAN_RSP as _
          {
            callback(advertised_device);
          }
        }
      }
      esp_idf_sys::BLE_GAP_EVENT_DISC_COMPLETE => {
        if let Some(callback) = scan.on_completed.as_mut() {
          callback();
        }
        scan.signal.signal(());
      }
      _ => {}
    }
    0
  }
}
