use crate::serial_core::codec::Codec;
use crate::serial_core::error::CodecError;
use crate::serial_core::frame::{
    TactileAFrame, TactileAFrameMetaData, TactileAFrameStatusCode, TactileARepFrame,
    TactileAReqFrame,
};
use crate::serial_core::utils::{calc_crc8_itu, elapsed_millis};

const FRAME_BUFFER_MIN_LENGTH: usize = 15;

pub struct TactileACodec {
    buffer: Vec<u8>,
    expected_data_len: usize,
}

impl From<u8> for TactileAFrameStatusCode {
    fn from(value: u8) -> Self {
        match value {
            0 => TactileAFrameStatusCode::Success,
            _ => TactileAFrameStatusCode::Failure,
        }
    }
}

impl TactileACodec {
    pub fn new(cols: usize, rows: usize) -> TactileACodec {
        Self {
            buffer: Vec::new(),
            expected_data_len: cols * rows * 2,
        }
    }

    pub fn parse_data_frame(data: &[u8]) -> Result<Vec<i32>, CodecError> {
        if data.len() % 2 != 0 {
            return Err(CodecError::InvalidLength);
        }

        let vals: Vec<i32> = data
            .chunks_exact(2)
            .map(|chunk| {
                let raw = u16::from_le_bytes([chunk[0], chunk[1]]) as i32;
                if raw < 15 { 0 } else { raw }
            })
            .collect::<Vec<i32>>();

        Ok(vals)
    }

    pub fn build_req_frame(cols: usize, rows: usize) -> anyhow::Result<TactileAFrame> {
        let header = [0x55, 0xAA];
        let payload_len: usize = 9;
        let device_addr: u8 = 0x34;
        let extend_code: u8 = 0x00;
        let func_code: u8 = 0xFB;
        let start_addr: u32 = 7168;
        let except_data_len: usize = cols * rows * 2;
        let checksum: u8 = 0;
        Ok(TactileAFrame::Req(TactileAReqFrame {
            meta: TactileAFrameMetaData {
                header,
                payload_len,
                device_addr,
                extend_code,
                func_code,
                start_addr,
                except_data_len,
                checksum,
            },
        }))
    }
}

impl Codec<TactileAFrame> for TactileACodec {
    fn decode(
        &mut self,
        input: &[u8],
        session_started_at: std::time::Instant,
    ) -> Result<Vec<TactileAFrame>, CodecError> {
        self.buffer.extend_from_slice(input);
        let mut frames: Vec<TactileAFrame> = Vec::new();

        loop {
            if self.buffer.len() < FRAME_BUFFER_MIN_LENGTH {
                break;
            }

            // Search for response header: [0xAA, 0x55]
            let header_pos = self.buffer.windows(2).position(|w| w == [0xAA, 0x55]);

            let Some(pos) = header_pos else {
                self.buffer.clear();
                break;
            };
            if pos > 0 {
                self.buffer.drain(0..pos);
            }

            if self.buffer.len() < FRAME_BUFFER_MIN_LENGTH {
                break;
            }

            let header = [self.buffer[0], self.buffer[1]];
            let payload_len = u16::from_le_bytes([self.buffer[2], self.buffer[3]]) as usize;
            let device_addr = self.buffer[4];
            let extend_code = self.buffer[5];
            let func_code = self.buffer[6];
            let start_addr = u32::from_le_bytes([
                self.buffer[7],
                self.buffer[8],
                self.buffer[9],
                self.buffer[10],
            ]);
            let except_data_len = u16::from_le_bytes([self.buffer[11], self.buffer[12]]) as usize;
            let status = TactileAFrameStatusCode::from(self.buffer[13]);

            if except_data_len != self.expected_data_len {
                log::debug!(
                    "unexpected payload length: expected {}, got {}, buffer_len={}",
                    self.expected_data_len,
                    except_data_len,
                    self.buffer.len()
                );
                self.buffer.drain(0..1);
                continue;
            }

            let frame_length = except_data_len + FRAME_BUFFER_MIN_LENGTH;
            if self.buffer.len() < frame_length {
                break;
            }

            let need_check_data = self.buffer[0..14 + except_data_len].to_vec();
            let payload = self.buffer[14..14 + except_data_len].to_vec();
            let crc8_itu_alg = crc::Crc::<u8>::new(&crc::CRC_8_I_432_1);
            let checksum = crc8_itu_alg.checksum(&need_check_data.as_slice());
            if self.buffer[frame_length - 1] != checksum {
                log::debug!(
                    "checksum mismatch: expected {:02X}, got {:02X}, frame_len={}",
                    checksum,
                    self.buffer[frame_length - 1],
                    frame_length
                );
                self.buffer.drain(0..1);
                continue;
            }

            let dts_ms = elapsed_millis(session_started_at);
            let meta = TactileAFrameMetaData {
                header,
                payload_len,
                device_addr,
                extend_code,
                func_code,
                start_addr,
                except_data_len,
                checksum,
            };
            frames.push(TactileAFrame::Rep(TactileARepFrame {
                meta,
                status,
                payload,
                dts_ms,
            }));

            self.buffer.drain(0..frame_length);
        }

        Ok(frames)
    }

    fn encode(&self, frame: &TactileAFrame) -> Result<Vec<u8>, CodecError> {
        match frame {
            TactileAFrame::Req(f) => {
                let mut req_bytes: Vec<u8> = Vec::new();
                req_bytes.extend_from_slice(f.meta.header.as_slice());
                req_bytes.extend_from_slice((f.meta.payload_len as u16).to_le_bytes().as_slice());
                req_bytes.push(f.meta.device_addr);
                req_bytes.push(f.meta.extend_code);
                req_bytes.push(f.meta.func_code);
                req_bytes.extend_from_slice(f.meta.start_addr.to_le_bytes().as_slice());
                req_bytes
                    .extend_from_slice((f.meta.except_data_len as u16).to_le_bytes().as_slice());
                let checksum = calc_crc8_itu(req_bytes.as_slice());
                req_bytes.push(checksum);
                Ok(req_bytes)
            }
            _ => Err(CodecError::InvalidFrameType),
        }
    }
}
