use std::io::Read as _;

use num_enum::TryFromPrimitive as _;
use uuid::Uuid;

use crate::{FAMILY_ID, MANUFACTURER_ID, SysexOp};

#[derive(Debug, Clone)]
#[repr(u8)]
pub enum Request {
    Diag,
    Put {
        data: Vec<u8>,
    },
    Get {
        patch_id: Uuid,
    },
    RenderHash {
        patch_id: Uuid,
        note: u32,
        velocity: u32,
    },
    GetCryptoParams,
}

pub fn parse_sysex_request(frame: &[u8]) -> anyhow::Result<Request> {
    if frame.len() < 5 || frame.first() != Some(&0xF0) || frame.last() != Some(&0xF7) {
        return Err(anyhow::anyhow!("invalid SysEx frame"));
    }
    if frame[1] != MANUFACTURER_ID || frame[2] != FAMILY_ID[0] || frame[3] != FAMILY_ID[1] {
        return Err(anyhow::anyhow!("wrong manufacturer"));
    }
    let op = frame[4];
    let mut rdr = std::io::Cursor::new(&frame[5..frame.len() - 1]);
    match SysexOp::try_from_primitive(op).map_err(|_| anyhow::anyhow!("unknown op"))? {
        SysexOp::Diag => Ok(Request::Diag),
        SysexOp::Put => {
            let mut data = Vec::new();
            rdr.read_to_end(&mut data)?;
            Ok(Request::Put { data })
        }
        SysexOp::Get => {
            let patch_id = {
                let mut id_bytes = [0u8; 16];
                rdr.read_exact(&mut id_bytes)?;
                Uuid::from_bytes(id_bytes)
            };

            if rdr.position() != rdr.get_ref().len() as u64 {
                anyhow::bail!("trailing extra data");
            }

            Ok(Request::Get { patch_id })
        }
        SysexOp::RenderHash => {
            let patch_id = {
                let mut id_bytes = [0u8; 16];
                rdr.read_exact(&mut id_bytes)?;
                Uuid::from_bytes(id_bytes)
            };

            let note = {
                let mut note_bytes = [0u8; 4];
                rdr.read_exact(&mut note_bytes)?;
                u32::from_be_bytes(note_bytes)
            };

            let velocity = {
                let mut velocity_bytes = [0u8; 4];
                rdr.read_exact(&mut velocity_bytes)?;
                u32::from_be_bytes(velocity_bytes)
            };

            if rdr.position() != rdr.get_ref().len() as u64 {
                anyhow::bail!("trailing extra data");
            }

            Ok(Request::RenderHash {
                patch_id,
                note,
                velocity,
            })
        }
        SysexOp::GetCryptoParams => Ok(Request::GetCryptoParams),
        _ => anyhow::bail!("unexpected op"),
    }
}

impl Request {
    pub fn opcode(&self) -> SysexOp {
        match self {
            Request::Diag => SysexOp::Diag,
            Request::Put { .. } => SysexOp::Put,
            Request::Get { .. } => SysexOp::Get,
            Request::RenderHash { .. } => SysexOp::RenderHash,
            Request::GetCryptoParams => SysexOp::GetCryptoParams,
        }
    }

    pub fn encode(&self) -> Vec<u8> {
        let mut out = vec![
            0xF0,
            MANUFACTURER_ID,
            FAMILY_ID[0],
            FAMILY_ID[1],
            self.opcode() as u8,
        ];

        match self {
            Request::Diag => {}
            Request::Put { data } => {
                out.extend_from_slice(data);
            }
            Request::Get { patch_id } => {
                out.extend_from_slice(patch_id.as_bytes());
            }
            Request::RenderHash {
                patch_id,
                note,
                velocity,
            } => {
                out.extend_from_slice(patch_id.as_bytes());
                out.extend_from_slice(&note.to_be_bytes());
                out.extend_from_slice(&velocity.to_be_bytes());
            }
            Request::GetCryptoParams => {}
        }

        out.push(0xF7);
        out
    }
}
