use base64::{Engine, engine::general_purpose::STANDARD};
use serde_json::Value;

use crate::error::{Result, SocketIoError};

const RECORD_SEPARATOR: char = '\x1e';

#[derive(Clone, Debug, PartialEq)]
pub enum EnginePacket {
    Open(Value),
    Close,
    Ping(Option<String>),
    Pong(Option<String>),
    Message(SocketPayload),
    Upgrade,
    Noop,
}

#[derive(Clone, Debug, PartialEq)]
pub enum SocketPayload {
    Text(String),
    Binary(Vec<u8>),
}

pub fn encode_engine_payload(
    packets: &[EnginePacket],
    polling: bool,
) -> String {
    packets
        .iter()
        .map(|packet| encode_engine_packet(packet, polling))
        .collect::<Vec<_>>()
        .join(&RECORD_SEPARATOR.to_string())
}

pub fn decode_engine_payload(payload: &str) -> Result<Vec<EnginePacket>> {
    payload
        .split(RECORD_SEPARATOR)
        .filter(|item| !item.is_empty())
        .map(decode_engine_text_packet)
        .collect()
}

pub fn encode_engine_packet(packet: &EnginePacket, _polling: bool) -> String {
    match packet {
        EnginePacket::Open(data) => format!("0{data}"),
        EnginePacket::Close => "1".to_owned(),
        EnginePacket::Ping(data) => {
            format!("2{}", data.as_deref().unwrap_or_default())
        }
        EnginePacket::Pong(data) => {
            format!("3{}", data.as_deref().unwrap_or_default())
        }
        EnginePacket::Message(SocketPayload::Text(text)) => format!("4{text}"),
        EnginePacket::Message(SocketPayload::Binary(bytes)) => {
            format!("b{}", STANDARD.encode(bytes))
        }
        EnginePacket::Upgrade => "5".to_owned(),
        EnginePacket::Noop => "6".to_owned(),
    }
}

pub fn decode_engine_text_packet(input: &str) -> Result<EnginePacket> {
    if let Some(encoded) = input.strip_prefix('b') {
        return Ok(EnginePacket::Message(SocketPayload::Binary(
            STANDARD.decode(encoded).map_err(|_| {
                SocketIoError::InvalidPacket(
                    "invalid base64 payload".to_owned(),
                )
            })?,
        )));
    }

    let mut chars = input.chars();
    let packet_type = chars.next().ok_or_else(|| {
        SocketIoError::InvalidPacket("empty engine packet".to_owned())
    })?;
    let rest = chars.as_str();
    match packet_type {
        '0' => Ok(EnginePacket::Open(serde_json::from_str(rest)?)),
        '1' => Ok(EnginePacket::Close),
        '2' => Ok(EnginePacket::Ping(non_empty(rest))),
        '3' => Ok(EnginePacket::Pong(non_empty(rest))),
        '4' => Ok(EnginePacket::Message(SocketPayload::Text(rest.to_owned()))),
        '5' => Ok(EnginePacket::Upgrade),
        '6' => Ok(EnginePacket::Noop),
        _ => Err(SocketIoError::InvalidPacket(format!(
            "unknown engine packet type {packet_type}"
        ))),
    }
}

fn non_empty(value: &str) -> Option<String> {
    if value.is_empty() {
        None
    } else {
        Some(value.to_owned())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn polling_payload_uses_record_separator() {
        let packets = vec![
            EnginePacket::Message(SocketPayload::Text("40".to_owned())),
            EnginePacket::Message(SocketPayload::Text(
                "42[\"ready\",null]".to_owned(),
            )),
        ];
        let encoded = encode_engine_payload(&packets, true);
        assert_eq!(decode_engine_payload(&encoded).unwrap(), packets);
    }
}
