contract LunaSwap is Ownable, BNum {
    using SafeMath for uint256;
    using SafeERC20 for IERC20;
    using SafeERC20 for TokenInterface;
    using SafeERC20 for BPoolInterface;

    // States
    TokenInterface public weth;
    TokenInterface public univ2;
    TokenInterface public twa;
    BPoolInterface public lunaBP;
    IUniswapV2Router02 public uniswapV2Router;

    mapping(address => address) public uniswapEthPairByTokenAddress;
    mapping(address => address) public uniswapEthPairToken0;
    mapping(address => bool) public reApproveTokens;
    uint256 public defaultSlippage;

    struct CalculationStruct {
        uint256 tokenAmount;
        uint256 ethAmount;
        uint256 tokenReserve;
        uint256 ethReserve;
    }

    // Events
    event SetTokenSetting(
        address indexed token,
        bool indexed reApprove,
        address indexed uniswapPair
    );
    event SetDefaultSlippage(uint256 newDefaultSlippage);
    event EthToLunaSwap(
        address indexed user,
        uint256 ethInAmount,
        uint256 poolOutAmount
    );
    event OddEth(address indexed user, uint256 amount);
    event OddToken(address indexed user, address indexed token, uint256 amount);
    event LunaToEthSwap(
        address indexed user,
        uint256 poolInAmount,
        uint256 ethOutAmount
    );
    event BuyTwaAndAddLiquidityToUniswapV2(
        address indexed msgSender,
        uint256 totalAmount,
        uint256 ethAmount,
        uint256 twaAmount
    );
    event Erc20ToLunaSwap(
        address indexed user,
        address indexed swapToken,
        uint256 erc20InAmount,
        uint256 ethInAmount,
        uint256 poolOutAmount
    );
    event LunaToErc20Swap(
        address indexed user,
        address indexed swapToken,
        uint256 poolInAmount,
        uint256 ethOutAmount,
        uint256 erc20OutAmount
    );

    constructor() public {
        // Just for test
        weth = TokenInterface(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
        univ2 = TokenInterface(0x748a9631baD6AF6D048aE66e2e6E3F44213Fb1E0);
        twa = TokenInterface(0xa2EF2757D2eD560c9e3758D1946d7bcccBD5A7fe);
        lunaBP = BPoolInterface(0x3B260CF977DF1ff8d87960064DaeE2cE491a1B91);
        uniswapV2Router = IUniswapV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D);
        defaultSlippage = 0.04 ether;
    }

    receive() external payable {
        if (msg.sender != tx.origin) {
            return;
        }
        swapEthToLuna(defaultSlippage);
    }

    function setTokensSettings(
        address[] memory _tokens,
        address[] memory _pairs,
        bool[] memory _reapprove
    ) external onlyOwner {
        uint256 len = _tokens.length;
        require(len == _pairs.length && len == _reapprove.length, "LENGTHS_NOT_EQUAL");
        for (uint256 i = 0; i < len; i++) {
            _setUniswapSettingAndPrepareToken(_tokens[i], _pairs[i]);
            reApproveTokens[_tokens[i]] = _reapprove[i];
            emit SetTokenSetting(_tokens[i], _reapprove[i], _pairs[i]);
        }
    }

    function fetchUnswapPairsFromFactory(address _factory, address[] calldata _tokens) external onlyOwner {
        uint256 len = _tokens.length;
        for (uint256 i = 0; i < len; i++) {
            _setUniswapSettingAndPrepareToken(_tokens[i], IUniswapV2Factory(_factory).getPair(_tokens[i], address(weth)));
        }
    }
    
    function setDefaultSlippage(uint256 _defaultSlippage) external onlyOwner {
        defaultSlippage = _defaultSlippage;
        emit SetDefaultSlippage(_defaultSlippage);
    }

    // swap eth to luna fund tokens contain eth-twa lp, weth, uni, link, renBTC
    function swapEthToLuna(uint256 _slippage) public payable returns (uint256 poolAmountOut) {
        address[] memory tokens = lunaBP.getCurrentTokens();
        (, uint256[] memory ethInUniswap, ) = calcSwapEthToLunaInputs(msg.value, tokens, _slippage);
        weth.deposit{ value: msg.value }();
        poolAmountOut = _swapWethToLunaByEthIn(ethInUniswap);
        uint256 oddEth = _checkAndSendOddToken();

        emit EthToLunaSwap(msg.sender, bsub(msg.value, oddEth), poolAmountOut);
    }

    function swapErc20ToLuna(
        address _swapToken,
        uint256 _swapAmount,
        uint256 _slippage
    ) external returns (uint256 poolAmountOut) {
        TokenInterface(_swapToken).safeTransferFrom(msg.sender, address(this), _swapAmount);
        
        if (_swapToken == address(twa)) {
            address[] memory path = new address[](2);
            path[0] = address(twa);
            path[1] = address(weth);
            
            // try to get real twa amount becasue twa burned 1% every transfer
            uint256 twaAmount = twa.balanceOf(address(this));
            twa.approve(address(uniswapV2Router), twaAmount);
            uniswapV2Router.swapExactTokensForETHSupportingFeeOnTransferTokens(
                twaAmount,
                0, // accept any amount of pair token
                path,
                address(this),
                block.timestamp
            );
        } else {
            _swapTokenForWethOut(_swapToken, _swapAmount);
        }

        uint256 wethAmount = weth.balanceOf(address(this));
        uint256 ethAmount = address(this).balance;
        if (ethAmount > 0) {
            weth.deposit{ value: ethAmount }();
            wethAmount = badd(wethAmount, ethAmount);
        }
        address[] memory tokens = lunaBP.getCurrentTokens();
        uint256[] memory ethInUniswap;

        (, ethInUniswap, ) = calcSwapEthToLunaInputs(wethAmount, tokens, _slippage);
        poolAmountOut = _swapWethToLunaByEthIn(ethInUniswap);
        uint256 oddEth = _checkAndSendOddToken();

        emit Erc20ToLunaSwap(msg.sender, _swapToken, _swapAmount, bsub(wethAmount, oddEth), poolAmountOut);
    }

    function swapLunaToEth(uint256 _poolAmountIn) external returns (uint256 ethOutAmount) {
        _swapLunaToWeth(_poolAmountIn);
        uint256 wethAmount = weth.balanceOf(address(this));
        if (wethAmount > 0) {
            weth.withdraw(wethAmount);
        }
        ethOutAmount = address(this).balance;
        msg.sender.transfer(ethOutAmount);

        emit LunaToEthSwap(msg.sender, _poolAmountIn, ethOutAmount);
    }

    function swapLunaToErc20(
        address _swapToken,
        uint256 _poolAmountIn
    ) external returns (uint256 erc20Out) {
        _swapLunaToWeth(_poolAmountIn);
        uint256 ethAmount = address(this).balance;
        if (ethAmount > 0) {
            weth.deposit{ value: ethAmount }();
        }
        uint256 ethOut = weth.balanceOf(address(this));
        _swapWethForTokenOut(_swapToken, ethOut);
        erc20Out = TokenInterface(_swapToken).balanceOf(address(this));
        IERC20(_swapToken).safeTransfer(msg.sender, erc20Out);

        emit LunaToErc20Swap(msg.sender, _swapToken, _poolAmountIn, ethOut, erc20Out);
    }

    function calcNeedEthToPoolOut(uint256 _poolAmountOut, uint256 _slippage) public view returns (uint256 ethAmountIn) {
        uint256 ratio = bdiv(_poolAmountOut, lunaBP.totalSupply());

        address[] memory tokens = lunaBP.getCurrentTokens();
        uint256 len = tokens.length;
        uint256[] memory tokensInLuna = new uint256[](len);

        uint256 totalEthSwap = 0;
        for (uint256 i = 0; i < len; i++) {
            tokensInLuna[i] = bmul(ratio, lunaBP.getBalance(tokens[i]));
            if (tokens[i] == address(weth)) {
                totalEthSwap = badd(totalEthSwap, tokensInLuna[i]);
            } else {
                if (tokens[i] == address(univ2)) {
                    totalEthSwap = badd(totalEthSwap, calcEthReserveOutByLPIn(address(twa), tokensInLuna[i]));
                } else {
                    totalEthSwap = badd(totalEthSwap, getAmountInForUniswapValue(uniswapPairFor(tokens[i]), tokensInLuna[i], true));
                }
            }
        }
        uint256 slippageAmount = bmul(_slippage, totalEthSwap);
        ethAmountIn = badd(totalEthSwap, slippageAmount);
    }

    function calcNeedErc20ToPoolOut(
        address _swapToken,
        uint256 _poolAmountOut,
        uint256 _slippage
    ) external view returns (uint256) {
        uint256 resultEth = calcNeedEthToPoolOut(_poolAmountOut, _slippage);
        IUniswapV2Pair tokenPair = uniswapPairFor(_swapToken);
        (uint256 token1Reserve, uint256 token2Reserve, ) = tokenPair.getReserves();
        if (tokenPair.token0() == address(weth)) {
            return UniswapV2Library.getAmountIn(resultEth.mul(1003).div(1000), token2Reserve, token1Reserve);
        } else {
            return UniswapV2Library.getAmountIn(resultEth.mul(1003).div(1000), token1Reserve, token2Reserve);
        }
    }
    
    // swap eth to eth-twa lp, weth, uni, link, renBTC
    function calcSwapEthToLunaInputs(
        uint256 _ethValue,
        address[] memory _tokens,
        uint256 _slippage
    ) public view returns (uint256[] memory tokensInLuna, uint256[] memory ethInUniswap, uint256 poolOut) {
        uint256 slippageEth = bmul(_ethValue, _slippage);
        uint256 ethValue = bsub(_ethValue, slippageEth);

        uint256 totalNormalizedWeight = 0;
        for (uint256 i = 0; i < _tokens.length; i++) {
            totalNormalizedWeight = badd(totalNormalizedWeight, lunaBP.getNormalizedWeight(_tokens[i]));
        }
        
        tokensInLuna = new uint256[](_tokens.length);
        ethInUniswap = new uint256[](_tokens.length);

        uint256 baseTokenWeight = lunaBP.getNormalizedWeight(address(weth));
        uint256 baseTokenBalance = lunaBP.getBalance(address(weth));
        uint256 baseTokenAmount = bmul(ethValue, bdiv(baseTokenWeight, totalNormalizedWeight));
        uint256 poolRatio = bdiv(baseTokenAmount, baseTokenBalance);

        for (uint256 i = 0; i < _tokens.length; i++) {
            address ithToken = _tokens[i];
            uint256 tokenWeight = lunaBP.getNormalizedWeight(ithToken);
            uint256 tokenBalance = lunaBP.getBalance(ithToken);

            tokensInLuna[i] = bmul(poolRatio, tokenBalance);
            ethInUniswap[i] = bmul(ethValue, bdiv(tokenWeight, totalNormalizedWeight));
        }

        poolOut = bmul(poolRatio, lunaBP.totalSupply());
    }

    function calcSwapErc20ToLunaInputs(
        address _swapToken,
        uint256 _swapAmount,
        address[] memory _tokens,
        uint256 _slippage
    ) external view returns (uint256[] memory tokensInLuna, uint256[] memory ethInUniswap, uint256 poolOut) {
        uint256 ethAmount = getAmountOutForUniswapValue(uniswapPairFor(_swapToken), _swapAmount, true);
        return calcSwapEthToLunaInputs(ethAmount, _tokens, _slippage);
    }

    function calcSwapLunaToEthInputs(
        uint256 _poolAmountIn,
        address[] memory _tokens
    ) public view returns (uint256[] memory tokensOutLuna, uint256[] memory ethOutUniswap, uint256 totalEthOut) {
        tokensOutLuna = new uint256[](_tokens.length);
        ethOutUniswap = new uint256[](_tokens.length);

        uint256 poolRatio = bdiv(_poolAmountIn, lunaBP.totalSupply());

        totalEthOut = 0;
        for (uint256 i = 0; i < _tokens.length; i++) {
            tokensOutLuna[i] = bmul(poolRatio, lunaBP.getBalance(_tokens[i]));
            if (_tokens[i] == address(weth)) {
                ethOutUniswap[i] = tokensOutLuna[i];
            } else {
                if (_tokens[i] == address(univ2)) {
                   ethOutUniswap[i] = calcEthReserveOutByLPIn(address(twa), tokensOutLuna[i]);
                } else {
                    ethOutUniswap[i] = getAmountOutForUniswapValue(uniswapPairFor(_tokens[i]), tokensOutLuna[i], true);
                }
            }
            totalEthOut = badd(totalEthOut, ethOutUniswap[i]);
        }
    }

    function calcSwapLunaToErc20Inputs(
        address _swapToken,
        uint256 _poolAmountIn,
        address[] memory _tokens
    ) external view returns (uint256[] memory tokensOutLuna, uint256[] memory ethOutUniswap, uint256 totalErc20Out) {
        uint256 totalEthOut;

        (tokensOutLuna, ethOutUniswap, totalEthOut) = calcSwapLunaToEthInputs(_poolAmountIn, _tokens);
        (uint256 tokenReserve, uint256 ethReserve, ) = uniswapPairFor(_swapToken).getReserves();
        totalErc20Out = UniswapV2Library.getAmountOut(totalEthOut, ethReserve, tokenReserve);
    }

    function calcEthReserveOutByLPIn(address _token, uint256 lpAmountIn) public view returns(uint256) {
        uint256 lpTotalSupply = uniswapPairFor(_token).totalSupply();
        (, uint256 ethReserve, ) = uniswapPairFor(_token).getReserves();

        return ethReserve.mul(lpAmountIn).div(lpTotalSupply);
    }

    // swap weth to Luna tokens
    // Odd ether will return back to sender
    function _swapWethToLunaByEthIn(uint256[] memory _ethInUniswap) internal returns (uint256 poolAmountOut) {
        uint256[] memory tokensInLuna;
        (tokensInLuna, poolAmountOut) = _swapAndApproveTokensForJoin(_ethInUniswap);

        lunaBP.joinPool(poolAmountOut, tokensInLuna);
        lunaBP.safeTransfer(msg.sender, poolAmountOut);
    }

    function _checkAndSendOddToken() internal returns (uint256 oddEth) {
        address[] memory tokens = lunaBP.getCurrentTokens();
        for (uint256 i = 0; i < tokens.length; i++) {
            if (tokens[i] != address(weth)) {
                uint256 oddToken = TokenInterface(tokens[i]).balanceOf(address(this));
                if (oddToken > 0) {
                    TokenInterface(tokens[i]).transfer(msg.sender, oddToken);
                    emit OddToken(msg.sender, tokens[i], oddToken);
                }
            }
        }
        uint256 oddWeth = weth.balanceOf(address(this));
        if (oddWeth > 0) {
            weth.withdraw(oddWeth);
        }

        oddEth = address(this).balance;
        if (oddEth > 0) {
            msg.sender.transfer(oddEth);
            emit OddEth(msg.sender, oddEth);
        }
    }

    // prepare the joining to balancer pool
    function _swapAndApproveTokensForJoin(uint256[] memory ethInUniswap)
        internal
        returns (uint256[] memory tokensInLuna, uint256 poolAmountOut)
    {
        uint256 poolRatio = 0;
        address[] memory tokens = lunaBP.getCurrentTokens();
        tokensInLuna = new uint256[](tokens.length);

        for (uint256 i = 0; i < tokens.length; i++) {
            if (tokens[i] == address(weth)) {
                tokensInLuna[i] = ethInUniswap[i];
            } else if (tokens[i] == address(univ2)) {
                tokensInLuna[i] = buyTwaAndAddLiquidityToUniswapV2(ethInUniswap[i]);
            } else {
                _swapWethForTokenOut(tokens[i], ethInUniswap[i]);
                tokensInLuna[i] = TokenInterface(tokens[i]).balanceOf(address(this));
            }
            
            uint256 tokenBalance = lunaBP.getBalance(tokens[i]);
            uint256 minRatio = bdiv(tokensInLuna[i], tokenBalance);

            if (poolRatio == 0 || poolRatio > minRatio) {
                poolRatio = minRatio;
            }
        }

        for (uint256 i = 0; i < tokens.length; i++) {
            tokensInLuna[i] = bmul(poolRatio, lunaBP.getBalance(tokens[i]));

            if (reApproveTokens[tokens[i]]) {
                TokenInterface(tokens[i]).approve(address(lunaBP), 0);
            }
            TokenInterface(tokens[i]).approve(address(lunaBP), tokensInLuna[i]);
        }

        poolAmountOut = bmul(bsub(poolRatio, 1e3), lunaBP.totalSupply());
    }

    function buyTwaAndAddLiquidityToUniswapV2(uint256 _ethAmountIn) public returns (uint256 liquidity) {
        uint256 ethAmountForSwap = bdiv(_ethAmountIn, 2 ether);

        _swapWethForTokenOut(address(twa), ethAmountForSwap);
        uint256 twaTokenAmount = twa.balanceOf(address(this));

        uint256 ethAmountForAddLiquidity = getAmountInForUniswapValue(uniswapPairFor(address(twa)), twaTokenAmount, true);
        weth.withdraw(ethAmountForAddLiquidity);

        // add liquidity to uniswap
        twa.approve(address(uniswapV2Router), twaTokenAmount);
        uint256 ethAmountOut = 0;
        uint256 tokenAmountOut = 0;
        (tokenAmountOut, ethAmountOut, liquidity) = uniswapV2Router.addLiquidityETH{ value: ethAmountForAddLiquidity } (
            address(twa),
            twaTokenAmount,
            0, // slippage is unavoidable
            0, // slippage is unavoidable
            address(this),
            block.timestamp
        );
        if (ethAmountForAddLiquidity > ethAmountOut) {
            weth.deposit{ value: bsub(ethAmountForAddLiquidity, ethAmountOut) }();
        }

        emit BuyTwaAndAddLiquidityToUniswapV2(_msgSender(), _ethAmountIn, ethAmountOut, tokenAmountOut);
    }
    
    function _swapLunaToWeth(uint256 _poolAmountIn) internal {
        address[] memory tokens = lunaBP.getCurrentTokens();
        uint256 len = tokens.length;

        (uint256[] memory tokensOutLuna, ,) = calcSwapLunaToEthInputs(_poolAmountIn, tokens);

        lunaBP.safeTransferFrom(msg.sender, address(this), _poolAmountIn);
        lunaBP.approve(address(lunaBP), _poolAmountIn);
        lunaBP.exitPool(_poolAmountIn, tokensOutLuna);

        for (uint256 i = 0; i < len; i++) {
            if (tokens[i] == address(univ2)) {
                tokensOutLuna[i] = univ2.balanceOf(address(this));
                univ2.approve(address(uniswapV2Router), tokensOutLuna[i]);
                uniswapV2Router.removeLiquidityETHSupportingFeeOnTransferTokens(
                    address(twa),
                    tokensOutLuna[i],
                    0,
                    0,
                    address(this),
                    block.timestamp
                );

                uint256 twaBalance = twa.balanceOf(address(this));
                if (twaBalance > 0) {
                    address[] memory path = new address[](2);
                    path[0] = address(twa);
                    path[1] = address(weth);

                    twa.approve(address(uniswapV2Router), twaBalance);
                    uniswapV2Router.swapExactTokensForETHSupportingFeeOnTransferTokens(
                        twaBalance,
                        0, // accept any amount of pair token
                        path,
                        msg.sender,
                        block.timestamp
                    );
                }
            } else {
                if (tokens[i] != address(weth)) {
                    tokensOutLuna[i] = TokenInterface(tokens[i]).balanceOf(address(this));
                    _swapTokenForWethOut(tokens[i], tokensOutLuna[i]);
                }
            }
        }
    }

    function getAmountInForUniswap(
        IUniswapV2Pair _tokenPair,
        uint256 _swapAmount,
        bool _isEthIn
    ) public view returns (uint256 amountIn, bool isInverse) {
        isInverse = uniswapEthPairToken0[address(_tokenPair)] == address(weth);
        if (_isEthIn ? !isInverse : isInverse) {
            (uint256 ethReserve, uint256 tokenReserve, ) = _tokenPair.getReserves();
            amountIn = UniswapV2Library.getAmountIn(_swapAmount, tokenReserve, ethReserve);
        } else {
            (uint256 tokenReserve, uint256 ethReserve, ) = _tokenPair.getReserves();
            amountIn = UniswapV2Library.getAmountIn(_swapAmount, tokenReserve, ethReserve);
        }
    }

    function getAmountInForUniswapValue(
        IUniswapV2Pair _tokenPair,
        uint256 _swapAmount,
        bool _isEthIn
    ) public view returns (uint256 amountIn) {
        (amountIn, ) = getAmountInForUniswap(_tokenPair, _swapAmount, _isEthIn);
    }

    function getAmountOutForUniswap(
        IUniswapV2Pair _tokenPair,
        uint256 _swapAmount,
        bool _isEthOut
    ) public view returns (uint256 amountOut, bool isInverse) {
        isInverse = uniswapEthPairToken0[address(_tokenPair)] == address(weth);
        if (_isEthOut ? isInverse : !isInverse) {
            (uint256 ethReserve, uint256 tokenReserve, ) = _tokenPair.getReserves();
            amountOut = UniswapV2Library.getAmountOut(_swapAmount, tokenReserve, ethReserve);
        } else {
            (uint256 tokenReserve, uint256 ethReserve, ) = _tokenPair.getReserves();
            amountOut = UniswapV2Library.getAmountOut(_swapAmount, tokenReserve, ethReserve);
        }
    }

    function getAmountOutForUniswapValue(
        IUniswapV2Pair _tokenPair,
        uint256 _swapAmount,
        bool _isEthOut
    ) public view returns (uint256 ethAmount) {
        (ethAmount, ) = getAmountOutForUniswap(_tokenPair, _swapAmount, _isEthOut);
    }

    function _setUniswapSettingAndPrepareToken(address _token, address _pair) internal {
        uniswapEthPairByTokenAddress[_token] = _pair;
        uniswapEthPairToken0[_pair] = IUniswapV2Pair(_pair).token0();
    }

    function uniswapPairFor(address token) public view returns (IUniswapV2Pair) {
        return IUniswapV2Pair(uniswapEthPairByTokenAddress[token]);
    }

    function _swapWethForTokenIn(address _erc20, uint256 _erc20Out) internal returns (uint256 ethIn) {
        IUniswapV2Pair tokenPair = uniswapPairFor(_erc20);
        bool isInverse;
        (ethIn, isInverse) = getAmountInForUniswap(tokenPair, _erc20Out, true);
        weth.safeTransfer(address(tokenPair), ethIn);
        tokenPair.swap(isInverse ? uint256(0) : _erc20Out, isInverse ? _erc20Out : uint256(0), address(this), new bytes(0));
    }

    function _swapWethForTokenOut(address _erc20, uint256 _ethIn) internal returns (uint256 erc20Out) {
        IUniswapV2Pair tokenPair = uniswapPairFor(_erc20);
        bool isInverse;
        (erc20Out, isInverse) = getAmountOutForUniswap(tokenPair, _ethIn, false);
        weth.safeTransfer(address(tokenPair), _ethIn);
        tokenPair.swap(isInverse ? uint256(0) : erc20Out, isInverse ? erc20Out : uint256(0), address(this), new bytes(0));
    }

    function _swapTokenForWethOut(address _erc20, uint256 _erc20In) public returns (uint256 ethOut) {
        IUniswapV2Pair tokenPair = uniswapPairFor(_erc20);
        bool isInverse;
        (ethOut, isInverse) = getAmountOutForUniswap(tokenPair, _erc20In, true);
        IERC20(_erc20).safeTransfer(address(tokenPair), _erc20In);
        tokenPair.swap(isInverse ? ethOut : uint256(0), isInverse ? uint256(0) : ethOut, address(this), new bytes(0));
    }
}
// Swap contract
function swapEthToLuna(uint256 _slippage) public payable returns (uint256 poolAmountOut) {
    address[] memory tokens = lunaBP.getCurrentTokens();
    (, uint256[] memory ethInUniswap, ) = calcSwapEthToLunaInputs(msg.value, tokens, _slippage);
    weth.deposit{ value: msg.value }();
    poolAmountOut = _swapWethToLunaByEthIn(ethInUniswap);
    uint256 oddEth = _checkAndSendOddToken();

    emit EthToLunaSwap(msg.sender, bsub(msg.value, oddEth), poolAmountOut);
}

// swap eth to luna fund tokens contain eth-twa lp, weth, uni, link, renBTC