// Copyright 2023 Google LLC
//
// 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.

//! HCI

// re-export here, and internal usages of these imports should refer to this mod, not the internal
// mod
pub(crate) use crate::internal::hci::WithPacketType;
pub use crate::internal::hci::{packets, Error, Packet};

use crate::wrapper::{
    hci::packets::{AddressType, Command, ErrorCode},
    ConversionError,
};
use itertools::Itertools as _;
use pyo3::{
    exceptions::PyException, intern, types::PyModule, FromPyObject, IntoPy, PyAny, PyErr, PyObject,
    PyResult, Python, ToPyObject,
};

/// Provides helpers for interacting with HCI
pub struct HciConstant;

impl HciConstant {
    /// Human-readable error name
    pub fn error_name(status: ErrorCode) -> PyResult<String> {
        Python::with_gil(|py| {
            PyModule::import(py, intern!(py, "bumble.hci"))?
                .getattr(intern!(py, "HCI_Constant"))?
                .call_method1(intern!(py, "error_name"), (status.to_object(py),))?
                .extract()
        })
    }
}

/// Bumble's representation of an HCI command.
pub(crate) struct HciCommand(pub(crate) PyObject);

impl HciCommand {
    fn from_bytes(bytes: &[u8]) -> PyResult<Self> {
        Python::with_gil(|py| {
            PyModule::import(py, intern!(py, "bumble.hci"))?
                .getattr(intern!(py, "HCI_Command"))?
                .call_method1(intern!(py, "from_bytes"), (bytes,))
                .map(|obj| Self(obj.to_object(py)))
        })
    }
}

impl TryFrom<Command> for HciCommand {
    type Error = PyErr;

    fn try_from(value: Command) -> Result<Self, Self::Error> {
        HciCommand::from_bytes(&value.to_vec_with_packet_type())
    }
}

impl IntoPy<PyObject> for HciCommand {
    fn into_py(self, _py: Python<'_>) -> PyObject {
        self.0
    }
}

/// A Bluetooth address
#[derive(Clone)]
pub struct Address(pub(crate) PyObject);

impl Address {
    /// Creates a new [Address] object.
    pub fn new(address: &str, address_type: AddressType) -> PyResult<Self> {
        Python::with_gil(|py| {
            PyModule::import(py, intern!(py, "bumble.hci"))?
                .getattr(intern!(py, "Address"))?
                .call1((address, address_type))
                .map(|any| Self(any.into()))
        })
    }

    /// The type of address
    pub fn address_type(&self) -> PyResult<AddressType> {
        Python::with_gil(|py| {
            self.0
                .getattr(py, intern!(py, "address_type"))?
                .extract::<u8>(py)?
                .try_into()
                .map_err(|addr_type| {
                    PyErr::new::<PyException, _>(format!(
                        "Failed to convert {addr_type} to AddressType"
                    ))
                })
        })
    }

    /// True if the address is static
    pub fn is_static(&self) -> PyResult<bool> {
        Python::with_gil(|py| {
            self.0
                .getattr(py, intern!(py, "is_static"))?
                .extract::<bool>(py)
        })
    }

    /// True if the address is resolvable
    pub fn is_resolvable(&self) -> PyResult<bool> {
        Python::with_gil(|py| {
            self.0
                .getattr(py, intern!(py, "is_resolvable"))?
                .extract::<bool>(py)
        })
    }

    /// Address bytes in _little-endian_ format
    pub fn as_le_bytes(&self) -> PyResult<Vec<u8>> {
        Python::with_gil(|py| {
            self.0
                .call_method0(py, intern!(py, "to_bytes"))?
                .extract::<Vec<u8>>(py)
        })
    }

    /// Address bytes as big-endian colon-separated hex
    pub fn as_hex(&self) -> PyResult<String> {
        self.as_le_bytes().map(|bytes| {
            bytes
                .into_iter()
                .rev()
                .map(|byte| hex::encode_upper([byte]))
                .join(":")
        })
    }
}

impl ToPyObject for Address {
    fn to_object(&self, _py: Python<'_>) -> PyObject {
        self.0.clone()
    }
}

/// An error meaning that the u64 value did not represent a valid BT address.
#[derive(Debug)]
pub struct InvalidAddress(#[allow(unused)] u64);

impl TryInto<packets::Address> for Address {
    type Error = ConversionError<InvalidAddress>;

    fn try_into(self) -> Result<packets::Address, Self::Error> {
        let addr_le_bytes = self.as_le_bytes().map_err(ConversionError::Python)?;

        // packets::Address only supports converting from a u64 (TODO: update if/when it supports converting from [u8; 6] -- https://github.com/google/pdl/issues/75)
        // So first we take the python `Address` little-endian bytes (6 bytes), copy them into a
        // [u8; 8] in little-endian format, and finally convert it into a u64.
        let mut buf = [0_u8; 8];
        buf[0..6].copy_from_slice(&addr_le_bytes);
        let address_u64 = u64::from_le_bytes(buf);

        packets::Address::try_from(address_u64)
            .map_err(InvalidAddress)
            .map_err(ConversionError::Native)
    }
}

impl IntoPy<PyObject> for AddressType {
    fn into_py(self, py: Python<'_>) -> PyObject {
        u8::from(self).to_object(py)
    }
}

impl<'source> FromPyObject<'source> for ErrorCode {
    fn extract(ob: &'source PyAny) -> PyResult<Self> {
        // Bumble represents error codes simply as a single-byte number (in Rust, u8)
        let value: u8 = ob.extract()?;
        ErrorCode::try_from(value).map_err(|b| {
            PyErr::new::<PyException, _>(format!("Failed to map {b} to an error code"))
        })
    }
}

impl ToPyObject for ErrorCode {
    fn to_object(&self, py: Python<'_>) -> PyObject {
        u8::from(self).to_object(py)
    }
}
