use crate::{api::*, constant, NiCanFrame};
use rs_can::{CanError, CanFilter, CanFrame, CanResult};
use std::{
    collections::HashMap,
    ffi::{c_char, CStr, CString},
};
use windows::{
    core::PCSTR,
    Win32::Foundation::HMODULE,
    Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress},
};

#[derive(Debug, Clone)]
pub(crate) struct NiCanContext {
    pub(crate) handle: NCTYPE_OBJH,
    pub(crate) filters: Vec<CanFilter>,
    pub(crate) bitrate: u32,
    pub(crate) log_errors: bool,
}

#[allow(non_snake_case)]
#[derive(Debug, Clone)]
pub struct NiCan {
    pub(crate) _dll: HMODULE,
    pub(crate) channels: HashMap<String, NiCanContext>,
    pub(crate) ncConfig: unsafe extern "system" fn(
        NCTYPE_STRING,
        NCTYPE_UINT32,
        NCTYPE_ATTRID_P,
        NCTYPE_UINT32_P,
    ) -> NCTYPE_STATUS,
    pub(crate) ncOpenObject:
        unsafe extern "system" fn(NCTYPE_STRING, NCTYPE_OBJH_P) -> NCTYPE_STATUS,
    pub(crate) ncAction:
        unsafe extern "system" fn(NCTYPE_OBJH, NCTYPE_OPCODE, NCTYPE_UINT32) -> NCTYPE_STATUS,
    pub(crate) ncCloseObject: unsafe extern "system" fn(NCTYPE_OBJH) -> NCTYPE_STATUS,
    pub(crate) ncWrite:
        unsafe extern "system" fn(NCTYPE_OBJH, NCTYPE_UINT32, NCTYPE_ANY_P) -> NCTYPE_STATUS,
    pub(crate) ncRead:
        unsafe extern "system" fn(NCTYPE_OBJH, NCTYPE_UINT32, NCTYPE_ANY_P) -> NCTYPE_STATUS,
    pub(crate) ncWaitForState: unsafe extern "system" fn(
        NCTYPE_OBJH,
        NCTYPE_STATE,
        NCTYPE_DURATION,
        NCTYPE_STATE_P,
    ) -> NCTYPE_STATUS,
    pub(crate) ncStatusToString:
        unsafe extern "system" fn(NCTYPE_STATUS, NCTYPE_UINT32, NCTYPE_STRING) -> NCTYPE_STATUS,
}

unsafe impl Send for NiCan {}
unsafe impl Sync for NiCan {}

impl NiCan {
    pub fn new(dll_path: Option<&str>) -> CanResult<Self> {
        let dll_path = dll_path.unwrap_or(r"Nican.dll");
        let dll_path = PCSTR::from_raw(dll_path.as_ptr());
        unsafe {
            let dll =
                GetModuleHandleA(dll_path).map_err(|e| CanError::InitializeError(e.to_string()))?;

            Ok(Self {
                _dll: dll,
                channels: Default::default(),
                ncConfig: std::mem::transmute(GetProcAddress(
                    dll,
                    PCSTR::from_raw(b"ncConfig\0".as_ptr()),
                )),
                ncOpenObject: std::mem::transmute(GetProcAddress(
                    dll,
                    PCSTR::from_raw(b"ncOpenObject\0".as_ptr()),
                )),
                ncAction: std::mem::transmute(GetProcAddress(
                    dll,
                    PCSTR::from_raw(b"ncAction\0".as_ptr()),
                )),
                ncCloseObject: std::mem::transmute(GetProcAddress(
                    dll,
                    PCSTR::from_raw(b"ncCloseObject\0".as_ptr()),
                )),
                ncWrite: std::mem::transmute(GetProcAddress(
                    dll,
                    PCSTR::from_raw(b"ncWrite\0".as_ptr()),
                )),
                ncRead: std::mem::transmute(GetProcAddress(
                    dll,
                    PCSTR::from_raw(b"ncRead\0".as_ptr()),
                )),
                ncWaitForState: std::mem::transmute(GetProcAddress(
                    dll,
                    PCSTR::from_raw(b"ncWaitForState\0".as_ptr()),
                )),
                ncStatusToString: std::mem::transmute(GetProcAddress(
                    dll,
                    PCSTR::from_raw(b"ncStatusToString\0".as_ptr()),
                )),
            })
        }
    }

    pub fn open(
        &mut self,
        channel: &str,
        filters: Vec<CanFilter>,
        bitrate: u32,
        log_errors: bool,
    ) -> CanResult<()> {
        let mut attr_id = vec![NC_ATTR_START_ON_OPEN, NC_ATTR_LOG_COMM_ERRS];
        let mut attr_val = vec![1, if log_errors { 1 } else { 0 }];

        match filters.len() {
            0 => {
                attr_id.extend([
                    NC_ATTR_CAN_COMP_STD,
                    NC_ATTR_CAN_MASK_STD,
                    NC_ATTR_CAN_COMP_XTD,
                    NC_ATTR_CAN_MASK_XTD,
                ]);
                attr_val.extend([0; 4])
            }
            _ => filters.iter().for_each(|&f| {
                attr_id.extend([NC_ATTR_CAN_COMP_XTD, NC_ATTR_CAN_MASK_XTD]);
                match f {
                    CanFilter::Standard { id, mask } => {
                        attr_val.extend([id.as_raw() as u32, mask as u32]);
                    }
                    CanFilter::Extended { id, mask } => {
                        attr_val.extend([id.as_raw() | NC_FL_CAN_ARBID_XTD, mask]);
                    }
                }
            }),
        }

        attr_id.push(NC_ATTR_BAUD_RATE);
        attr_val.push(bitrate);

        let chl_ascii = CString::new(channel).map_err(|e| CanError::OtherError(e.to_string()))?;
        let ret = unsafe {
            (self.ncConfig)(
                chl_ascii.clone().into_raw(),
                attr_id.len() as NCTYPE_UINT32,
                attr_id.as_mut_ptr() as NCTYPE_ATTRID_P,
                attr_val.as_mut_ptr() as NCTYPE_UINT32_P,
            )
        };
        if ret != 0 {
            return Err(CanError::InitializeError(
                "device Configuration error".into(),
            ));
        }

        let mut handle = 0;
        let ret = unsafe { (self.ncOpenObject)(chl_ascii.into_raw(), &mut handle) };
        if ret != 0 {
            return Err(CanError::InitializeError("device open error".into()));
        }

        self.channels.insert(
            channel.into(),
            NiCanContext {
                handle,
                filters,
                bitrate,
                log_errors,
            },
        );

        Ok(())
    }

    pub fn reset(&mut self, channel: String) -> CanResult<()> {
        match self.channels.get(&channel) {
            Some(ctx) => {
                let ret = unsafe { (self.ncAction)(ctx.handle, NC_OP_RESET as NCTYPE_OPCODE, 0) };

                self.check_status(channel.as_str(), ret).map_err(|r| {
                    let info = format!(
                        "{} error {} when reset",
                        Self::channel_info(&channel),
                        self.status_to_str(r)
                    );
                    rsutil::warn!("{}", info);

                    CanError::OperationError(info)
                })
            }
            None => Err(CanError::channel_not_opened(Self::channel_info(&channel))),
        }
    }

    pub fn close(&mut self, channel: String) -> CanResult<()> {
        match self.channels.get(&channel) {
            Some(ctx) => {
                let ret = unsafe { (self.ncCloseObject)(ctx.handle) };
                self.channels.remove(&channel);

                self.check_status(channel.as_str(), ret).map_err(|r| {
                    let info = format!(
                        "{} error {} when close",
                        Self::channel_info(&channel),
                        self.status_to_str(r)
                    );
                    rsutil::warn!("{}", info);

                    CanError::OperationError(info)
                })
            }
            None => Err(CanError::channel_not_opened(Self::channel_info(&channel))),
        }
    }

    pub fn transmit_can(&self, msg: NiCanFrame) -> CanResult<()> {
        let channel = msg.channel();
        match self.channels.get(&channel) {
            Some(ctx) => {
                let raw_msg = msg.into();

                let ret = unsafe {
                    (self.ncWrite)(
                        ctx.handle,
                        std::mem::size_of::<NCTYPE_CAN_FRAME>() as NCTYPE_UINT32,
                        &raw_msg as *const NCTYPE_CAN_FRAME as NCTYPE_ANY_P,
                    )
                };

                if let Err(r) = self.check_status(channel.as_str(), ret) {
                    let info = format!(
                        "{} error {} when transmit",
                        Self::channel_info(&channel),
                        self.status_to_str(r)
                    );
                    rsutil::warn!("{}", info);
                    return Err(CanError::OperationError(info));
                }

                Ok(())
            }
            None => Err(CanError::channel_not_opened(Self::channel_info(&channel))),
        }
    }

    pub fn receive_can(&self, channel: String, timeout: Option<u32>) -> CanResult<Vec<NiCanFrame>> {
        match self.channels.get(&channel) {
            Some(ctx) => {
                if let Err(ret) = self.wait_for_state(channel.as_str(), ctx.handle, timeout) {
                    let info = format!("{} wait for state timeout", Self::channel_info(&channel));
                    if ret == constant::CanErrFunctionTimeout as NCTYPE_STATUS {
                        rsutil::warn!("{}", info);
                    }
                    return Err(CanError::channel_timeout(Self::channel_info(&channel)));
                }

                let raw_msg = NCTYPE_CAN_STRUCT {
                    Timestamp: NCTYPE_UINT64 {
                        LowPart: Default::default(),
                        HighPart: Default::default(),
                    },
                    ArbitrationId: Default::default(),
                    FrameType: Default::default(),
                    DataLength: Default::default(),
                    Data: Default::default(),
                };

                let ret = unsafe {
                    (self.ncRead)(
                        ctx.handle,
                        std::mem::size_of::<NCTYPE_CAN_STRUCT>() as NCTYPE_UINT32,
                        &raw_msg as *const NCTYPE_CAN_STRUCT as NCTYPE_ANY_P,
                    )
                };

                if let Err(r) = self.check_status(channel.as_str(), ret) {
                    let info = format!(
                        "{} error {} when receive",
                        Self::channel_info(&channel),
                        self.status_to_str(r)
                    );
                    rsutil::warn!("{}", info);
                    return Err(CanError::OperationError(info));
                }

                let mut msg = <NCTYPE_CAN_STRUCT as TryInto<NiCanFrame>>::try_into(raw_msg)?;
                msg.set_channel(channel.clone());

                Ok(vec![msg])
            }
            None => Err(CanError::channel_not_opened(Self::channel_info(&channel))),
        }
    }

    #[inline]
    pub fn channel_info(channel: &str) -> String {
        format!("NI-CAN: {}", channel)
    }

    #[inline]
    pub fn filters(&self, channel: String) -> CanResult<Vec<CanFilter>> {
        self.channel_util(channel, |ctx| Ok(ctx.filters.clone()))
    }

    #[inline]
    pub fn bitrate(&self, channel: String) -> CanResult<u32> {
        self.channel_util(channel, |ctx| Ok(ctx.bitrate))
    }

    #[inline]
    pub fn is_log_errors(&self, channel: String) -> CanResult<bool> {
        self.channel_util(channel, |ctx| Ok(ctx.log_errors))
    }

    #[inline]
    fn channel_util<R>(
        &self,
        channel: String,
        cb: fn(ctx: &NiCanContext) -> CanResult<R>,
    ) -> CanResult<R> {
        match self.channels.get(&channel) {
            Some(ctx) => cb(ctx),
            None => Err(CanError::channel_not_opened(Self::channel_info(&channel))),
        }
    }

    fn wait_for_state(
        &self,
        channel: &str,
        handle: NCTYPE_OBJH,
        timeout: Option<u32>,
    ) -> Result<(), NCTYPE_STATUS> {
        let timeout = timeout.unwrap_or(NC_DURATION_INFINITE) as NCTYPE_DURATION;

        let mut state = 0;
        let ret = unsafe {
            (self.ncWaitForState)(
                handle,
                NC_ST_READ_AVAIL as NCTYPE_STATE,
                timeout,
                &mut state,
            )
        };

        self.check_status(channel, ret)
    }

    pub(crate) fn check_status(
        &self,
        channel: &str,
        result: NCTYPE_STATUS,
    ) -> Result<(), NCTYPE_STATUS> {
        if result > 0 {
            rsutil::warn!(
                "{} {}",
                Self::channel_info(channel),
                self.status_to_str(result)
            );
            Ok(())
        } else if result < 0 {
            Err(result)
        } else {
            Ok(())
        }
    }

    pub(crate) fn status_to_str(&self, code: NCTYPE_STATUS) -> String {
        let mut err = [0u8; 1024];
        unsafe {
            (self.ncStatusToString)(
                code,
                err.len() as NCTYPE_UINT32,
                err.as_mut_ptr() as NCTYPE_STRING,
            )
        };
        let cstr = unsafe { CStr::from_ptr(err.as_ptr() as *const c_char) };

        cstr.to_str().unwrap_or("Unknown").to_string()
    }
}