use byteorder::{ByteOrder, NativeEndian};
use core::cell::RefCell;
use phy::Medium;
#[cfg(feature = "std")]
use std::io::Write;

use crate::phy::{self, Device, DeviceCapabilities};
use crate::time::Instant;

enum_with_unknown! {
    /// Captured packet header type.
    pub enum PcapLinkType(u32) {
        /// Ethernet frames
        Ethernet =   1,
        /// IPv4 or IPv6 packets (depending on the version field)
        Ip       = 101,
        /// IEEE 802.15.4 packets without FCS.
        Ieee802154WithoutFcs = 230,
    }
}

/// Packet capture mode.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum PcapMode {
    /// Capture both received and transmitted packets.
    Both,
    /// Capture only received packets.
    RxOnly,
    /// Capture only transmitted packets.
    TxOnly,
}

/// A packet capture sink.
pub trait PcapSink {
    /// Write data into the sink.
    fn write(&mut self, data: &[u8]);

    /// Flush data written into the sync.
    fn flush(&mut self) {}

    /// Write an `u16` into the sink, in native byte order.
    fn write_u16(&mut self, value: u16) {
        let mut bytes = [0u8; 2];
        NativeEndian::write_u16(&mut bytes, value);
        self.write(&bytes[..])
    }

    /// Write an `u32` into the sink, in native byte order.
    fn write_u32(&mut self, value: u32) {
        let mut bytes = [0u8; 4];
        NativeEndian::write_u32(&mut bytes, value);
        self.write(&bytes[..])
    }

    /// Write the libpcap global header into the sink.
    ///
    /// This method may be overridden e.g. if special synchronization is necessary.
    fn global_header(&mut self, link_type: PcapLinkType) {
        self.write_u32(0xa1b2c3d4); // magic number
        self.write_u16(2); // major version
        self.write_u16(4); // minor version
        self.write_u32(0); // timezone (= UTC)
        self.write_u32(0); // accuracy (not used)
        self.write_u32(65535); // maximum packet length
        self.write_u32(link_type.into()); // link-layer header type
    }

    /// Write the libpcap packet header into the sink.
    ///
    /// See also the note for [global_header](#method.global_header).
    ///
    /// # Panics
    /// This function panics if `length` is greater than 65535.
    fn packet_header(&mut self, timestamp: Instant, length: usize) {
        assert!(length <= 65535);

        self.write_u32(timestamp.secs() as u32); // timestamp seconds
        self.write_u32(timestamp.micros() as u32); // timestamp microseconds
        self.write_u32(length as u32); // captured length
        self.write_u32(length as u32); // original length
    }

    /// Write the libpcap packet header followed by packet data into the sink.
    ///
    /// See also the note for [global_header](#method.global_header).
    fn packet(&mut self, timestamp: Instant, packet: &[u8]) {
        self.packet_header(timestamp, packet.len());
        self.write(packet);
        self.flush();
    }
}

#[cfg(feature = "std")]
impl<T: Write> PcapSink for T {
    fn write(&mut self, data: &[u8]) {
        T::write_all(self, data).expect("cannot write")
    }

    fn flush(&mut self) {
        T::flush(self).expect("cannot flush")
    }
}

/// A packet capture writer device.
///
/// Every packet transmitted or received through this device is timestamped
/// and written (in the [libpcap] format) using the provided [sink].
/// Note that writes are fine-grained, and buffering is recommended.
///
/// The packet sink should be cheaply cloneable, as it is cloned on every
/// transmitted packet. For example, `&'a mut Vec<u8>` is cheaply cloneable
/// but `&std::io::File`
///
/// [libpcap]: https://wiki.wireshark.org/Development/LibpcapFileFormat
/// [sink]: trait.PcapSink.html
#[derive(Debug)]
pub struct PcapWriter<D, S>
where
    D: Device,
    S: PcapSink,
{
    lower: D,
    sink: RefCell<S>,
    mode: PcapMode,
}

impl<D: Device, S: PcapSink> PcapWriter<D, S> {
    /// Creates a packet capture writer.
    pub fn new(lower: D, mut sink: S, mode: PcapMode) -> PcapWriter<D, S> {
        let medium = lower.capabilities().medium;
        let link_type = match medium {
            #[cfg(feature = "medium-ip")]
            Medium::Ip => PcapLinkType::Ip,
            #[cfg(feature = "medium-ethernet")]
            Medium::Ethernet => PcapLinkType::Ethernet,
            #[cfg(feature = "medium-ieee802154")]
            Medium::Ieee802154 => PcapLinkType::Ieee802154WithoutFcs,
        };
        sink.global_header(link_type);
        PcapWriter {
            lower,
            sink: RefCell::new(sink),
            mode,
        }
    }

    /// Get a reference to the underlying device.
    ///
    /// Even if the device offers reading through a standard reference, it is inadvisable to
    /// directly read from the device as doing so will circumvent the packet capture.
    pub fn get_ref(&self) -> &D {
        &self.lower
    }

    /// Get a mutable reference to the underlying device.
    ///
    /// It is inadvisable to directly read from the device as doing so will circumvent the packet capture.
    pub fn get_mut(&mut self) -> &mut D {
        &mut self.lower
    }
}

impl<D: Device, S> Device for PcapWriter<D, S>
where
    S: PcapSink,
{
    type RxToken<'a> = RxToken<'a, D::RxToken<'a>, S>
    where
        Self: 'a;
    type TxToken<'a> = TxToken<'a, D::TxToken<'a>, S>
    where
        Self: 'a;

    fn capabilities(&self) -> DeviceCapabilities {
        self.lower.capabilities()
    }

    fn receive(&mut self, timestamp: Instant) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> {
        let sink = &self.sink;
        let mode = self.mode;
        self.lower
            .receive(timestamp)
            .map(move |(rx_token, tx_token)| {
                let rx = RxToken {
                    token: rx_token,
                    sink,
                    mode,
                    timestamp,
                };
                let tx = TxToken {
                    token: tx_token,
                    sink,
                    mode,
                    timestamp,
                };
                (rx, tx)
            })
    }

    fn transmit(&mut self, timestamp: Instant) -> Option<Self::TxToken<'_>> {
        let sink = &self.sink;
        let mode = self.mode;
        self.lower.transmit(timestamp).map(move |token| TxToken {
            token,
            sink,
            mode,
            timestamp,
        })
    }
}

#[doc(hidden)]
pub struct RxToken<'a, Rx: phy::RxToken, S: PcapSink> {
    token: Rx,
    sink: &'a RefCell<S>,
    mode: PcapMode,
    timestamp: Instant,
}

impl<'a, Rx: phy::RxToken, S: PcapSink> phy::RxToken for RxToken<'a, Rx, S> {
    fn consume<R, F: FnOnce(&mut [u8]) -> R>(self, f: F) -> R {
        self.token.consume(|buffer| {
            match self.mode {
                PcapMode::Both | PcapMode::RxOnly => self
                    .sink
                    .borrow_mut()
                    .packet(self.timestamp, buffer.as_ref()),
                PcapMode::TxOnly => (),
            }
            f(buffer)
        })
    }

    fn meta(&self) -> phy::PacketMeta {
        self.token.meta()
    }
}

#[doc(hidden)]
pub struct TxToken<'a, Tx: phy::TxToken, S: PcapSink> {
    token: Tx,
    sink: &'a RefCell<S>,
    mode: PcapMode,
    timestamp: Instant,
}

impl<'a, Tx: phy::TxToken, S: PcapSink> phy::TxToken for TxToken<'a, Tx, S> {
    fn consume<R, F>(self, len: usize, f: F) -> R
    where
        F: FnOnce(&mut [u8]) -> R,
    {
        self.token.consume(len, |buffer| {
            let result = f(buffer);
            match self.mode {
                PcapMode::Both | PcapMode::TxOnly => {
                    self.sink.borrow_mut().packet(self.timestamp, buffer)
                }
                PcapMode::RxOnly => (),
            };
            result
        })
    }

    fn set_meta(&mut self, meta: phy::PacketMeta) {
        self.token.set_meta(meta)
    }
}
