import { ForwarderContract } from '@0x/abi-gen-wrappers';
import { Forwarder } from '@0x/contract-artifacts';
import { schemas } from '@0x/json-schemas';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import { ContractAbi } from 'ethereum-types';
import * as _ from 'lodash';

import { orderTxOptsSchema } from '../schemas/order_tx_opts_schema';
import { txOptsSchema } from '../schemas/tx_opts_schema';
import { OrderTransactionOpts } from '../types';
import { assert } from '../utils/assert';
import { calldataOptimizationUtils } from '../utils/calldata_optimization_utils';
import { constants } from '../utils/constants';
import { _getDefaultContractAddresses } from '../utils/contract_addresses';
import { decorators } from '../utils/decorators';
import { utils } from '../utils/utils';

import { ContractWrapper } from './contract_wrapper';

/**
 * This class includes the functionality related to interacting with the Forwarder contract.
 */
export class ForwarderWrapper extends ContractWrapper {
    public abi: ContractAbi = Forwarder.compilerOutput.abi;
    public address: string;
    public zrxTokenAddress: string;
    public etherTokenAddress: string;
    private _forwarderContractIfExists?: ForwarderContract;

    /**
     * Instantiate ForwarderWrapper
     * @param web3Wrapper Web3Wrapper instance to use.
     * @param networkId Desired networkId.
     * @param address The address of the Exchange contract. If undefined, will
     * default to the known address corresponding to the networkId.
     * @param zrxTokenAddress The address of the ZRXToken contract. If
     * undefined, will default to the known address corresponding to the
     * networkId.
     * @param etherTokenAddress The address of a WETH (Ether token) contract. If
     * undefined, will default to the known address corresponding to the
     * networkId.
     */
    constructor(
        web3Wrapper: Web3Wrapper,
        networkId: number,
        address?: string,
        zrxTokenAddress?: string,
        etherTokenAddress?: string,
    ) {
        super(web3Wrapper, networkId);
        this.address = address === undefined ? _getDefaultContractAddresses(networkId).exchange : address;
        this.zrxTokenAddress =
            zrxTokenAddress === undefined ? _getDefaultContractAddresses(networkId).zrxToken : zrxTokenAddress;
        this.etherTokenAddress =
            etherTokenAddress === undefined ? _getDefaultContractAddresses(networkId).etherToken : etherTokenAddress;
    }
    /**
     * Purchases as much of orders' makerAssets as possible by selling up to 95% of transaction's ETH value.
     * Any ZRX required to pay fees for primary orders will automatically be purchased by this contract.
     * 5% of ETH value is reserved for paying fees to order feeRecipients (in ZRX) and forwarding contract feeRecipient (in ETH).
     * Any ETH not spent will be refunded to sender.
     * @param   signedOrders            An array of objects that conform to the SignedOrder interface. All orders must specify the same makerAsset.
     *                                  All orders must specify WETH as the takerAsset
     * @param   takerAddress            The user Ethereum address who would like to fill this order. Must be available via the supplied
     *                                  Provider provided at instantiation.
     * @param   ethAmount               The amount of eth to send with the transaction (in wei).
     * @param   signedFeeOrders         An array of objects that conform to the SignedOrder interface. All orders must specify ZRX as makerAsset and WETH as takerAsset.
     *                                  Used to purchase ZRX for primary order fees.
     * @param   feePercentage           The percentage of WETH sold that will payed as fee to forwarding contract feeRecipient.
     *                                  Defaults to 0.
     * @param   feeRecipientAddress     The address that will receive ETH when signedFeeOrders are filled.
     * @param   orderTransactionOpts    Transaction parameters.
     * @return  Transaction hash.
     */
    @decorators.asyncZeroExErrorHandler
    public async marketSellOrdersWithEthAsync(
        signedOrders: SignedOrder[],
        takerAddress: string,
        ethAmount: BigNumber,
        signedFeeOrders: SignedOrder[] = [],
        feePercentage: number = 0,
        feeRecipientAddress: string = constants.NULL_ADDRESS,
        orderTransactionOpts: OrderTransactionOpts = { shouldValidate: true },
    ): Promise<string> {
        // type assertions
        assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema);
        await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper);
        assert.isBigNumber('ethAmount', ethAmount);
        assert.doesConformToSchema('signedFeeOrders', signedFeeOrders, schemas.signedOrdersSchema);
        assert.isNumber('feePercentage', feePercentage);
        assert.isETHAddressHex('feeRecipientAddress', feeRecipientAddress);
        assert.doesConformToSchema('orderTransactionOpts', orderTransactionOpts, orderTxOptsSchema, [txOptsSchema]);
        // other assertions
        assert.ordersCanBeUsedForForwarderContract(signedOrders, this.etherTokenAddress);
        assert.feeOrdersCanBeUsedForForwarderContract(signedFeeOrders, this.zrxTokenAddress, this.etherTokenAddress);
        // format feePercentage
        const formattedFeePercentage = utils.numberPercentageToEtherTokenAmountPercentage(feePercentage);
        // lowercase input addresses
        const normalizedTakerAddress = takerAddress.toLowerCase();
        const normalizedFeeRecipientAddress = feeRecipientAddress.toLowerCase();
        // optimize orders
        const optimizedMarketOrders = calldataOptimizationUtils.optimizeForwarderOrders(signedOrders);
        const optimizedFeeOrders = calldataOptimizationUtils.optimizeForwarderFeeOrders(signedFeeOrders);
        // compile signatures
        const signatures = _.map(optimizedMarketOrders, order => order.signature);
        const feeSignatures = _.map(optimizedFeeOrders, order => order.signature);
        // get contract
        const forwarderContractInstance = await this._getForwarderContractAsync();
        // validate transaction
        if (orderTransactionOpts.shouldValidate) {
            await forwarderContractInstance.marketSellOrdersWithEth.callAsync(
                optimizedMarketOrders,
                signatures,
                optimizedFeeOrders,
                feeSignatures,
                formattedFeePercentage,
                normalizedFeeRecipientAddress,
                {
                    value: ethAmount,
                    from: normalizedTakerAddress,
                    gas: orderTransactionOpts.gasLimit,
                    gasPrice: orderTransactionOpts.gasPrice,
                    nonce: orderTransactionOpts.nonce,
                },
            );
        }
        // send transaction
        const txHash = await forwarderContractInstance.marketSellOrdersWithEth.sendTransactionAsync(
            optimizedMarketOrders,
            signatures,
            optimizedFeeOrders,
            feeSignatures,
            formattedFeePercentage,
            feeRecipientAddress,
            {
                value: ethAmount,
                from: normalizedTakerAddress,
                gas: orderTransactionOpts.gasLimit,
                gasPrice: orderTransactionOpts.gasPrice,
                nonce: orderTransactionOpts.nonce,
            },
        );
        return txHash;
    }
    /**
     * Attempt to purchase makerAssetFillAmount of makerAsset by selling ethAmount provided with transaction.
     * Any ZRX required to pay fees for primary orders will automatically be purchased by the contract.
     * Any ETH not spent will be refunded to sender.
     * @param   signedOrders            An array of objects that conform to the SignedOrder interface. All orders must specify the same makerAsset.
     *                                  All orders must specify WETH as the takerAsset
     * @param   makerAssetFillAmount    The amount of the order (in taker asset baseUnits) that you wish to fill.
     * @param   takerAddress            The user Ethereum address who would like to fill this order. Must be available via the supplied
     *                                  Provider provided at instantiation.
     * @param   ethAmount               The amount of eth to send with the transaction (in wei).
     * @param   signedFeeOrders         An array of objects that conform to the SignedOrder interface. All orders must specify ZRX as makerAsset and WETH as takerAsset.
     *                                  Used to purchase ZRX for primary order fees.
     * @param   feePercentage           The percentage of WETH sold that will payed as fee to forwarding contract feeRecipient.
     *                                  Defaults to 0.
     * @param   feeRecipientAddress     The address that will receive ETH when signedFeeOrders are filled.
     * @param   orderTransactionOpts    Transaction parameters.
     * @return  Transaction hash.
     */
    @decorators.asyncZeroExErrorHandler
    public async marketBuyOrdersWithEthAsync(
        signedOrders: SignedOrder[],
        makerAssetFillAmount: BigNumber,
        takerAddress: string,
        ethAmount: BigNumber,
        signedFeeOrders: SignedOrder[] = [],
        feePercentage: number = 0,
        feeRecipientAddress: string = constants.NULL_ADDRESS,
        orderTransactionOpts: OrderTransactionOpts = { shouldValidate: true },
    ): Promise<string> {
        // type assertions
        assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema);
        assert.isBigNumber('makerAssetFillAmount', makerAssetFillAmount);
        await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper);
        assert.isBigNumber('ethAmount', ethAmount);
        assert.doesConformToSchema('signedFeeOrders', signedFeeOrders, schemas.signedOrdersSchema);
        assert.isNumber('feePercentage', feePercentage);
        assert.isETHAddressHex('feeRecipientAddress', feeRecipientAddress);
        assert.doesConformToSchema('orderTransactionOpts', orderTransactionOpts, orderTxOptsSchema, [txOptsSchema]);
        // other assertions
        assert.ordersCanBeUsedForForwarderContract(signedOrders, this.etherTokenAddress);
        assert.feeOrdersCanBeUsedForForwarderContract(signedFeeOrders, this.zrxTokenAddress, this.etherTokenAddress);
        // format feePercentage
        const formattedFeePercentage = utils.numberPercentageToEtherTokenAmountPercentage(feePercentage);
        // lowercase input addresses
        const normalizedTakerAddress = takerAddress.toLowerCase();
        const normalizedFeeRecipientAddress = feeRecipientAddress.toLowerCase();
        // optimize orders
        const optimizedMarketOrders = calldataOptimizationUtils.optimizeForwarderOrders(signedOrders);
        const optimizedFeeOrders = calldataOptimizationUtils.optimizeForwarderFeeOrders(signedFeeOrders);
        // compile signatures
        const signatures = _.map(optimizedMarketOrders, order => order.signature);
        const feeSignatures = _.map(optimizedFeeOrders, order => order.signature);
        // get contract
        const forwarderContractInstance = await this._getForwarderContractAsync();
        // validate transaction
        if (orderTransactionOpts.shouldValidate) {
            await forwarderContractInstance.marketBuyOrdersWithEth.callAsync(
                optimizedMarketOrders,
                makerAssetFillAmount,
                signatures,
                optimizedFeeOrders,
                feeSignatures,
                formattedFeePercentage,
                normalizedFeeRecipientAddress,
                {
                    value: ethAmount,
                    from: normalizedTakerAddress,
                    gas: orderTransactionOpts.gasLimit,
                    gasPrice: orderTransactionOpts.gasPrice,
                    nonce: orderTransactionOpts.nonce,
                },
            );
        }
        // send transaction
        const txHash = await forwarderContractInstance.marketBuyOrdersWithEth.sendTransactionAsync(
            optimizedMarketOrders,
            makerAssetFillAmount,
            signatures,
            optimizedFeeOrders,
            feeSignatures,
            formattedFeePercentage,
            feeRecipientAddress,
            {
                value: ethAmount,
                from: normalizedTakerAddress,
                gas: orderTransactionOpts.gasLimit,
                gasPrice: orderTransactionOpts.gasPrice,
                nonce: orderTransactionOpts.nonce,
            },
        );
        return txHash;
    }
    private async _getForwarderContractAsync(): Promise<ForwarderContract> {
        if (this._forwarderContractIfExists !== undefined) {
            return this._forwarderContractIfExists;
        }
        const contractInstance = new ForwarderContract(
            this.address,
            this._web3Wrapper.getProvider(),
            this._web3Wrapper.getContractDefaults(),
        );
        this._forwarderContractIfExists = contractInstance;
        return this._forwarderContractIfExists;
    }
}
