{% extends "layout.njk" %}
{% import "macros.njk" as macros %}

{% block main %}

#[cfg(feature = "shared-data")]
use std::sync::Arc;

#[cfg(feature = "shared-data")]
use yellowstone_vixen_core::InstructionUpdateOutput;

use crate::deserialize_checked;

{{ imports }}

/// {{ programName | pascalCase }} Instructions
#[derive(Debug)]
#[cfg_attr(feature = "tracing", derive(strum_macros::Display))]
pub enum {{ programName  | pascalCase }}ProgramIx {
    {% for instruction in instructions %}
        {% if instruction.hasArgs %}
            {{ instruction.name | pascalCase }}({{ instruction.name | pascalCase }}IxAccounts, {{ instruction.name | pascalCase }}IxData),
        {% else %}
            {{ instruction.name | pascalCase }}({{ instruction.name | pascalCase }}IxAccounts),
        {% endif %}
    {% endfor %}
}

#[derive(Debug, Copy, Clone)]
pub struct InstructionParser;

impl yellowstone_vixen_core::Parser for InstructionParser {
    type Input = yellowstone_vixen_core::instruction::InstructionUpdate;

    #[cfg(not(feature = "shared-data"))]
    type Output = {{ programName  | pascalCase }}ProgramIx;

    #[cfg(feature = "shared-data")]
    type Output = InstructionUpdateOutput<{{ programName  | pascalCase }}ProgramIx>;
    
    fn id(&self) -> std::borrow::Cow<str> {
        "{{ programName  | pascalCase }}::InstructionParser".into()
    }

    fn prefilter(&self) -> yellowstone_vixen_core::Prefilter {
        yellowstone_vixen_core::Prefilter::builder()
            .transaction_accounts([ID])
            .build()
            .unwrap()
    }

    async fn parse(&self, ix_update: &yellowstone_vixen_core::instruction::InstructionUpdate) -> yellowstone_vixen_core::ParseResult<Self::Output> {
        if ix_update.program.equals_ref(ID) {
            let res = InstructionParser::parse_impl(ix_update);

            #[cfg(feature = "tracing")]
            if let Err(e) = &res {
                let ix_discriminator:[u8; {{ ixDiscLen }}] = ix_update.data[0..{{ ixDiscLen }}].try_into()?;

                tracing::info!(
                    name: "incorrectly_parsed_instruction",
                    name = "ix_update",
                    program = ID.to_string(),
                    ix = "deserialization_error",
                    discriminator = ?ix_discriminator,
                    error = ?e
                );
            }

            res
        } else {
            Err(yellowstone_vixen_core::ParseError::Filtered)
        }
    }
}

impl yellowstone_vixen_core::ProgramParser for InstructionParser {
    #[inline]
    fn program_id(&self) -> yellowstone_vixen_core::Pubkey {
        ID.to_bytes().into()
    }
}

impl InstructionParser {
    pub(crate) fn parse_impl(ix: &yellowstone_vixen_core::instruction::InstructionUpdate) -> yellowstone_vixen_core::ParseResult<<Self as yellowstone_vixen_core::Parser>::Output> {

        let accounts_len = ix.accounts.len();
        let accounts = &mut ix.accounts.iter();

        #[cfg(feature = "shared-data")]
        let shared_data = Arc::clone(&ix.shared);
        
        let ix_discriminator:[u8; {{ ixDiscLen }}] = ix.data[0..{{ ixDiscLen }}].try_into()?;
        let ix_data = &ix.data[{{ ixDiscLen }}..];
            let ix = match ix_discriminator {
                {% for instruction in instructions %}
                    {% if instruction.discriminator != null %}
                        {{ instruction.discriminator }} => {
                            {% if instruction.totalOptionalOmittedAccounts > 0 %}
                                let mut expected_accounts_len = {{ instruction.accounts | length - instruction.totalOptionalOmittedAccounts }};
                            {% else %}
                                let expected_accounts_len = {{ instruction.accounts | length }};
                            {% endif %}
                            check_min_accounts_req(accounts_len, expected_accounts_len)?;
                            let ix_accounts = {{ instruction.name | pascalCase }}IxAccounts{
                                {% for account in instruction.accounts %}
                                    {% if account.isOptional %}
                                        {% if instruction.optionalAccountStrategy == 'programId' %}
                                            {{ account.name | snakeCase }}: next_program_id_optional_account(accounts)?,
                                        {% else %}
                                            {{ account.name | snakeCase }}: next_optional_account(accounts, accounts_len, &mut expected_accounts_len)?,
                                        {% endif %}
                                    {% else %}
                                        {{ account.name | snakeCase }}: next_account(accounts)?,
                                    {% endif %}
                                {% endfor %}
                            };
                            {% if instruction.hasArgs %}
                                let de_ix_data:{{ instruction.name | pascalCase }}IxData = deserialize_checked(ix_data, &ix_discriminator)?;
                                Ok({{ programName  | pascalCase }}ProgramIx::{{ instruction.name | pascalCase }}(ix_accounts, de_ix_data))
                            {% else %}
                                Ok({{ programName  | pascalCase }}ProgramIx::{{ instruction.name | pascalCase }}(ix_accounts))
                            {% endif %}
                        },
                    {% endif %}
                {% endfor %}
                _ => Err(yellowstone_vixen_core::ParseError::from("Invalid Instruction discriminator".to_owned())),
            };

            #[cfg(feature = "tracing")]
            match &ix {
                Ok(ix) => {
                    tracing::info!(
                        name: "correctly_parsed_instruction",
                        name = "ix_update",
                        program = ID.to_string(),
                        ix = ix.to_string()
                    );
                },
                Err(e) => {
                    tracing::info!(
                        name: "incorrectly_parsed_instruction",
                        name = "ix_update",
                        program = ID.to_string(),
                        ix = "error",
                        discriminator = ?ix_discriminator,
                        error = ?e
                    );
                },
            }

        #[cfg(not(feature = "shared-data"))]
        return ix;

        #[cfg(feature = "shared-data")]
        ix.map(|ix| InstructionUpdateOutput {
            parsed_ix: ix,
            shared_data,
        })
    }

}

pub fn check_min_accounts_req(actual: usize, expected: usize) -> yellowstone_vixen_core::ParseResult<()> {
    if actual < expected {
        Err(yellowstone_vixen_core::ParseError::from(format!(
            "Too few accounts provided: expected {expected}, got {actual}"
        )))
    } else {
        Ok(())
    }
}

fn next_account<'a, T: Iterator<Item = &'a yellowstone_vixen_core::KeyBytes<32>>>(
    accounts: &mut T,
) -> Result<solana_pubkey::Pubkey, yellowstone_vixen_core::ParseError> {
    accounts
        .next()
        .ok_or(yellowstone_vixen_core::ParseError::from(
            "No more accounts to parse",
        ))
        .map(|acc| acc.0.into())
}

/// Gets the next optional account using the ommited account strategy (account is not passed at all at the instruction).
/// ### Be careful to use this function when more than one account is optional in the Instruction.
///  Only by order there is no way to which ones of the optional accounts are present.
pub fn next_optional_account<'a, T: Iterator<Item = &'a yellowstone_vixen_core::KeyBytes<32>>>(
    accounts: &mut T,
    actual_accounts_len: usize,
    expected_accounts_len: &mut usize,
) -> Result<Option<solana_pubkey::Pubkey>, yellowstone_vixen_core::ParseError> {
    if actual_accounts_len == *expected_accounts_len + 1 {
        *expected_accounts_len += 1;
        Ok(Some(next_account(accounts)?))
    } else {
        Ok(None)
    }
}

/// Gets the next optional account using the traditional Program ID strategy.
///  (If account key is the program ID, means account is not present)
pub fn next_program_id_optional_account<
    'a,
    T: Iterator<Item = &'a yellowstone_vixen_core::KeyBytes<32>>,
>(
    accounts: &mut T,
) -> Result<Option<solana_pubkey::Pubkey>, yellowstone_vixen_core::ParseError> {
    let account_key = next_account(accounts)?;
    if account_key.eq(&ID) {
        Ok(None)
    } else {
        Ok(Some(account_key))
    }
}


// #[cfg(feature = "proto")]
mod proto_parser {
    use super::{InstructionParser,{{ programName | pascalCase }}ProgramIx};
    {% if hasProtoHelpers %}
    use crate::{proto_def, proto_helpers::proto_types_parsers::IntoProto};
    {% else %}
    use crate::proto_def;
    use yellowstone_vixen_core::proto_helper_traits;
    proto_helper_traits!();
    {% endif %}
    use yellowstone_vixen_core::proto::ParseProto;
    
    {% for instruction in instructions %}
        use super::{{ instruction.name | pascalCase }}IxAccounts;
        impl IntoProto<proto_def::{{ instruction.name | pascalCase }}IxAccounts> for {{ instruction.name | pascalCase }}IxAccounts  {
            fn into_proto(self) -> proto_def::{{ instruction.name | pascalCase }}IxAccounts {
                proto_def::{{ instruction.name | pascalCase }}IxAccounts {
                    {% for account in instruction.accounts %}
                        {% if account.isOptional %}
                            {{ account.name }}: self.{{ account.name }}.map(|p| p.to_string()),
                        {% else %}
                            {{ account.name }}: self.{{ account.name }}.to_string(),
                        {% endif %}
                    {% endfor %}
                }
            }
        }   
        {% if instruction.hasArgs  %}
            use super::{{ instruction.name | pascalCase }}IxData;
            impl IntoProto<proto_def::{{ instruction.name | pascalCase }}IxData> for {{ instruction.name | pascalCase }}IxData  {
                fn into_proto(self) -> proto_def::{{ instruction.name | pascalCase }}IxData {
                    proto_def::{{ instruction.name | pascalCase }}IxData {
                        {% for field in instruction.ixArgs %}
                            {{ field.name }}: {{ field.transform }},
                        {% endfor %}
                    }
                }
            }
        {% endif %}
    {% endfor %}

    impl IntoProto<proto_def::ProgramIxs> for {{ programName | pascalCase }}ProgramIx {
        fn into_proto(self) -> proto_def::ProgramIxs {
           match self {
                {% for instruction in instructions %}
                    {% if instruction.hasArgs %}
                        {{ programName | pascalCase }}ProgramIx::{{ instruction.name | pascalCase }}(acc, data) => proto_def::ProgramIxs {
                            ix_oneof: Some(proto_def::program_ixs::IxOneof::{{ instruction.name | pascalCase }}(proto_def::{{ instruction.name | pascalCase }}Ix {
                                accounts: Some(acc.into_proto()),
                                data: Some(data.into_proto()),
                            })),
                        },
                    {% else %}
                        {{ programName | pascalCase }}ProgramIx::{{ instruction.name | pascalCase }}(acc) => proto_def::ProgramIxs {
                            ix_oneof: Some(proto_def::program_ixs::IxOneof::{{ instruction.name | pascalCase }}(proto_def::{{ instruction.name | pascalCase }}Ix {
                                accounts: Some(acc.into_proto()),
                            })),
                        },
                    {% endif %}
                {% endfor %}
            }
        }
    }
      
    impl ParseProto for InstructionParser {
        type Message = proto_def::ProgramIxs;

        fn output_into_message(value: Self::Output) -> Self::Message { 
            #[cfg(not(feature = "shared-data"))]
            return value.into_proto();

            #[cfg(feature = "shared-data")]
            value.parsed_ix.into_proto()
        }
    }
}



{% endblock %}
