//! Tools for handling the extension encoding of CoAP options

use core::convert::TryInto;

/// Nibble value for "1 byte extension"
pub const VALUE_1B: u8 = 13u8;

/// Nibble value for "2 byte extension"
pub const VALUE_2B: u8 = 14u8;

/// Nibble value reserved for the payload marker
pub const VALUE_RESERVED: u8 = 15u8;

/// The offset added to a 1-byte extended value
pub const OFFSET_1B: u16 = 13u16;

/// The offset added to a 2-byte extended value
pub const OFFSET_2B: u16 = 269u16;

#[derive(Debug)]
/// All the ways decoding an extension value can go wrong
pub enum ExtensionError {
    /// The reserved value 15 was present in the nibble.
    Reserved15,
    /// The remaining message did not contain the necessary bytes to read the next.
    OutOfBytes,
    /// The encoded value exceeds the encodable maximum.
    ///
    /// While the encoding itself can represent values up to `2^16 + OFFSET_2B`, all practical uses
    /// are capped to `2^16` anyway, allowing the value to be stored in a [u16] and to err outside
    /// of that.
    Overflow,
}

/// Decode an extended value.
///
/// Given a value that was initially extracted from an Option Delta or Option Length nibble of an
/// option, read up to 2 extended bytes from the slice that contains the remaining data, and
/// advance the slice by that amount.
///
/// This returns `Some(())` on success, or None to indicate any error condition (which may be
/// exhaustion of the slice, the presence of the value 15, or an overflow induced by an
/// almost-maximum value in the 2-byte extension
///
/// Typical use
/// -----------
///
/// ```
/// # use coap_message_utils::option_extension::take_extension;
/// let data = b"\xbd\x01long-path-name\xff";
///
/// let nibble = data[0];
/// let mut delta = (nibble >> 4).into();
/// let mut length = (nibble & 0x0f).into();
///
/// let mut view = &data[1..];
/// // Delta is 11; this does not modify anything
/// take_extension(&mut delta, &mut view).unwrap();
/// // Length is 13 + 1; this takes one byte off view
/// take_extension(&mut length, &mut view).unwrap();
///
/// assert!(delta == 11);
/// assert!(length == 14);
/// assert!(&view[..14] == b"long-path-name");
/// ```
pub fn take_extension(value: &mut u16, slice: &mut &[u8]) -> Result<(), ExtensionError> {
    assert!(*value < 16);
    match *value as u8 {
        VALUE_RESERVED => Err(ExtensionError::Reserved15),
        VALUE_2B => {
            *value = u16::from_be_bytes(
                slice
                    .get(..2)
                    .ok_or(ExtensionError::OutOfBytes)?
                    .try_into()
                    .expect("Slice is 2-long by construction"),
            )
            .checked_add(OFFSET_2B)
            .ok_or(ExtensionError::Overflow)?;
            *slice = &slice[2..];
            Ok(())
        }
        VALUE_1B => {
            *value = OFFSET_1B + u16::from(*slice.get(0).ok_or(ExtensionError::OutOfBytes)?);
            *slice = &slice[1..];
            Ok(())
        }
        _ => Ok(()),
    }
}

/// Write an extended option delta or length value into a vector.
///
/// The resulting nibble then needs to be shifted and stored in the respective half the previously
/// pushed value.
///
/// While this can be used to push the extension data into a
/// [`heapless::Vec`](https://docs.rs/heapless/latest/heapless/struct.Vec.html), beware that
/// heapless indicates its error condition by panicking. Before calling push_extensions on such a
/// target, the caller may want to check whether 2 more bytes (the maximum this function appends)
/// are still free.
///
/// Typical use
/// -----------
///
/// ```
/// # use coap_message_utils::option_extension::push_extensions;
/// let mut options = Vec::new();
///
/// let delta = 11;
/// let length = 14;
///
/// let nibbles_index = options.len();
/// options.push(0);
/// let delta = push_extensions(&mut options, delta);
/// let length = push_extensions(&mut options, length);
/// options[nibbles_index] = (delta << 4) | length;
///
/// assert!(options == b"\xbd\x01");
/// ```
///
/// Deprecation
/// -----------
///
/// Using this is needlessly complex; use [encode_extensions] instead and rely on the compiler to
/// see through the stack allocation and put the data right into place.
#[deprecated(since = "0.0.2", note = "Use encode_extensions again")]
pub fn push_extensions(target: &mut impl core::iter::Extend<u8>, value: u16) -> u8 {
    if value < OFFSET_1B {
        value as u8
    } else if value < OFFSET_2B {
        let value = (value - OFFSET_1B) as u8;
        target.extend([value].iter().map(|x| *x));
        VALUE_1B
    } else {
        let value = value - OFFSET_2B;
        // FIXME I'm sure this can be expressed more concisesly
        target.extend(value.to_be_bytes().iter().map(|x| *x));
        VALUE_2B
    }
}

/// Encode delta and length into a small returned buffer
///
/// This function performs all of the option encoding steps except handling the actual data value.
///
/// Typical use
/// -----------
///
/// ```
/// # use std::convert::TryInto;
/// # use coap_message_utils::option_extension::encode_extensions;
///
/// let mut last_option = 11u16;
/// let mut buffer = heapless::Vec::<u8, heapless::consts::U20>::new();
///
/// let option = 11u16;
/// let delta = option - last_option;
/// let last_option = option;
/// let data = b"core";
///
/// buffer.extend_from_slice(encode_extensions(
///         delta,
///         data.len().try_into().expect("Too long to express"),
///         ).as_ref()).expect("Buffer too short");
/// buffer.extend_from_slice(data).expect("Buffer too short");
///
/// assert!(buffer == b"\x04core");
///
/// ```
#[allow(deprecated)] // It's deprecated from *public* use
#[inline]
pub fn encode_extensions(delta: u16, length: u16) -> impl AsRef<[u8]> {
    let mut ret = heapless::Vec::<u8, heapless::consts::U5>::new();
    ret.push(0).expect("Vector is sufficiently large");
    ret[0] |= push_extensions(&mut ret, delta) << 4;
    ret[0] |= push_extensions(&mut ret, length);

    ret
}
