//! Contains type definitions for Solana Instructions and account metadata.
//!
//! These types are based on those from the [`solana_instruction`] crate, but using more efficient
//! serialization of binary data by default.
//!
//! Conversions between these types and the solana SDK types can be enabled via the
//! `solana-instruction-convert` feature.
//!
//! [`solana_instruction`]: https://crates.io/crates/solana-instruction

use super::pubkey::Pubkey;

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

/// Contains metadata for a single account read or written to by a program during instruction
/// execution.
///
/// See [`solana_instruction::AccountMeta`] for more information.
#[repr(C)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Default, PartialEq, Eq, Clone)]
pub struct AccountMeta {
    /// The account's public key.
    ///
    /// When serialized, encoded to a field named "p" for space efficiency.
    #[cfg_attr(feature = "serde", serde(rename = "p"))]
    pub pubkey: Pubkey,
    /// True if an instruction requires a transaction signature matching this account's `pubkey`.
    ///
    /// When serialized, encoded to a field named "s" for space efficiency.
    #[cfg_attr(feature = "serde", serde(rename = "s"))]
    pub is_signer: bool,
    /// True if the account data or metadata may be mutated during program execution.
    ///
    /// When serialized, encoded to a field named "w" for space efficiency.
    #[cfg_attr(feature = "serde", serde(rename = "w"))]
    pub is_writable: bool,
}

impl AccountMeta {
    /// Constructs metatdata for a writable account.
    pub fn new(pubkey: Pubkey, is_signer: bool) -> Self {
        Self {
            pubkey,
            is_signer,
            is_writable: true,
        }
    }

    /// Constructs metadata for a readable account.
    pub fn new_readonly(pubkey: Pubkey, is_signer: bool) -> Self {
        Self {
            pubkey,
            is_signer,
            is_writable: false,
        }
    }
}

/// Contains account lookup table metadata
///
/// See [`solana_instruction::AccountMeta`] for more information.
#[repr(C)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Default, PartialEq, Eq, Clone)]
pub struct AddressLookupTableAccount {
    /// The address lookup tables public key.
    ///
    /// When serialized, encoded to a field named "p" for space efficiency.
    #[cfg_attr(feature = "serde", serde(rename = "p"))]
    pub key: Pubkey,
    /// List of addresses which the address lookup table has coverage for.
    ///
    /// When serialized, encoded to a field named "a" for space efficiency.
    #[cfg_attr(feature = "serde", serde(rename = "a"))]
    pub addresses: Vec<Pubkey>,
}

#[cfg(feature = "solana-alt-account-convert-v2")]
impl From<solana_message_v2::AddressLookupTableAccount> for AddressLookupTableAccount {
    #[inline]
    fn from(value: solana_message_v2::AddressLookupTableAccount) -> Self {
        Self {
            key: value.key.into(),
            addresses: value.addresses.into_iter().map(|key| key.into()).collect(),
        }
    }
}

#[cfg(feature = "solana-alt-account-convert-v2")]
impl From<AddressLookupTableAccount> for solana_message_v2::AddressLookupTableAccount {
    #[inline]
    fn from(value: AddressLookupTableAccount) -> Self {
        Self {
            key: value.key.into(),
            addresses: value.addresses.into_iter().map(|key| key.into()).collect(),
        }
    }
}

#[cfg(feature = "solana-alt-account-convert-v3")]
impl From<solana_message_v3::AddressLookupTableAccount> for AddressLookupTableAccount {
    #[inline]
    fn from(value: solana_message_v3::AddressLookupTableAccount) -> Self {
        Self {
            key: value.key.into(),
            addresses: value.addresses.into_iter().map(|key| key.into()).collect(),
        }
    }
}

#[cfg(feature = "solana-alt-account-convert-v3")]
impl From<AddressLookupTableAccount> for solana_message_v3::AddressLookupTableAccount {
    #[inline]
    fn from(value: AddressLookupTableAccount) -> Self {
        Self {
            key: value.key.into(),
            addresses: value.addresses.into_iter().map(|key| key.into()).collect(),
        }
    }
}

#[cfg(feature = "solana-alt-account-convert-v4")]
impl From<solana_message_v4::AddressLookupTableAccount> for AddressLookupTableAccount {
    #[inline]
    fn from(value: solana_message_v4::AddressLookupTableAccount) -> Self {
        Self {
            key: value.key.into(),
            addresses: value.addresses.into_iter().map(|key| key.into()).collect(),
        }
    }
}

#[cfg(feature = "solana-alt-account-convert-v4")]
impl From<AddressLookupTableAccount> for solana_message_v4::AddressLookupTableAccount {
    #[inline]
    fn from(value: AddressLookupTableAccount) -> Self {
        Self {
            key: value.key.into(),
            addresses: value.addresses.into_iter().map(|key| key.into()).collect(),
        }
    }
}

#[cfg(feature = "solana-instruction-convert-v2")]
impl From<solana_instruction_v2::AccountMeta> for AccountMeta {
    #[inline]
    fn from(value: solana_instruction_v2::AccountMeta) -> Self {
        Self {
            pubkey: value.pubkey.into(),
            is_signer: value.is_signer,
            is_writable: value.is_writable,
        }
    }
}

#[cfg(feature = "solana-instruction-convert-v2")]
impl From<AccountMeta> for solana_instruction_v2::AccountMeta {
    #[inline]
    fn from(value: AccountMeta) -> Self {
        Self {
            pubkey: value.pubkey.into(),
            is_signer: value.is_signer,
            is_writable: value.is_writable,
        }
    }
}

#[cfg(feature = "solana-instruction-convert-v3")]
impl From<solana_instruction_v3::AccountMeta> for AccountMeta {
    #[inline]
    fn from(value: solana_instruction_v3::AccountMeta) -> Self {
        Self {
            pubkey: value.pubkey.into(),
            is_signer: value.is_signer,
            is_writable: value.is_writable,
        }
    }
}

#[cfg(feature = "solana-instruction-convert-v3")]
impl From<AccountMeta> for solana_instruction_v3::AccountMeta {
    #[inline]
    fn from(value: AccountMeta) -> Self {
        Self {
            pubkey: value.pubkey.into(),
            is_signer: value.is_signer,
            is_writable: value.is_writable,
        }
    }
}

/// A directive for a single invocation of a Solana program.
///
/// See [`solana_instruction::Instruction`] for more information.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Instruction {
    /// Pubkey of the program that executes this transaction.
    ///
    /// When serialized, encoded to a field named "p" for space efficiency.
    #[cfg_attr(feature = "serde", serde(rename = "p"))]
    pub program_id: Pubkey,
    /// Metadata describing accounts that should be passed to the program.
    ///
    /// When serialized, encoded to a field named "a" for space efficiency.
    #[cfg_attr(feature = "serde", serde(rename = "a"))]
    pub accounts: Vec<AccountMeta>,
    /// Opaque data passed to the program for its own interpretation.
    ///
    /// When serialized, encoded to a field named "d" for space efficiency.
    #[cfg_attr(feature = "serde", serde(with = "data_bytes", rename = "d"))]
    pub data: Vec<u8>,
}

#[cfg(feature = "serde")]
mod data_bytes {
    use serde::Serialize;

    #[cfg(feature = "serde-instruction-human-base64")]
    use base64::{
        alphabet,
        engine::{
            general_purpose::{GeneralPurpose, GeneralPurposeConfig},
            DecodePaddingMode,
        },
        Engine,
    };
    use serde::{de::Visitor, Deserializer, Serializer};
    #[cfg(feature = "serde-instruction-human-base64")]
    const CONFIG: GeneralPurposeConfig = GeneralPurposeConfig::new()
        .with_encode_padding(false)
        .with_decode_padding_mode(DecodePaddingMode::Indifferent);
    #[cfg(feature = "serde-instruction-human-base64")]
    const ENGINE: GeneralPurpose = GeneralPurpose::new(&alphabet::STANDARD, CONFIG);

    struct BinaryVisitor;

    impl<'de> Visitor<'de> for BinaryVisitor {
        type Value = Vec<u8>;
        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
            formatter.write_str("binary data")
        }

        #[cfg(feature = "serde-instruction-human-base64")]
        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
        where
            E: serde::de::Error,
        {
            ENGINE.decode(v).map_err(E::custom)
        }

        fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
        where
            E: serde::de::Error,
        {
            Ok(Vec::from(v))
        }

        fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
        where
            E: serde::de::Error,
        {
            Ok(v)
        }

        fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
        where
            A: serde::de::SeqAccess<'de>,
        {
            let mut data = Vec::with_capacity(seq.size_hint().unwrap_or(0));
            while let Some(elem) = seq.next_element()? {
                data.push(elem);
            }
            Ok(data)
        }
    }

    pub fn serialize<S, T>(value: T, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
        T: AsRef<[u8]>,
    {
        #[cfg(feature = "serde-instruction-human-base64")]
        if serializer.is_human_readable() {
            let b64_str = ENGINE.encode(value);
            return b64_str.serialize(serializer);
        }

        serde_bytes::serialize(value.as_ref(), serializer)
    }

    /// Deserializes the value from binary, array of u8s, or a Base64 string, with or without padding.
    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
    where
        D: Deserializer<'de>,
    {
        deserializer.deserialize_any(BinaryVisitor)
    }
}

#[cfg(feature = "solana-instruction-convert-v2")]
impl From<solana_instruction_v2::Instruction> for Instruction {
    fn from(value: solana_instruction_v2::Instruction) -> Self {
        Self {
            program_id: value.program_id.into(),
            accounts: value.accounts.into_iter().map(Into::into).collect(),
            data: value.data,
        }
    }
}

#[cfg(feature = "solana-instruction-convert-v2")]
impl From<Instruction> for solana_instruction_v2::Instruction {
    fn from(value: Instruction) -> Self {
        Self {
            program_id: value.program_id.into(),
            accounts: value.accounts.into_iter().map(Into::into).collect(),
            data: value.data,
        }
    }
}

#[cfg(feature = "solana-instruction-convert-v3")]
impl From<solana_instruction_v3::Instruction> for Instruction {
    fn from(value: solana_instruction_v3::Instruction) -> Self {
        Self {
            program_id: value.program_id.into(),
            accounts: value.accounts.into_iter().map(Into::into).collect(),
            data: value.data,
        }
    }
}

#[cfg(feature = "solana-instruction-convert-v3")]
impl From<Instruction> for solana_instruction_v3::Instruction {
    fn from(value: Instruction) -> Self {
        Self {
            program_id: value.program_id.into(),
            accounts: value.accounts.into_iter().map(Into::into).collect(),
            data: value.data,
        }
    }
}

#[cfg(test)]
mod test {
    use super::AccountMeta;
    use crate::common::pubkey::{Pubkey, PUBKEY_BYTES};
    use rmpv::Value;

    const SOL_ADDR_BYTES: [u8; PUBKEY_BYTES] = [
        6, 155, 136, 87, 254, 171, 129, 132, 251, 104, 127, 99, 70, 24, 192, 53, 218, 196, 57, 220,
        26, 235, 59, 85, 152, 160, 240, 0, 0, 0, 0, 0,
    ];
    const SOL_PUBKEY: Pubkey = Pubkey::new_from_array(SOL_ADDR_BYTES);
    const SOL_ADDR_BASE58: &str = "So11111111111111111111111111111111111111111";

    #[cfg(feature = "serde")]
    #[test]
    fn test_accountmeta_serialize_messagepack_unnamed() {
        // Should encode to a fixarray with length 3 (0x93) with the elements:
        // - bin8 with 32 bytes of data (0xc4 0x20 <data>)
        // - bool false (0xc2)
        // - bool true (0xc3)
        let account = AccountMeta {
            pubkey: SOL_PUBKEY,
            is_signer: false,
            is_writable: true,
        };

        let encoded = rmp_serde::encode::to_vec(&account).expect("should encode via messagepack");
        assert_eq!(encoded.len(), 37);
        assert_eq!(encoded[0], 0x93_u8); // fixarray with length 3
        assert_eq!(encoded[1..3], [0xc4_u8, 32_u8]); // bin8 with length 32
        assert_eq!(encoded[3..35], SOL_ADDR_BYTES); // pubkey data
        assert_eq!(encoded[35], 0xc2_u8); // bool false
        assert_eq!(encoded[36], 0xc3_u8); // bool true
    }

    #[cfg(feature = "serde")]
    #[test]
    fn test_accountmeta_deserialize_messagepack_unnamed() {
        let expected = AccountMeta {
            pubkey: SOL_PUBKEY,
            is_signer: false,
            is_writable: true,
        };
        let encoded: [u8; 37] = [
            0x93, // fixarray len=3
            0xc4, 32, // bin8 len=32
            // 32 bytes of data
            6, 155, 136, 87, 254, 171, 129, 132, 251, 104, 127, 99, 70, 24, 192, 53, 218, 196, 57,
            220, 26, 235, 59, 85, 152, 160, 240, 0, 0, 0, 0, 0, 0xc2, // bool false
            0xc3, // bool true
        ];

        let decoded = rmp_serde::decode::from_slice::<AccountMeta>(&encoded)
            .expect("should decode via messagepack");
        assert_eq!(decoded, expected);
    }

    // Test to see if decoding will ignore extra fields while decoding if new optional fields are
    // added in the future.
    // The unnamed encoding fails due to requiring the lengths to match exactly
    #[cfg(feature = "serde")]
    #[test]
    fn test_accountmeta_deserialize_messagepack_extra_field_unnamed() {
        let encoded: [u8; 38] = [
            0x94, // fixarray len=4
            0xc4, 32, // bin8 len=32
            // 32 bytes of data
            6, 155, 136, 87, 254, 171, 129, 132, 251, 104, 127, 99, 70, 24, 192, 53, 218, 196, 57,
            220, 26, 235, 59, 85, 152, 160, 240, 0, 0, 0, 0, 0, 0xc2, // bool false
            0xc3, // bool true
            0xc0, // nil
        ];

        let decoded = rmp_serde::decode::from_slice::<AccountMeta>(&encoded)
            .expect_err("should fail to decode");
        match decoded {
            rmp_serde::decode::Error::LengthMismatch(_) => (),
            other => panic!("unexpected error: {}", other),
        }
    }

    #[cfg(feature = "serde")]
    #[test]
    fn test_accountmeta_serialize_messagepack_named() {
        // Should encode to a fixmap with length 3 (0x83) with the elements:
        // - fixstr len=1 (0xa1) followed by "p"
        // - bin8 with 32 bytes of data (0xc4 0x20 <data>)
        // - fixstr len=1 (0xa1) data "s"
        // - bool false (0xc2)
        // - fixstr len=1 (0xa1) data "w"
        // - bool true (0xc3)
        //
        // total len = 43
        let account = AccountMeta {
            pubkey: SOL_PUBKEY,
            is_signer: false,
            is_writable: true,
        };

        let encoded =
            rmp_serde::encode::to_vec_named(&account).expect("should encode via messagepack");
        assert_eq!(encoded.len(), 43);
        assert_eq!(encoded[0], 0x83_u8); // fixmap with length 3
        assert_eq!(encoded[1..3], [0xa1_u8, 0x70_u8]); // fixstr len=1, data="p"
        assert_eq!(encoded[3..5], [0xc4_u8, 32_u8]); // bin8 with length 32
        assert_eq!(encoded[5..37], SOL_ADDR_BYTES); // pubkey data
        assert_eq!(encoded[37..39], [0xa1_u8, 0x73_u8]); // fixstr len=1, data="s"
        assert_eq!(encoded[39], 0xc2_u8); // bool false
        assert_eq!(encoded[40..42], [0xa1_u8, 0x77_u8]); // fixstr len=1, data="w"
        assert_eq!(encoded[42], 0xc3_u8); // bool true
    }

    #[cfg(feature = "serde")]
    #[test]
    fn test_accountmeta_deserialize_messagepack_named() {
        let expected = AccountMeta {
            pubkey: SOL_PUBKEY,
            is_signer: false,
            is_writable: true,
        };
        let encoded: [u8; 43] = [
            0x83, // fixmap len=3
            0xa1, 0x70, // fixstr "p"
            0xc4, 32, // bin8 len=32
            6, 155, 136, 87, 254, 171, 129, 132, 251, 104, 127, 99, 70, 24, 192, 53, 218, 196, 57,
            220, 26, 235, 59, 85, 152, 160, 240, 0, 0, 0, 0, 0, // 32 bytes of data
            0xa1, 0x73, // fixstr "s"
            0xc2, // bool false
            0xa1, 0x77, // fixstr "w"
            0xc3, // bool true
        ];

        let decoded = rmp_serde::decode::from_slice::<AccountMeta>(&encoded)
            .expect("should decode via messagepack");
        assert_eq!(decoded, expected);
    }

    #[cfg(feature = "serde")]
    #[test]
    fn test_accountmeta_deserialize_messagepack_extra_field_named() {
        let expected = AccountMeta {
            pubkey: SOL_PUBKEY,
            is_signer: false,
            is_writable: true,
        };
        let encoded: [u8; 46] = [
            0x84, // fixmap len=4
            0xa1, 0x70, // fixstr "p"
            0xc4, 32, // bin8 len=32
            6, 155, 136, 87, 254, 171, 129, 132, 251, 104, 127, 99, 70, 24, 192, 53, 218, 196, 57,
            220, 26, 235, 59, 85, 152, 160, 240, 0, 0, 0, 0, 0, // 32 bytes of data
            0xa1, 0x73, // fixstr "s"
            0xc2, // bool false
            0xa1, 0x77, // fixstr "w"
            0xc3, // bool true
            0xa1, 0x65, // fixstr "e" for extra
            0xc0, // nil
        ];

        let decoded = rmp_serde::decode::from_slice::<AccountMeta>(&encoded)
            .expect("should decode via messagepack");
        assert_eq!(decoded, expected);
    }

    #[cfg(feature = "serde")]
    #[test]
    fn test_instruction_messagepack_roundtrip() {
        let instruction = super::Instruction {
            program_id: SOL_PUBKEY,
            accounts: Vec::new(),
            data: Vec::from([104, 101, 108, 108, 111]), // hello
        };
        let expected_msgpack = Value::Map(vec![
            (
                Value::String("p".into()),
                Value::Binary(Vec::from(SOL_ADDR_BYTES)),
            ),
            (Value::String("a".into()), Value::Array(vec![])),
            (
                Value::String("d".into()),
                Value::Binary(vec![104, 101, 108, 108, 111]),
            ),
        ]);

        let encoded_instruction =
            rmp_serde::to_vec_named(&instruction).expect("should encode to messagepack");
        let encoded_expected =
            rmp_serde::to_vec_named(&expected_msgpack).expect("should encode to messagepack");
        assert_eq!(encoded_instruction, encoded_expected);

        let decoded: super::Instruction =
            rmp_serde::from_slice(&encoded_expected).expect("should decode from messagepack");
        assert_eq!(decoded, instruction);
    }

    #[cfg(feature = "serde-instruction-human-base64")]
    #[test]
    fn test_instruction_json_human() {
        let instruction = super::Instruction {
            program_id: SOL_PUBKEY,
            accounts: Vec::new(),
            data: Vec::from([104, 101, 108, 108, 111]), // hello
        };
        let expected_json = serde_json::json!({
            "p": SOL_ADDR_BASE58,
            "a": [],
            "d": "aGVsbG8",
        });
        let encoded_value = serde_json::to_value(&instruction).expect("should encode to JSON");
        assert_eq!(encoded_value, expected_json);

        let decoded: super::Instruction =
            serde_json::from_value(expected_json).expect("should decoded from JSON");
        assert_eq!(decoded, instruction);
    }

    #[cfg(feature = "serde")]
    #[test]
    fn test_instruction_decode_json_nonhuman() {
        use super::Instruction;
        let instruction = Instruction {
            program_id: SOL_PUBKEY,
            accounts: Vec::new(),
            data: Vec::from([104, 101, 108, 108, 111]), // hello
        };
        let json_data = serde_json::json!({
            "p": SOL_ADDR_BYTES,
            "a": [],
            "d": [104, 101, 108, 108, 111]
        });

        let decoded: Instruction =
            serde_json::from_value(json_data).expect("should decode from JSON");
        assert_eq!(decoded, instruction);
    }
}
