contract Balancer_ZapIn_General_V1 is ReentrancyGuard, Ownable {
    using SafeMath for uint256;
    using Address for address;
    bool private stopped = false;
    uint16 public goodwill;
    address public dzgoodwillAddress;
    uint256 public defaultSlippage;

    IuniswapFactory_Balancer_ZapIn_General_V1 public UniSwapFactoryAddress = IuniswapFactory_Balancer_ZapIn_General_V1(
        0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95
    );
    IBFactory_Balancer_ZapIn_General_V1 BalancerFactory = IBFactory_Balancer_ZapIn_General_V1(
        0x9424B1412450D0f8Fc2255FAf6046b98213B76Bd
    );

    address wethTokenAddress = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;

    event Zapin(
        address _toWhomToIssue,
        address _toBalancerPoolAddress,
        uint256 _OutgoingBPT
    );

    constructor(
        uint16 _goodwill,
        address _dzgoodwillAddress,
        uint256 _slippage
    ) public {
        goodwill = _goodwill;
        dzgoodwillAddress = _dzgoodwillAddress;
        defaultSlippage = _slippage;
    }

    // circuit breaker modifiers
    modifier stopInEmergency {
        if (stopped) {
            revert("Temporarily Paused");
        } else {
            _;
        }
    }

    /**
    @notice This function is used to invest in given balancer pool through ETH/ERC20 Tokens
    @param _FromTokenContractAddress The token used for investment (address(0x00) if ether)
    @param _ToBalancerPoolAddress The address of balancer pool to zapin
    @param _amount The amount of ERC to invest
    @param slippage slippage user wants
    @return success or failure
     */
    function EasyZapIn(
        address _FromTokenContractAddress,
        address _ToBalancerPoolAddress,
        uint256 _amount,
        uint256 slippage
    ) public payable nonReentrant stopInEmergency returns (uint256 tokensBought) {
        require(
            BalancerFactory.isBPool(_ToBalancerPoolAddress),
            "Invalid Balancer Pool"
        );

        uint256 withSlippage = slippage > 0 && slippage < 10000
            ? slippage
            : defaultSlippage;

        if (_FromTokenContractAddress == address(0)) {
            require(msg.value > 0, "ERR: No ETH sent");

            address _ToTokenContractAddress = _getBestDeal(
                _ToBalancerPoolAddress,
                msg.value,
                _FromTokenContractAddress
            );

            tokensBought = _performZapIn(
                msg.sender,
                _FromTokenContractAddress,
                _ToBalancerPoolAddress,
                msg.value,
                _ToTokenContractAddress,
                withSlippage
            );

            return tokensBought;
        }

        require(_amount > 0, "ERR: No ERC sent");
        require(msg.value == 0, "ERR: ETH sent with tokens");

        //transfer tokens to contract
        require(
            IERC20(_FromTokenContractAddress).transferFrom(
                msg.sender,
                address(this),
                _amount
            ),
            "Error in transferring ERC: 1"
        );

        address _ToTokenContractAddress = _getBestDeal(
            _ToBalancerPoolAddress,
            _amount,
            _FromTokenContractAddress
        );

        tokensBought = _performZapIn(
            msg.sender,
            _FromTokenContractAddress,
            _ToBalancerPoolAddress,
            _amount,
            _ToTokenContractAddress,
            withSlippage
        );
    }

    /**
    @notice This function is used to invest in given balancer pool through ETH/ERC20 Tokens with interface
    @param _toWhomToIssue The user address who want to invest
    @param _FromTokenContractAddress The token used for investment (address(0x00) if ether)
    @param _ToBalancerPoolAddress The address of balancer pool to zapin
    @param _amount The amount of ERC to invest
    @param _IntermediateToken The token for intermediate conversion before zapin
    @param slippage slippage user wants
    @return The quantity of Balancer Pool tokens returned
     */
    function ZapIn(
        address payable _toWhomToIssue,
        address _FromTokenContractAddress,
        address _ToBalancerPoolAddress,
        uint256 _amount,
        address _IntermediateToken,
        uint256 slippage
    ) public payable nonReentrant stopInEmergency returns (uint256 tokensBought) {
        uint256 withSlippage = slippage > 0 && slippage < 10000
            ? slippage
            : defaultSlippage;

        if (_FromTokenContractAddress == address(0)) {
            require(msg.value > 0, "ERR: No ETH sent");

            tokensBought = _performZapIn(
                _toWhomToIssue,
                _FromTokenContractAddress,
                _ToBalancerPoolAddress,
                msg.value,
                _IntermediateToken,
                withSlippage
            );

            return tokensBought;
        }

        require(_amount > 0, "ERR: No ERC sent");
        require(msg.value == 0, "ERR: ETH sent with tokens");

        //transfer tokens to contract
        require(
            IERC20(_FromTokenContractAddress).transferFrom(
                msg.sender,
                address(this),
                _amount
            ),
            "Error in transferring ERC: 2"
        );

        tokensBought = _performZapIn(
            _toWhomToIssue,
            _FromTokenContractAddress,
            _ToBalancerPoolAddress,
            _amount,
            _IntermediateToken,
            withSlippage
        );
    }

    /**
    @notice This function internally called by ZapIn() and EasyZapIn()
    @param _toWhomToIssue The user address who want to invest
    @param _FromTokenContractAddress The token used for investment (address(0x00) if ether)
    @param _ToBalancerPoolAddress The address of balancer pool to zapin
    @param _amount The amount of ETH/ERC to invest
    @param _IntermediateToken The token for intermediate conversion before zapin
    @param slippage slippage user wants
    @return The quantity of Balancer Pool tokens returned
     */
    function _performZapIn(
        address _toWhomToIssue,
        address _FromTokenContractAddress,
        address _ToBalancerPoolAddress,
        uint256 _amount,
        address _IntermediateToken,
        uint256 slippage
    ) internal returns (uint256 tokensBought) {
        // check if isBound()
        bool isBound = IBPool_Balancer_ZapIn_General_V1(_ToBalancerPoolAddress)
            .isBound(_FromTokenContractAddress);

        uint256 balancerTokens;

        if (isBound) {
            balancerTokens = _enter2Balancer(
                _ToBalancerPoolAddress,
                _FromTokenContractAddress,
                _amount
            );
        } else {
            // swap tokens or eth
            uint256 tokenBought;
            if (_FromTokenContractAddress == address(0)) {
                tokenBought = _eth2Token(_IntermediateToken, slippage);
            } else {
                tokenBought = _token2Token(
                    _FromTokenContractAddress,
                    _IntermediateToken,
                    _amount,
                    slippage
                );
            }

            //get BPT
            balancerTokens = _enter2Balancer(
                _ToBalancerPoolAddress,
                _IntermediateToken,
                tokenBought
            );
        }

        //transfer goodwill
        uint256 goodwillPortion = _transferGoodwill(
            _ToBalancerPoolAddress,
            balancerTokens
        );

        emit Zapin(
            _toWhomToIssue,
            _ToBalancerPoolAddress,
            SafeMath.sub(balancerTokens, goodwillPortion)
        );

        //transfer tokens to user
        IERC20(_ToBalancerPoolAddress).transfer(
            _toWhomToIssue,
            SafeMath.sub(balancerTokens, goodwillPortion)
        );
        return SafeMath.sub(balancerTokens, goodwillPortion);
    }

    /**
    @notice This function is used to calculate and transfer goodwill
    @param _tokenContractAddress Token in which goodwill is deducted
    @param tokens2Trade The total amount of tokens to be zapped in
    @return The quantity of goodwill deducted
     */
    function _transferGoodwill(
        address _tokenContractAddress,
        uint256 tokens2Trade
    ) internal returns (uint256 goodwillPortion) {
        goodwillPortion = SafeMath.div(
            SafeMath.mul(tokens2Trade, goodwill),
            10000
        );

        if (goodwillPortion == 0) {
            return 0;
        }

        require(
            IERC20(_tokenContractAddress).transfer(
                dzgoodwillAddress,
                goodwillPortion
            ),
            "Error in transferring BPT:1"
        );
    }

    /**
    @notice This function is used to zapin to balancer pool
    @param _ToBalancerPoolAddress The address of balancer pool to zap in
    @param _FromTokenContractAddress The token used to zap in
    @param tokens2Trade The amount of tokens to invest
    @return The quantity of Balancer Pool tokens returned
     */
    function _enter2Balancer(
        address _ToBalancerPoolAddress,
        address _FromTokenContractAddress,
        uint256 tokens2Trade
    ) internal returns (uint256 poolTokensOut) {
        require(
            IBPool_Balancer_ZapIn_General_V1(_ToBalancerPoolAddress).isBound(
                _FromTokenContractAddress
            ),
            "Token not bound"
        );

        uint256 allowance = IERC20(_FromTokenContractAddress).allowance(
            address(this),
            _ToBalancerPoolAddress
        );

        if (allowance < tokens2Trade) {
            IERC20(_FromTokenContractAddress).approve(
                _ToBalancerPoolAddress,
                uint256(-1)
            );
        }

        poolTokensOut = IBPool_Balancer_ZapIn_General_V1(_ToBalancerPoolAddress)
            .joinswapExternAmountIn(_FromTokenContractAddress, tokens2Trade, 1);

        require(poolTokensOut > 0, "Error in entering balancer pool");
    }

    /**
    @notice This function finds best token from the final tokens of balancer pool
    @param _ToBalancerPoolAddress The address of balancer pool to zap in
    @param _amount amount of eth/erc to invest
    @param _FromTokenContractAddress the token address which is used to invest
    @return The token address having max liquidity
     */
    function _getBestDeal(
        address _ToBalancerPoolAddress,
        uint256 _amount,
        address _FromTokenContractAddress
    ) internal view returns (address _token) {
        //get token list
        address[] memory tokens = IBPool_Balancer_ZapIn_General_V1(
            _ToBalancerPoolAddress
        ).getFinalTokens();

        uint256 amount = _amount;

        if (_FromTokenContractAddress != address(0)) {
            // check if isBound()
            bool isBound = IBPool_Balancer_ZapIn_General_V1(
                _ToBalancerPoolAddress
            ).isBound(_FromTokenContractAddress);

            if (isBound) return _FromTokenContractAddress;

            //get eth value for given token

            Iuniswap_Balancer_ZapIn_General_V1 FromUniSwapExchangeContractAddress
            = Iuniswap_Balancer_ZapIn_General_V1(
                UniSwapFactoryAddress.getExchange(_FromTokenContractAddress)
            );

            //get qty of eth expected
            amount = Iuniswap_Balancer_ZapIn_General_V1(
                FromUniSwapExchangeContractAddress
            ).getTokenToEthInputPrice(_amount);
        }

        uint256 maxBPT;

        for (uint256 index = 0; index < tokens.length; index++) {

            Iuniswap_Balancer_ZapIn_General_V1 FromUniSwapExchangeContractAddress
            = Iuniswap_Balancer_ZapIn_General_V1(
                UniSwapFactoryAddress.getExchange(tokens[index])
            );

            if (address(FromUniSwapExchangeContractAddress) == address(0)) {
                continue;
            }

            //get qty of tokens
            uint256 expectedTokens = Iuniswap_Balancer_ZapIn_General_V1(
                FromUniSwapExchangeContractAddress
            ).getEthToTokenInputPrice(amount);

            //get bpt for given tokens
            uint256 expectedBPT = getToken2BPT(
                _ToBalancerPoolAddress,
                expectedTokens,
                tokens[index]
            );

            //get token giving max BPT
            if (maxBPT < expectedBPT) {
                maxBPT = expectedBPT;
                _token = tokens[index];
            }
        }
    }

    /**
    @notice Function gives the expected amount of pool tokens on investing
    @param _ToBalancerPoolAddress Address of balancer pool to zapin
    @param _IncomingERC The amount of ERC to invest
    @param _FromToken Address of token to zap in with
    @return Amount of BPT token
     */
    function getToken2BPT(
        address _ToBalancerPoolAddress,
        uint256 _IncomingERC,
        address _FromToken
    ) internal view returns (uint256 tokensReturned) {
        uint256 totalSupply = IBPool_Balancer_ZapIn_General_V1(
            _ToBalancerPoolAddress
        ).totalSupply();
        uint256 swapFee = IBPool_Balancer_ZapIn_General_V1(
            _ToBalancerPoolAddress
        ).getSwapFee();
        uint256 totalWeight = IBPool_Balancer_ZapIn_General_V1(
            _ToBalancerPoolAddress
        ).getTotalDenormalizedWeight();
        uint256 balance = IBPool_Balancer_ZapIn_General_V1(
            _ToBalancerPoolAddress
        ).getBalance(_FromToken);
        uint256 denorm = IBPool_Balancer_ZapIn_General_V1(
            _ToBalancerPoolAddress
        ).getDenormalizedWeight(_FromToken);

        tokensReturned = IBPool_Balancer_ZapIn_General_V1(
            _ToBalancerPoolAddress
        ).calcPoolOutGivenSingleIn(
            balance,
            denorm,
            totalSupply,
            totalWeight,
            _IncomingERC,
            swapFee
        );
    }

    /**
    @notice This function is used to buy tokens from eth
    @param _tokenContractAddress Token address which we want to buy
    @return The quantity of token bought
     */
    function _eth2Token(address _tokenContractAddress, uint256 slippage)
        internal
        returns (uint256 tokenBought)
    {
        if(_tokenContractAddress == wethTokenAddress) {
            IWETH(wethTokenAddress).deposit.value(msg.value)();
            return msg.value;
        }

        Iuniswap_Balancer_ZapIn_General_V1 FromUniSwapExchangeContractAddress
        = Iuniswap_Balancer_ZapIn_General_V1(
            UniSwapFactoryAddress.getExchange(_tokenContractAddress)
        );

        uint256 minTokenBought = FromUniSwapExchangeContractAddress
            .getEthToTokenInputPrice(msg.value);
        minTokenBought = SafeMath.div(
            SafeMath.mul(minTokenBought, SafeMath.sub(10000, slippage)),
            10000
        );

        tokenBought = FromUniSwapExchangeContractAddress
            .ethToTokenSwapInput
            .value(msg.value)(minTokenBought, SafeMath.add(now, 300));
    }

    /**
    @notice This function is used to swap tokens
    @param _FromTokenContractAddress The token address to swap from
    @param _ToTokenContractAddress The token address to swap to
    @param tokens2Trade The amount of tokens to swap
    @return The quantity of tokens bought
     */
    function _token2Token(
        address _FromTokenContractAddress,
        address _ToTokenContractAddress,
        uint256 tokens2Trade,
        uint256 slippage
    ) internal returns (uint256 tokenBought) {

        Iuniswap_Balancer_ZapIn_General_V1 FromUniSwapExchangeContractAddress
        = Iuniswap_Balancer_ZapIn_General_V1(
            UniSwapFactoryAddress.getExchange(_FromTokenContractAddress)
        );

        Iuniswap_Balancer_ZapIn_General_V1 ToUniSwapExchangeContractAddress
        = Iuniswap_Balancer_ZapIn_General_V1(
            UniSwapFactoryAddress.getExchange(_ToTokenContractAddress)
        );

        IERC20(_FromTokenContractAddress).approve(
            address(FromUniSwapExchangeContractAddress),
            tokens2Trade
        );

        uint256 minEthBought = FromUniSwapExchangeContractAddress
            .getTokenToEthInputPrice(tokens2Trade);
        minEthBought = SafeMath.div(
            SafeMath.mul(minEthBought, SafeMath.sub(10000, slippage)),
            10000
        );

        uint256 minTokenBought = ToUniSwapExchangeContractAddress
            .getEthToTokenInputPrice(minEthBought);
        minTokenBought = SafeMath.div(
            SafeMath.mul(minTokenBought, SafeMath.sub(10000, slippage)),
            10000
        );

        tokenBought = FromUniSwapExchangeContractAddress.tokenToTokenSwapInput(
            tokens2Trade,
            minTokenBought,
            minEthBought,
            SafeMath.add(now, 1800),
            _ToTokenContractAddress
        );
        require(tokenBought > 0, "Error in swapping ERC: 1");
    }

    function updateSlippage(uint256 _newSlippage) public onlyOwner {
        require(
            _newSlippage > 0 && _newSlippage < 10000,
            "Slippage Value not allowed"
        );
        defaultSlippage = _newSlippage;
    }

    function set_new_goodwill(uint16 _new_goodwill) public onlyOwner {
        require(
            _new_goodwill >= 0 && _new_goodwill < 10000,
            "GoodWill Value not allowed"
        );
        goodwill = _new_goodwill;
    }

    function set_new_dzgoodwillAddress(address _new_dzgoodwillAddress)
        public
        onlyOwner
    {
        dzgoodwillAddress = _new_dzgoodwillAddress;
    }

    function inCaseTokengetsStuck(IERC20 _TokenAddress) public onlyOwner {
        uint256 qty = _TokenAddress.balanceOf(address(this));
        _TokenAddress.transfer(owner(), qty);
    }

    // - to Pause the contract
    function toggleContractActive() public onlyOwner {
        stopped = !stopped;
    }

    // - to withdraw any ETH balance sitting in the contract
    function withdraw() public onlyOwner {
        uint256 contractBalance = address(this).balance;
        address payable _to = owner().toPayable();
        _to.transfer(contractBalance);
    }

    // - to kill the contract
    function destruct() public onlyOwner {
        address payable _to = owner().toPayable();
        selfdestruct(_to);
    }

    function() external payable {}
}
function getToken2BPT(
    address _ToBalancerPoolAddress,
    uint256 _IncomingERC,
    address _FromToken
) internal view returns (uint256 tokensReturned) {
    uint256 totalSupply = IBPool_Balancer_ZapIn_General_V1(
        _ToBalancerPoolAddress
    ).totalSupply();
    uint256 swapFee = IBPool_Balancer_ZapIn_General_V1(
        _ToBalancerPoolAddress
    ).getSwapFee();
    uint256 totalWeight = IBPool_Balancer_ZapIn_General_V1(
        _ToBalancerPoolAddress
    ).getTotalDenormalizedWeight();
    uint256 balance = IBPool_Balancer_ZapIn_General_V1(
        _ToBalancerPoolAddress
    ).getBalance(_FromToken);
    uint256 denorm = IBPool_Balancer_ZapIn_General_V1(
        _ToBalancerPoolAddress
    ).getDenormalizedWeight(_FromToken);

    tokensReturned = IBPool_Balancer_ZapIn_General_V1(
        _ToBalancerPoolAddress
    ).calcPoolOutGivenSingleIn(
        balance,
        denorm,
        totalSupply,
        totalWeight,
        _IncomingERC,
        swapFee
    );
}

/**
 @notice Function gives the expected amount of pool tokens on investing
 @param _ToBalancerPoolAddress Address of balancer pool to zapin
 @param _IncomingERC The amount of ERC to invest
 @param _FromToken Address of token to zap in with
 @return Amount of BPT token
 */