//! Transparent-related (Bitcoin-inherited) functionality.

use std::{collections::HashMap, fmt, iter};

use crate::{
    amount::{Amount, NonNegative},
    block,
    parameters::Network,
    primitives::zcash_primitives,
    transaction,
};

mod address;
mod keys;
mod opcodes;
mod script;
mod serialize;
mod utxo;

pub use address::Address;
pub use script::Script;
pub use serialize::{GENESIS_COINBASE_DATA, MAX_COINBASE_DATA_LEN, MAX_COINBASE_HEIGHT_DATA_LEN};
pub use utxo::{
    new_ordered_outputs, new_outputs, outputs_from_utxos, utxos_from_ordered_utxos,
    CoinbaseSpendRestriction, OrderedUtxo, Utxo,
};

#[cfg(any(test, feature = "proptest-impl"))]
pub use utxo::{
    new_ordered_outputs_with_height, new_outputs_with_height, new_transaction_ordered_outputs,
};

#[cfg(any(test, feature = "proptest-impl"))]
mod arbitrary;

#[cfg(test)]
mod tests;

#[cfg(any(test, feature = "proptest-impl"))]
use proptest_derive::Arbitrary;

/// The maturity threshold for transparent coinbase outputs.
///
/// "A transaction MUST NOT spend a transparent output of a coinbase transaction
/// from a block less than 100 blocks prior to the spend. Note that transparent
/// outputs of coinbase transactions include Founders' Reward outputs and
/// transparent Funding Stream outputs."
/// [7.1](https://zips.z.cash/protocol/nu5.pdf#txnencodingandconsensus)
//
// TODO: change type to HeightDiff
pub const MIN_TRANSPARENT_COINBASE_MATURITY: u32 = 100;

/// Extra coinbase data that identifies some coinbase transactions generated by Zebra.
/// <https://emojipedia.org/zebra/>
//
// # Note
//
// rust-analyzer will crash in some editors when moving over an actual Zebra emoji,
// so we encode it here. This is a known issue in emacs-lsp and other lsp implementations:
// - https://github.com/rust-lang/rust-analyzer/issues/9121
// - https://github.com/emacs-lsp/lsp-mode/issues/2080
// - https://github.com/rust-lang/rust-analyzer/issues/13709
pub const EXTRA_ZEBRA_COINBASE_DATA: &str = "z\u{1F993}";

/// Arbitrary data inserted by miners into a coinbase transaction.
//
// TODO: rename to ExtraCoinbaseData, because height is also part of the coinbase data?
#[derive(Clone, Eq, PartialEq)]
#[cfg_attr(
    any(test, feature = "proptest-impl", feature = "elasticsearch"),
    derive(Serialize)
)]
pub struct CoinbaseData(
    /// Invariant: this vec, together with the coinbase height, must be less than
    /// 100 bytes. We enforce this by only constructing CoinbaseData fields by
    /// parsing blocks with 100-byte data fields, and checking newly created
    /// CoinbaseData lengths in the transaction builder.
    pub(super) Vec<u8>,
);

#[cfg(any(test, feature = "proptest-impl"))]
impl CoinbaseData {
    /// Create a new `CoinbaseData` containing `data`.
    ///
    /// Only for use in tests.
    pub fn new(data: Vec<u8>) -> CoinbaseData {
        CoinbaseData(data)
    }
}

impl AsRef<[u8]> for CoinbaseData {
    fn as_ref(&self) -> &[u8] {
        self.0.as_ref()
    }
}

impl std::fmt::Debug for CoinbaseData {
    #[allow(clippy::unwrap_in_result)]
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let escaped = String::from_utf8(
            self.0
                .iter()
                .cloned()
                .flat_map(std::ascii::escape_default)
                .collect(),
        )
        .expect("ascii::escape_default produces utf8");
        f.debug_tuple("CoinbaseData").field(&escaped).finish()
    }
}

/// OutPoint
///
/// A particular transaction output reference.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
#[cfg_attr(
    any(test, feature = "proptest-impl", feature = "elasticsearch"),
    derive(Serialize)
)]
pub struct OutPoint {
    /// References the transaction that contains the UTXO being spent.
    ///
    /// # Correctness
    ///
    /// Consensus-critical serialization uses
    /// [`ZcashSerialize`](crate::serialization::ZcashSerialize).
    /// [`serde`]-based hex serialization must only be used for testing.
    #[cfg_attr(any(test, feature = "proptest-impl"), serde(with = "hex"))]
    pub hash: transaction::Hash,

    /// Identifies which UTXO from that transaction is referenced; the
    /// first output is 0, etc.
    pub index: u32,
}

impl OutPoint {
    /// Returns a new [`OutPoint`] from an in-memory output `index`.
    ///
    /// # Panics
    ///
    /// If `index` doesn't fit in a [`u32`].
    pub fn from_usize(hash: transaction::Hash, index: usize) -> OutPoint {
        OutPoint {
            hash,
            index: index
                .try_into()
                .expect("valid in-memory output indexes fit in a u32"),
        }
    }
}

/// A transparent input to a transaction.
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(
    any(test, feature = "proptest-impl", feature = "elasticsearch"),
    derive(Serialize)
)]
pub enum Input {
    /// A reference to an output of a previous transaction.
    PrevOut {
        /// The previous output transaction reference.
        outpoint: OutPoint,
        /// The script that authorizes spending `outpoint`.
        unlock_script: Script,
        /// The sequence number for the output.
        sequence: u32,
    },
    /// New coins created by the block reward.
    Coinbase {
        /// The height of this block.
        height: block::Height,
        /// Free data inserted by miners after the block height.
        data: CoinbaseData,
        /// The sequence number for the output.
        sequence: u32,
    },
}

impl fmt::Display for Input {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Input::PrevOut {
                outpoint,
                unlock_script,
                ..
            } => {
                let mut fmter = f.debug_struct("transparent::Input::PrevOut");

                fmter.field("unlock_script_len", &unlock_script.as_raw_bytes().len());
                fmter.field("outpoint", outpoint);

                fmter.finish()
            }
            Input::Coinbase { height, data, .. } => {
                let mut fmter = f.debug_struct("transparent::Input::Coinbase");

                fmter.field("height", height);
                fmter.field("data_len", &data.0.len());

                fmter.finish()
            }
        }
    }
}

impl Input {
    /// Returns a new coinbase input for `height` with optional `data` and `sequence`.
    ///
    /// # Consensus
    ///
    /// The combined serialized size of `height` and `data` can be at most 100 bytes.
    ///
    /// > A coinbase transaction script MUST have length in {2 .. 100} bytes.
    ///
    /// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
    ///
    /// # Panics
    ///
    /// If the coinbase data is greater than [`MAX_COINBASE_DATA_LEN`].
    #[cfg(feature = "getblocktemplate-rpcs")]
    pub fn new_coinbase(
        height: block::Height,
        data: Option<Vec<u8>>,
        sequence: Option<u32>,
    ) -> Input {
        // "No extra coinbase data" is the default.
        let data = data.unwrap_or_default();
        let height_size = height.coinbase_zcash_serialized_size();

        assert!(
            data.len() + height_size <= MAX_COINBASE_DATA_LEN,
            "invalid coinbase data: extra data {} bytes + height {height_size} bytes \
             must be {} or less",
            data.len(),
            MAX_COINBASE_DATA_LEN,
        );

        Input::Coinbase {
            height,
            data: CoinbaseData(data),

            // If the caller does not specify the sequence number,
            // use a sequence number that activates the LockTime.
            sequence: sequence.unwrap_or(0),
        }
    }

    /// Returns the extra coinbase data in this input, if it is an [`Input::Coinbase`].
    pub fn extra_coinbase_data(&self) -> Option<&CoinbaseData> {
        match self {
            Input::PrevOut { .. } => None,
            Input::Coinbase { data, .. } => Some(data),
        }
    }

    /// Returns the input's sequence number.
    pub fn sequence(&self) -> u32 {
        match self {
            Input::PrevOut { sequence, .. } | Input::Coinbase { sequence, .. } => *sequence,
        }
    }

    /// Sets the input's sequence number.
    ///
    /// Only for use in tests.
    #[cfg(any(test, feature = "proptest-impl"))]
    pub fn set_sequence(&mut self, new_sequence: u32) {
        match self {
            Input::PrevOut { sequence, .. } | Input::Coinbase { sequence, .. } => {
                *sequence = new_sequence
            }
        }
    }

    /// If this is a [`Input::PrevOut`] input, returns this input's
    /// [`OutPoint`]. Otherwise, returns `None`.
    pub fn outpoint(&self) -> Option<OutPoint> {
        if let Input::PrevOut { outpoint, .. } = self {
            Some(*outpoint)
        } else {
            None
        }
    }

    /// Set this input's [`OutPoint`].
    ///
    /// Should only be called on [`Input::PrevOut`] inputs.
    ///
    /// # Panics
    ///
    /// If `self` is a coinbase input.
    #[cfg(any(test, feature = "proptest-impl"))]
    pub fn set_outpoint(&mut self, new_outpoint: OutPoint) {
        if let Input::PrevOut {
            ref mut outpoint, ..
        } = self
        {
            *outpoint = new_outpoint;
        } else {
            unreachable!("unexpected variant: Coinbase Inputs do not have OutPoints");
        }
    }

    /// Get the value spent by this input, by looking up its [`OutPoint`] in `outputs`.
    /// See [`Self::value`] for details.
    ///
    /// # Panics
    ///
    /// If the provided [`Output`]s don't have this input's [`OutPoint`].
    pub(crate) fn value_from_outputs(
        &self,
        outputs: &HashMap<OutPoint, Output>,
    ) -> Amount<NonNegative> {
        match self {
            Input::PrevOut { outpoint, .. } => {
                outputs
                    .get(outpoint)
                    .unwrap_or_else(|| {
                        panic!(
                            "provided Outputs (length {:?}) don't have spent {:?}",
                            outputs.len(),
                            outpoint
                        )
                    })
                    .value
            }
            Input::Coinbase { .. } => Amount::zero(),
        }
    }

    /// Get the value spent by this input, by looking up its [`OutPoint`] in
    /// [`Utxo`]s.
    ///
    /// This amount is added to the transaction value pool by this input.
    ///
    /// # Panics
    ///
    /// If the provided [`Utxo`]s don't have this input's [`OutPoint`].
    pub fn value(&self, utxos: &HashMap<OutPoint, utxo::Utxo>) -> Amount<NonNegative> {
        if let Some(outpoint) = self.outpoint() {
            // look up the specific Output and convert it to the expected format
            let output = utxos
                .get(&outpoint)
                .expect("provided Utxos don't have spent OutPoint")
                .output
                .clone();
            self.value_from_outputs(&iter::once((outpoint, output)).collect())
        } else {
            // coinbase inputs don't need any UTXOs
            self.value_from_outputs(&HashMap::new())
        }
    }

    /// Get the value spent by this input, by looking up its [`OutPoint`] in
    /// [`OrderedUtxo`]s.
    ///
    /// See [`Self::value`] for details.
    ///
    /// # Panics
    ///
    /// If the provided [`OrderedUtxo`]s don't have this input's [`OutPoint`].
    pub fn value_from_ordered_utxos(
        &self,
        ordered_utxos: &HashMap<OutPoint, utxo::OrderedUtxo>,
    ) -> Amount<NonNegative> {
        if let Some(outpoint) = self.outpoint() {
            // look up the specific Output and convert it to the expected format
            let output = ordered_utxos
                .get(&outpoint)
                .expect("provided Utxos don't have spent OutPoint")
                .utxo
                .output
                .clone();
            self.value_from_outputs(&iter::once((outpoint, output)).collect())
        } else {
            // coinbase inputs don't need any UTXOs
            self.value_from_outputs(&HashMap::new())
        }
    }
}

/// A transparent output from a transaction.
///
/// The most fundamental building block of a transaction is a
/// transaction output -- the ZEC you own in your "wallet" is in
/// fact a subset of unspent transaction outputs (or "UTXO"s) of the
/// global UTXO set.
///
/// UTXOs are indivisible, discrete units of value which can only be
/// consumed in their entirety. Thus, if I want to send you 1 ZEC and
/// I only own one UTXO worth 2 ZEC, I would construct a transaction
/// that spends my UTXO and sends 1 ZEC to you and 1 ZEC back to me
/// (just like receiving change).
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary, Deserialize))]
#[cfg_attr(
    any(test, feature = "proptest-impl", feature = "elasticsearch"),
    derive(Serialize)
)]
pub struct Output {
    /// Transaction value.
    // At https://en.bitcoin.it/wiki/Protocol_documentation#tx, this is an i64.
    pub value: Amount<NonNegative>,

    /// The lock script defines the conditions under which this output can be spent.
    pub lock_script: Script,
}

impl Output {
    /// Returns a new coinbase output that pays `amount` using `lock_script`.
    #[cfg(feature = "getblocktemplate-rpcs")]
    pub fn new_coinbase(amount: Amount<NonNegative>, lock_script: Script) -> Output {
        Output {
            value: amount,
            lock_script,
        }
    }

    /// Get the value contained in this output.
    /// This amount is subtracted from the transaction value pool by this output.
    pub fn value(&self) -> Amount<NonNegative> {
        self.value
    }

    /// Return the destination address from a transparent output.
    ///
    /// Returns None if the address type is not valid or unrecognized.
    pub fn address(&self, network: &Network) -> Option<Address> {
        zcash_primitives::transparent_output_address(self, network)
    }
}
