use actix::Message;
use log::error;

use libc::c_ushort;

use std::fmt;

use serde::ser::SerializeSeq;
use serde::{Serialize, Serializer};

use crate::event::IO;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseError {
    message: String,
}

impl ParseError {
    fn new(message: impl Into<String>) -> Self {
        Self {
            message: message.into(),
        }
    }
}

impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Terminado parse error: {}", self.message)
    }
}

impl std::error::Error for ParseError {}

impl Message for TerminadoMessage {
    type Result = ();
}

#[derive(Debug, Eq, PartialEq, Clone)]
pub enum TerminadoMessage {
    Resize { rows: c_ushort, cols: c_ushort },
    Stdin(IO),
    Stdout(IO),
}

impl TerminadoMessage {
    pub fn from_json(json: &str) -> Result<Self, ParseError> {
        let value: serde_json::Value = serde_json::from_str(json).map_err(|e| {
            let msg = format!("Invalid JSON: {}", e);
            error!("Invalid Terminado message: {}", msg);
            ParseError::new(msg)
        })?;

        let list: &Vec<serde_json::Value> = value.as_array().ok_or_else(|| {
            let msg = "Needs to be an array";
            error!("Invalid Terminado message: {}", msg);
            ParseError::new(msg)
        })?;

        match list
            .first()
            .ok_or_else(|| {
                let msg = "Empty array";
                error!("Invalid Terminado message: {}", msg);
                ParseError::new(msg)
            })?
            .as_str()
            .ok_or_else(|| {
                let msg = "Type field not a string";
                error!("Invalid Terminado message: {}", msg);
                ParseError::new(msg)
            })? {
            "stdin" => {
                if list.len() != 2 {
                    let msg = r#""stdin" length != 2"#;
                    error!("Invalid Terminado message: {}", msg);
                    return Err(ParseError::new(msg));
                }

                Ok(TerminadoMessage::Stdin(IO::from(
                    list[1].as_str().ok_or_else(|| {
                        let msg = r#""stdin" needs to be a String"#;
                        error!("Invalid Terminado message: {}", msg);
                        ParseError::new(msg)
                    })?,
                )))
            }
            "stdout" => {
                if list.len() != 2 {
                    let msg = r#""stdout" length != 2"#;
                    error!("Invalid Terminado message: {}", msg);
                    return Err(ParseError::new(msg));
                }

                Ok(TerminadoMessage::Stdout(IO::from(
                    list[1].as_str().ok_or_else(|| {
                        let msg = r#""stdout" needs to be a String"#;
                        error!("Invalid Terminado message: {}", msg);
                        ParseError::new(msg)
                    })?,
                )))
            }
            "set_size" => {
                if list.len() != 3 {
                    let msg = r#""set_size" length != 3"#;
                    error!("Invalid Terminado message: {}", msg);
                    return Err(ParseError::new(msg));
                }

                let rows: u16 = u16::try_from(list[1].as_u64().ok_or_else(|| {
                    let msg = r#""set_size" element 1 needs to be an integer"#;
                    error!("Invalid Terminado message: {}", msg);
                    ParseError::new(msg)
                })?)
                .map_err(|_| {
                    let msg = r#""set_size" rows out of range"#;
                    error!("Invalid Terminado message: {}", msg);
                    ParseError::new(msg)
                })?;

                let cols: u16 = u16::try_from(list[2].as_u64().ok_or_else(|| {
                    let msg = r#""set_size" element 2 needs to be an integer"#;
                    error!("Invalid Terminado message: {}", msg);
                    ParseError::new(msg)
                })?)
                .map_err(|_| {
                    let msg = r#""set_size" cols out of range"#;
                    error!("Invalid Terminado message: {}", msg);
                    ParseError::new(msg)
                })?;

                Ok(TerminadoMessage::Resize { rows, cols })
            }
            v => {
                let msg = format!("Unknown type {:?}", v);
                error!("Invalid Terminado message: {}", msg);
                Err(ParseError::new(msg))
            }
        }
    }
}

impl Serialize for TerminadoMessage {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        match self {
            TerminadoMessage::Resize { rows, cols } => {
                let mut seq = serializer.serialize_seq(Some(3))?;
                seq.serialize_element("set_size")?;
                seq.serialize_element(rows)?;
                seq.serialize_element(cols)?;
                seq.end()
            }
            TerminadoMessage::Stdin(stdin) => {
                let mut seq = serializer.serialize_seq(Some(2))?;
                seq.serialize_element("stdin")?;
                seq.serialize_element(&String::from_utf8_lossy(stdin.0.as_ref()))?;
                seq.end()
            }
            TerminadoMessage::Stdout(stdin) => {
                let mut seq = serializer.serialize_seq(Some(2))?;
                seq.serialize_element("stdout")?;
                seq.serialize_element(&String::from_utf8_lossy(stdin.0.as_ref()))?;
                seq.end()
            }
        }
    }
}

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

    #[test]
    fn test_serialize_resize() {
        let res = TerminadoMessage::Resize { rows: 25, cols: 80 };

        assert_eq!(
            serde_json::to_string(&res).unwrap(),
            r#"["set_size",25,80]"#
        );
    }

    #[test]
    fn test_serialize_stdin() {
        let res = TerminadoMessage::Stdin(IO::from("hello world"));

        assert_eq!(
            serde_json::to_string(&res).unwrap(),
            r#"["stdin","hello world"]"#
        );
    }

    #[test]
    fn test_serialize_stdout() {
        let res = TerminadoMessage::Stdout(IO::from("hello world"));

        assert_eq!(
            serde_json::to_string(&res).unwrap(),
            r#"["stdout","hello world"]"#
        );
    }

    #[test]
    fn test_deserialize_resize() {
        let json = r#"["set_size", 25, 80]"#;

        let value = TerminadoMessage::from_json(json).expect("Could not parse TerminadoMessage");

        assert_eq!(value, TerminadoMessage::Resize { rows: 25, cols: 80 });
    }

    #[test]
    fn test_deserialize_stdin() {
        let json = r#"["stdin", "hello world"]"#;

        let value = TerminadoMessage::from_json(json).expect("Could not parse TerminadoMessage");

        assert_eq!(value, TerminadoMessage::Stdin("hello world".into()));
    }

    #[test]
    fn test_deserialize_stdout() {
        let json = r#"["stdout", "hello world"]"#;

        let value = TerminadoMessage::from_json(json).expect("Could not parse TerminadoMessage");

        assert_eq!(value, TerminadoMessage::Stdout("hello world".into()));
    }
}
