library ReefUniswap {
    using SafeMath for uint256;
    using Address for address;

    address public constant uniswapV2RouterAddress = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
    address private constant wethTokenAddress = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;

    IUniswapV2Router private constant uniswapV2Router = IUniswapV2Router(
        uniswapV2RouterAddress
    );

    IUniswapV2Factory private constant UniSwapV2FactoryAddress = IUniswapV2Factory(
        0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f
    );


    function _investIntoUniswapPool(
        address _FromTokenContractAddress,
        address _ToUnipoolToken0,
        address _ToUnipoolToken1,
        address _toAccount,
        uint256 _amount
    ) internal returns (uint256) {
        uint256 token0Bought;
        uint256 token1Bought;

        if (canSwapFromV2(_ToUnipoolToken0, _ToUnipoolToken1)) {
            (token0Bought, token1Bought) = exchangeTokensV2(
                _FromTokenContractAddress,
                _ToUnipoolToken0,
                _ToUnipoolToken1,
                _amount
            );
        }

        require(token0Bought > 0 && token1Bought > 0, "Could not exchange");

        TransferHelper.safeApprove(
            _ToUnipoolToken0,
            address(uniswapV2Router),
            token0Bought
        );

        TransferHelper.safeApprove(
            _ToUnipoolToken1,
            address(uniswapV2Router),
            token1Bought
        );

        (uint256 amountA, uint256 amountB, uint256 LP) = uniswapV2Router
            .addLiquidity(
            _ToUnipoolToken0,
            _ToUnipoolToken1,
            token0Bought,
            token1Bought,
            1,
            1,
            _toAccount,
            now + 60
        );

        uint256 residue;
        if (SafeMath.sub(token0Bought, amountA) > 0) {
            if (canSwapFromV2(_ToUnipoolToken0, _FromTokenContractAddress)) {
                residue = swapFromV2(
                    _ToUnipoolToken0,
                    _FromTokenContractAddress,
                    SafeMath.sub(token0Bought, amountA)
                );
            } else {
                TransferHelper.safeTransfer(
                    _ToUnipoolToken0,
                    msg.sender,
                    SafeMath.sub(token0Bought, amountA)
                );
            }
        }

        if (SafeMath.sub(token1Bought, amountB) > 0) {
            if (canSwapFromV2(_ToUnipoolToken1, _FromTokenContractAddress)) {
                residue += swapFromV2(
                    _ToUnipoolToken1,
                    _FromTokenContractAddress,
                    SafeMath.sub(token1Bought, amountB)
                );
            } else {
                TransferHelper.safeTransfer(
                    _ToUnipoolToken1,
                    msg.sender,
                    SafeMath.sub(token1Bought, amountB)
                );
            }
        }

        if (residue > 0) {
            TransferHelper.safeTransfer(
                _FromTokenContractAddress,
                msg.sender,
                residue
            );
        }

        return LP;
    }

    /**
    @notice This function is used to zapout of given Uniswap pair in the bounded tokens
    @param _token0 Token 0 address
    @param _token1 Token 1 address
    @param _IncomingLP The amount of LP
    @return amountA the amount of first token received after zapout
    @return amountB the amount of second token received after zapout
     */
    function _disinvestFromUniswapPool(
        address _ToTokenContractAddress,
        address _token0,
        address _token1,
        uint256 _IncomingLP
    ) internal returns (uint256 amountA, uint256 amountB) {
        address _FromUniPoolAddress = UniSwapV2FactoryAddress.getPair(
            _token0,
            _token1
        );
        IUniswapV2Pair pair = IUniswapV2Pair(_FromUniPoolAddress);
        require(address(pair) != address(0), "Error: Invalid Unipool Address");

        TransferHelper.safeApprove(
            _FromUniPoolAddress,
            address(uniswapV2Router),
            _IncomingLP
        );

        if (_token0 == wethTokenAddress || _token1 == wethTokenAddress) {
            address _token = _token0 == wethTokenAddress ? _token1 : _token0;
            address _wethToken = _token0 != wethTokenAddress
                ? _token1
                : _token0;
            (amountA, amountB) = uniswapV2Router.removeLiquidityETH(
                _token,
                _IncomingLP,
                1,
                1,
                address(this),
                now + 60
            );

            if (canSwapFromV2(_token1, _ToTokenContractAddress)) {
                swapFromV2(_token, _ToTokenContractAddress, amountA);
            } else {
                TransferHelper.safeTransfer(_token, msg.sender, amountA);
            }
        } else {
            (amountA, amountB) = uniswapV2Router.removeLiquidity(
                _token0,
                _token1,
                _IncomingLP,
                1,
                1,
                address(this),
                now + 60
            );

            if (canSwapFromV2(_token0, _ToTokenContractAddress)) {
                swapFromV2(_token0, _ToTokenContractAddress, amountA);
            } else {
                TransferHelper.safeTransfer(_token0, msg.sender, amountA);
            }

            if (canSwapFromV2(_token1, _ToTokenContractAddress)) {
                swapFromV2(_token1, _ToTokenContractAddress, amountB);
            } else {
                TransferHelper.safeTransfer(_token1, msg.sender, amountB);
            }
        }
    }


    function exchangeTokensV2(
        address _FromTokenContractAddress,
        address _ToUnipoolToken0,
        address _ToUnipoolToken1,
        uint256 _amount
    ) internal returns (uint256 token0Bought, uint256 token1Bought) {
        IUniswapV2Pair pair = IUniswapV2Pair(
            UniSwapV2FactoryAddress.getPair(_ToUnipoolToken0, _ToUnipoolToken1)
        );
        (uint256 res0, uint256 res1, ) = pair.getReserves();
        if (
            canSwapFromV2(_FromTokenContractAddress, _ToUnipoolToken0) &&
            canSwapFromV2(_ToUnipoolToken0, _ToUnipoolToken1)
        ) {
            token0Bought = swapFromV2(
                _FromTokenContractAddress,
                _ToUnipoolToken0,
                _amount
            );
            uint256 amountToSwap = calculateSwapInAmount(res0, token0Bought);
            //if no reserve or a new pair is created
            if (amountToSwap <= 0) amountToSwap = SafeMath.div(token0Bought, 2);
            token1Bought = swapFromV2(
                _ToUnipoolToken0,
                _ToUnipoolToken1,
                amountToSwap
            );
            token0Bought = SafeMath.sub(token0Bought, amountToSwap);
        } else if (
            canSwapFromV2(_FromTokenContractAddress, _ToUnipoolToken1) &&
            canSwapFromV2(_ToUnipoolToken0, _ToUnipoolToken1)
        ) {
            token1Bought = swapFromV2(
                _FromTokenContractAddress,
                _ToUnipoolToken1,
                _amount
            );
            uint256 amountToSwap = calculateSwapInAmount(res1, token1Bought);
            //if no reserve or a new pair is created
            if (amountToSwap <= 0) amountToSwap = SafeMath.div(token1Bought, 2);
            token0Bought = swapFromV2(
                _ToUnipoolToken1,
                _ToUnipoolToken0,
                amountToSwap
            );
            token1Bought = SafeMath.sub(token1Bought, amountToSwap);
        }
    }

    function canSwapFromV2(address _fromToken, address _toToken)
        public
        view
        returns (bool)
    {
        require(
            _fromToken != address(0) || _toToken != address(0),
            "Invalid Exchange values"
        );

        if (_fromToken == _toToken) return true;

        if (_fromToken == address(0) || _fromToken == wethTokenAddress) {
            if (_toToken == wethTokenAddress || _toToken == address(0))
                return true;
            IUniswapV2Pair pair = IUniswapV2Pair(
                UniSwapV2FactoryAddress.getPair(_toToken, wethTokenAddress)
            );
            if (_haveReserve(pair)) return true;
        } else if (_toToken == address(0) || _toToken == wethTokenAddress) {
            if (_fromToken == wethTokenAddress || _fromToken == address(0))
                return true;
            IUniswapV2Pair pair = IUniswapV2Pair(
                UniSwapV2FactoryAddress.getPair(_fromToken, wethTokenAddress)
            );
            if (_haveReserve(pair)) return true;
        } else {
            IUniswapV2Pair pair1 = IUniswapV2Pair(
                UniSwapV2FactoryAddress.getPair(_fromToken, wethTokenAddress)
            );
            IUniswapV2Pair pair2 = IUniswapV2Pair(
                UniSwapV2FactoryAddress.getPair(_toToken, wethTokenAddress)
            );
            IUniswapV2Pair pair3 = IUniswapV2Pair(
                UniSwapV2FactoryAddress.getPair(_fromToken, _toToken)
            );
            if (_haveReserve(pair1) && _haveReserve(pair2)) return true;
            if (_haveReserve(pair3)) return true;
        }
        return false;
    }

    //checks if the UNI v2 contract have reserves to swap tokens
    function _haveReserve(IUniswapV2Pair pair) internal view returns (bool) {
        if (address(pair) != address(0)) {
            (uint256 res0, uint256 res1, ) = pair.getReserves();
            if (res0 > 0 && res1 > 0) {
                return true;
            }
        }
    }

    function calculateSwapInAmount(uint256 reserveIn, uint256 userIn)
        public
        pure
        returns (uint256)
    {
        return
            Babylonian
                .sqrt(
                reserveIn.mul(userIn.mul(3988000) + reserveIn.mul(3988009))
            )
                .sub(reserveIn.mul(1997)) / 1994;
    }

    //swaps _fromToken for _toToken
    //for eth, address(0) otherwise ERC token address
    function swapFromV2(
        address _fromToken,
        address _toToken,
        uint256 amount
    ) internal returns (uint256) {
        require(
            _fromToken != address(0) || _toToken != address(0),
            "Invalid Exchange values"
        );
        if (_fromToken == _toToken) return amount;

        require(canSwapFromV2(_fromToken, _toToken), "Cannot be exchanged");
        require(amount > 0, "Invalid amount");

        if (_fromToken == address(0)) {
            if (_toToken == wethTokenAddress) {
                IWETH(wethTokenAddress).deposit{value: amount}();
                return amount;
            }
            address[] memory path = new address[](2);
            path[0] = wethTokenAddress;
            path[1] = _toToken;

            uint256[] memory amounts = uniswapV2Router.swapExactETHForTokens{
                value: amount
            }(0, path, address(this), now + 180);
            return amounts[1];
        } else if (_toToken == address(0)) {
            if (_fromToken == wethTokenAddress) {
                IWETH(wethTokenAddress).withdraw(amount);
                return amount;
            }
            address[] memory path = new address[](2);
            TransferHelper.safeApprove(
                _fromToken,
                address(uniswapV2Router),
                amount
            );
            path[0] = _fromToken;
            path[1] = wethTokenAddress;

            uint256[] memory amounts = uniswapV2Router.swapExactTokensForETH(
                amount,
                0,
                path,
                address(this),
                now + 180
            );
            return amounts[1];
        } else {
            TransferHelper.safeApprove(
                _fromToken,
                address(uniswapV2Router),
                amount
            );
            uint256 returnedAmount = _swapTokenToTokenV2(
                _fromToken,
                _toToken,
                amount
            );
            require(returnedAmount > 0, "Error in swap");
            return returnedAmount;
        }
    }

    //swaps 2 ERC tokens (UniV2)
    function _swapTokenToTokenV2(
        address _fromToken,
        address _toToken,
        uint256 amount
    ) internal returns (uint256) {
        IUniswapV2Pair pair1 = IUniswapV2Pair(
            UniSwapV2FactoryAddress.getPair(_fromToken, wethTokenAddress)
        );
        IUniswapV2Pair pair2 = IUniswapV2Pair(
            UniSwapV2FactoryAddress.getPair(_toToken, wethTokenAddress)
        );
        IUniswapV2Pair pair3 = IUniswapV2Pair(
            UniSwapV2FactoryAddress.getPair(_fromToken, _toToken)
        );

        uint256[] memory amounts;

        if (_haveReserve(pair3)) {
            address[] memory path = new address[](2);
            path[0] = _fromToken;
            path[1] = _toToken;

            amounts = uniswapV2Router.swapExactTokensForTokens(
                amount,
                0,
                path,
                address(this),
                now + 180
            );
            return amounts[1];
        } else if (_haveReserve(pair1) && _haveReserve(pair2)) {
            address[] memory path = new address[](3);
            path[0] = _fromToken;
            path[1] = wethTokenAddress;
            path[2] = _toToken;

            amounts = uniswapV2Router.swapExactTokensForTokens(
                amount,
                0,
                path,
                address(this),
                now + 180
            );
            return amounts[2];
        }
        return 0;
    }
}
function swapFromV2(
    address _fromToken,
    address _toToken,
    uint256 amount
) internal returns (uint256) {
    require(
        _fromToken != address(0) || _toToken != address(0),
        "Invalid Exchange values"
    );
    if (_fromToken == _toToken) return amount;

    require(canSwapFromV2(_fromToken, _toToken), "Cannot be exchanged");
    require(amount > 0, "Invalid amount");

    if (_fromToken == address(0)) {
        if (_toToken == wethTokenAddress) {
            IWETH(wethTokenAddress).deposit{value: amount}();
            return amount;
        }
        address[] memory path = new address[](2);
        path[0] = wethTokenAddress;
        path[1] = _toToken;

        uint256[] memory amounts = uniswapV2Router.swapExactETHForTokens{
            value: amount
        }(0, path, address(this), now + 180);
        return amounts[1];
    } else if (_toToken == address(0)) {
        if (_fromToken == wethTokenAddress) {
            IWETH(wethTokenAddress).withdraw(amount);
            return amount;
        }
        address[] memory path = new address[](2);
        TransferHelper.safeApprove(
            _fromToken,
            address(uniswapV2Router),
            amount
        );
        path[0] = _fromToken;
        path[1] = wethTokenAddress;

        uint256[] memory amounts = uniswapV2Router.swapExactTokensForETH(
            amount,
            0,
            path,
            address(this),
            now + 180
        );
        return amounts[1];
    } else {
        TransferHelper.safeApprove(
            _fromToken,
            address(uniswapV2Router),
            amount
        );
        uint256 returnedAmount = _swapTokenToTokenV2(
            _fromToken,
            _toToken,
            amount
        );
        require(returnedAmount > 0, "Error in swap");
        return returnedAmount;
    }
}

//swaps _fromToken for _toToken
//for eth, address(0) otherwise ERC token address