//! Implementation of [`MutableWritableMessage`].
//!
//! [`MessageMut`] is the main struct of this module. A [`MessageMut`] is constructed based on an
//! [`MessageBufferMut`], which can encapsulate anything from slices (when `EM` is a
//! [`SliceBufferMut`]), to owned data, or something inbetween (like a single
//! [`core::cell::RefMut`] that is being sliced into).
#![cfg_attr(feature = "downcast", allow(unsafe_code))]

use super::*;

use crate::option_extension::option_header_len;

/// A CoAP messag that resides in contiguous mutable memory.
///
/// Exceeding the guarantees of `MutableWritableMessage`, this does allow some out-of-sequence
/// invocations: Even after payload has been written to, options can be added, memmove'ing
/// (right-rotating) the written payload, as long as it does not truncate it. This is needed
/// to accommodate libOSCORE's requirements (because while libOSCORE can also do without a
/// memmove-ing message, that'd require its use through `WritableMessage` to adhere to in-OSCORE
/// write sequence conventions, making the whole situation no easier on the coap-message
/// abstraction). Data will only be moved if an option is added after content has been set, so this
/// comes at no runtime cost for those who do not need it. (It may be later turned into a feature.
/// Then, the memmove code would be removed; carrying the latest option number in the `WriteState`
/// should come at no extra cost due to the struct's content and alignment).
///
/// When viewed through the [`coap_message::ReadableMessage`] trait, this will behave as if writing
/// had stopped (but still allows writes after reading); in particular, the payload will be shown
/// as empty. (This may be obvious from most points of view, but when coming from a
/// [`coap_message::MutableWritableMessage`] point of view where payloads can only ever be truncated and not
/// made longer, this clarification is relevant).
///
/// The type is covariant over its lifetime. This also means that we can never add any methods
/// where we move references into Self, but we don't do this anyway: The lifetime `'a` only serves
/// to describe the memory backing it; data is moved in there, not stored by reference. This
/// property is accessible through downcasting.
///
/// # Invariants
///
/// The message in the `EM` is always a well-formed CoAP message. This is upheld by construction or
/// checked at creation time (when existing messages are consumed). As the underlying
/// [`MessageBufferMut`] can be implemented without unsafe, this crate can only panic (but not
/// invoke undefined behavior) when that is violated.
#[derive(Clone)]
pub struct MessageMut<EM: MessageBufferMut> {
    /// Pointers to the actuall message data.
    pub(crate) encoded: EM,

    /// Latest option that has been written.
    pub(crate) latest: u16,
    /// Index after the last written byte.
    pub(crate) end: usize,
    /// First byte of any written payload.
    ///
    /// If this has been set, the byte before it was written 0xff. This is None if the was an empty
    /// payload, or none has been set yet.
    pub(crate) payload_start: Option<usize>,
}

/// Helper for extracting the type ID used with an MessageBuffer out of a value with unnamed
/// (RPIT) type.
#[cfg(feature = "downcast")]
fn type_id_of_emv_of_lml<T: 'static + MessageBufferMut>(
    _val: &LifetimesMatterLittle<T>,
) -> core::any::TypeId {
    core::any::TypeId::of::<MessageMut<T>>()
}

impl<EM: MessageBufferMut> core::fmt::Debug for MessageMut<EM> {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("MessageMut")
            .field("MessageBufferMut impl", &core::any::type_name::<EM>())
            .field("encoded.code", &self.encoded.code())
            .field("encoded.tail", &self.encoded.tail())
            .field("latest", &self.latest)
            .field("end", &self.end)
            .field("payload_start", &self.payload_start)
            .finish()
    }
}

#[cfg(feature = "defmt")]
impl<EM: MessageBufferMut> defmt::Format for MessageMut<EM> {
    fn format(&self, f: defmt::Formatter<'_>) {
        defmt::write!(
            f,
            "MessageMut {{ MessageBufferMut impl {=str}, code {=u8}, tail {=[u8]}, latest {=u16}, end {}, payload_start {} }}",
            &core::any::type_name::<EM>(),
            self.encoded.code(),
            self.encoded.tail(),
            self.latest,
            self.end,
            self.payload_start
        );
    }
}

impl<'a> MessageMut<SliceBufferMut<'a>> {
    /// Constructor of a message buffer around exclusive memory.
    ///
    /// This is a short-cut for creating a [`SliceBufferMut`] and passing it to
    /// [`Self::new_empty()`].
    pub fn new_in_slice(code: &'a mut u8, tail: &'a mut [u8]) -> Self {
        MessageMut {
            encoded: SliceBufferMut::new(code, tail),
            latest: 0,
            end: 0,
            payload_start: None,
        }
    }

    /// Creates a `MutableWritableMessage` on a buffer that already contains a serialized message.
    ///
    /// While this is generally not useful (as the message has been completed and there is no room
    /// for anything more), it allows mutable access to the option bytes and the payload. This is
    /// primarily useful in situations when data is processed in place, eg. decrypted (in OSCORE),
    /// or CBOR is shifted around to get contiguous slices out of indefinite length strings.
    ///
    /// If the message has been parsed as a [`Message`] around a mutable back-end
    /// before, it is preferable to use its
    /// [`.into_mutable()`][Message::into_mutable] method, as that can avoid the
    /// full parsing that is needed to uphold this type's guarantees.
    ///
    /// # Errors
    ///
    /// … are returned if the input slice does not contain a well-formed message.
    pub fn new_from_encoded_slice(
        code: &'a mut u8,
        tail: &'a mut [u8],
    ) -> Result<Self, ParsingError> {
        let em = SliceBufferMut::new(code, tail);
        let read_only = Message::new(em);
        read_only.into_mutable()
    }
}

impl<T: MessageBufferMut> MessageMut<T> {
    /// Constructor of a message buffer around any [`MessageBufferMut`].
    pub fn new_empty(encoded: T) -> Self {
        MessageMut {
            encoded,
            latest: 0,
            end: 0,
            payload_start: None,
        }
    }

    /// Returns a full [`Self`] from input that is expected (but not known at type level) to be one.
    #[cfg(feature = "downcast")]
    pub fn downcast_from<M: MinimalWritableMessage>(generic: &mut M) -> Option<&mut Self> {
        let (reference, type_id) = generic.with_static_type_annotation()?.into_inner();

        let our_static = T::static_mut_variant()?;
        let our_id = type_id_of_emv_of_lml(&our_static);

        if type_id != our_id {
            return None;
        }
        // One of us, one of us.
        let ptr = reference as *mut M as *mut Self;
        // SAFETY: The RefWithStaticType matching our type ID will only be constructed if M is
        // MessageMut, and whatever MessageMut<'b> it was before, it will live at least for 'a (because
        // we got a &'a MessageMut<'b> and is covariant.
        Some(unsafe { &mut *ptr })
    }
}

impl<EM: MessageBufferMut> MessageMut<EM> {
    /// Index up to which the last option was written.
    fn options_end(&self) -> usize {
        self.payload_start.map_or(self.end, |s| s - 1)
    }

    /// Inserts an option.
    ///
    /// This behaves similarly to [`add_option()`][MinimalWritableMessage::add_option()], but without the limitation of needing to
    /// uphold option ordering manually.
    ///
    /// This comes at the cost of iterating over all options and re-encoding one.
    ///
    /// # Errors
    ///
    /// This function returns a [`WriteError`] if there isn't sufficient space left for the
    /// insertion.
    #[allow(clippy::missing_panics_doc, reason = "backed by type invariants")]
    pub fn insert_option(&mut self, number: u16, data: &[u8]) -> Result<(), WriteError> {
        use crate::{
            option_extension::encode_extensions,
            option_iteration::{OptItem, OptPayloadReader},
        };

        let Ok(data_len) = u16::try_from(data.len()) else {
            return Err(WriteError::OutOfSpace);
        };

        let mut insertion_point = 0;
        let mut previous_option = 0u16;
        let mut next_option = 0u16;
        let mut next_len = None;

        let options_end = self.options_end();

        let tail = self.encoded.tail_mut();

        if self.latest <= number {
            insertion_point = options_end;
            previous_option = self.latest;
        } else {
            let mut opt_iter = OptPayloadReader::new(tail);
            loop {
                match opt_iter.next() {
                    Some(OptItem::Option { number: num, data }) => {
                        if num <= number {
                            let (slice, base) = opt_iter.destruct();
                            insertion_point = tail.len() - slice.len();
                            previous_option = base;
                            opt_iter = OptPayloadReader::new_from(slice, base);
                            continue;
                        }

                        next_option = num;
                        next_len = Some(
                            u16::try_from(data.len()).expect("Guaranteed by OptPayloadReader"),
                        );
                        break;
                    }
                    _ => unreachable!("encoding invariant"),
                }
            }
        }

        // Encode the to-be-inserted option.
        let delta = number.checked_sub(previous_option).expect("Checked above");
        let new_encoded = encode_extensions(delta, data_len);
        let new_encoded = new_encoded.as_ref();

        let current_encoded_len = option_header_len(tail[insertion_point]);

        // Except in edgecases this will be 0.
        let encoding_length_delta;
        let next_encoded;

        // Check if option is last.
        let added_len = if let Some(next_len) = next_len {
            // Re-encode following option. After that encoding doesn't change due to being a delta.
            let next_delta = next_option - number;
            let next_encoded_tmp = encode_extensions(next_delta, next_len);

            // Due to changes in option-number-delta the encoding of the option header could shrink
            // by one or two bytes.
            encoding_length_delta = usize::from(current_encoded_len)
                .checked_sub(next_encoded_tmp.as_ref().len())
                .expect("Encoding length can only shrink");

            next_encoded = Some(next_encoded_tmp);
            new_encoded.len() - encoding_length_delta + data.len()
        } else {
            next_encoded = None;

            encoding_length_delta = 0;
            new_encoded.len() + data.len()
        };

        if self.end + added_len > tail.len() {
            return Err(WriteError::OutOfSpace);
        }

        // Could also rotate_right, but we don't need the shifted-out bytes preserved in the
        // area we'll overwrite in the next instructions.
        //
        // This range is always less than `added_len`, as the new encoding always fits into the same space as (or less than) the
        // old encoding.
        let src = insertion_point + encoding_length_delta..self.end;
        self.encoded
            .tail_mut()
            .copy_within(src, insertion_point + added_len);

        if let Some(payload_start) = self.payload_start {
            self.payload_start = Some(payload_start + added_len);
        }

        self.encoded
            .tail_mut()
            .get_mut(insertion_point..insertion_point + new_encoded.len())
            .expect("Remaining length has been checked")
            .copy_from_slice(new_encoded);
        insertion_point += new_encoded.len();
        self.encoded
            .tail_mut()
            .get_mut(insertion_point..insertion_point + data.len())
            .expect("Remaining length has been checked")
            .copy_from_slice(data);
        insertion_point += data.len();

        if let Some(next_encoded) = next_encoded {
            self.encoded
                .tail_mut()
                .get_mut(insertion_point..insertion_point + next_encoded.as_ref().len())
                .expect("Remaining length has been checked")
                .copy_from_slice(next_encoded.as_ref());
        } else {
            self.latest = number;
        }

        self.end += added_len;

        Ok(())
    }

    /// Removes an option from a slice located at `index`.
    ///
    /// Requires the current `end` of the message contained in `tail` and the previous option,
    /// in case a following option exists and needs to be re-encoded.
    ///
    /// # Errors
    ///
    /// All errors of this function stem from either invariants of the message not being upheld, or
    /// from the indicated arguments not aligning with removing an option from a well-formed
    /// message.
    ///
    /// They are returned as an error because that's more space efficient to unwrap.
    ///
    /// # Space analysis
    ///
    /// Removing an option can never shorten a message due to the encoding properties as shown
    /// here:
    ///
    /// * We call the removed option B, from a sequence of options A B C.
    ///
    ///   The cases where there is no option A and C are easy:
    ///
    ///   * If there is no option C, nothing gets re-encoded, and just B is removed.
    ///   * If there is no option A, all is equivalent to there being an option A with the reserved
    ///     option number zero, or a regular option (with all option numbers shifted, which has no
    ///     effect on the delta encoding).
    ///
    /// * We assume without loss of generality all options are zero-length. (Length does not change
    ///   when options are removed, and if there is any non-zero length in what is being removed,
    ///   we just gain *more* space to use).
    ///
    /// * The extended delta of C can grow from 0 to 1 byte, from 1 to 2 bytes, or from 0 to 2
    ///   bytes.
    ///
    ///   The cases of growing by 1 byte trivial because removing an option also removes 1 byte.
    ///
    /// * C's extended delta growing from 0 to 2 bytes means that B bridged some gap: B's option
    ///   number was no more than 12 smaller than from C's.
    ///
    ///   The most extreme case of having no space available is when while A-C just barely requires a 2-byte
    ///   extended delta: C-A = 269 (encoded as `E0 00 00 = 269 + 0x0000`). As it was 0 bytes
    ///   before, B has to have been in the range 157..=268, all of which require 1 byte of
    ///   extended delta.
    ///
    ///   Thus, B was encoded in at least 2 bytes, so the required room is present.
    #[expect(
        clippy::cast_sign_loss,
        reason = "differences are checked by construction"
    )]
    #[expect(
        clippy::cast_possible_wrap,
        reason = "usize values stem from actual sizes"
    )]
    fn remove_option_at(
        tail: &mut [u8],
        index: usize,
        end: usize,
        previous_option: u16,
    ) -> Result<isize, InvariantViolated> {
        use crate::{
            option_extension::encode_extensions,
            option_iteration::{OptItem, OptPayloadReader},
        };

        let mut opt_iter = OptPayloadReader::new_from(&tail[index..], previous_option);

        // Get nescessary info from the to-be-removed option.
        let Some(OptItem::Option { number, data }) = opt_iter.next() else {
            unreachable!("caller promised that opt0on is there")
        };

        // Get next option if any.
        let next_opt = match opt_iter.next().ok_or(InvariantViolated)? {
            OptItem::Option { number, data } => Some((number, data)),
            OptItem::Payload(_) => None,
            OptItem::Error(_) => return Err(InvariantViolated),
        };

        let current_encoded_len = isize::from(option_header_len(tail[index]));

        // Except in edgecases this will be 0.
        let encoding_length_delta;
        let next_encoded;

        // Check if option is last.
        let len_removed = if let Some((next_number, next_data)) = next_opt {
            let next_data_len = u16::try_from(next_data.len()).map_err(|_| InvariantViolated)?;

            // Get the current length of the option header encoding of the next option.
            let current_next_delta = next_number - number;
            let current_next_encoded = encode_extensions(current_next_delta, next_data_len);
            let current_next_encoded_len = current_next_encoded.as_ref().len() as isize;

            // Re-encode following option. After that encoding doesn't change due to being a delta.
            let next_delta = next_number - previous_option;
            let next_encoded_tmp = encode_extensions(next_delta, next_data_len);

            // Due to changes in option-number-delta the encoding of the option header could grow
            // by one or two bytes.
            encoding_length_delta =
                next_encoded_tmp.as_ref().len() as isize - current_next_encoded_len;

            next_encoded = Some(next_encoded_tmp);
            current_encoded_len - encoding_length_delta + data.len() as isize
        } else {
            next_encoded = None;

            encoding_length_delta = 0;
            current_encoded_len + data.len() as isize
        };

        // As per space analysis, this will not happen.
        if tail.len() < (end as isize - len_removed) as usize {
            return Err(InvariantViolated);
        }

        // Check if we hit the end of the message
        if end.saturating_sub(index + current_encoded_len as usize + data.len()) != 0 {
            // Move remaining options and payload in place.
            let src =
                index + current_encoded_len as usize + data.len() + encoding_length_delta as usize
                    ..end;
            tail.copy_within(src, index + encoding_length_delta as usize);
        }

        // Check if last.
        if let Some(next_encoded) = next_encoded {
            // Insert re-encoded option header.
            tail.get_mut(index..index + next_encoded.as_ref().len())
                .expect("checked above")
                .copy_from_slice(next_encoded.as_ref());
        }

        Ok(len_removed)
    }

    /// Retains all options the `predicate` applies to, the rest will be removed.
    #[expect(
        clippy::cast_sign_loss,
        reason = "differences are checked by construction"
    )]
    #[expect(
        clippy::cast_possible_wrap,
        reason = "usize values stem from actual sizes"
    )]
    pub fn retain_options<P>(&mut self, predicate: P)
    where
        P: Fn(u16, &[u8]) -> bool,
    {
        use crate::option_iteration::{OptItem, OptPayloadReader};

        if self.latest == 0 {
            return;
        }

        let tail = self.encoded.tail_mut();
        let mut opt_iter = OptPayloadReader::new(tail);
        let mut previous_option = 0u16;
        let mut opt_index = 0;

        loop {
            match opt_iter.next() {
                Some(OptItem::Option { number, data }) => {
                    let (slice, _) = opt_iter.destruct();

                    if predicate(number, data) {
                        previous_option = number;
                        opt_index = tail.len() - slice.len();

                        opt_iter = OptPayloadReader::new_from(slice, number);
                        continue;
                    }

                    let Ok(len_removed) =
                        Self::remove_option_at(tail, opt_index, self.end, previous_option)
                    else {
                        unreachable!(
                            "Internal invariant violated (bug in coap-message-implementations)"
                        );
                    };

                    self.end = (self.end as isize - len_removed) as usize;
                    let options_end = if let Some(payload_start) = self.payload_start {
                        let new = (payload_start as isize - len_removed) as usize;
                        self.payload_start = Some(new);
                        new - 1
                    } else {
                        self.end
                    };

                    if opt_index == options_end {
                        self.latest = previous_option;
                        return;
                    }

                    opt_iter = OptPayloadReader::new_from(&tail[opt_index..], previous_option);
                }
                None | Some(OptItem::Payload(_)) => return,
                Some(OptItem::Error(_)) => unreachable!(
                    "Options not encoded as expected (bug in coap-message-implementations)"
                ),
            }
        }
    }

    /// Discards anything that has been written in the message, and allows any operation that would
    /// be allowed after calling [`Self::new_empty()`].
    ///
    /// This is a short-cut to messages that can be snapshotted and rewound, and a band-aid until
    /// that is available in traits.
    pub fn reset(&mut self) {
        self.latest = 0;
        self.end = 0;
        self.payload_start = None;
        // This is not strictly necessary, but at least a temporarily useful thing that will keep
        // users from relying on the code to be persisted.
        *self.encoded.code_mut() = 0;
    }

    /// Expands the bounds of the payload by `added_len`.
    ///
    /// Similar to [`payload_mut_with_len()`][MutableWritableMessage::payload_mut_with_len()],
    /// but it has no restriction on length (beyond the length of the underlying buffer).
    ///
    /// # Errors
    ///
    /// … are raised if there is insufficient space for extending the payload.
    pub fn untruncate(&mut self, mut added_len: usize) -> Result<(), WriteError> {
        if self.payload_start.is_none() {
            added_len += 1;
        }

        if self.encoded.tail().len() < self.end + added_len {
            return Err(WriteError::OutOfSpace);
        }

        if self.payload_start.is_none() {
            // Insert payload marker.
            self.encoded.tail_mut()[self.end] = 0xff;
            self.payload_start = Some(self.end + 1);
        }

        self.end += added_len;

        Ok(())
    }

    /// Returns the number of bytes that were populated inside tail, along with the encoded data.
    ///
    /// For `EM`s that are just referenced (i.e., slices), the 2nd output can often just be
    /// discarded, but it is essential for owned ones.
    pub fn finish(self) -> (usize, EM) {
        (self.end, self.encoded)
    }
}

/// Error returned in specific places where erring is preferable over panicking when an internal
/// invariant is violated.
struct InvariantViolated;

impl<EM: MessageBufferMut> coap_message::ReadableMessage for MessageMut<EM> {
    type Code = u8;
    type MessageOption<'b>
        = MessageOption<'b>
    where
        Self: 'b;
    type OptionsIter<'b>
        = OptionsIter<'b>
    where
        Self: 'b;
    fn code(&self) -> u8 {
        self.encoded.code()
    }
    // Funny detail on the side: If this is called often, an inmemory_write::MessageMut might be more
    // efficient even for plain reading than an inmemory::Message (because we don't have to iterate)
    fn payload(&self) -> &[u8] {
        match self.payload_start {
            None => &[],
            Some(start) => &self.encoded.tail()[start..self.end],
        }
    }
    fn options(&self) -> <Self as coap_message::ReadableMessage>::OptionsIter<'_> {
        OptionsIter(
            crate::option_iteration::OptPayloadReader::new(&self.encoded.tail()[..self.end]),
            None,
        )
    }
}

impl<EM: MessageBufferMut> coap_message::WithSortedOptions for MessageMut<EM> {}

impl<EM: MessageBufferMut> MinimalWritableMessage for MessageMut<EM> {
    type Code = u8;
    type OptionNumber = u16;
    type UnionError = WriteError;
    type AddOptionError = WriteError;
    type SetPayloadError = WriteError;

    fn set_code(&mut self, code: u8) {
        *self.encoded.code_mut() = code;
    }

    fn add_option(&mut self, number: u16, data: &[u8]) -> Result<(), WriteError> {
        let delta = number
            .checked_sub(self.latest)
            .ok_or(WriteError::OutOfSequence)?;
        self.latest = number;
        let encoded = crate::option_extension::encode_extensions(delta, data.len() as u16);
        let encoded = encoded.as_ref();
        let added_len = encoded.len() + data.len();

        // Where we encode the option
        let option_cursor;
        // What .end will be after this operation (but we can't set it yet as we may still err out)
        let eventual_end = self.end + added_len;
        if let Some(payload_start) = self.payload_start {
            if added_len > self.encoded.tail().len() - self.end {
                return Err(WriteError::OutOfSpace);
            }
            option_cursor = payload_start - 1;
            // Could also rotate_right, but we don't need the shifted-out bytes preserved in the
            // area we'll overwrite in the next instructions
            let src = option_cursor..self.end;
            self.encoded
                .tail_mut()
                .copy_within(src, option_cursor + added_len);

            self.payload_start = Some(payload_start + added_len);
        } else {
            option_cursor = self.end;
        }

        self.encoded
            .tail_mut()
            .get_mut(option_cursor..option_cursor + encoded.len())
            .ok_or(WriteError::OutOfSpace)?
            .copy_from_slice(encoded);
        let option_cursor = option_cursor + encoded.len();
        self.encoded
            .tail_mut()
            .get_mut(option_cursor..option_cursor + data.len())
            .ok_or(WriteError::OutOfSpace)?
            .copy_from_slice(data);
        self.end = eventual_end;

        Ok(())
    }

    fn set_payload(&mut self, payload: &[u8]) -> Result<(), WriteError> {
        if self.payload_start.is_some() {
            // We might allow double setting the payload through later extensions, but as for this
            // interface it's once only. We don't detect double setting of empty payloads, but it's
            // not this implementation's purpose to act as a linter.
            //
            // (And whoever uses the options-and-payload-mixed properties will use payload_mut
            // instead).
            return Err(WriteError::OutOfSequence);
        }
        if !payload.is_empty() {
            *self
                .encoded
                .tail_mut()
                .get_mut(self.end)
                .ok_or(WriteError::OutOfSpace)? = 0xff;
            let start = self.end + 1;
            self.end = start + payload.len();
            self.encoded
                .tail_mut()
                .get_mut(start..self.end)
                .ok_or(WriteError::OutOfSpace)?
                .copy_from_slice(payload);
            self.payload_start = Some(start);
        }
        Ok(())
    }

    #[cfg(feature = "downcast")]
    fn with_static_type_annotation(
        &mut self,
    ) -> Option<coap_message::helpers::RefMutWithStaticType<'_, Self>> {
        let em_static = EM::static_mut_variant()?;

        // SAFETY: It is this type's policy that its RefMutWithStaticType ID is the given
        // MessageMut<'static>.
        Some(unsafe {
            coap_message::helpers::RefMutWithStaticType::new(
                self,
                type_id_of_emv_of_lml(&em_static),
            )
        })
    }

    #[inline]
    #[allow(refining_impl_trait_reachable)]
    fn promote_to_mutable_writable_message(&mut self) -> Option<&mut Self> {
        Some(self)
    }
}

impl<EM: MessageBufferMut> MutableWritableMessage for MessageMut<EM> {
    fn available_space(&self) -> usize {
        self.encoded.tail().len() - self.options_end()
    }

    fn payload_mut_with_len(&mut self, len: usize) -> Result<&mut [u8], WriteError> {
        if len == 0 {
            // Just finish the side effect and return something good enough; this allows the easier
            // path for the rest of the function to pick a start, end, and serve that.
            self.truncate(0)?;
            return Ok(&mut []);
        }

        let start = match self.payload_start {
            None => {
                self.encoded.tail_mut()[self.end] = 0xff;
                self.end + 1
            }
            Some(payload_start) => payload_start,
        };
        let end = start + len;

        let end = end.clamp(0, self.encoded.tail().len());

        self.payload_start = Some(start);
        self.end = end;
        self.encoded
            .tail_mut()
            .get_mut(start..end)
            .ok_or(WriteError::OutOfSpace)
    }

    fn truncate(&mut self, len: usize) -> Result<(), WriteError> {
        match (len, self.payload_start) {
            (0, Some(payload_start)) => {
                self.end = payload_start - 1;
                self.payload_start = None;
            }
            (0, None) => {}
            (len, Some(payload_start)) if self.end - payload_start >= len => {
                self.end = payload_start + len;
            }
            _ => return Err(WriteError::OutOfSpace),
        }
        Ok(())
    }

    fn mutate_options<F>(&mut self, mut f: F)
    where
        F: FnMut(u16, &mut [u8]),
    {
        // TBD this is excessively complex, and grounds for finding a better interface. ("Set
        // option and give me a key to update it later with a mutable reference")?

        let optend = self.options_end();

        // May end in a payload marker or just plain end
        let mut slice = &mut self.encoded.tail_mut()[..optend];

        let mut option_base = 0;

        while !slice.is_empty() {
            // This is copied and adapted from
            // coap_messsage_utils::option_iteration::OptPayloadReader and not used through it,
            // because that'd be a whole separate implementation there with mut.
            // (It's bad enough that take_extension needs the trickery)
            let delta_len = slice[0];
            slice = &mut slice[1..];

            if delta_len == 0xff {
                break;
            }

            let mut delta = u16::from(delta_len) >> 4;
            let mut len = u16::from(delta_len) & 0x0f;

            let new_len = {
                // Workaround for https://github.com/rust-lang/rust-clippy/issues/10608
                #[allow(clippy::redundant_slicing)]
                // To get take_extension to cooperate...
                let mut readable = &slice[..];

                crate::option_extension::take_extension(&mut delta, &mut readable)
                    .expect("Invalid encoded option in being-written message");
                crate::option_extension::take_extension(&mut len, &mut readable)
                    .expect("Invalid encoded option in being-written message");

                readable.len()
            };
            // ... and get back to a mutable form
            let trim = slice.len() - new_len;
            slice = &mut slice[trim..];

            option_base += delta;

            let len = len.into();
            f(option_base, &mut slice[..len]);
            slice = &mut slice[len..];
        }
    }
}
