contract MultiSwapper is ReentrancyGuard, Ownable {
    using SafeMath for uint256;
    using SafeERC20 for IERC20;
    using Address for address;

    TokenTransferProxy private _tokenTransferProxy;

    bool private _paused;

    IWhitelisted private _whitelisted;
    address private _gasToken;

    event Paused();
    event Unpaused();

    event Swapped(
        address indexed user,
        address indexed srcToken,
        address indexed destToken,
        uint256 srcAmount,
        uint256 receivedAmount
    );
    event Payed(
        address indexed to,
        address indexed srcToken,
        address indexed destToken,
        uint256 srcAmount,
        uint256 receivedAmount
    );

    /**
     * @dev Modifier to make a function callable only when the contract is not paused.
     */
    modifier whenNotPaused() {
        require(!_paused, "Pausable: paused");
        _;
    }

    /**
     * @dev Modifier to make a function callable only when the contract is paused.
     */
    modifier whenPaused() {
        require(_paused, "Pausable: not paused");
        _;
    }

    constructor(address whitelist, address gasToken) public {

        _tokenTransferProxy = new TokenTransferProxy();
        _whitelisted = IWhitelisted(whitelist);
        _gasToken = gasToken;
    }

     /**
    * @dev Fallback method to allow exchanges to transfer back ethers for a particular swap
    * It will only allow contracts to send funds to it
    */
    function() external payable whenNotPaused {
        address account = msg.sender;
        require(
            account.isContract(),
            "Sender is not a contract"
        );
    }

    function getWhitelistAddress() external view returns(address) {
        return address(_whitelisted);
    }

    function setWhitelistAddress(address whitelisted) external onlyOwner {
        require(whitelisted != address(0), "Invalid whitelist address");
        _whitelisted = IWhitelisted(whitelisted);
    }

    function getTokenTransferProxy() external view returns (address) {
        return address(_tokenTransferProxy);
    }

    /**
     * @dev Returns true if the contract is paused, and false otherwise.
     */
    function paused() external view returns (bool) {
        return _paused;
    }

    /**
     * @dev Called by a pauser to pause, triggers stopped state.
     */
    function pause() external onlyOwner whenNotPaused {
        _paused = true;
        emit Paused();
    }

    /**
     * @dev Called by a pauser to unpause, returns to normal state.
     */
    function unpause() external onlyOwner whenPaused {
        _paused = false;
        emit Unpaused();
    }

    /**
    * @dev Allows owner of the contract to transfer tokens any tokens which are assigned to the contract
    * This method is for saftey if by any chance tokens or ETHs are assigned to the contract by mistake
    * @dev token Address of the token to be transferred
    * @dev destination Recepient of the token
    * @dev amount Amount of tokens to be transferred
    */
    function ownerTransferTokens(
        address token,
        address payable destination,
        uint256 amount
    )
    external
    onlyOwner
    {
        Utils.transferTokens(token, destination, amount);
    }

    /**
    * @dev Allows owner of the contract to mint more gas tokens
    * @param amount Amount of gas tokens to mint
    */
    function mintGasTokens(uint256 amount) external onlyOwner {
        Utils.mintGasTokens(_gasToken, amount);
    }

    /**
   * @dev The function which performs the multi path swap.
   * It is a nonreentrant function
   * @param fromToken Address of the source token
   * @param toToken Address of the destination token
   * @param fromAmount Amount of source tokens to be swapped
   * @param toAmount Minimu destination token amount expected out of this swap
   * @param path Route to be taken for this swap to take place
   * @param mintPrice Price of gas at the time of minting of gas tokens, if any. In wei
   */
    function multiSwap(
        IERC20 fromToken,
        IERC20 toToken,
        uint256 fromAmount,
        uint256 toAmount,
        Utils.Path[] memory path,
        uint256 mintPrice
    )
        public
        payable
        whenNotPaused
        nonReentrant
        returns (uint256)
    {
        uint256 receivedAmount = performSwap(
            fromToken,
            toToken,
            fromAmount,
            toAmount,
            path,
            mintPrice
        );

        Utils.transferTokens(address(toToken), msg.sender, receivedAmount);

        emit Swapped(
            msg.sender,
            address(fromToken),
            address(toToken),
            fromAmount,
            receivedAmount
        );

        return receivedAmount;
    }

    //Same as multiswap just allows different receiver then msg.sender
    function pay(
        address payable receiver,
        IERC20 fromToken,
        IERC20 toToken,
        uint256 fromAmount,
        uint256 toAmount,
        Utils.Path[] memory path,
        uint256 mintPrice
    )
        public
        payable
        whenNotPaused
        nonReentrant
        returns (uint256)
    {
        require(receiver != address(0), "Invalid receiver");

        uint256 receivedAmount = performSwap(
            fromToken,
            toToken,
            fromAmount,
            toAmount,
            path,
            mintPrice
        );
        address payable payer = msg.sender;

        Utils.transferTokens(address(toToken), receiver, toAmount);

        if (receivedAmount > toAmount) {
            uint rest = receivedAmount.sub(toAmount);

            Utils.transferTokens(address(toToken), payer, rest);
        }

        emit Payed(
            receiver,
            address(fromToken),
            address(toToken),
            fromAmount,
            receivedAmount
        );

        return receivedAmount;
    }

    function performSwap(
        IERC20 fromToken,
        IERC20 toToken,
        uint256 fromAmount,
        uint256 toAmount,
        Utils.Path[] memory path,
        uint256 mintPrice
    )
        private
        returns(uint256)
    {
        uint initialGas = gasleft();

        uint _fromAmount = fromAmount;
        
        require(path.length > 0, "Path noth provided for swap");
        require(path[0].from == address(fromToken), "First from token does not match");

        //if fromToken is not ETH then transfer tokens from user to this contract
        if (address(fromToken) != Utils.ethAddress()) {
            _tokenTransferProxy.transferFrom(
                address(fromToken),
                msg.sender,
                address(this),
                fromAmount
            );
        }

        for (uint i = 0; i < path.length; i++) {
            IERC20 _fromToken = IERC20(path[i].from);
            IERC20 _toToken = IERC20(path[i].to);

            //TODO: check that percents adds up to 100

            for (uint j = 0; j < path[i].routes.length; j++) {
                Utils.Route memory route = path[i].routes[j];

                //Calculating tokens to be passed to the relevant exchange
                //percentage should be 200 for 2%
                uint fromAmountSlice = _fromAmount.mul(route.percent).div(10000);

                if (j == path[i].routes.length.sub(1)) {
                    uint256 remBal = Utils.tokenBalance(address(_fromToken), address(this));
                    fromAmountSlice = remBal;
                }

                //Check if exchange is supported
                require(_whitelisted.isWhitelisted(route.exchange), "Exchange not whitelisted");

                IExchange dex = IExchange(route.exchange);

                Utils.approve(route.exchange, address(_fromToken));

                //Call to the exchange
                if (address(_fromToken) == Utils.ethAddress()) {
                    dex.swap.value(fromAmountSlice)(_fromToken, _toToken, fromAmountSlice, 1, route.targetExchange, route.payload);
                }
                else {
                    _fromToken.safeTransfer(route.exchange, fromAmountSlice);

                    dex.swap(_fromToken, _toToken, fromAmountSlice, 1, route.targetExchange, route.payload);
                }
            }

            _fromAmount = Utils.tokenBalance(address(_toToken), address(this));
        }

        uint256 receivedAmount = Utils.tokenBalance(address(toToken), address(this));
        require(
            receivedAmount >= toAmount,
            "Received amount of tokens are less then expected"
        );

        if (mintPrice > 0) {
            Utils.refundGas(_gasToken, initialGas, mintPrice);
        }
        return receivedAmount;
    }
}
function paused() external view returns (bool) {
        return _paused;
    }
/**
 * @dev Returns true if the contract is paused, and false otherwise.
 */