use core::convert::{TryFrom, TryInto};

use coap_message::{MessageOption, MutableWritableMessage, ReadableMessage};

use coap_numbers::option;

// See also comment on implementation::Code
pub(crate) fn codeconvert<O: TryFrom<u8>>(code: u8) -> O {
    code.try_into()
        .map_err(|_| "Responde type can't even express hard-coded code")
        .unwrap()
}
pub(crate) fn optconvert<O: TryFrom<u16>>(option: u16) -> O {
    option
        .try_into()
        .map_err(|_| "Response type can't even express Content-Format")
        .unwrap()
}

/// Request data from a Block2 request
///
/// As the M flag is unused in requests, it is not captured in here (and ignored at construction).
pub struct Block2RequestData {
    blknum: u32,
    szx: u8,
}

impl Block2RequestData {
    /// Extract a request block 2 value from a request message.
    ///
    /// Absence of the option is not an error and results in the default value to be returned;
    /// exceeding length or duplicate entries are an error and are indicated by returning an error,
    /// which should be responded to with a Bad Option error.
    pub fn from_message(message: &impl ReadableMessage) -> Result<Self, ()> {
        let mut b2options = message.options().filter(|o| o.number() == option::BLOCK2);

        match b2options.next() {
            None => Ok(Self::default()),
            Some(o) => {
                if let None = b2options.next() {
                    Self::from_option(&o)
                } else {
                    Err(())
                }
            }
        }
    }

    /// Extract a request block 2 value from a single option. An error is indicated on a malformed
    /// (ie. overly long) option.
    ///
    /// Compared to [Block2RequestData::from_message()], this can easily be packed into a single
    /// loop that processes all options and fails on unknown critical ones; on the other hand, this
    /// does not automate the check for duplicate options.
    ///
    /// # Panics
    ///
    /// In debug mode if the option is not Block2
    pub fn from_option<'a>(option: &'a impl MessageOption) -> Result<Self, ()> {
        debug_assert!(option.number() == option::BLOCK2);
        let o: u32 = option.value_uint().ok_or(())?;
        // Block2 is up to 3 long
        if o >= 0x1000000 {
            return Err(());
        }
        Ok(Self {
            szx: (o as u8) & 0x7,
            blknum: o >> 4,
        })
    }

    /// Size of a single block
    pub fn size(&self) -> u16 {
        1 << (4 + self.szx)
    }

    /// Number of bytes before the indicated block
    pub fn before(&self) -> u32 {
        self.size() as u32 * self.blknum
    }

    /// Return a block that has identical .before(), but a block size smaller or equal to the given
    /// one.
    ///
    /// Returns None if the given size is not expressible as a CoAP block (ie. is less than 16).
    pub fn shrink(mut self, size: u16) -> Option<Self> {
        while self.size() > size {
            if self.szx == 0 {
                return None;
            }
            self.szx -= 1;
            self.blknum *= 2;
        }
        Some(self)
    }
}

impl Default for Block2RequestData {
    fn default() -> Self {
        Self { szx: 6, blknum: 0 }
    }
}

/// Provide a writer into the response message
///
/// Anything written into the writer is put into the message's payload, and the Block2 and ETag
/// option of the message are set automatically based on what is written.
///
/// As some cleanup is required at the end of the write (eg. setting the ETag and the M flag in the
/// Block2 option), the actual writing needs to take place inside a callback.
///
/// Note that only a part of the write (that which was requested by the Block2 operation) is
/// actually persisted; the rest is discarded. When the M flag indicates that the client did not
/// obtain the full message yet, it typically sends another request that is then processed the same
/// way again, for a different "window".
///
/// The type passed in should not be relied on too much -- ideally it'd be `F: for<W:
/// core::fmt::Write> FnOnce(&mut W) -> R`, and the signature may still change in that direction.
pub fn block2_write<F, R>(
    block2: Block2RequestData,
    response: &mut impl MutableWritableMessage,
    f: F,
) -> R
where
    F: FnOnce(&mut BlockWriter) -> R,
{
    block2_write_with_cf(block2, response, f, None)
}

// Not fully public because it's a crude workaround for not having zippable write injectors yet
pub(crate) fn block2_write_with_cf<F, R>(
    block2: Block2RequestData,
    response: &mut impl MutableWritableMessage,
    f: F,
    cf: Option<u16>,
) -> R
where
    F: FnOnce(&mut BlockWriter) -> R,
{
    let estimated_option_size = 25; // 9 bytes ETag, up to 5 bytes Block2, up to 5 bytes Size2, 1 byte payload marker
    let payload_budget = response.available_space() - estimated_option_size;
    let block2 = block2
        .shrink(payload_budget as u16)
        .expect("Tiny buffer allocated");

    response.add_option(optconvert(option::ETAG), &[0, 0, 0, 0, 0, 0, 0, 0]);
    if let Some(cf) = cf {
        response.add_option_uint(optconvert(option::CONTENT_FORMAT), cf);
    }
    response.add_option_uint(
        optconvert(option::BLOCK2),
        (block2.blknum << 4) | block2.szx as u32,
    );

    let (cursor, etag, ret) = {
        let full_payload = response.payload_mut_with_len(block2.size().into());
        let mut writer = BlockWriter {
            data: &mut full_payload[..block2.size() as usize],
            cursor: -(block2.before() as isize),
            etag: 0,
        };

        let ret = f(&mut writer);

        (writer.cursor, writer.etag, ret)
    };

    let set_more;
    if cursor > block2.size() as isize {
        response.truncate(block2.size() as usize);
        set_more = true;
    } else if cursor < 0 {
        unimplemented!("Report out-of-band seek");
    } else {
        response.truncate(cursor as usize);
        set_more = false;
    }

    response.mutate_options(|optnum, value| {
        match optnum.into() {
            option::ETAG => {
                use byteorder::ByteOrder;
                byteorder::NetworkEndian::write_u64(value, etag);
            }
            option::BLOCK2 if set_more => {
                value[value.len() - 1] |= 0x08;
            }
            _ => (),
        };
    });

    ret
}

/// Workhorse of block2_write.
///
/// Don't expect anything from this other than implementing [core::fmt::Write].
// FIXME: deduplicate with windowed-infinity, possibly as a generic EtagWriter that wraps another
// (possibly WindowedInfinity) writer
pub struct BlockWriter<'a> {
    data: &'a mut [u8],
    cursor: isize,
    etag: u64,
}

impl<'a> BlockWriter<'a> {
    fn new(data: &'a mut [u8], cursor: isize) -> Self {
        let etag = 0;
        Self { data, cursor, etag }
    }

    fn bytes_in_buffer(&self) -> Option<usize> {
        if self.cursor < 0 {
            None
        } else {
            Some(::core::cmp::min(self.cursor as usize, self.data.len()))
        }
    }

    fn cursor(&self) -> isize {
        self.cursor
    }

    fn etag(&self) -> u64 {
        self.etag
    }

    fn did_overflow(&self) -> bool {
        self.cursor > self.data.len() as isize
    }
}

impl<'a> ::core::fmt::Write for BlockWriter<'a> {
    fn write_str(&mut self, s: &str) -> ::core::fmt::Result {
        let mut s = s.as_bytes();

        self.etag = crc::crc64::update(self.etag, &crc::crc64::ECMA_TABLE, s);

        if self.cursor >= self.data.len() as isize {
            // Still counting up to give a reliable Size2
            self.cursor += s.len() as isize;
            return Ok(());
        }
        if self.cursor < -(s.len() as isize) {
            self.cursor += s.len() as isize;
            return Ok(());
        }
        if self.cursor < 0 {
            s = &s[(-self.cursor) as usize..];
            self.cursor = 0;
        }

        let mut s_to_copy = s;
        if self.cursor as usize + s.len() > self.data.len() {
            let copy_bytes = self.data.len() - self.cursor as usize;
            s_to_copy = &s[..copy_bytes]
        }

        self.data[self.cursor as usize..self.cursor as usize + s_to_copy.len()]
            .copy_from_slice(s_to_copy);

        self.cursor += s.len() as isize;
        Ok(())
    }
}

/// Wrapper around a ReadableMessage that hides the Uri-Host and Uri-Path options from view
///
/// This is used by a [HandlerBuilder] (in particular, its path-based [ForkingHandler]) to free the
/// resources from the strange duty of skipping over a critical option they are unaware of.
pub struct MaskingUriUpToPath<'m, M: ReadableMessage>(pub &'m M);

impl<'m, M: ReadableMessage> ReadableMessage for MaskingUriUpToPath<'m, M> {
    type Code = M::Code;
    type MessageOption<'a> = M::MessageOption<'a>;
    type OptionsIter<'a> = MaskingUriUpToPathIter<M::OptionsIter<'a>>;

    fn options<'a>(&'a self) -> Self::OptionsIter<'a> {
        MaskingUriUpToPathIter(self.0.options())
    }

    fn code(&self) -> M::Code {
        self.0.code()
    }

    fn payload(&self) -> &[u8] {
        self.0.payload()
    }
}

pub struct MaskingUriUpToPathIter<I>(I);

impl<MO: MessageOption, I: Iterator<Item = MO>> Iterator for MaskingUriUpToPathIter<I> {
    type Item = MO;

    fn next(&mut self) -> Option<MO> {
        loop {
            let result = self.0.next()?;
            match result.number() {
                coap_numbers::option::URI_HOST => continue,
                coap_numbers::option::URI_PATH => continue,
                _ => return Some(result),
            }
        }
    }
}
