library ReefBalancer {
    using SafeMath for uint256;
    using Address for address;
    using SafeERC20 for IERC20;

    IUniswapV2Factory
        private constant UniSwapV2FactoryAddress = IUniswapV2Factory(
        0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f
    );
    IUniswapV2Router private constant uniswapRouter = IUniswapV2Router(
        0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D
    );
    IBFactory private constant BalancerFactory = IBFactory(
        0x9424B1412450D0f8Fc2255FAf6046b98213B76Bd
    );

    address
        private constant wethTokenAddress = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;

    uint256
        private constant deadline = 0xf000000000000000000000000000000000000000000000000000000000000000;

    /**
    @notice This function for investing into BalancerPool
    @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 _minPoolTokens for slippage
    @return tokensBought The quantity of Balancer Pool tokens returned
    */
    function investIntoBalancerPool(
        address _toWhomToIssue,
        address _FromTokenContractAddress,
        address _ToBalancerPoolAddress,
        uint256 _amount,
        uint256 _minPoolTokens
    ) public returns (uint256 tokensBought) {

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

        // check if isBound()
        bool isBound = IBPool(_ToBalancerPoolAddress).isBound(
            _FromTokenContractAddress
        );

        uint256 balancerTokens;

        if (isBound) {
            balancerTokens = _enter2Balancer(
                _ToBalancerPoolAddress,
                _FromTokenContractAddress,
                _amount,
                _minPoolTokens
            );
        } else {
            // swap tokens or eth
            uint256 tokenBought;
            if (_FromTokenContractAddress == address(0)) {
                tokenBought = ReefUniswap.swapFromV2(_FromTokenContractAddress,
                                                     _IntermediateToken, _amount);
            } else {
                tokenBought = _token2Token(
                    _FromTokenContractAddress,
                    address(this),
                    _IntermediateToken,
                    _amount
                );
            }

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

        //transfer tokens to user
        IERC20(_ToBalancerPoolAddress).safeTransfer(
            _toWhomToIssue,
            balancerTokens
        );

        return balancerTokens;
    }

    /**
    @notice This function is used for zapping out of balancer pools
    @param _ToTokenContractAddress The token in which we want zapout (for ethers, its zero address)
    @param _FromBalancerPoolAddress The address of balancer pool to zap out
    @param _IncomingBPT The quantity of balancer pool tokens
    @param _minTokensRec slippage user wants
    @return success or failure
    */
    function disinvestFromBalancerPool(
        address payable _toWhomToIssue,
        address _ToTokenContractAddress,
        address _FromBalancerPoolAddress,
        uint256 _IncomingBPT,
        uint256 _minTokensRec
    ) public returns (uint256) {
        require(
            BalancerFactory.isBPool(_FromBalancerPoolAddress),
            "Invalid Balancer Pool"
        );

        address _FromTokenAddress;
        if (IBPool(_FromBalancerPoolAddress).isBound(_ToTokenContractAddress)) {
            _FromTokenAddress = _ToTokenContractAddress;
        } else if (
            _ToTokenContractAddress == address(0) &&
            IBPool(_FromBalancerPoolAddress).isBound(wethTokenAddress)
        ) {
            _FromTokenAddress = wethTokenAddress;
        } else {
            _FromTokenAddress = _getBestDeal(
                _FromBalancerPoolAddress,
                _IncomingBPT
            );
        }
        return (
            _performZapOut(
                _toWhomToIssue,
                _ToTokenContractAddress,
                _FromBalancerPoolAddress,
                _IncomingBPT,
                _FromTokenAddress,
                _minTokensRec
            )
        );
    }

    /**
    @notice This method is called by disinvestFromBalancerPool()
    @param _toWhomToIssue is the address of user
    @param _ToTokenContractAddress is the address of the token to which you want to convert to
    @param _FromBalancerPoolAddress the address of the Balancer Pool from which you want to ZapOut
    @param _IncomingBPT is the quantity of Balancer Pool tokens that the user wants to ZapOut
    @param _IntermediateToken is the token to which the Balancer Pool should be Zapped Out
    @notice this is only used if the outgoing token is not amongst the Balancer Pool tokens
    @return success or failure
    */
    function _performZapOut(
        address payable _toWhomToIssue,
        address _ToTokenContractAddress,
        address _FromBalancerPoolAddress,
        uint256 _IncomingBPT,
        address _IntermediateToken,
        uint256 _minTokensRec
    ) internal returns (uint256) {
        if (IBPool(_FromBalancerPoolAddress).isBound(_ToTokenContractAddress)) {
            return (
                _directZapout(
                    _FromBalancerPoolAddress,
                    _ToTokenContractAddress,
                    _toWhomToIssue,
                    _IncomingBPT,
                    _minTokensRec
                )
            );
        }

        //exit balancer
        uint256 _returnedTokens = _exitBalancer(
            _FromBalancerPoolAddress,
            _IntermediateToken,
            _IncomingBPT
        );

        if (_ToTokenContractAddress == address(0)) {
            uint256 ethBought = ReefUniswap.swapFromV2(_IntermediateToken, address(0),
                                               _returnedTokens);

            require(ethBought >= _minTokensRec, "High slippage");

            _toWhomToIssue.transfer(ethBought);
            return ethBought;
        } else {
            uint256 tokenBought = _token2Token(
                _IntermediateToken,
                _toWhomToIssue,
                _ToTokenContractAddress,
                _returnedTokens
            );
            require(tokenBought >= _minTokensRec, "High slippage");
            return tokenBought;
        }
    }

    /**
    @notice This function is used for zapping out of balancer pool
    @param _FromBalancerPoolAddress The address of balancer pool to zap out
    @param _ToTokenContractAddress The token in which we want to zapout (for ethers, its zero address)
    @param _toWhomToIssue The address of user
    @param tokens2Trade The quantity of balancer pool tokens
    @return returnedTokens success or failure
    */
    function _directZapout(
        address _FromBalancerPoolAddress,
        address _ToTokenContractAddress,
        address _toWhomToIssue,
        uint256 tokens2Trade,
        uint256 _minTokensRec
    ) internal returns (uint256 returnedTokens) {
        returnedTokens = _exitBalancer(
            _FromBalancerPoolAddress,
            _ToTokenContractAddress,
            tokens2Trade
        );

        require(returnedTokens >= _minTokensRec, "High slippage");

        IERC20(_ToTokenContractAddress).transfer(
            _toWhomToIssue,
            returnedTokens
        );
    }

    /**
    @notice This function gives the amount of tokens on zapping out from given
    IBPool
    @param _FromBalancerPoolAddress Address of balancer pool to zapout from
    @param _IncomingBPT The amount of BPT to zapout
    @param _toToken Address of token to zap out with
    @return tokensReturned Amount of ERC token
     */
    function _getBPT2Token(
        address _FromBalancerPoolAddress,
        uint256 _IncomingBPT,
        address _toToken
    ) internal view returns (uint256 tokensReturned) {
        uint256 totalSupply = IBPool(_FromBalancerPoolAddress).totalSupply();
        uint256 swapFee = IBPool(_FromBalancerPoolAddress).getSwapFee();
        uint256 totalWeight = IBPool(_FromBalancerPoolAddress)
            .getTotalDenormalizedWeight();
        uint256 balance = IBPool(_FromBalancerPoolAddress).getBalance(_toToken);
        uint256 denorm = IBPool(_FromBalancerPoolAddress).getDenormalizedWeight(
            _toToken
        );

        tokensReturned = IBPool(_FromBalancerPoolAddress)
            .calcSingleOutGivenPoolIn(
            balance,
            denorm,
            totalSupply,
            totalWeight,
            _IncomingBPT,
            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 tokensReturned Amount of BPT token
    */
    function getToken2BPT(
        address _ToBalancerPoolAddress,
        uint256 _IncomingERC,
        address _FromToken
    ) internal view returns (uint256 tokensReturned) {
        uint256 totalSupply = IBPool(_ToBalancerPoolAddress).totalSupply();
        uint256 swapFee = IBPool(_ToBalancerPoolAddress).getSwapFee();
        uint256 totalWeight = IBPool(_ToBalancerPoolAddress)
            .getTotalDenormalizedWeight();
        uint256 balance = IBPool(_ToBalancerPoolAddress).getBalance(_FromToken);
        uint256 denorm = IBPool(_ToBalancerPoolAddress).getDenormalizedWeight(
            _FromToken
        );

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


    /**
    @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 poolTokensOut The quantity of Balancer Pool tokens returned
    */
    function _enter2Balancer(
        address _ToBalancerPoolAddress,
        address _FromTokenContractAddress,
        uint256 tokens2Trade,
        uint256 _minPoolTokens
    ) internal returns (uint256 poolTokensOut) {
        require(
            IBPool(_ToBalancerPoolAddress).isBound(_FromTokenContractAddress),
            "Token not bound"
        );

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

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

        poolTokensOut = IBPool(_ToBalancerPoolAddress).joinswapExternAmountIn(
            _FromTokenContractAddress,
            tokens2Trade,
            _minPoolTokens
        );

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

    /**
    @notice This function is used to zap out of the given balancer pool
    @param _FromBalancerPoolAddress The address of balancer pool to zap out
    @param _ToTokenContractAddress The Token address which will be zapped out
    @param _amount The amount of token for zapout
    @return returnedTokens The amount of tokens received after zap out
     */
    function _exitBalancer(
        address _FromBalancerPoolAddress,
        address _ToTokenContractAddress,
        uint256 _amount
    ) internal returns (uint256 returnedTokens) {
        require(
            IBPool(_FromBalancerPoolAddress).isBound(_ToTokenContractAddress),
            "Token not bound"
        );

        uint256 minTokens = _getBPT2Token(
            _FromBalancerPoolAddress,
            _amount,
            _ToTokenContractAddress
        );
        minTokens = SafeMath.div(SafeMath.mul(minTokens, 98), 100);

        returnedTokens = IBPool(_FromBalancerPoolAddress).exitswapPoolAmountIn(
            _ToTokenContractAddress,
            _amount,
            minTokens
        );

        require(returnedTokens > 0, "Error in exiting balancer pool");
    }

    /**
    @notice This function is used to swap tokens
    @param _FromTokenContractAddress The token address to swap from
    @param _ToWhomToIssue The address to transfer after swap
    @param _ToTokenContractAddress The token address to swap to
    @param tokens2Trade The quantity of tokens to swap
    @return tokenBought The amount of tokens returned after swap
     */
    function _token2Token(
        address _FromTokenContractAddress,
        address _ToWhomToIssue,
        address _ToTokenContractAddress,
        uint256 tokens2Trade
    ) internal returns (uint256 tokenBought) {
        IERC20(_FromTokenContractAddress).approve(
            address(uniswapRouter),
            tokens2Trade
        );

        if (_FromTokenContractAddress != wethTokenAddress) {
            if (_ToTokenContractAddress != wethTokenAddress) {
                address[] memory path = new address[](3);
                path[0] = _FromTokenContractAddress;
                path[1] = wethTokenAddress;
                path[2] = _ToTokenContractAddress;
                tokenBought = uniswapRouter.swapExactTokensForTokens(
                    tokens2Trade,
                    1,
                    path,
                    _ToWhomToIssue,
                    deadline
                )[path.length - 1];
            } else {
                address[] memory path = new address[](2);
                path[0] = _FromTokenContractAddress;
                path[1] = wethTokenAddress;

                tokenBought = uniswapRouter.swapExactTokensForTokens(
                    tokens2Trade,
                    1,
                    path,
                    _ToWhomToIssue,
                    deadline
                )[path.length - 1];
            }
        } else {
            address[] memory path = new address[](2);
            path[0] = wethTokenAddress;
            path[1] = _ToTokenContractAddress;
            tokenBought = uniswapRouter.swapExactTokensForTokens(
                tokens2Trade,
                1,
                path,
                _ToWhomToIssue,
                deadline
            )[path.length - 1];
        }

        require(tokenBought > 0, "Error in swapping ERC: 1");
    }

    /**
    @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 _token The token address having max liquidity
    */
    function _getBestDeal(
        address _ToBalancerPoolAddress,
        uint256 _amount,
        address _FromTokenContractAddress
    ) internal view returns (address _token) {
        // If input is not eth or weth
        if (
            _FromTokenContractAddress != address(0) &&
            _FromTokenContractAddress != wethTokenAddress
        ) {
            // check if input token or weth is bound and if so return it as intermediate
            bool isBound = IBPool(_ToBalancerPoolAddress).isBound(
                _FromTokenContractAddress
            );
            if (isBound) return _FromTokenContractAddress;
        }

        bool wethIsBound = IBPool(_ToBalancerPoolAddress).isBound(
            wethTokenAddress
        );
        if (wethIsBound) return wethTokenAddress;

        //get token list
        address[] memory tokens = IBPool(_ToBalancerPoolAddress)
            .getFinalTokens();

        uint256 amount = _amount;
        address[] memory path = new address[](2);

        if (
            _FromTokenContractAddress != address(0) &&
            _FromTokenContractAddress != wethTokenAddress
        ) {
            path[0] = _FromTokenContractAddress;
            path[1] = wethTokenAddress;
            //get eth value for given token
            amount = uniswapRouter.getAmountsOut(_amount, path)[1];
        }

        uint256 maxBPT;
        path[0] = wethTokenAddress;

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

            if (tokens[index] != wethTokenAddress) {
                if (
                    UniSwapV2FactoryAddress.getPair(
                        tokens[index],
                        wethTokenAddress
                    ) == address(0)
                ) {
                    continue;
                }

                //get qty of tokens
                path[1] = tokens[index];
                uint256 expectedTokens = uniswapRouter.getAmountsOut(
                    amount,
                    path
                )[1];

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

                //get token giving max BPT
                if (maxBPT < expectedBPT) {
                    maxBPT = expectedBPT;
                    _token = tokens[index];
                }
            } else {
                //get bpt for given weth tokens
                expectedBPT = getToken2BPT(
                    _ToBalancerPoolAddress,
                    amount,
                    tokens[index]
                );
            }

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


    /**
    @notice This function finds best token from the final tokens of balancer pool
    @param _FromBalancerPoolAddress The address of balancer pool to zap out
    @param _IncomingBPT The amount of balancer pool token to covert
    @return _token The token address having max liquidity
     */
    function _getBestDeal(
        address _FromBalancerPoolAddress,
        uint256 _IncomingBPT
    ) internal view returns (address _token) {
        //get token list
        address[] memory tokens = IBPool(_FromBalancerPoolAddress)
            .getFinalTokens();

        uint256 maxEth;

        for (uint256 index = 0; index < tokens.length; index++) {
            //get token for given bpt amount
            uint256 tokensForBPT = _getBPT2Token(
                _FromBalancerPoolAddress,
                _IncomingBPT,
                tokens[index]
            );

            //get eth value for each token
            if (tokens[index] != wethTokenAddress) {
                if (
                    UniSwapV2FactoryAddress.getPair(
                        tokens[index],
                        wethTokenAddress
                    ) == address(0)
                ) {
                    continue;
                }

                address[] memory path = new address[](2);
                path[0] = tokens[index];
                path[1] = wethTokenAddress;
                uint256 ethReturned = uniswapRouter.getAmountsOut(
                    tokensForBPT,
                    path
                )[1];

                //get max eth value
                if (maxEth < ethReturned) {
                    maxEth = ethReturned;
                    _token = tokens[index];
                }
            } else {
                //get max eth value
                if (maxEth < tokensForBPT) {
                    maxEth = tokensForBPT;
                    _token = tokens[index];
                }
            }
        }
    }
}
function _exitBalancer(
    address _FromBalancerPoolAddress,
    address _ToTokenContractAddress,
    uint256 _amount
) internal returns (uint256 returnedTokens) {
    require(
        IBPool(_FromBalancerPoolAddress).isBound(_ToTokenContractAddress),
        "Token not bound"
    );

    uint256 minTokens = _getBPT2Token(
        _FromBalancerPoolAddress,
        _amount,
        _ToTokenContractAddress
    );
    minTokens = SafeMath.div(SafeMath.mul(minTokens, 98), 100);

    returnedTokens = IBPool(_FromBalancerPoolAddress).exitswapPoolAmountIn(
        _ToTokenContractAddress,
        _amount,
        minTokens
    );

    require(returnedTokens > 0, "Error in exiting balancer pool");
}

/**
 @notice This function is used to zap out of the given balancer pool
 @param _FromBalancerPoolAddress The address of balancer pool to zap out
 @param _ToTokenContractAddress The Token address which will be zapped out
 @param _amount The amount of token for zapout
 @return returnedTokens The amount of tokens received after zap out
 */