use crate::error::CoreError;
use crate::error::CoreResult;
use crate::frame::Compression;
use crate::frame::Encoding;
use crate::frame::MessageType;
use crate::frame::serialize_body_as_message;
use crate::protocol::TypeCode;
use crate::value::Atom;
use crate::value::List;
use crate::value::Table;
use crate::value::Value;
use crate::value::Vector;
use crate::value::VectorData;

fn push_i16(buffer: &mut Vec<u8>, value: i16) {
    buffer.extend_from_slice(&value.to_le_bytes());
}

fn push_i32(buffer: &mut Vec<u8>, value: i32) {
    buffer.extend_from_slice(&value.to_le_bytes());
}

fn push_i64(buffer: &mut Vec<u8>, value: i64) {
    buffer.extend_from_slice(&value.to_le_bytes());
}

fn push_f32(buffer: &mut Vec<u8>, value: f32) {
    buffer.extend_from_slice(&value.to_le_bytes());
}

fn push_f64(buffer: &mut Vec<u8>, value: f64) {
    buffer.extend_from_slice(&value.to_le_bytes());
}

fn push_length(buffer: &mut Vec<u8>, value: usize) {
    let value = i32::try_from(value).expect("supported q vectors fit in 32-bit length");
    push_i32(buffer, value);
}

fn encode_atom(atom: &Atom, buffer: &mut Vec<u8>) {
    match atom {
        Atom::Boolean(value) => {
            buffer.push(TypeCode::BooleanAtom as i8 as u8);
            buffer.push(u8::from(*value));
        }
        Atom::Guid(value) => {
            buffer.push(TypeCode::GuidAtom as i8 as u8);
            buffer.extend_from_slice(value);
        }
        Atom::Byte(value) => {
            buffer.push(TypeCode::ByteAtom as i8 as u8);
            buffer.push(*value);
        }
        Atom::Short(value) => {
            buffer.push(TypeCode::ShortAtom as i8 as u8);
            push_i16(buffer, *value);
        }
        Atom::Int(value) => {
            buffer.push(TypeCode::IntAtom as i8 as u8);
            push_i32(buffer, *value);
        }
        Atom::Long(value) => {
            buffer.push(TypeCode::LongAtom as i8 as u8);
            push_i64(buffer, *value);
        }
        Atom::Real(value) => {
            buffer.push(TypeCode::RealAtom as i8 as u8);
            push_f32(buffer, *value);
        }
        Atom::Float(value) => {
            buffer.push(TypeCode::FloatAtom as i8 as u8);
            push_f64(buffer, *value);
        }
        Atom::Char(value) => {
            buffer.push(TypeCode::CharAtom as i8 as u8);
            buffer.push(*value);
        }
        Atom::Symbol(value) => {
            buffer.push(TypeCode::SymbolAtom as i8 as u8);
            buffer.extend_from_slice(value);
            buffer.push(0);
        }
        Atom::Timestamp(value) => {
            buffer.push(TypeCode::TimestampAtom as i8 as u8);
            push_i64(buffer, *value);
        }
        Atom::Month(value) => {
            buffer.push(TypeCode::MonthAtom as i8 as u8);
            push_i32(buffer, *value);
        }
        Atom::Date(value) => {
            buffer.push(TypeCode::DateAtom as i8 as u8);
            push_i32(buffer, *value);
        }
        Atom::Datetime(value) => {
            buffer.push(TypeCode::DatetimeAtom as i8 as u8);
            push_f64(buffer, *value);
        }
        Atom::Timespan(value) => {
            buffer.push(TypeCode::TimespanAtom as i8 as u8);
            push_i64(buffer, *value);
        }
        Atom::Minute(value) => {
            buffer.push(TypeCode::MinuteAtom as i8 as u8);
            push_i32(buffer, *value);
        }
        Atom::Second(value) => {
            buffer.push(TypeCode::SecondAtom as i8 as u8);
            push_i32(buffer, *value);
        }
        Atom::Time(value) => {
            buffer.push(TypeCode::TimeAtom as i8 as u8);
            push_i32(buffer, *value);
        }
    }
}

fn encode_vector(vector: &Vector, buffer: &mut Vec<u8>) {
    let attribute = i8::from(vector.attribute()) as u8;
    let data = vector.data();
    let len = data.len();

    // All non-Symbol variants store raw Bytes; pick the type code, write header + raw bytes.
    let (type_code, raw) = match data {
        VectorData::Boolean(b) => (TypeCode::BooleanVector, Some(b)),
        VectorData::Guid(b) => (TypeCode::GuidVector, Some(b)),
        VectorData::Byte(b) => (TypeCode::ByteVector, Some(b)),
        VectorData::Short(b) => (TypeCode::ShortVector, Some(b)),
        VectorData::Int(b) => (TypeCode::IntVector, Some(b)),
        VectorData::Long(b) => (TypeCode::LongVector, Some(b)),
        VectorData::Real(b) => (TypeCode::RealVector, Some(b)),
        VectorData::Float(b) => (TypeCode::FloatVector, Some(b)),
        VectorData::Char(b) => (TypeCode::CharVector, Some(b)),
        VectorData::Timestamp(b) => (TypeCode::TimestampVector, Some(b)),
        VectorData::Month(b) => (TypeCode::MonthVector, Some(b)),
        VectorData::Date(b) => (TypeCode::DateVector, Some(b)),
        VectorData::Datetime(b) => (TypeCode::DatetimeVector, Some(b)),
        VectorData::Timespan(b) => (TypeCode::TimespanVector, Some(b)),
        VectorData::Minute(b) => (TypeCode::MinuteVector, Some(b)),
        VectorData::Second(b) => (TypeCode::SecondVector, Some(b)),
        VectorData::Time(b) => (TypeCode::TimeVector, Some(b)),
        VectorData::Symbol(_) => (TypeCode::SymbolVector, None),
    };

    buffer.push(type_code as i8 as u8);
    buffer.push(attribute);
    push_length(buffer, len);

    if let Some(raw) = raw {
        buffer.extend_from_slice(raw);
    } else if let VectorData::Symbol(values) = data {
        for value in values {
            buffer.extend_from_slice(value);
            buffer.push(0);
        }
    }
}

fn encode_table(table: &Table, buffer: &mut Vec<u8>) -> CoreResult<()> {
    buffer.push(TypeCode::Table as i8 as u8);
    buffer.push(i8::from(table.attribute()) as u8);

    buffer.push(TypeCode::Dictionary as i8 as u8);
    buffer.push(TypeCode::SymbolVector as i8 as u8);
    buffer.push(0);
    push_length(buffer, table.column_names().len());
    for name in table.column_names() {
        buffer.extend_from_slice(name);
        buffer.push(0);
    }

    buffer.push(TypeCode::GeneralList as i8 as u8);
    buffer.push(0);
    push_length(buffer, table.columns().len());
    for column in table.columns() {
        encode_value_into(column, buffer)?;
    }

    Ok(())
}

fn encode_list(list: &List, buffer: &mut Vec<u8>) -> CoreResult<()> {
    buffer.push(TypeCode::GeneralList as i8 as u8);
    buffer.push(i8::from(list.attribute()) as u8);
    push_length(buffer, list.len());
    for value in list.values() {
        encode_value_into(value, buffer)?;
    }

    Ok(())
}

fn encode_value_into(value: &Value, buffer: &mut Vec<u8>) -> CoreResult<()> {
    match value {
        Value::Atom(atom) => encode_atom(atom, buffer),
        Value::Vector(vector) => encode_vector(vector, buffer),
        Value::List(list) => encode_list(list, buffer)?,
        Value::Dictionary(dictionary) => {
            dictionary.validate()?;
            buffer.push(if dictionary.sorted() {
                TypeCode::SortedDictionary as i8 as u8
            } else {
                TypeCode::Dictionary as i8 as u8
            });
            encode_value_into(dictionary.keys(), buffer)?;
            encode_value_into(dictionary.values(), buffer)?;
        }
        Value::Table(table) => {
            table.validate()?;
            encode_table(table, buffer)?;
        }
        Value::UnaryPrimitive { opcode } => {
            buffer.push(TypeCode::UnaryPrimitive as i8 as u8);
            buffer.push(*opcode as u8);
        }
    }

    Ok(())
}

/// Encodes a supported q value as a little-endian q IPC body.
///
/// Returns `UnsupportedEndianness` for big-endian encoding.
pub fn encode_value(value: &Value, encoding: Encoding) -> CoreResult<Vec<u8>> {
    if encoding != Encoding::LittleEndian {
        return Err(CoreError::UnsupportedEndianness(encoding));
    }
    let mut buffer = Vec::new();
    encode_value_into(value, &mut buffer)?;
    Ok(buffer)
}

/// Encodes a supported q value as a full q IPC message.
///
/// Returns `UnsupportedEndianness` for big-endian encoding.
pub fn encode_message(
    value: &Value,
    encoding: Encoding,
    message_type: MessageType,
    compression: Compression,
) -> CoreResult<Vec<u8>> {
    let body = encode_value(value, encoding)?;
    serialize_body_as_message(&body, encoding, message_type, compression)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::decode::decode_value;
    use crate::protocol::Attribute;
    use crate::value::Dictionary;
    use crate::value::List;
    use crate::value::Table;

    #[test]
    fn encode_int_atom_body() {
        let value = Value::Atom(Atom::Int(42));
        let body = encode_value(&value, Encoding::LittleEndian).unwrap();

        assert_eq!(body, vec![250, 42, 0, 0, 0]);
        assert_eq!(
            decode_value(bytes::Bytes::from(body.clone()), Encoding::LittleEndian).unwrap(),
            value
        );
    }

    #[test]
    fn encode_rejects_big_endian() {
        let value = Value::Vector(Vector::new(
            Attribute::Sorted,
            VectorData::from_i32s(&[1, 2, 3]),
        ));
        assert!(matches!(
            encode_value(&value, Encoding::BigEndian),
            Err(CoreError::UnsupportedEndianness(Encoding::BigEndian))
        ));
    }

    #[test]
    fn encode_symbol_vector_body() {
        let value = Value::Vector(Vector::new(
            Attribute::None,
            VectorData::Symbol(vec![
                bytes::Bytes::from_static(b"alpha"),
                bytes::Bytes::from_static(b"beta"),
            ]),
        ));
        let body = encode_value(&value, Encoding::LittleEndian).unwrap();

        assert_eq!(
            body,
            bytes::Bytes::from_static(b"\x0b\x00\x02\0\0\0alpha\0beta\0")
        );
        assert_eq!(
            decode_value(bytes::Bytes::from(body.clone()), Encoding::LittleEndian).unwrap(),
            value
        );
    }

    #[test]
    fn encode_list_body() {
        let value = Value::List(List::new(
            Attribute::None,
            vec![
                Value::Atom(Atom::Int(42)),
                Value::Atom(Atom::Symbol(bytes::Bytes::from_static(b"ab"))),
            ],
        ));
        let body = encode_value(&value, Encoding::LittleEndian).unwrap();

        assert_eq!(
            decode_value(bytes::Bytes::from(body.clone()), Encoding::LittleEndian).unwrap(),
            value
        );
    }

    #[test]
    fn encode_dictionary_body() {
        let value = Value::Dictionary(Dictionary::new(
            false,
            Value::Vector(Vector::new(
                Attribute::None,
                VectorData::Symbol(vec![
                    bytes::Bytes::from_static(b"a"),
                    bytes::Bytes::from_static(b"b"),
                ]),
            )),
            Value::Vector(Vector::new(Attribute::None, VectorData::from_i32s(&[1, 2]))),
        ));
        let body = encode_value(&value, Encoding::LittleEndian).unwrap();

        assert_eq!(
            decode_value(bytes::Bytes::from(body.clone()), Encoding::LittleEndian).unwrap(),
            value
        );
    }

    #[test]
    fn encode_table_body() {
        let value = Value::Table(Table::new(
            Attribute::None,
            vec![
                bytes::Bytes::from_static(b"sym"),
                bytes::Bytes::from_static(b"px"),
            ],
            vec![
                Value::Vector(Vector::new(
                    Attribute::None,
                    VectorData::Symbol(vec![
                        bytes::Bytes::from_static(b"a"),
                        bytes::Bytes::from_static(b"b"),
                    ]),
                )),
                Value::Vector(Vector::new(
                    Attribute::None,
                    VectorData::from_i32s(&[10, 20]),
                )),
            ],
        ));
        let body = encode_value(&value, Encoding::LittleEndian).unwrap();

        assert_eq!(
            decode_value(bytes::Bytes::from(body.clone()), Encoding::LittleEndian).unwrap(),
            value
        );
    }

    #[test]
    fn encode_rejects_malformed_table_structure() {
        let value = Value::Table(Table::new(
            crate::protocol::Attribute::None,
            vec![
                bytes::Bytes::from_static(b"sym"),
                bytes::Bytes::from_static(b"px"),
            ],
            vec![Value::Vector(Vector::new(
                crate::protocol::Attribute::None,
                VectorData::Symbol(vec![bytes::Bytes::from_static(b"a")]),
            ))],
        ));

        let err = encode_value(&value, Encoding::LittleEndian).unwrap_err();
        assert!(matches!(err, crate::error::CoreError::InvalidStructure(_)));
    }
}
