contract DefswapExchange is IDefswapExchange, ERC20, ReentrancyGuard {
    using SafeDefERC20 for IERC20;
    using SafeMath for uint256;

    IERC20 private p_base;
    IERC20 private p_token;
    IUniswapExchange private p_uniswap;

    string private constant SYMBOL_PREFIX = "SHUF-";
    string private constant NAME_SUFIX = " - defswap.io pooled";
    uint256 private constant DECIMALS = 18;

    event AddLiquidity(address indexed _provider, uint256 _baseAmount, uint256 _tokenAmount, uint256 _minted);
    event RemoveLiquidity(address indexed _provider, uint256 _baseAmount, uint256 _tokenAmount, uint256 _burned);
    event TokenPurchase(address indexed _buyer, uint256 _baseSold, uint256 _tokensBought, address _recipient);
    event BasePurchase(address indexed _buyer, uint256 _tokensSold, uint256 _baseBought, address _recipient);

    constructor(IERC20 _base, IERC20 _token, IUniswapExchange _uniswap) public {
        require(_uniswap.tokenAddress() == address(_base), "defswap: uniswap token doesn't match");
        require(address(_base) != address(_token), "defswap: token and base can't be the same");

        p_base = _base;
        p_token = _token;
        p_uniswap = _uniswap;

        approveUniswap();
        buildMetadata();
    }

    function approveUniswap() public nonReentrant {
        p_base.approve(
            address(p_uniswap),
            uint(-1)
        );
    }

    function buildMetadata() public nonReentrant {
        address token = address(p_token);
        require(gasleft() >= 402000, "defswap: gasleft build metadata is not enough");

        (
            bool successName,
            bytes memory name
        ) = token.call.gas(200000)(
            abi.encodeWithSelector(p_token.name.selector)
        );

        (
            bool successSymbol,
            bytes memory symbol
        ) = token.call.gas(200000)(
            abi.encodeWithSelector(p_token.symbol.selector)
        );

        _setMetadata(
            string(abi.encodePacked(SYMBOL_PREFIX, successSymbol ? abi.decode(symbol, (string)) : "???")),
            string(abi.encodePacked(successName ? abi.decode(name, (string)) : "Unknown", NAME_SUFIX)),
            DECIMALS
        );
    }

    function base() external view returns (IERC20) {
        return p_base;
    }

    function token() external view returns (IERC20) {
        return p_token;
    }

    function uniswap() external view returns (IUniswapExchange) {
        return p_uniswap;
    }

    function addLiquidity(
        uint256 _tokens,
        uint256 _maxBase
    ) external nonReentrant returns (uint256 minted) {
        IERC20 t_token = p_token;
        IERC20 t_base = p_base;

        uint256 received = t_token.safeTransferFrom(msg.sender, address(this), _tokens);
        uint256 pulled;
        require(received != 0, "defswap: pull zero tokens is not allowed");

        uint256 t_totalSupply = p_totalSupply;
        if (t_totalSupply == 0) {
            require(t_base.transferFrom(msg.sender, address(this), _maxBase), "defswap: error pulling base tokens");
            minted = ShufUtils.takeFee(_maxBase);
        } else {
            // TODO Check received
            uint256 tokenReserve = t_token.balanceOf(address(this)).sub(received);
            uint256 baseReserve = t_base.balanceOf(address(this));
            uint256 requiredPull = received.mul(baseReserve).divRound(tokenReserve);

            pulled = t_base.safeTransferFrom(msg.sender, address(this), ShufUtils.untakeFee(requiredPull));
            require(pulled <= _maxBase, "defswap: _maxBase is below of pulled required");
            require(pulled >= requiredPull, "defswap: pulled base is not enough");

            minted = pulled.mul(t_totalSupply).div(baseReserve);
        }

        emit AddLiquidity(msg.sender, pulled, received, minted);
        _mint(msg.sender, minted);
    }

    function removeLiquidity(
        uint256 _amount,
        uint256 _minBase,
        uint256 _minTokens
    ) external nonReentrant returns (
        uint256 baseAmount,
        uint256 tokenAmount
    ) {
        uint256 t_totalSupply = p_totalSupply;
        require(t_totalSupply != 0, "defswap: pool is empty");

        IERC20 t_token = p_token;
        IERC20 t_base = p_base;

        uint256 tokenReserve = t_token.balanceOf(address(this));
        uint256 baseReserve = t_base.balanceOf(address(this));

        baseAmount = _amount.mul(baseReserve) / t_totalSupply;
        tokenAmount = _amount.mul(tokenReserve) / t_totalSupply;

        require(ShufUtils.takeFee(baseAmount) >= _minBase, "defswap: baseAmount is below _minBase");

        emit RemoveLiquidity(msg.sender, baseAmount, tokenAmount, _amount);
        _burn(msg.sender, _amount);

        require(t_token.safeTransfer(msg.sender, tokenAmount) >= _minTokens, "defswap: tokenAmount is below _minTokens");
        t_base.transferWithFee(msg.sender, baseAmount); // t_base is trusted
    }

    function removeBaseLiquidity(
        uint256 _amount,
        uint256 _minBase
    ) external nonReentrant returns (
        uint256 baseAmount
    ) {
        uint256 t_totalSupply = p_totalSupply;
        require(t_totalSupply != 0, "defswap: pool is empty");

        IERC20 t_base = p_base;

        uint256 baseReserve = t_base.balanceOf(address(this));

        baseAmount = _amount.mul(baseReserve) / t_totalSupply;

        require(ShufUtils.takeFee(baseAmount) >= _minBase, "defswap: baseAmount is below _minBase");

        emit RemoveLiquidity(msg.sender, baseAmount, 0, _amount);
        _burn(msg.sender, _amount);

        t_base.transferWithFee(msg.sender, baseAmount); // t_base is trusted
    }

    function removeTokenLiquidity(
        uint256 _amount,
        uint256 _minTokens
    ) external nonReentrant returns (
        uint256 tokenAmount
    ) {
        uint256 t_totalSupply = p_totalSupply;
        require(t_totalSupply != 0, "defswap: pool is empty");

        IERC20 t_token = p_token;

        uint256 tokenReserve = t_token.balanceOf(address(this));

        tokenAmount = _amount.mul(tokenReserve) / t_totalSupply;

        emit RemoveLiquidity(msg.sender, 0, tokenAmount, _amount);
        _burn(msg.sender, _amount);

        require(t_token.safeTransfer(msg.sender, tokenAmount) >= _minTokens, "defswap: tokenAmount is below _minTokens");
    }

    function getBaseToTokenPrice(uint256 _baseSold) external view returns (uint256 tokensBought) {
        return _getInputPrice(_baseSold, p_base.balanceOf(address(this)), p_token.balanceOf(address(this)));
    }

    function getTokenToBasePrice(uint256 _tokenSold) external view returns (uint256 baseBought) {
        return _getInputPrice(_tokenSold, p_token.balanceOf(address(this)), p_base.balanceOf(address(this)));
    }

    function ethToTokenSwapInput(
        uint256 _minTokens
    ) external nonReentrant payable returns (uint256 bought) {
        uint256 baseBought = msg.value != 0 ? ShufUtils.takeFee(
            p_uniswap.ethToTokenSwapInput.value(msg.value)(1, uint(-1))
        ) : 0;

        bought = _baseToToken(
            p_base,
            p_token,
            baseBought,
            _minTokens,
            msg.sender,
            msg.sender
        );
    }

    function ethToTokenTransferInput(
        uint256 _minTokens,
        address _recipient
    ) external nonReentrant payable returns (uint256 bought) {
        uint256 baseBought = msg.value != 0 ? ShufUtils.takeFee(
            p_uniswap.ethToTokenSwapInput.value(msg.value)(1, uint(-1))
        ) : 0;

        bought = _baseToToken(
            p_base,
            p_token,
            baseBought,
            _minTokens,
            msg.sender,
            _recipient
        );
    }

    // Uniswap exchange compat
    function ethToTokenTransferInput(
        uint256 _minTokens,
        uint256 _deadline,
        address _recipient
    ) external nonReentrant payable returns (uint256 bought) {
        require(_deadline >= block.timestamp, "defswap: expired transaction");

        uint256 baseBought = msg.value != 0 ? ShufUtils.takeFee(
            p_uniswap.ethToTokenSwapInput.value(msg.value)(1, uint(-1))
        ) : 0;

        bought = _baseToToken(
            p_base,
            p_token,
            baseBought,
            _minTokens,
            msg.sender,
            _recipient
        );
    }

    function baseToTokenSwapInput(
        uint256 _amount,
        uint256 _minTokens
    ) external nonReentrant returns (uint256 bought) {
        IERC20 t_base = p_base;

        t_base.transferFromWithFee(msg.sender, address(this), _amount);

        bought = _baseToToken(
            t_base,
            p_token,
            ShufUtils.takeFee(_amount),
            _minTokens,
            msg.sender,
            msg.sender
        );
    }

    function baseToTokenTransferInput(
        uint256 _amount,
        uint256 _minTokens,
        address _recipient
    ) external nonReentrant returns (uint256 bought) {
        IERC20 t_base = p_base;

        t_base.transferFromWithFee(msg.sender, address(this), _amount);

        bought = _baseToToken(
            t_base,
            p_token,
            ShufUtils.takeFee(_amount),
            _minTokens,
            msg.sender,
            _recipient
        );
    }

    function tokenToEthSwapInput(
        uint256 _amount,
        uint256 _minEth
    ) external nonReentrant returns (uint256 bought) {
        IERC20 t_token = p_token;
        IERC20 t_base = p_base;

        uint256 amount = t_token.safeTransferFrom(msg.sender, address(this), _amount);

        uint256 baseBought = ShufUtils.takeFee(
            _tokenToBase(
                t_base,
                t_token,
                amount,
                0,
                msg.sender,
                address(this)
            )
        );

        bought = baseBought != 0 ? p_uniswap.tokenToEthTransferInput(
            baseBought,
            1,
            uint(-1),
            msg.sender
        ) : 0;

        require(bought >= _minEth, "defswap: eth bought is below _minEth");
    }

    function tokenToEthTransferInput(
        uint256 _amount,
        uint256 _minEth,
        address _recipient
    ) external nonReentrant returns (uint256 bought) {
        IERC20 t_token = p_token;
        IERC20 t_base = p_base;

        uint256 amount = t_token.safeTransferFrom(msg.sender, address(this), _amount);

        uint256 baseBought = ShufUtils.takeFee(
            _tokenToBase(
                t_base,
                t_token,
                amount,
                0,
                msg.sender,
                address(this)
            )
        );

        bought = baseBought != 0 ? p_uniswap.tokenToEthTransferInput(
            baseBought,
            1,
            uint(-1),
            _recipient
        ) : 0;

        require(bought >= _minEth, "defswap: eth bought is below _minEth");
    }

    function tokenToEthExchangeTransferInput(
        uint256 _amount,
        uint256 _minTokens,
        address _recipient,
        address _exchangeAddr
    ) external nonReentrant returns (uint256 bought) {
        IERC20 t_token = p_token;
        IERC20 t_base = p_base;

        uint256 amount = t_token.safeTransferFrom(msg.sender, address(this), _amount);

        uint256 baseBought = ShufUtils.takeFee(
            _tokenToBase(
                t_base,
                t_token,
                amount,
                0,
                msg.sender,
                address(this)
            )
        );

        uint256 ethBought = baseBought != 0 ? p_uniswap.tokenToEthSwapInput(
            baseBought,
            1,
            uint(-1)
        ) : 0;

        bought = ethBought != 0 ? IUniswapExchange(_exchangeAddr).ethToTokenTransferInput.value(
            ethBought
        )(
            1, uint(-1), _recipient
        ) : 0;

        require(bought >= _minTokens, "defswap: tokens bought is below _minTokens");
    }

    function tokenToBaseSwapInput(
        uint256 _amount,
        uint256 _minBase
    ) external nonReentrant returns (uint256 bought) {
        IERC20 t_token = p_token;
        IERC20 t_base = p_base;

        uint256 amount = t_token.safeTransferFrom(msg.sender, address(this), _amount);

        bought = _tokenToBase(
            t_base,
            t_token,
            amount,
            _minBase,
            msg.sender,
            msg.sender
        );
    }

    function tokenToBaseTransferInput(
        uint256 _amount,
        uint256 _minBase,
        address _recipient
    ) external nonReentrant returns (uint256 bought) {
        IERC20 t_token = p_token;
        IERC20 t_base = p_base;

        uint256 amount = t_token.safeTransferFrom(msg.sender, address(this), _amount);

        bought = _tokenToBase(
            t_base,
            t_token,
            amount,
            _minBase,
            msg.sender,
            _recipient
        );
    }

    function tokenToBaseExchangeTransferInput(
        uint256 _amount,
        uint256 _minTokens,
        address _recipient,
        address _exchangeAddr
    ) external nonReentrant returns (uint256 bought) {
        require(_exchangeAddr != address(p_uniswap), "defswap: _exchange can't be Uniswap");

        // Convert amount tokens to base
        IERC20 t_token = p_token;
        IERC20 t_base = p_base;

        uint256 amount = t_token.safeTransferFrom(msg.sender, address(this), _amount);

        uint256 tokenReserve = t_token.balanceOf(address(this));
        uint256 baseReserve = t_base.balanceOf(address(this));

        uint256 baseBought = _getInputPrice(amount, tokenReserve.sub(amount), baseReserve);
        emit BasePurchase(msg.sender, amount, baseBought, address(this));

        // Trade base to tokens using exchange
        t_base.approve(_exchangeAddr, baseBought);
        bought = IDefswapExchange(_exchangeAddr).baseToTokenTransferInput(
            baseBought,
            _minTokens,
            _recipient
        );

        t_base.approve(_exchangeAddr, 0);
    }

    function _getInputPrice(
        uint256 _inputAmount,
        uint256 _inputReserve,
        uint256 _outputReserve
    ) private pure returns (uint256) {
        require(_inputReserve != 0 && _outputReserve != 0, "defswap: one reserve is empty");
        uint256 inputAmountWithFee = _inputAmount.mul(997);
        uint256 numerator = inputAmountWithFee.mul(_outputReserve);
        uint256 denominator = _inputReserve.mul(1000).add(inputAmountWithFee);
        return numerator / denominator;
    }

    function _baseToToken(
        IERC20 t_base,
        IERC20 t_token,
        uint256 _amount,
        uint256 _minTokens,
        address _buyer,
        address _recipient
    ) private returns (uint256 tokensBought) {
        uint256 tokenReserve = t_token.balanceOf(address(this));
        uint256 baseReserve = t_base.balanceOf(address(this));

        tokensBought = _getInputPrice(_amount, baseReserve.sub(_amount), tokenReserve);
        require(tokensBought >= _minTokens, "defswap: bought tokens below _minTokens");
        require(t_token.safeTransfer(_recipient, tokensBought) != 0 || tokensBought == 0, "defswap: error sending tokens");

        emit TokenPurchase(_buyer, _amount, tokensBought, _recipient);
    }

    function _tokenToBase(
        IERC20 t_base,
        IERC20 t_token,
        uint256 _amount,
        uint256 _minBase,
        address _buyer,
        address _recipient
    ) private returns (uint256 baseBought) {
        uint256 tokenReserve = t_token.balanceOf(address(this));
        uint256 baseReserve = t_base.balanceOf(address(this));

        baseBought = _getInputPrice(_amount, tokenReserve.sub(_amount), baseReserve);
        require(baseBought >= _minBase, "defswap: bought base below _minBase");
        t_base.transferWithFee(_recipient, baseBought);

        emit BasePurchase(_buyer, _amount, baseBought, _recipient);
    }

    function() external payable {
        require(msg.sender != tx.origin, "defswap: ETH rejected");
    }
}
function ethToTokenTransferInput(
    uint256 _minTokens,
    uint256 _deadline,
    address _recipient
) external nonReentrant payable returns (uint256 bought) {
    require(_deadline >= block.timestamp, "defswap: expired transaction");

    uint256 baseBought = msg.value != 0 ? ShufUtils.takeFee(
        p_uniswap.ethToTokenSwapInput.value(msg.value)(1, uint(-1))
    ) : 0;

    bought = _baseToToken(
        p_base,
        p_token,
        baseBought,
        _minTokens,
        msg.sender,
        _recipient
    );
}

// Uniswap exchange compat