//! Unsigned transaction builder

use ergo_lib::wallet;

use crate::{
    address::{Address, AddressPtr, ConstAddressPtr},
    box_selector::{BoxSelection, BoxSelectionPtr, ConstBoxSelectionPtr},
    collections::{Collection, CollectionPtr, ConstCollectionPtr},
    context_extension::ContextExtensionPtr,
    data_input::DataInput,
    ergo_box::{BoxIdPtr, BoxValue, BoxValuePtr, ConstBoxValuePtr, ErgoBoxCandidate},
    token::Token,
    transaction::{UnsignedTransaction, UnsignedTransactionPtr},
    util::{const_ptr_as_ref, mut_ptr_as_mut},
    Error,
};

/// Unsigned transaction builder
pub struct TxBuilder(
    wallet::tx_builder::TxBuilder<ergo_lib::ergotree_ir::chain::ergo_box::ErgoBox>,
);
pub type TxBuilderPtr = *mut TxBuilder;
pub type ConstTxBuilderPtr = *const TxBuilder;

/// Suggested transaction fee (semi-default value used across wallets and dApps as of Oct 2020)
pub unsafe fn tx_builder_suggested_tx_fee(value_out: *mut BoxValuePtr) -> Result<(), Error> {
    let value_out = mut_ptr_as_mut(value_out, "value_out")?;
    *value_out = Box::into_raw(Box::new(BoxValue(wallet::tx_builder::SUGGESTED_TX_FEE())));
    Ok(())
}

/// Creates new TxBuilder
/// `box_selection` - selected input boxes (via [`super::box_selector`])
/// `output_candidates` - output boxes to be "created" in this transaction,
/// `current_height` - chain height that will be used in additionally created boxes (change, miner's fee, etc.),
/// `fee_amount` - miner's fee,
/// `change_address` - change (inputs - outputs) will be sent to this address,
/// will be given to miners,
pub unsafe fn tx_builder_new(
    box_selection_ptr: ConstBoxSelectionPtr,
    output_candidates_ptr: ConstCollectionPtr<ErgoBoxCandidate>,
    current_height: u32,
    fee_amount_ptr: ConstBoxValuePtr,
    change_address_ptr: ConstAddressPtr,
    tx_builder_out: *mut TxBuilderPtr,
) -> Result<(), Error> {
    let box_selection = const_ptr_as_ref(box_selection_ptr, "box_selection_ptr")?;
    let output_candidates = const_ptr_as_ref(output_candidates_ptr, "output_candidates_ptr")?;
    let fee_amount = const_ptr_as_ref(fee_amount_ptr, "fee_amount_ptr")?;
    let change_address = const_ptr_as_ref(change_address_ptr, "change_address_ptr")?;
    let tx_builder_out = mut_ptr_as_mut(tx_builder_out, "tx_builder_out")?;
    *tx_builder_out = Box::into_raw(Box::new(TxBuilder(
        ergo_lib::wallet::tx_builder::TxBuilder::new(
            box_selection.0.clone(),
            output_candidates
                .0
                .clone()
                .into_iter()
                .map(|b| b.0)
                .collect(),
            current_height,
            fee_amount.0,
            change_address.0.clone(),
        ),
    )));
    Ok(())
}

/// Set transaction's data inputs
pub unsafe fn tx_builder_set_data_inputs(
    tx_builder_mut: TxBuilderPtr,
    data_inputs_ptr: ConstCollectionPtr<DataInput>,
) -> Result<(), Error> {
    let data_inputs = const_ptr_as_ref(data_inputs_ptr, "data_inputs_ptr")?;
    let tx_builder_mut = mut_ptr_as_mut(tx_builder_mut, "tx_builder_mut")?;
    tx_builder_mut
        .0
        .set_data_inputs(data_inputs.0.clone().into_iter().map(|d| d.0).collect());
    Ok(())
}

/// Set context extension for a given input
pub unsafe fn tx_builder_set_context_extension(
    tx_builder_mut: TxBuilderPtr,
    box_id_ptr: BoxIdPtr,
    ctx_ext_ptr: ContextExtensionPtr,
) -> Result<(), Error> {
    let box_id = const_ptr_as_ref(box_id_ptr, "box_id_ptr")?;
    let ctx_ext = const_ptr_as_ref(ctx_ext_ptr, "ctx_ext_ptr")?;
    let tx_builder_mut = mut_ptr_as_mut(tx_builder_mut, "tx_builder_mut")?;
    tx_builder_mut
        .0
        .set_context_extension(box_id.0, ctx_ext.0.clone());
    Ok(())
}

/// Permits the burn of the given token amount, i.e. allows this token amount to be omitted in the outputs
pub unsafe fn tx_builder_set_token_burn_permit(
    tx_builder_mut: TxBuilderPtr,
    target_tokens_ptr: ConstCollectionPtr<Token>,
) -> Result<(), Error> {
    let target_tokens = const_ptr_as_ref(target_tokens_ptr, "target_tokens_ptr")?;
    let tx_builder_mut = mut_ptr_as_mut(tx_builder_mut, "tx_builder_mut")?;
    tx_builder_mut.0.set_token_burn_permit(target_tokens.into());
    Ok(())
}

/// Build the unsigned transaction
pub unsafe fn tx_builder_build(
    tx_builder_ptr: ConstTxBuilderPtr,
    unsigned_transaction_out: *mut UnsignedTransactionPtr,
) -> Result<(), Error> {
    let tx_builder = const_ptr_as_ref(tx_builder_ptr, "tx_builder_ptr")?;
    let unsigned_transaction_out =
        mut_ptr_as_mut(unsigned_transaction_out, "unsigned_transaction_out")?;
    let unsigned_tx = tx_builder.0.clone().build().map(UnsignedTransaction)?;
    *unsigned_transaction_out = Box::into_raw(Box::new(unsigned_tx));
    Ok(())
}

/// Get box selection
pub unsafe fn tx_builder_box_selection(
    tx_builder_ptr: ConstTxBuilderPtr,
    box_selection_out: *mut BoxSelectionPtr,
) -> Result<(), Error> {
    let tx_builder = const_ptr_as_ref(tx_builder_ptr, "tx_builder_ptr")?;
    let box_selection_out = mut_ptr_as_mut(box_selection_out, "box_selection_out")?;
    *box_selection_out = Box::into_raw(Box::new(BoxSelection(tx_builder.0.box_selection())));
    Ok(())
}

/// Get data inputs
pub unsafe fn tx_builder_data_inputs(
    tx_builder_ptr: ConstTxBuilderPtr,
    data_inputs_out: *mut CollectionPtr<DataInput>,
) -> Result<(), Error> {
    let tx_builder = const_ptr_as_ref(tx_builder_ptr, "tx_builder_ptr")?;
    let data_inputs_out = mut_ptr_as_mut(data_inputs_out, "data_inputs_out")?;
    *data_inputs_out = Box::into_raw(Box::new(Collection(
        tx_builder
            .0
            .data_inputs()
            .into_iter()
            .map(DataInput)
            .collect(),
    )));
    Ok(())
}

/// Get outputs EXCLUDING fee and change
pub unsafe fn tx_builder_output_candidates(
    tx_builder_ptr: ConstTxBuilderPtr,
    output_candidates_out: *mut CollectionPtr<ErgoBoxCandidate>,
) -> Result<(), Error> {
    let tx_builder = const_ptr_as_ref(tx_builder_ptr, "tx_builder_ptr")?;
    let output_candidates_out = mut_ptr_as_mut(output_candidates_out, "output_candidates_out")?;
    *output_candidates_out = Box::into_raw(Box::new(Collection(
        tx_builder
            .0
            .output_candidates()
            .into_iter()
            .map(ErgoBoxCandidate)
            .collect(),
    )));
    Ok(())
}

/// Get current height
pub unsafe fn tx_builder_current_height(tx_builder_ptr: ConstTxBuilderPtr) -> Result<u32, Error> {
    let tx_builder = const_ptr_as_ref(tx_builder_ptr, "tx_builder_ptr")?;
    Ok(tx_builder.0.current_height())
}

/// Get fee amount
pub unsafe fn tx_builder_fee_amount(
    tx_builder_ptr: ConstTxBuilderPtr,
    value_out: *mut BoxValuePtr,
) -> Result<(), Error> {
    let tx_builder = const_ptr_as_ref(tx_builder_ptr, "tx_builder_ptr")?;
    let value_out = mut_ptr_as_mut(value_out, "value_out")?;
    *value_out = Box::into_raw(Box::new(BoxValue(tx_builder.0.fee_amount())));
    Ok(())
}

/// Get change address
pub unsafe fn tx_builder_change_address(
    tx_builder_ptr: ConstTxBuilderPtr,
    address_out: *mut AddressPtr,
) -> Result<(), Error> {
    let tx_builder = const_ptr_as_ref(tx_builder_ptr, "tx_builder_ptr")?;
    let address_out = mut_ptr_as_mut(address_out, "address_out")?;
    *address_out = Box::into_raw(Box::new(Address(tx_builder.0.change_address())));
    Ok(())
}
