use std::io::ErrorKind;
use std::time::Duration;

use crate::bus::{CanBus, CanFrame, CanId};
use crate::error::{Error, Result};

use socketcan::{CanSocket, EmbeddedFrame, ExtendedId, Frame, Id, Socket, StandardId};

#[derive(Debug)]
pub struct SocketCanBus {
    socket: CanSocket,
}

impl SocketCanBus {
    pub fn open(interface: &str) -> Result<Self> {
        let socket = CanSocket::open(interface).map_err(transport_error)?;
        Ok(Self { socket })
    }

    pub const fn socket(&self) -> &CanSocket {
        &self.socket
    }

    pub fn into_inner(self) -> CanSocket {
        self.socket
    }
}

impl CanBus for SocketCanBus {
    fn send(&mut self, frame: &CanFrame) -> Result<()> {
        let id = to_socketcan_id(frame.id())?;
        let socket_frame = socketcan::CanFrame::new(id, frame.data())
            .ok_or(Error::PayloadTooLong { len: frame.len() })?;

        self.socket
            .write_frame(&socket_frame)
            .map_err(transport_error)
    }

    fn receive(&mut self, timeout: Duration) -> Result<Option<CanFrame>> {
        match self.socket.read_frame_timeout(timeout) {
            Ok(frame) => from_socketcan_frame(frame),
            Err(err) if matches!(err.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut) => {
                Ok(None)
            }
            Err(err) => Err(transport_error(err)),
        }
    }
}

fn to_socketcan_id(id: CanId) -> Result<Id> {
    match id {
        CanId::Standard(id) => StandardId::new(id)
            .map(Id::Standard)
            .ok_or_else(|| Error::InvalidCanId(u32::from(id))),
        CanId::Extended(id) => ExtendedId::new(id)
            .map(Id::Extended)
            .ok_or(Error::InvalidCanId(id)),
    }
}

fn from_socketcan_frame(frame: socketcan::CanFrame) -> Result<Option<CanFrame>> {
    if frame.is_error_frame() || frame.is_remote_frame() {
        return Ok(None);
    }

    let id = match frame.id() {
        Id::Standard(id) => CanId::standard(id.as_raw())?,
        Id::Extended(id) => CanId::extended(id.as_raw())?,
    };

    Ok(Some(CanFrame::new(id, frame.data())?))
}

fn transport_error(error: std::io::Error) -> Error {
    Error::Transport(error.to_string())
}
