use crate::contract::Context;
use crate::util::expand_doc;
use ethcontract_common::Address;
use proc_macro2::{Literal, TokenStream};
use quote::quote;

pub(crate) fn expand(cx: &Context) -> TokenStream {
    let artifact_json = &cx.artifact_json;
    let contract_name = &cx.contract_name;

    let doc_str = cx
        .artifact
        .devdoc
        .details
        .as_deref()
        .unwrap_or("Generated by `ethcontract`");
    let doc = expand_doc(doc_str);

    let deployments = cx.deployments.iter().map(|(network_id, address)| {
        let network_id = Literal::string(&network_id.to_string());
        let address = expand_address(*address);

        quote! {
            artifact.networks.insert(
                #network_id.to_owned(),
                self::ethcontract::common::truffle::Network {
                    address: #address,
                    transaction_hash: None,
                },
            );
        }
    });

    quote! {
        #doc
        #[derive(Clone)]
        pub struct Contract {
            methods: Methods,
        }

        impl Contract {
            /// Retrieves the truffle artifact used to generate the type safe
            /// API for this contract.
            pub fn artifact() -> &'static self::ethcontract::Artifact {
                use self::ethcontract::private::lazy_static;
                use self::ethcontract::Artifact;

                lazy_static! {
                    pub static ref ARTIFACT: Artifact = {
                        #[allow(unused_mut)]
                        let mut artifact = Artifact::from_json(#artifact_json)
                            .expect("valid artifact JSON");
                        #( #deployments )*

                        artifact
                    };
                }
                &ARTIFACT
            }

            /// Creates a new contract instance with the specified `web3`
            /// provider at the given `Address`.
            ///
            /// Note that this does not verify that a contract with a maching
            /// `Abi` is actually deployed at the given address.
            pub fn at<F, T>(
                web3: &self::ethcontract::web3::api::Web3<T>,
                address: self::ethcontract::Address,
            ) -> Self
            where
                F: std::future::Future<
                       Output = Result<self::ethcontract::json::Value, self::ethcontract::web3::Error>
                   > + Send + Unpin + 'static,
                T: self::ethcontract::web3::Transport<Out = F> + Send + Sync + 'static,
            {
                Contract::with_transaction(web3, address, None)
            }


            /// Creates a new contract instance with the specified `web3` provider with
            /// the given `Abi` at the given `Address` and an optional transaction hash.
            /// This hash is used to retrieve contract related information such as the
            /// creation block (which is useful for fetching all historic events).
            ///
            /// Note that this does not verify that a contract with a matching `Abi` is
            /// actually deployed at the given address nor that the transaction hash,
            /// when provided, is actually for this contract deployment.
            pub fn with_transaction<F, T>(
                web3: &self::ethcontract::web3::api::Web3<T>,
                address: self::ethcontract::Address,
                transaction_hash: Option<self::ethcontract::H256>,
            ) -> Self
            where
                F: std::future::Future<
                       Output = Result<self::ethcontract::json::Value, self::ethcontract::web3::Error>
                   > + Send + Unpin + 'static,
                T: self::ethcontract::web3::Transport<Out = F> + Send + Sync + 'static,
            {
                use self::ethcontract::Instance;
                use self::ethcontract::transport::DynTransport;
                use self::ethcontract::web3::api::Web3;

                let transport = DynTransport::new(web3.transport().clone());
                let web3 = Web3::new(transport);
                let abi = Self::artifact().abi.clone();
                let instance = Instance::with_transaction(web3, abi, address, transaction_hash);

                Contract::from_raw(instance)
            }

            /// Creates a contract from a raw instance.
            fn from_raw(instance: self::ethcontract::dyns::DynInstance) -> Self {
                let methods = Methods { instance };
                Contract { methods }
            }

            /// Returns the contract address being used by this instance.
            pub fn address(&self) -> self::ethcontract::Address {
                self.raw_instance().address()
            }

            /// Returns the hash for the transaction that deployed the contract
            /// if it is known, `None` otherwise.
            pub fn transaction_hash(&self) -> Option<self::ethcontract::H256> {
                self.raw_instance().transaction_hash()
            }

            /// Returns a reference to the default method options used by this
            /// contract.
            pub fn defaults(&self) -> &self::ethcontract::contract::MethodDefaults {
                &self.raw_instance().defaults
            }

            /// Returns a mutable reference to the default method options used
            /// by this contract.
            pub fn defaults_mut(&mut self) -> &mut self::ethcontract::contract::MethodDefaults {
                &mut self.raw_instance_mut().defaults
            }

            /// Returns a reference to the raw runtime instance used by this
            /// contract.
            pub fn raw_instance(&self) -> &self::ethcontract::dyns::DynInstance {
                &self.methods.instance
            }

            /// Returns a mutable reference to the raw runtime instance used by
            /// this contract.
            fn raw_instance_mut(&mut self) -> &mut self::ethcontract::dyns::DynInstance {
                &mut self.methods.instance
            }
        }

        impl std::fmt::Debug for Contract {
            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                f.debug_tuple(stringify!(#contract_name))
                    .field(&self.address())
                    .finish()
            }
        }
    }
}

/// Expands an `Address` into a literal representation that can be used with
/// quasi-quoting for code generation.
fn expand_address(address: Address) -> TokenStream {
    let bytes = address
        .as_bytes()
        .iter()
        .copied()
        .map(Literal::u8_unsuffixed);

    quote! {
        self::ethcontract::H160([#( #bytes ),*])
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[rustfmt::skip]
    fn expand_address_value() {
        assert_quote!(
            expand_address(Address::zero()),
            {
                self::ethcontract::H160([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ])
            },
        );

        assert_quote!(
            expand_address("000102030405060708090a0b0c0d0e0f10111213".parse().unwrap()),
            {
                self::ethcontract::H160([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
            },
        );
    }
}
