//! Data structures that are used to provide non-argument inputs to program endpoints

use {
    crate::{
        solana_program::{account_info::AccountInfo, instruction::AccountMeta, pubkey::Pubkey},
        Accounts, Bumps, ToAccountInfos, ToAccountMetas,
    },
    std::fmt,
};

/// Provides non-argument inputs to the program.
///
/// # Example
/// ```ignore
/// pub fn set_data(ctx: Context<SetData>, age: u64, other_data: u32) -> Result<()> {
///     // Set account data like this
///     (*ctx.accounts.my_account).age = age;
///     (*ctx.accounts.my_account).other_data = other_data;
///     // or like this
///     let my_account = &mut ctx.account.my_account;
///     my_account.age = age;
///     my_account.other_data = other_data;
///     Ok(())
/// }
/// ```
pub struct Context<'info, T: Bumps> {
    /// Currently executing program id.
    pub program_id: &'info Pubkey,
    /// Deserialized accounts.
    pub accounts: &'info mut T,
    /// Remaining accounts given but not deserialized or validated.
    /// Be very careful when using this directly.
    pub remaining_accounts: &'info [AccountInfo<'info>],
    /// Bump seeds found during constraint validation. This is provided as a
    /// convenience so that handlers don't have to recalculate bump seeds or
    /// pass them in as arguments.
    /// Type is the bumps struct generated by #[derive(Accounts)]
    pub bumps: T::Bumps,
}

impl<T> fmt::Debug for Context<'_, T>
where
    T: fmt::Debug + Bumps,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Context")
            .field("program_id", &self.program_id)
            .field("accounts", &self.accounts)
            .field("remaining_accounts", &self.remaining_accounts)
            .field("bumps", &self.bumps)
            .finish()
    }
}

impl<'info, T> Context<'info, T>
where
    T: Bumps + Accounts<'info, T::Bumps>,
{
    pub fn new(
        program_id: &'info Pubkey,
        accounts: &'info mut T,
        remaining_accounts: &'info [AccountInfo<'info>],
        bumps: T::Bumps,
    ) -> Self {
        Self {
            program_id,
            accounts,
            remaining_accounts,
            bumps,
        }
    }
}

/// Context specifying non-argument inputs for cross-program-invocations.
///
/// # Example with and without PDA signature
/// ```ignore
/// // Callee Program
///
/// use anchor_lang::prelude::*;
///
/// declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
///
/// #[program]
/// pub mod callee {
///     use super::*;
///     pub fn init(ctx: Context<Init>) -> Result<()> {
///         (*ctx.accounts.data).authority = ctx.accounts.authority.key();
///         Ok(())
///     }
///
///     pub fn set_data(ctx: Context<SetData>, data: u64) -> Result<()> {
///         (*ctx.accounts.data_acc).data = data;
///         Ok(())
///     }
/// }
///
/// #[account]
/// #[derive(Default)]
/// pub struct Data {
///     data: u64,
///     authority: Pubkey,
/// }
///
/// #[derive(Accounts)]
/// pub struct Init<'info> {
///     #[account(init, payer = payer)]
///     pub data: Account<'info, Data>,
///     pub payer: Signer<'info>,
///     pub authority: UncheckedAccount<'info>,
///     pub system_program: Program<'info, System>
/// }
///
/// #[derive(Accounts)]
/// pub struct SetData<'info> {
///     #[account(mut, has_one = authority)]
///     pub data_acc: Account<'info, Data>,
///     pub authority: Signer<'info>,
/// }
///
/// // Caller Program
///
/// use anchor_lang::prelude::*;
/// use callee::{self, program::Callee};
///
/// declare_id!("Sxg7dBh5VLT8S1o6BqncZCPq9nhHHukjfVd6ohQJeAk");
///
/// #[program]
/// pub mod caller {
///     use super::*;
///     pub fn do_cpi(ctx: Context<DoCpi>, data: u64) -> Result<()> {
///         let callee_id = ctx.accounts.callee.key();
///         let callee_accounts = callee::cpi::accounts::SetData {
///             data_acc: ctx.accounts.data_acc.to_account_info(),
///             authority: ctx.accounts.callee_authority.to_account_info(),
///         };
///         let cpi_ctx = CpiContext::new(callee_id, callee_accounts);
///         callee::cpi::set_data(cpi_ctx, data)
///     }
///
///     pub fn do_cpi_with_pda_authority(ctx: Context<DoCpiWithPDAAuthority>, bump: u8, data: u64) -> Result<()> {
///         let seeds = &[&[b"example_seed", bytemuck::bytes_of(&bump)][..]];
///         let callee_id = ctx.accounts.callee.to_account_info();
///         let callee_accounts = callee::cpi::accounts::SetData {
///             data_acc: ctx.accounts.data_acc.to_account_info(),
///             authority: ctx.accounts.callee_authority.to_account_info(),
///         };
///         let cpi_ctx = CpiContext::new_with_signer(callee_id, callee_accounts, seeds);
///         callee::cpi::set_data(cpi_ctx, data)
///     }
/// }
///
/// // We can use "UncheckedAccount"s here because
/// // the callee program does the checks.
/// // We use "mut" so the autogenerated clients know
/// // that this account should be mutable.
/// #[derive(Accounts)]
/// pub struct DoCpi<'info> {
///     #[account(mut)]
///     pub data_acc: UncheckedAccount<'info>,
///     pub callee_authority: UncheckedAccount<'info>,
///     pub callee: Program<'info, Callee>,
/// }
///
/// #[derive(Accounts)]
/// pub struct DoCpiWithPDAAuthority<'info> {
///     #[account(mut)]
///     pub data_acc: UncheckedAccount<'info>,
///     pub callee_authority: UncheckedAccount<'info>,
///     pub callee: Program<'info, Callee>,
/// }
/// ```
pub struct CpiContext<'a, 'b, 'c, 'info, T>
where
    T: ToAccountMetas + ToAccountInfos<'info>,
{
    pub accounts: T,
    pub remaining_accounts: Vec<AccountInfo<'info>>,
    pub program_id: Pubkey,
    pub signer_seeds: &'a [&'b [&'c [u8]]],
}

impl<'a, 'b, 'c, 'info, T> CpiContext<'a, 'b, 'c, 'info, T>
where
    T: ToAccountMetas + ToAccountInfos<'info>,
{
    #[must_use]
    pub fn new(program_id: Pubkey, accounts: T) -> Self {
        Self {
            accounts,
            program_id,
            remaining_accounts: Vec::new(),
            signer_seeds: &[],
        }
    }

    #[must_use]
    pub fn new_with_signer(
        program_id: Pubkey,
        accounts: T,
        signer_seeds: &'a [&'b [&'c [u8]]],
    ) -> Self {
        Self {
            accounts,
            program_id,
            signer_seeds,
            remaining_accounts: Vec::new(),
        }
    }

    #[must_use]
    pub fn with_signer(mut self, signer_seeds: &'a [&'b [&'c [u8]]]) -> Self {
        self.signer_seeds = signer_seeds;
        self
    }

    #[must_use]
    pub fn with_remaining_accounts(mut self, ra: Vec<AccountInfo<'info>>) -> Self {
        self.remaining_accounts = ra;
        self
    }
}

impl<'info, T: ToAccountInfos<'info> + ToAccountMetas> ToAccountInfos<'info>
    for CpiContext<'_, '_, '_, 'info, T>
{
    fn to_account_infos(&self) -> Vec<AccountInfo<'info>> {
        let mut infos = self.accounts.to_account_infos();
        infos.extend_from_slice(&self.remaining_accounts);
        infos
    }
}

impl<'info, T: ToAccountInfos<'info> + ToAccountMetas> ToAccountMetas
    for CpiContext<'_, '_, '_, 'info, T>
{
    fn to_account_metas(&self, is_signer: Option<bool>) -> Vec<AccountMeta> {
        let mut metas = self.accounts.to_account_metas(is_signer);
        metas.append(
            &mut self
                .remaining_accounts
                .iter()
                .map(|acc| match acc.is_writable {
                    false => AccountMeta::new_readonly(*acc.key, acc.is_signer),
                    true => AccountMeta::new(*acc.key, acc.is_signer),
                })
                .collect(),
        );
        metas
    }
}
