use base64::{decode_config_buf, CharacterSet, Config};
use libflate::zlib::Decoder;
use serde_json::{json, Value};

use crate::reader::{ReplayBufReadExt, ReplayReadError, ReplayResult};
use std::io::{self, BufRead, Read, Write};

/// Turns data from a `.fafreplay` format into `.scfareplay`format.
///
/// In contrast to the `.scfareplay` format, `.fafreplay` uses zlib compression for the command
/// data. It also adds some uncompressed metadata that ties the replay into the FAF ecosystem.
///
/// # Returns
/// The faf metadata as a [`serde_json::Value`]
pub fn extract_scfa(
    reader: &mut (impl Read + BufRead),
    writer: &mut impl Write,
) -> ReplayResult<Value> {
    let mut buf = Vec::with_capacity(2048);
    // Metadata
    let metadata_size = reader.read_until(b'\n', &mut buf)?;
    let metadata: Value = serde_json::from_slice(&buf[..metadata_size])?;
    // Check metadata for replay version
    let obj = metadata.as_object().ok_or(ReplayReadError::Malformed(
        "metadata has the wrong json type, expecting 'object'",
    ))?;
    let version =
        obj.get("version")
            .unwrap_or(&json!(1))
            .as_u64()
            .ok_or(ReplayReadError::Malformed(
                "version has the wrong json type, expecting unsigned integer",
            ))?;

    match version {
        1 => extract_scfa_contents_v1(&mut buf, reader, writer)?,
        2 => zstd::stream::copy_decode(reader, writer)?,
        _ => return Err(ReplayReadError::Malformed("unsupported replay version")),
    };

    Ok(metadata)
}

/// Version 1 uses base64 and zlib
fn extract_scfa_contents_v1(
    buf: &mut Vec<u8>,
    reader: &mut impl Read,
    writer: &mut impl Write,
) -> ReplayResult<()> {
    let decoded = extract_scfa_base64(buf, reader)?;

    let deflated = &decoded[4..];
    let mut decoder = Decoder::new(deflated)?;
    io::copy(&mut decoder, writer)?;

    Ok(())
}

fn extract_scfa_base64(buf: &mut Vec<u8>, reader: &mut impl Read) -> ReplayResult<Vec<u8>> {
    // Base64 decode
    buf.resize(0, 0);
    let b64encoded_size = reader.read_to_end(buf)?;
    let mut decoded = Vec::with_capacity((b64encoded_size + 3) / 4 * 3);
    decode_config_buf(
        &buf[..b64encoded_size],
        Config::new(CharacterSet::Standard, false),
        &mut decoded,
    )?;

    if decoded.len() < 4 {
        return Err(ReplayReadError::IO(io::Error::new(
            io::ErrorKind::UnexpectedEof,
            "missing zlib header",
        )));
    }

    Ok(decoded)
}

/// Get the faf metadata from a `.fafreplay` data format.
fn fafreplay_metadata(reader: &mut (impl Read + BufRead)) -> ReplayResult<Value> {
    let metadata = reader.vec_read_until(b'\n')?;

    Ok(serde_json::from_slice(&metadata)?)
}

// ReplayReadError conversion from foreign types
impl From<serde_json::Error> for ReplayReadError {
    fn from(err: serde_json::Error) -> ReplayReadError {
        use serde_json::error::Category::*;

        match err.classify() {
            Io => ReplayReadError::IO(io::Error::new(io::ErrorKind::Other, format!("{}", err))),
            Syntax => ReplayReadError::Malformed("invalid json syntax"),
            Data => ReplayReadError::Malformed("invalid json type"),
            Eof => ReplayReadError::IO(io::Error::new(
                io::ErrorKind::UnexpectedEof,
                "incomplete json data",
            )),
        }
    }
}

impl From<base64::DecodeError> for ReplayReadError {
    fn from(_err: base64::DecodeError) -> ReplayReadError {
        ReplayReadError::Malformed("invalid base64")
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use byteorder::{BigEndian, ByteOrder};
    use std::io::ErrorKind;

    #[test]
    fn test_extract_scfa_invalid_json() {
        let mut data: &[u8] = b"{\"foo\": foobar}\n";
        let mut out = Vec::new();

        match extract_scfa(&mut data, &mut out).unwrap_err() {
            ReplayReadError::Malformed(msg) => assert_eq!(msg, "invalid json syntax"),
            err => panic!("Wrong enum variant! {:?}", err),
        }
    }

    #[test]
    fn test_extract_scfa_invalid_base64() {
        let mut data: &[u8] = b"{}\nABC";
        let mut out = Vec::new();

        match extract_scfa(&mut data, &mut out).unwrap_err() {
            ReplayReadError::Malformed(msg) => assert_eq!(msg, "invalid base64"),
            err => panic!("Wrong enum variant! {:?}", err),
        }
    }

    #[test]
    fn test_extract_scfa_missing_base64() {
        let mut data: &[u8] = b"{}\n";
        let mut out = Vec::new();

        match extract_scfa(&mut data, &mut out).unwrap_err() {
            ReplayReadError::IO(err) => assert_eq!(err.kind(), ErrorKind::UnexpectedEof),
            err => panic!("Wrong enum variant! {:?}", err),
        }
    }

    #[test]
    fn test_extract_scfa_huge_inflate_size() {
        let mut buf = [0; 4];
        BigEndian::write_u32(&mut buf, u32::MAX);

        let mut data = Vec::new();
        data.extend(b"{}\n");
        data.resize(data.len() + 8, 0);
        base64::encode_config_slice(buf, base64::STANDARD, &mut data[3..]);

        // If no sanity checking is done on the zlib data size, this may crash the process due
        // to an out of memory error.
        let mut out = Vec::new();
        match extract_scfa(&mut data.as_slice(), &mut out).unwrap_err() {
            ReplayReadError::IO(err) => assert_eq!(err.kind(), ErrorKind::UnexpectedEof),
            err => panic!("Wrong enum variant! {:?}", err),
        }
    }
}
