use crate::contract::{methods, Context};
use crate::util;
use anyhow::{Context as _, Result};
use inflector::Inflector;
use proc_macro2::{Literal, TokenStream};
use quote::quote;

pub(crate) fn expand(cx: &Context) -> Result<TokenStream> {
    let deployed = expand_deployed(&cx);
    let deploy =
        expand_deploy(&cx).context("error generating contract `deploy` associated function")?;

    Ok(quote! {
        #deployed
        #deploy
    })
}

fn expand_deployed(cx: &Context) -> TokenStream {
    if cx.artifact.networks.is_empty() && cx.deployments.is_empty() {
        return quote! {};
    }

    quote! {
        impl Contract {
            /// Locates a deployed contract based on the current network ID
            /// reported by the `web3` provider.
            ///
            /// Note that this does not verify that a contract with a maching
            /// `Abi` is actually deployed at the given address.
            pub async fn deployed<F, T>(
                web3: &self::ethcontract::web3::api::Web3<T>,
            ) -> Result<Self, self::ethcontract::errors::DeployError>
            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, Web3};
                use self::ethcontract::transport::DynTransport;

                let transport = DynTransport::new(web3.transport().clone());
                let web3 = Web3::new(transport);
                let instance = Instance::deployed(web3, Contract::artifact().clone()).await?;

                Ok(Contract::from_raw(instance))
            }
        }
    }
}

fn expand_deploy(cx: &Context) -> Result<TokenStream> {
    if cx.artifact.bytecode.is_empty() {
        // do not generate deploy method for contracts that have empty bytecode
        return Ok(quote! {});
    }

    // TODO(nlordell): not sure how contructor documentation get generated as I
    //   can't seem to get truffle to output it
    let doc = util::expand_doc("Generated by `ethcontract`");

    let (input, arg) = match cx.artifact.abi.constructor() {
        Some(contructor) => (
            methods::expand_inputs(&contructor.inputs)?,
            methods::expand_inputs_call_arg(&contructor.inputs),
        ),
        None => (quote! {}, quote! {()}),
    };

    let libs: Vec<_> = cx
        .artifact
        .bytecode
        .undefined_libraries()
        .map(|name| (name, util::safe_ident(&name.to_snake_case())))
        .collect();
    let (lib_struct, lib_input, link) = if !libs.is_empty() {
        let lib_struct = {
            let lib_struct_fields = libs.iter().map(|(name, field)| {
                let doc = util::expand_doc(&format!("Address of the `{}` library.", name));

                quote! {
                    #doc pub #field: self::ethcontract::Address
                }
            });

            quote! {
                /// Undefinied libraries in the contract bytecode that are
                /// required for linking in order to deploy.
                pub struct Libraries {
                    #( #lib_struct_fields, )*
                }
            }
        };

        let link = {
            let link_libraries = libs.iter().map(|(name, field)| {
                let name_lit = Literal::string(&name);

                quote! {
                    bytecode.link(#name_lit, libs.#field).expect("valid library");
                }
            });

            quote! {
                let mut bytecode = bytecode;
                #( #link_libraries )*
            }
        };

        (lib_struct, quote! { , libs: Libraries }, link)
    } else {
        Default::default()
    };

    Ok(quote! {
        #lib_struct

        impl Contract {
            #doc
            pub fn builder<F, T>(
                web3: &self::ethcontract::web3::api::Web3<T> #lib_input #input ,
            ) -> self::ethcontract::dyns::DynDeployBuilder<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::dyns::DynTransport;
                use self::ethcontract::contract::DeployBuilder;
                use self::ethcontract::web3::api::Web3;

                let transport = DynTransport::new(web3.transport().clone());
                let web3 = Web3::new(transport);

                let bytecode = Self::artifact().bytecode.clone();
                #link

                DeployBuilder::new(web3, bytecode, #arg).expect("valid deployment args")
            }
        }

        impl self::ethcontract::contract::Deploy<self::ethcontract::dyns::DynTransport> for Contract {
            type Context = self::ethcontract::common::Bytecode;

            fn bytecode(cx: &Self::Context) -> &self::ethcontract::common::Bytecode {
                cx
            }

            fn abi(_: &Self::Context) -> &self::ethcontract::common::Abi {
                &Self::artifact().abi
            }

            fn from_deployment(
                web3: self::ethcontract::dyns::DynWeb3,
                address: self::ethcontract::Address,
                transaction_hash: self::ethcontract::H256,
                _: Self::Context,
            ) -> Self {
                Self::with_transaction(&web3, address, Some(transaction_hash))
            }
        }
    })
}
