use bincode::{Decode, Encode};
use bitflags::bitflags;
use int_enum::IntEnum;

use crate::{
    bincode_enum,
    hmi::proto::ports::{ReadableDataPort, UnsafelyWritableDataPort},
};

#[derive(Debug, Clone, Copy, PartialEq, Eq, IntEnum, Default)]
#[repr(u8)]
pub enum ServiceRequestCode {
    PLCStatus = 0x00,
    ReturnProgName = 0x03,
    ReadSysMemory = 0x04, // Used to read general memory register (Example: %R12344)
    ReadTaskMemory = 0x05,
    ReadProgMemory = 0x06,
    WriteSysMemory = 0x07, // Used to write general memory
    WriteTaskMemory = 0x08,
    WriteProgMemory = 0x09,
    ProgLogon = 0x20,
    ChangePriv = 0x21,
    SetCpuId = 0x22,
    SetPlcRun = 0x23,
    SetPlcTime = 0x24,
    ReturnDateTime = 0x25,
    GetFault = 0x38,
    ClearFault = 0x39,
    ReturnControllerType = 0x43,
    Magic = 0x4f,
    #[default]
    Unknown = 0xff,
}
bincode_enum!(with_default ServiceRequestCode : u8);

#[derive(Debug, Clone, Copy, PartialEq, Eq, IntEnum, Default)]
#[repr(u8)]
pub enum SegmentSelector {
    Init = 0x00,
    Magic = 0x01,
    InputBit = 0x46,
    InputByte = 0x10,
    OutputBit = 0x48,
    OutputByte = 0x12,
    TimerBit = 0x4a,
    TimerByte = 0x14,
    FlagBit = 0x4c,
    FlagByte = 0x16,
    SABit = 0x4e,
    SAByte = 0x18,
    SBBit = 0x50,
    SBByte = 0x1a,
    SCBit = 0x52,
    SCByte = 0x1c,
    SBit = 0x54,
    SByte = 0x1e,
    GlobalBit = 0x56,
    GlobalByte = 0x38,
    AnalogInput = 0x0a,
    AnalogOutput = 0x0c,
    Registers = 0x08,
    ProgramBlockData = 0x04,
    #[default]
    Unknown = 0xff,
}
bincode_enum!(with_default SegmentSelector : u8);

#[derive(Debug, Clone, Copy, PartialEq, Eq, IntEnum, Default)]
#[repr(u16)]
pub(super) enum PktType {
    InitTx = 0x00,
    InitRx = 0x01,
    Transmit = 0x02,
    Receive = 0x03,
    #[default]
    Unknown = 0x08,
}
bincode_enum!(with_default PktType : u16);

#[derive(Debug, Clone, Copy, PartialEq, Eq, IntEnum, Default)]
#[repr(u32)]
pub(super) enum OperationType {
    Init = 0x00000000,
    InitFinish = 0x00000001,
    Writing = 0x00000100,
    Reading = 0x00000200,
    #[default]
    Invalid = 0xffffffff,
}
bincode_enum!(with_default OperationType : u32);

#[derive(Debug, Clone, Copy, PartialEq, Eq, IntEnum, Default)]
#[repr(u8)]
pub(super) enum MsgTyp {
    Init = 0x00,
    Error = 0xd1,
    Req = 0xc0,
    Resp = 0xd4,
    ExtReq = 0x80,
    ExtResp = 0x94,
    #[default]
    Invalid = 0xff,
}
bincode_enum!(with_default MsgTyp : u8);

#[derive(Debug, Clone, Copy, PartialEq, Eq, IntEnum, Default)]
#[repr(u8)]
pub enum PlcState {
    RunIoEnabled = 0,
    RunIoDisabled = 1,
    StopIoDisabled = 2,
    CpuStopFaulted = 3,
    CpuHalted = 4,
    CpuSuspended = 5,
    StopIoEnabled = 6,
    #[default]
    Invalid = 0b1111,
}

bitflags! {
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    pub struct PlcStatusFlags: u16 {
        const OVERSWEEP = 0b0000_0000_0000_0001;
        const CONSTANT_SWEEP_MODE = 0b0000_0000_0000_0010;
        const PLC_FAULT_ENTRY_SINCE_LAST_READ = 0b0000_0000_0000_0100;
        const IO_FAULT_ENTRY_SINCE_LAST_READ = 0b0000_0000_0000_1000;
        const PLC_FAULT_ENTRY_PRESENT = 0b0000_0000_0001_0000;
        const IO_FAULT_ENTRY_PRESENT = 0b0000_0000_0010_0000;
        const PROGRAMMER_ATTACHMENT_FOUND = 0b0000_0000_0100_0000;
        const OUTPUTS_DISABLED = 0b0000_0000_1000_0000;
        const RUN_MODE = 0b0000_0001_0000_0000;
        const OEM_PROTECTION_IN_EFFECT = 0b0000_0010_0000_0000;
        const PLC_STATE_MASK = 0b1111_0000_0000_0000;
    }
}

impl bincode::Encode for PlcStatusFlags {
    fn encode<E: bincode::enc::Encoder>(
        &self,
        encoder: &mut E,
    ) -> Result<(), bincode::error::EncodeError> {
        self.bits().encode(encoder)
    }
}
impl<Ctx> bincode::Decode<Ctx> for PlcStatusFlags {
    fn decode<D: bincode::de::Decoder<Context = Ctx>>(
        decoder: &mut D,
    ) -> Result<Self, bincode::error::DecodeError> {
        let variant_int = <u16 as bincode::Decode<D::Context>>::decode(decoder)?;
        Ok(PlcStatusFlags::from_bits_retain(variant_int))
    }
}
impl<Ctx> bincode::de::BorrowDecode<'_, Ctx> for PlcStatusFlags {
    fn borrow_decode<D: bincode::de::Decoder<Context = Ctx>>(
        decoder: &mut D,
    ) -> Result<Self, bincode::error::DecodeError> {
        <Self as bincode::Decode<Ctx>>::decode(decoder)
    }
}

impl PlcStatusFlags {
    pub fn plc_state(&self) -> PlcState {
        let state_bits = ((self.bits() & Self::PLC_STATE_MASK.bits()) >> 12) as u8;
        PlcState::try_from(state_bits).unwrap_or_default()
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
pub struct PlcStatus {
    ctrl_prog_num: u8,
    priv_lvl: u8,
    last_sweep: u16,
    status_flags: PlcStatusFlags,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
#[repr(C)]
pub struct Header {
    pkt_type: PktType,
    seq1: u16,
    text_len: u16,
    unk1: u16,
    operation1: OperationType,
    unk2: u32,
    operation2: OperationType,
    unk3: u32,
    unk4: u16,
    time: (u8, u8, u8),
    unk5: u8,
    seq2: u8,
    msg_type: MsgTyp,
    mbox_src: u32,
    mbox_dst: u32,
    pkt_num: u8,
    total_pkt_num: u8,
}

impl Header {
    pub const INIT: Header = Header {
        pkt_type: PktType::InitTx,
        seq1: 0,
        text_len: 0,
        unk1: 0,
        operation1: OperationType::Init,
        unk2: 0,
        operation2: OperationType::Init,
        unk3: 0,
        unk4: 0,
        time: (0, 0, 0),
        unk5: 0,
        seq2: 0,
        msg_type: MsgTyp::Init,
        mbox_src: 0,
        mbox_dst: 0,
        pkt_num: 0,
        total_pkt_num: 0,
    };

    pub const INIT_ACK: Header = Header {
        pkt_type: PktType::InitRx,
        operation1: OperationType::InitFinish,
        ..Header::INIT
    };

    pub const TEMPLATE: Header = Header {
        pkt_type: PktType::Transmit,
        mbox_dst: 0x00000e10,
        pkt_num: 1,
        total_pkt_num: 1,
        ..Header::INIT
    };

    pub const MAGIC: Header = Header {
        pkt_type: PktType::Unknown,
        seq1: 1,
        text_len: 0,
        unk1: 0,
        operation1: OperationType::Writing,
        unk2: 0,
        operation2: OperationType::Writing,
        unk3: 0,
        unk4: 0,
        time: (0, 0, 0),
        unk5: 0,
        seq2: 1,
        msg_type: MsgTyp::Req,
        mbox_src: 0,
        mbox_dst: 0x00000e10,
        pkt_num: 1,
        total_pkt_num: 1,
    };

    pub fn payload_len(&self) -> u16 {
        self.text_len
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Decode)]
pub enum Body {
    Req {
        service_request: ServiceRequestCode,
        segment: SegmentSelector,
        target_index: u16,
        target_size: u16,
        payload: [u8; 6],
        unk: u16,
    },
    Resp {
        unk1: u16,
        payload: [u8; 6],
        plc_status: PlcStatus,
    },
    ExtReq {
        unk: [u8; 6],
        pkt_num: u8,
        total_pkt_num: u8,
        service_request: ServiceRequestCode,
        segment: SegmentSelector,
        target_index: u16,
        target_size: u16,
        payload: Box<[u8]>,
    },
    ExtResp {
        unk: [u8; 6],
        pkt_num: u8,
        total_pkt_num: u8,
        service_request: ServiceRequestCode,
        segment: SegmentSelector,
        unk2: u16,
        plc_flags: PlcStatusFlags,
        payload: Box<[u8]>,
    },
}

impl Body {
    pub const INIT: Body = Body::Req {
        service_request: ServiceRequestCode::PLCStatus,
        segment: SegmentSelector::Init,
        target_index: 0,
        target_size: 0,
        payload: [0; 6],
        unk: 0,
    };

    pub const MAGIC: Body = Body::Req {
        service_request: ServiceRequestCode::Magic,
        segment: SegmentSelector::Magic,
        target_index: 0,
        target_size: 0,
        payload: [0; 6],
        unk: 0,
    };

    pub fn size(&self) -> usize {
        match self {
            Self::Req { .. } => 16,
            Self::Resp { .. } => 16,
            Self::ExtReq { payload, .. } => 16 + payload.len(),
            Self::ExtResp { payload, .. } => 16 + payload.len(),
        }
    }
}

impl Encode for Body {
    fn encode<E: bincode::enc::Encoder>(
        &self,
        encoder: &mut E,
    ) -> Result<(), bincode::error::EncodeError> {
        match self {
            Self::Req {
                service_request,
                segment,
                target_index,
                target_size,
                payload,
                unk,
            } => {
                Encode::encode(service_request, encoder)?;
                Encode::encode(segment, encoder)?;
                Encode::encode(target_index, encoder)?;
                Encode::encode(target_size, encoder)?;
                Encode::encode(payload, encoder)?;
                Encode::encode(unk, encoder)?;
                Ok(())
            }
            Self::Resp {
                unk1,
                payload,
                plc_status,
            } => {
                Encode::encode(unk1, encoder)?;
                Encode::encode(payload, encoder)?;
                Encode::encode(plc_status, encoder)?;
                Ok(())
            }
            Self::ExtReq {
                unk,
                pkt_num,
                total_pkt_num,
                service_request,
                segment: memory_type,
                target_index,
                target_size,
                payload,
            } => {
                Encode::encode(unk, encoder)?;
                Encode::encode(pkt_num, encoder)?;
                Encode::encode(total_pkt_num, encoder)?;
                Encode::encode(service_request, encoder)?;
                Encode::encode(memory_type, encoder)?;
                Encode::encode(target_index, encoder)?;
                Encode::encode(target_size, encoder)?;
                for byte in payload.iter() {
                    Encode::encode(byte, encoder)?;
                }
                Ok(())
            }
            Self::ExtResp {
                unk,
                pkt_num,
                total_pkt_num,
                service_request,
                segment: memory_type,
                unk2,
                plc_flags,
                payload,
            } => {
                Encode::encode(unk, encoder)?;
                Encode::encode(pkt_num, encoder)?;
                Encode::encode(total_pkt_num, encoder)?;
                Encode::encode(service_request, encoder)?;
                Encode::encode(memory_type, encoder)?;
                Encode::encode(unk2, encoder)?;
                Encode::encode(plc_flags, encoder)?;
                for byte in payload.iter() {
                    Encode::encode(byte, encoder)?;
                }
                Ok(())
            }
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Encode)]
pub struct Message {
    pub header: Header,
    pub body: Body,
}

fn decode_bytes<Context, D: bincode::de::Decoder<Context = Context>>(
    decoder: &mut D,
    count: usize,
) -> Result<Box<[u8]>, bincode::error::DecodeError> {
    let mut arr = Vec::with_capacity(count);
    for _ in 0..count {
        let byte = <u8 as bincode::Decode<D::Context>>::decode(decoder)?;
        arr.push(byte);
    }
    Ok(arr.into_boxed_slice())
}

impl<Context> bincode::Decode<Context> for Message {
    fn decode<D: bincode::de::Decoder<Context = Context>>(
        decoder: &mut D,
    ) -> Result<Self, bincode::error::DecodeError> {
        let header: Header = Decode::decode(decoder)?;
        let body = match header.msg_type {
            MsgTyp::Init => Ok(Body::INIT),
            MsgTyp::Req => Ok(Body::Req {
                service_request: Decode::<D::Context>::decode(decoder)?,
                segment: Decode::<D::Context>::decode(decoder)?,
                target_index: Decode::<D::Context>::decode(decoder)?,
                target_size: Decode::<D::Context>::decode(decoder)?,
                payload: Decode::<D::Context>::decode(decoder)?,
                unk: Decode::<D::Context>::decode(decoder)?,
            }),
            MsgTyp::Resp => Ok(Body::Resp {
                unk1: Decode::<D::Context>::decode(decoder)?,
                payload: Decode::<D::Context>::decode(decoder)?,
                plc_status: Decode::<D::Context>::decode(decoder)?,
            }),
            MsgTyp::ExtReq => Ok(Body::ExtReq {
                unk: Decode::<D::Context>::decode(decoder)?,
                pkt_num: Decode::<D::Context>::decode(decoder)?,
                total_pkt_num: Decode::<D::Context>::decode(decoder)?,
                service_request: Decode::<D::Context>::decode(decoder)?,
                segment: Decode::<D::Context>::decode(decoder)?,
                target_index: Decode::<D::Context>::decode(decoder)?,
                target_size: Decode::<D::Context>::decode(decoder)?,
                payload: decode_bytes(decoder, header.text_len as usize)?,
            }),
            MsgTyp::ExtResp => Ok(Body::ExtResp {
                unk: Decode::<D::Context>::decode(decoder)?,
                pkt_num: Decode::<D::Context>::decode(decoder)?,
                total_pkt_num: Decode::<D::Context>::decode(decoder)?,
                service_request: Decode::<D::Context>::decode(decoder)?,
                segment: Decode::<D::Context>::decode(decoder)?,
                unk2: Decode::<D::Context>::decode(decoder)?,
                plc_flags: Decode::<D::Context>::decode(decoder)?,
                payload: decode_bytes(decoder, header.text_len as usize)?,
            }),
            MsgTyp::Invalid | MsgTyp::Error => {
                Err(::bincode::error::DecodeError::UnexpectedVariant {
                    found: header.msg_type as u32,
                    type_name: "Message",
                    allowed: &bincode::error::AllowedEnumVariants::Allowed(&[
                        MsgTyp::Req as u32,
                        MsgTyp::Resp as u32,
                        MsgTyp::ExtReq as u32,
                        MsgTyp::ExtResp as u32,
                    ]),
                })
            }
        }?;
        Ok(Self { header, body })
    }
}
impl<'de, Context> bincode::BorrowDecode<'de, Context> for Message {
    fn borrow_decode<D: bincode::de::BorrowDecoder<'de, Context = Context>>(
        decoder: &mut D,
    ) -> Result<Self, bincode::error::DecodeError> {
        <Self as bincode::Decode<Context>>::decode(decoder)
    }
}

impl Message {
    pub const INIT: Message = Message {
        header: Header::INIT,
        body: Body::INIT,
    };

    pub const INIT_ACK: Message = Message {
        header: Header::INIT_ACK,
        body: Body::INIT,
    };

    pub const MAGIC: Message = Message {
        header: Header::MAGIC,
        body: Body::MAGIC,
    };

    pub fn seq(&self) -> u8 {
        self.header.seq1 as u8
    }

    pub fn payload(&self) -> &[u8] {
        match &self.body {
            Body::Req { payload, .. } => payload,
            Body::Resp { payload, .. } => payload,
            Body::ExtReq { payload, .. } => payload,
            Body::ExtResp { payload, .. } => payload,
        }
    }

    pub(crate) fn new_read_req<T: ReadableDataPort>(seq: u8, index: u16, count: u16) -> Message {
        Message {
            header: Header {
                seq1: seq as u16,
                seq2: seq,
                msg_type: MsgTyp::Req,
                operation1: OperationType::Reading,
                operation2: OperationType::Reading,
                ..Header::TEMPLATE
            },
            body: Body::Req {
                service_request: ServiceRequestCode::ReadSysMemory,
                segment: T::SEGMENT,
                payload: [0; 6],
                target_index: T::OFFSET + index,
                target_size: count,
                unk: 0,
            },
        }
    }

    pub(crate) fn new_write_req<T: UnsafelyWritableDataPort>(
        seq: u8,
        mut index: u16,
        data: &[T::ValueType],
    ) -> Message {
        index += T::OFFSET;
        if !T::ZERO_INDEXED {
            index -= 1;
        }
        let data_count = T::item_count(index, data);
        let data_byte_cnt = T::size_of_array(index, data);
        if data_byte_cnt <= 6 {
            tracing::trace!(
                "Packing write request for port {} at index {} and count {} with data {:?} into inline payload",
                T::NAME,
                index,
                data_count,
                data
            );
            let mut payload = [0u8; 6];
            T::pack_array_into(index, data, &mut payload);
            Message {
                header: Header {
                    seq1: seq as u16,
                    seq2: seq,
                    text_len: 0,
                    msg_type: MsgTyp::Req,
                    operation1: OperationType::Writing,
                    operation2: OperationType::Writing,
                    ..Header::TEMPLATE
                },
                body: Body::Req {
                    service_request: ServiceRequestCode::WriteSysMemory,
                    segment: T::SEGMENT,
                    payload,
                    target_index: index,
                    target_size: data_count,
                    unk: 0,
                },
            }
        } else {
            tracing::trace!(
                "Packing write request for port {} at index {} and count {} with data {:?} into extended payload",
                T::NAME,
                index,
                data_count,
                data
            );
            let payload = T::pack_array(index, data);
            tracing::debug!("Extended write payload size: {} bytes", payload.len());
            Message {
                header: Header {
                    seq1: seq as u16,
                    seq2: seq,
                    text_len: payload.len() as u16,
                    msg_type: MsgTyp::ExtReq,
                    operation1: OperationType::Writing,
                    operation2: OperationType::Writing,
                    ..Header::TEMPLATE
                },
                body: Body::ExtReq {
                    unk: [0u8; 6],
                    pkt_num: 1,
                    total_pkt_num: 1,
                    service_request: ServiceRequestCode::WriteSysMemory,
                    segment: T::SEGMENT,
                    target_index: index,
                    target_size: data_count,
                    payload,
                },
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use bincode::config;

    #[test]
    fn test_message_size() {
        use super::*;
        let msg = Message::new_read_req::<crate::hmi::proto::ports::DigitalInput>(1, 0, 10);
        let encoded = bincode::encode_to_vec(msg, crate::hmi::BINCODE_CFG).unwrap();
        assert_eq!(encoded.len(), 56);
    }

    #[test]
    fn test_ext_message_size() {
        use super::*;
        let msg = Message::new_write_req::<crate::hmi::proto::ports::Command>(
            1,
            0,
            &["123456789012345678901234567890".to_string()],
        );
        println!("{}", msg.payload().len());
        let encoded = bincode::encode_to_vec(msg, crate::hmi::BINCODE_CFG).unwrap();
        assert_eq!(encoded.len(), 56 + 30);
    }

    #[test]
    fn test_message_decode() {
        use super::*;
        const BYTES: [u8; 136] = [
            0x3, 0x0, 0x4, 0x0, 0x50, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
            0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x94, 0x10, 0xe,
            0x0, 0x0, 0x30, 0x3a, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0xff,
            0x4, 0x0, 0x0, 0x7c, 0x21, 0x31, 0x30, 0x2e, 0x32, 0x33, 0x2e, 0x31, 0x36, 0x2e, 0x32,
            0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
            0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
            0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
            0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
            0x0, 0x0,
        ];
        const BINCODE_CFG: config::Configuration<config::LittleEndian, config::Fixint> =
            bincode::config::standard()
                .with_little_endian()
                .with_fixed_int_encoding();

        let (msg, _consumed) =
            bincode::decode_from_slice::<Message, _>(&BYTES, BINCODE_CFG).unwrap();
        println!("Decoded message: {:?}", msg);
    }

    #[test]
    fn test_message_decode2() {
        use super::*;
        const BYTES: [u8; 56] = [
            0x3, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
            0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0xd4, 0x10, 0xe,
            0x0, 0x0, 0x30, 0x3a, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0xff,
            0x4, 0x0, 0x0, 0x7c, 0x21,
        ];
        const BINCODE_CFG: config::Configuration<config::LittleEndian, config::Fixint> =
            bincode::config::standard()
                .with_little_endian()
                .with_fixed_int_encoding();

        let (msg, _consumed) =
            bincode::decode_from_slice::<Message, _>(&BYTES, BINCODE_CFG).unwrap();
        println!("Decoded message: {:?}", msg);
    }
}
