use pallas_addresses::{Address, ShelleyDelegationPart, ShelleyPaymentPart};
use pallas_codec::utils::{AnyUInt, Bytes, Int, KeyValuePairs};
use pallas_crypto::hash::Hash;
use pallas_primitives::babbage::{AssetName, BigInt, Constr, Mint, PlutusData, ScriptRef};
use pallas_primitives::babbage::{
    Certificate, DatumOption, Redeemer, Script, StakeCredential, TransactionInput,
    TransactionOutput, Value,
};
use pallas_traverse::ComputeHash;

use super::script_context::{ScriptContext, ScriptPurpose, TimeRange, TxInInfo, TxInfo, TxOut};

fn wrap_with_constr(index: u64, data: PlutusData) -> PlutusData {
    PlutusData::Constr(Constr {
        tag: constr_index(index),
        any_constructor: None,
        fields: vec![data],
    })
}

fn wrap_multiple_with_constr(index: u64, data: Vec<PlutusData>) -> PlutusData {
    PlutusData::Constr(Constr {
        tag: constr_index(index),
        any_constructor: None,
        fields: data,
    })
}

fn empty_constr(index: u64) -> PlutusData {
    PlutusData::Constr(Constr {
        tag: constr_index(index),
        any_constructor: None,
        fields: vec![],
    })
}

/// Translate constructor index to cbor tag.
fn constr_index(index: u64) -> u64 {
    121 + index
}

pub trait ToPlutusData {
    fn to_plutus_data(&self) -> PlutusData;
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub struct MintValue {
    pub mint_value: Mint,
}

impl ToPlutusData for Address {
    fn to_plutus_data(&self) -> PlutusData {
        match self {
            Address::Shelley(shelley_address) => {
                let payment_part = shelley_address.payment();
                let stake_part = shelley_address.delegation();

                let payment_part_plutus_data = match payment_part {
                    ShelleyPaymentPart::Key(payment_keyhash) => {
                        wrap_with_constr(0, payment_keyhash.to_plutus_data())
                    }
                    ShelleyPaymentPart::Script(script_hash) => {
                        wrap_with_constr(1, script_hash.to_plutus_data())
                    }
                };

                let stake_part_plutus_data = match stake_part {
                    ShelleyDelegationPart::Key(stake_keyhash) => {
                        Some(StakeCredential::AddrKeyhash(*stake_keyhash)).to_plutus_data()
                    }
                    ShelleyDelegationPart::Script(script_hash) => {
                        Some(StakeCredential::Scripthash(*script_hash)).to_plutus_data()
                    }
                    ShelleyDelegationPart::Pointer(pointer) => Some(wrap_multiple_with_constr(
                        1,
                        vec![
                            pointer.slot().to_plutus_data(),
                            pointer.tx_idx().to_plutus_data(),
                            pointer.cert_idx().to_plutus_data(),
                        ],
                    ))
                    .to_plutus_data(),
                    ShelleyDelegationPart::Null => None::<StakeCredential>.to_plutus_data(),
                };

                wrap_multiple_with_constr(0, vec![payment_part_plutus_data, stake_part_plutus_data])
            }
            _ => unreachable!(),
        }
    }
}

impl ToPlutusData for TransactionInput {
    fn to_plutus_data(&self) -> PlutusData {
        wrap_multiple_with_constr(
            0,
            vec![
                wrap_with_constr(0, self.transaction_id.to_plutus_data()),
                PlutusData::BigInt(BigInt::Int((self.index as i64).into())),
            ],
        )
    }
}

impl<const BYTES: usize> ToPlutusData for Hash<BYTES> {
    fn to_plutus_data(&self) -> PlutusData {
        PlutusData::BoundedBytes(self.to_vec().into())
    }
}

impl ToPlutusData for Bytes {
    fn to_plutus_data(&self) -> PlutusData {
        PlutusData::BoundedBytes(self.clone())
    }
}

impl<K: ToPlutusData, V: ToPlutusData> ToPlutusData for (K, V) {
    fn to_plutus_data(&self) -> PlutusData {
        wrap_multiple_with_constr(0, vec![self.0.to_plutus_data(), self.1.to_plutus_data()])
    }
}

impl<A> ToPlutusData for Vec<A>
where
    A: ToPlutusData,
{
    fn to_plutus_data(&self) -> PlutusData {
        PlutusData::Array(self.iter().map(|p| p.to_plutus_data()).collect())
    }
}

impl<K, V> ToPlutusData for KeyValuePairs<K, V>
where
    K: ToPlutusData + Clone,
    V: ToPlutusData + Clone,
{
    fn to_plutus_data(&self) -> PlutusData {
        let mut data_vec: Vec<(PlutusData, PlutusData)> = vec![];
        for (key, value) in self.iter() {
            data_vec.push((key.to_plutus_data(), value.to_plutus_data()))
        }
        PlutusData::Map(KeyValuePairs::Def(data_vec))
    }
}

impl<A: ToPlutusData> ToPlutusData for Option<A> {
    fn to_plutus_data(&self) -> PlutusData {
        match self {
            None => empty_constr(1),
            Some(data) => wrap_with_constr(0, data.to_plutus_data()),
        }
    }
}

// Does this here surely overwrite Option from above for DatumOption?
impl ToPlutusData for Option<DatumOption> {
    // NoOutputDatum = 0 | OutputDatumHash = 1 | OutputDatum = 2
    fn to_plutus_data(&self) -> PlutusData {
        match self {
            None => empty_constr(0),
            Some(option) => match option {
                DatumOption::Hash(hash) => wrap_with_constr(1, hash.to_plutus_data()),
                DatumOption::Data(data) => wrap_with_constr(2, data.0.clone()),
            },
        }
    }
}

impl ToPlutusData for AnyUInt {
    fn to_plutus_data(&self) -> PlutusData {
        match self {
            AnyUInt::U8(n) => PlutusData::BigInt(BigInt::Int(Int::from(*n as i64))),
            AnyUInt::U16(n) => PlutusData::BigInt(BigInt::Int(Int::from(*n as i64))),
            AnyUInt::U32(n) => PlutusData::BigInt(BigInt::Int(Int::from(*n as i64))),
            AnyUInt::U64(n) => PlutusData::BigInt(BigInt::Int(Int::from(*n as i64))),
            AnyUInt::MajorByte(n) => PlutusData::BigInt(BigInt::Int(Int::from(*n as i64))), // is this correct? I don't know exactly what is does
        }
    }
}

impl ToPlutusData for Int {
    fn to_plutus_data(&self) -> PlutusData {
        PlutusData::BigInt(BigInt::Int(*self))
    }
}

impl ToPlutusData for BigInt {
    fn to_plutus_data(&self) -> PlutusData {
        PlutusData::BigInt(self.clone())
    }
}

impl ToPlutusData for i64 {
    fn to_plutus_data(&self) -> PlutusData {
        PlutusData::BigInt(BigInt::Int(Int::from(*self)))
    }
}

impl ToPlutusData for u64 {
    fn to_plutus_data(&self) -> PlutusData {
        PlutusData::BigInt(BigInt::Int(Int::from(*self as i64)))
    }
}

impl ToPlutusData for Value {
    fn to_plutus_data(&self) -> PlutusData {
        match self {
            Value::Coin(coin) => PlutusData::Map(KeyValuePairs::Def(vec![(
                Bytes::from(vec![]).to_plutus_data(),
                PlutusData::Map(KeyValuePairs::Def(vec![(
                    AssetName::from(vec![]).to_plutus_data(),
                    coin.to_plutus_data(),
                )])),
            )])),
            Value::Multiasset(coin, multiassets) => {
                let mut data_vec: Vec<(PlutusData, PlutusData)> = vec![(
                    Bytes::from(vec![]).to_plutus_data(),
                    PlutusData::Map(KeyValuePairs::Def(vec![(
                        AssetName::from(vec![]).to_plutus_data(),
                        coin.to_plutus_data(),
                    )])),
                )];

                for (policy_id, assets) in multiassets.iter() {
                    let mut assets_vec = vec![];
                    for (asset, amount) in assets.iter() {
                        assets_vec.push((asset.to_plutus_data(), amount.to_plutus_data()));
                    }
                    data_vec.push((
                        policy_id.to_plutus_data(),
                        PlutusData::Map(KeyValuePairs::Def(assets_vec)),
                    ));
                }

                PlutusData::Map(KeyValuePairs::Def(data_vec))
            }
        }
    }
}

impl ToPlutusData for MintValue {
    fn to_plutus_data(&self) -> PlutusData {
        let mut data_vec: Vec<(PlutusData, PlutusData)> = vec![(
            Bytes::from(vec![]).to_plutus_data(),
            PlutusData::Map(KeyValuePairs::Def(vec![(
                AssetName::from(vec![]).to_plutus_data(),
                0_i64.to_plutus_data(),
            )])),
        )];

        for (policy_id, assets) in self.mint_value.iter() {
            let mut assets_vec = vec![];
            for (asset, amount) in assets.iter() {
                assets_vec.push((asset.to_plutus_data(), amount.to_plutus_data()));
            }
            data_vec.push((
                policy_id.to_plutus_data(),
                PlutusData::Map(KeyValuePairs::Def(assets_vec)),
            ));
        }

        PlutusData::Map(KeyValuePairs::Def(data_vec))
    }
}

impl ToPlutusData for ScriptRef {
    fn to_plutus_data(&self) -> PlutusData {
        match &self.0 {
            Script::NativeScript(native_script) => native_script.compute_hash().to_plutus_data(),
            Script::PlutusV1Script(plutus_v1) => plutus_v1.compute_hash().to_plutus_data(),
            Script::PlutusV2Script(plutus_v2) => plutus_v2.compute_hash().to_plutus_data(),
        }
    }
}

impl ToPlutusData for TxOut {
    fn to_plutus_data(&self) -> PlutusData {
        match self {
            TxOut::V1(output) => match output {
                TransactionOutput::Legacy(legacy_output) => wrap_multiple_with_constr(
                    0,
                    vec![
                        Address::from_bytes(&legacy_output.address)
                            .unwrap()
                            .to_plutus_data(),
                        legacy_output.amount.to_plutus_data(),
                        legacy_output.datum_hash.to_plutus_data(),
                    ],
                ),
                TransactionOutput::PostAlonzo(post_alonzo_output) => wrap_multiple_with_constr(
                    0,
                    vec![
                        Address::from_bytes(&post_alonzo_output.address)
                            .unwrap()
                            .to_plutus_data(),
                        post_alonzo_output.value.to_plutus_data(),
                        match post_alonzo_output.datum_option {
                            Some(DatumOption::Hash(hash)) => Some(hash).to_plutus_data(),
                            _ => None::<DatumOption>.to_plutus_data(),
                        },
                    ],
                ),
            },
            TxOut::V2(output) => match output {
                TransactionOutput::Legacy(legacy_output) => wrap_multiple_with_constr(
                    0,
                    vec![
                        Address::from_bytes(&legacy_output.address)
                            .unwrap()
                            .to_plutus_data(),
                        legacy_output.amount.to_plutus_data(),
                        match legacy_output.datum_hash {
                            Some(hash) => wrap_with_constr(1, hash.to_plutus_data()),
                            _ => empty_constr(0),
                        },
                        None::<ScriptRef>.to_plutus_data(),
                    ],
                ),
                TransactionOutput::PostAlonzo(post_alonzo_output) => wrap_multiple_with_constr(
                    0,
                    vec![
                        Address::from_bytes(&post_alonzo_output.address)
                            .unwrap()
                            .to_plutus_data(),
                        post_alonzo_output.value.to_plutus_data(),
                        post_alonzo_output.datum_option.to_plutus_data(),
                        post_alonzo_output.script_ref.to_plutus_data(),
                    ],
                ),
            },
        }
    }
}

impl ToPlutusData for StakeCredential {
    // Stake Credential needs to be wrapped inside another Constr, because we could have either a StakingHash or a StakingPtr
    // The current implementation of StakeCredential doesn't capture the credential of a Pointer address.
    // So a StakeCredential for a Pointer address needs to be converted separately
    fn to_plutus_data(&self) -> PlutusData {
        match self {
            StakeCredential::AddrKeyhash(addr_keyhas) => {
                wrap_with_constr(0, wrap_with_constr(0, addr_keyhas.to_plutus_data()))
            }
            StakeCredential::Scripthash(script_hash) => {
                wrap_with_constr(0, wrap_with_constr(1, script_hash.to_plutus_data()))
            }
        }
    }
}

impl ToPlutusData for Certificate {
    fn to_plutus_data(&self) -> PlutusData {
        match self {
            Certificate::StakeRegistration(stake_credential) => {
                wrap_with_constr(0, stake_credential.to_plutus_data())
            }
            Certificate::StakeDeregistration(stake_credential) => {
                wrap_with_constr(1, stake_credential.to_plutus_data())
            }
            Certificate::StakeDelegation(stake_credential, pool_keyhash) => {
                wrap_multiple_with_constr(
                    2,
                    vec![
                        stake_credential.to_plutus_data(),
                        pool_keyhash.to_plutus_data(),
                    ],
                )
            }
            Certificate::PoolRegistration {
                operator,
                vrf_keyhash,
                pledge: _,
                cost: _,
                margin: _,
                reward_account: _,
                pool_owners: _,
                relays: _,
                pool_metadata: _,
            } => wrap_multiple_with_constr(
                3,
                vec![operator.to_plutus_data(), vrf_keyhash.to_plutus_data()],
            ),
            Certificate::PoolRetirement(pool_keyhash, epoch) => wrap_multiple_with_constr(
                4,
                vec![pool_keyhash.to_plutus_data(), epoch.to_plutus_data()],
            ),
            Certificate::GenesisKeyDelegation(_, _, _) => empty_constr(5),
            Certificate::MoveInstantaneousRewardsCert(_) => empty_constr(6),
        }
    }
}

impl ToPlutusData for Redeemer {
    fn to_plutus_data(&self) -> PlutusData {
        self.data.clone()
    }
}

impl ToPlutusData for PlutusData {
    fn to_plutus_data(&self) -> PlutusData {
        self.clone()
    }
}

impl ToPlutusData for TimeRange {
    fn to_plutus_data(&self) -> PlutusData {
        match &self {
            TimeRange {
                lower_bound: Some(lower_bound),
                upper_bound: None,
            } => {
                wrap_multiple_with_constr(
                    0,
                    vec![
                        // LowerBound
                        wrap_multiple_with_constr(
                            0,
                            vec![
                                // Finite
                                wrap_with_constr(1, lower_bound.to_plutus_data()),
                                // Closure
                                true.to_plutus_data(),
                            ],
                        ), //UpperBound
                        wrap_multiple_with_constr(
                            0,
                            vec![
                                // PosInf
                                empty_constr(2),
                                // Closure
                                true.to_plutus_data(),
                            ],
                        ),
                    ],
                )
            }
            TimeRange {
                lower_bound: None,
                upper_bound: Some(upper_bound),
            } => {
                wrap_multiple_with_constr(
                    0,
                    vec![
                        // LowerBound
                        wrap_multiple_with_constr(
                            0,
                            vec![
                                // NegInf
                                empty_constr(0),
                                // Closure
                                true.to_plutus_data(),
                            ],
                        ),
                        //UpperBound
                        wrap_multiple_with_constr(
                            0,
                            vec![
                                // Finite
                                wrap_with_constr(1, upper_bound.to_plutus_data()),
                                // Closure
                                true.to_plutus_data(),
                            ],
                        ),
                    ],
                )
            }
            TimeRange {
                lower_bound: Some(lower_bound),
                upper_bound: Some(upper_bound),
            } => {
                wrap_multiple_with_constr(
                    0,
                    vec![
                        // LowerBound
                        wrap_multiple_with_constr(
                            0,
                            vec![
                                // Finite
                                wrap_with_constr(1, lower_bound.to_plutus_data()),
                                // Closure
                                true.to_plutus_data(),
                            ],
                        ),
                        //UpperBound
                        wrap_multiple_with_constr(
                            0,
                            vec![
                                // Finite
                                wrap_with_constr(1, upper_bound.to_plutus_data()),
                                // Closure
                                false.to_plutus_data(),
                            ],
                        ),
                    ],
                )
            }
            TimeRange {
                lower_bound: None,
                upper_bound: None,
            } => {
                wrap_multiple_with_constr(
                    0,
                    vec![
                        // LowerBound
                        wrap_multiple_with_constr(
                            0,
                            vec![
                                // NegInf
                                empty_constr(0),
                                // Closure
                                true.to_plutus_data(),
                            ],
                        ),
                        //UpperBound
                        wrap_multiple_with_constr(
                            0,
                            vec![
                                // PosInf
                                empty_constr(2),
                                // Closure
                                true.to_plutus_data(),
                            ],
                        ),
                    ],
                )
            }
        }
    }
}

impl ToPlutusData for TxInInfo {
    fn to_plutus_data(&self) -> PlutusData {
        wrap_multiple_with_constr(
            0,
            vec![
                self.out_ref.to_plutus_data(),
                self.resolved.to_plutus_data(),
            ],
        )
    }
}

impl ToPlutusData for ScriptPurpose {
    fn to_plutus_data(&self) -> PlutusData {
        match self {
            ScriptPurpose::Minting(policy_id) => wrap_with_constr(0, policy_id.to_plutus_data()),
            ScriptPurpose::Spending(out_ref) => wrap_with_constr(1, out_ref.to_plutus_data()),
            ScriptPurpose::Rewarding(stake_credential) => {
                wrap_with_constr(2, stake_credential.to_plutus_data())
            }
            ScriptPurpose::Certifying(dcert) => wrap_with_constr(3, dcert.to_plutus_data()),
        }
    }
}

impl ToPlutusData for TxInfo {
    fn to_plutus_data(&self) -> PlutusData {
        match self {
            TxInfo::V1(tx_info) => wrap_multiple_with_constr(
                0,
                vec![
                    tx_info.inputs.to_plutus_data(),
                    tx_info.outputs.to_plutus_data(),
                    tx_info.fee.to_plutus_data(),
                    tx_info.mint.to_plutus_data(),
                    tx_info.dcert.to_plutus_data(),
                    tx_info.wdrl.to_plutus_data(),
                    tx_info.valid_range.to_plutus_data(),
                    tx_info.signatories.to_plutus_data(),
                    tx_info.data.to_plutus_data(),
                    wrap_with_constr(0, tx_info.id.to_plutus_data()),
                ],
            ),
            TxInfo::V2(tx_info) => wrap_multiple_with_constr(
                0,
                vec![
                    tx_info.inputs.to_plutus_data(),
                    tx_info.reference_inputs.to_plutus_data(),
                    tx_info.outputs.to_plutus_data(),
                    tx_info.fee.to_plutus_data(),
                    tx_info.mint.to_plutus_data(),
                    tx_info.dcert.to_plutus_data(),
                    tx_info.wdrl.to_plutus_data(),
                    tx_info.valid_range.to_plutus_data(),
                    tx_info.signatories.to_plutus_data(),
                    tx_info.redeemers.to_plutus_data(),
                    tx_info.data.to_plutus_data(),
                    wrap_with_constr(0, tx_info.id.to_plutus_data()),
                ],
            ),
        }
    }
}

impl ToPlutusData for ScriptContext {
    fn to_plutus_data(&self) -> PlutusData {
        wrap_multiple_with_constr(
            0,
            vec![self.tx_info.to_plutus_data(), self.purpose.to_plutus_data()],
        )
    }
}

impl ToPlutusData for bool {
    fn to_plutus_data(&self) -> PlutusData {
        match self {
            false => empty_constr(0),
            true => empty_constr(1),
        }
    }
}
