// Copyright (C) 2021 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: Apache-2.0 or MIT

use core::convert;

use crate::{
    channel::Channel,
    command::Command,
    error::{ParseError, SerializationError},
};

/// A CTAPHID packet.
///
/// See [§ 11.2.4 of the CTAP specification][spec].
///
/// [spec]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#usb-message-and-packet-structure
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum Packet<T: AsRef<[u8]>> {
    /// A CTAPHID initialization packet.
    Initialization(InitializationPacket<T>),
    /// A CTAPHID continuation packet.
    Continuation(ContinuationPacket<T>),
}

impl<T: AsRef<[u8]>> Packet<T> {
    /// Returns the channel for this packet.
    pub fn channel(&self) -> Channel {
        match self {
            Self::Initialization(packet) => packet.channel,
            Self::Continuation(packet) => packet.channel,
        }
    }

    /// Returns the type of this packet.
    pub fn packet_type(&self) -> PacketType {
        match self {
            Self::Initialization(_) => PacketType::Initialization,
            Self::Continuation(_) => PacketType::Continuation,
        }
    }

    /// Serializes this packet to the given buffer.
    pub fn serialize(&self, buffer: &mut [u8]) -> Result<usize, SerializationError> {
        match self {
            Self::Initialization(packet) => packet.serialize(buffer),
            Self::Continuation(packet) => packet.serialize(buffer),
        }
    }
}

impl<T: AsRef<[u8]>> From<ContinuationPacket<T>> for Packet<T> {
    fn from(packet: ContinuationPacket<T>) -> Self {
        Self::Continuation(packet)
    }
}

impl<T: AsRef<[u8]>> From<InitializationPacket<T>> for Packet<T> {
    fn from(packet: InitializationPacket<T>) -> Self {
        Self::Initialization(packet)
    }
}

impl<'a, T: AsRef<[u8]> + convert::TryFrom<&'a [u8]>> convert::TryFrom<&'a [u8]> for Packet<T> {
    type Error = ParseError;

    fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
        if data.len() < 5 {
            return Err(ParseError::NotEnoughData);
        }
        let command_or_sequence = data[4];

        if command_or_sequence.leading_ones() > 0 {
            InitializationPacket::try_from(data).map(From::from)
        } else {
            ContinuationPacket::try_from(data).map(From::from)
        }
    }
}

/// The type of a CTAPHID [`Packet`][].
///
/// See [§ 11.2.4 of the CTAP specification][spec].
///
/// [spec]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#usb-message-and-packet-structure
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum PacketType {
    /// A CTAPHID initialization packet ([`Packet::Initialization`][], [`InitializationPacket`][]).
    Initialization,
    /// A CTAPHID continuation packet ([`Packet::Continuation`][], [`ContinuationPacket`][]).
    Continuation,
}

/// The size of the header of an initialization packet in bytes.
///
/// See [§ 11.2.4 of the CTAP specification][spec].
///
/// [spec]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#usb-message-and-packet-structure
pub const INITIALIZATION_HEADER_SIZE: usize = 7;

/// A CTAPHID initialization packet.
///
/// See [§ 11.2.4 of the CTAP specification][spec].
///
/// [spec]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#usb-message-and-packet-structure
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct InitializationPacket<T: AsRef<[u8]>> {
    /// The channel this packet is sent or received on.
    pub channel: Channel,
    /// The CTAPHID command.
    pub command: Command,
    /// The length of the payload data.
    pub length: u16,
    /// The payload data.
    pub data: T,
}

impl<T: AsRef<[u8]>> InitializationPacket<T> {
    /// Serializes this packet to the given buffer.
    pub fn serialize(&self, buffer: &mut [u8]) -> Result<usize, SerializationError> {
        let n = INITIALIZATION_HEADER_SIZE + self.data.as_ref().len();
        if buffer.len() < n {
            return Err(SerializationError::BufferTooSmall);
        }

        let channel = u32::from(self.channel).to_be_bytes();
        let length = self.length.to_be_bytes();

        buffer[0] = channel[0];
        buffer[1] = channel[1];
        buffer[2] = channel[2];
        buffer[3] = channel[3];
        buffer[4] = u8::from(self.command) | 0b1000_0000;
        buffer[5] = length[0];
        buffer[6] = length[1];

        buffer[7..n].copy_from_slice(self.data.as_ref());

        Ok(n)
    }
}

impl<T: AsRef<[u8]>> convert::TryFrom<Packet<T>> for InitializationPacket<T> {
    type Error = ParseError;

    fn try_from(packet: Packet<T>) -> Result<Self, Self::Error> {
        if let Packet::Initialization(packet) = packet {
            Ok(packet)
        } else {
            Err(ParseError::InvalidPacketType {
                expected: PacketType::Initialization,
                actual: packet.packet_type(),
            })
        }
    }
}

impl<'a, T: AsRef<[u8]> + convert::TryFrom<&'a [u8]>> convert::TryFrom<&'a [u8]>
    for InitializationPacket<T>
{
    type Error = ParseError;

    fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
        if data.len() < 7 {
            return Err(ParseError::NotEnoughData);
        }

        let channel = Channel::from([data[0], data[1], data[2], data[3]]);
        let command = data[4];
        let length = u16::from_be_bytes([data[5], data[6]]);
        let data = &data[7..];

        if command.leading_ones() == 0 {
            return Err(ParseError::InvalidPacketType {
                expected: PacketType::Initialization,
                actual: PacketType::Continuation,
            });
        }

        Ok(InitializationPacket {
            channel,
            command: Command::from(command & 0b0111_1111),
            length,
            data: T::try_from(data).map_err(|_| ParseError::BufferCreationFailed)?,
        })
    }
}

/// The size of the header of a continuation packet in bytes.
///
/// See [§ 11.2.4 of the CTAP specification][spec].
///
/// [spec]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#usb-message-and-packet-structure
pub const CONTINUATION_HEADER_SIZE: usize = 5;

/// A CTAPHID continuation packet.
///
/// See [§ 11.2.4 of the CTAP specification][spec].
///
/// [spec]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#usb-message-and-packet-structure
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct ContinuationPacket<T: AsRef<[u8]>> {
    /// The channel this packet is sent or received on.
    pub channel: Channel,
    /// The sequence number of this packet.
    pub sequence: u8,
    /// The payload data.
    pub data: T,
}

impl<T: AsRef<[u8]>> ContinuationPacket<T> {
    /// Serializes this packet to the given buffer.
    pub fn serialize(&self, buffer: &mut [u8]) -> Result<usize, SerializationError> {
        let n = CONTINUATION_HEADER_SIZE + self.data.as_ref().len();
        if buffer.len() < n {
            return Err(SerializationError::BufferTooSmall);
        }

        let channel = u32::from(self.channel).to_be_bytes();

        buffer[0] = channel[0];
        buffer[1] = channel[1];
        buffer[2] = channel[2];
        buffer[3] = channel[3];
        buffer[4] = self.sequence;

        buffer[5..n].copy_from_slice(self.data.as_ref());

        Ok(n)
    }
}

impl<T: AsRef<[u8]>> convert::TryFrom<Packet<T>> for ContinuationPacket<T> {
    type Error = ParseError;

    fn try_from(packet: Packet<T>) -> Result<Self, Self::Error> {
        if let Packet::Continuation(packet) = packet {
            Ok(packet)
        } else {
            Err(ParseError::InvalidPacketType {
                expected: PacketType::Continuation,
                actual: packet.packet_type(),
            })
        }
    }
}

impl<'a, T: AsRef<[u8]> + convert::TryFrom<&'a [u8]>> convert::TryFrom<&'a [u8]>
    for ContinuationPacket<T>
{
    type Error = ParseError;

    fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
        if data.len() < 5 {
            return Err(ParseError::NotEnoughData);
        }

        let channel = Channel::from([data[0], data[1], data[2], data[3]]);
        let sequence = data[4];
        let data = &data[5..];

        if sequence.leading_ones() > 0 {
            return Err(ParseError::InvalidPacketType {
                expected: PacketType::Continuation,
                actual: PacketType::Initialization,
            });
        }

        Ok(ContinuationPacket {
            channel,
            sequence,
            data: T::try_from(data).map_err(|_| ParseError::BufferCreationFailed)?,
        })
    }
}
