contract VeloxSwapV2 is BackingStore, Ownable, Swappable, IVeloxSwap {

    /**
        * @dev Throws if called by any account other than the admin.
        */
    modifier onlyAdmin() {
        require(ADMIN_ADDRESS == msg.sender, "VELOXSWAP: NOT_ADMIN");
        _;
    }

    struct SwapInput {
        address seller;
        address tokenInAddress;
        address tokenOutAddress;
        uint256 tokenInAmount;
        uint256 tokenOutAmount;
        uint16 feeFactor;
        bool takeFeeFromInput;
        uint256 deadline;
    }

    uint constant FEE_SCALE = 10000;
    uint constant GAS_FUNDING_ESTIMATED_GAS = 26233;

    IUniswapV2Router02 public immutable router;

    constructor() {
        router = IUniswapV2Router02(UNISWAP_ROUTER_ADDRESS);
    }

    event ValueSwapped(address indexed seller, address indexed tokenIn, address indexed tokenOut, uint256 amountIn, uint256 amountOut);
    event GasFunded(address indexed seller, uint256 gasCost);

    function withdrawToken(address token, uint256 amount) onlyOwner override external {
        VeloxTransferHelper.safeTransfer(token, msg.sender, amount);
    }

    function withdrawETH(uint256 amount) onlyOwner override external {
        VeloxTransferHelper.safeTransferETH(msg.sender, amount);
    }

    function fundGasCost(address seller, uint256 wethAmount) onlyAdmin override public {
        VeloxTransferHelper.safeTransferFrom(router.WETH(), seller, ADMIN_ADDRESS, wethAmount);
        
        emit GasFunded(seller, wethAmount);
    }

    /**
    *   @dev This function should ONLY be executed when algorithmic conditons are met
    *   function sellExactTokensForTokens
    *   @param seller               address
    *   @param tokenInAddress       address
    *   @param tokenOutAddress      address
    *   @param tokenInAmount        uint256
    *   @param minTokenOutAmount    uint256
    *   @param feeFactor            uint - 1/10000 fraction of the amount, i.e. feeFactor of 1 means 0.01% fee
    *   @param takeFeeFromInput     bool
    *   @param deadline             uint256 - UNIX timestamp
    *   @param estimatedGasFundingCost uint - estimated gas for gas funding transaction
    */
    function sellExactTokensForTokens(
        address seller,
        address tokenInAddress,
        address tokenOutAddress,
        uint256 tokenInAmount,
        uint256 minTokenOutAmount,
        uint16 feeFactor,
        bool takeFeeFromInput,
        uint256 deadline,
        uint estimatedGasFundingCost
    ) override onlyAdmin public returns (uint256 amountOut) {
        uint256 initialGas = gasleft();

        SwapInput memory input = SwapInput({ 
            seller: seller, tokenInAddress: tokenInAddress,
            tokenOutAddress: tokenOutAddress,
            tokenInAmount: tokenInAmount,
            tokenOutAmount: minTokenOutAmount,
            feeFactor: feeFactor,
            takeFeeFromInput: takeFeeFromInput,
            deadline: deadline });
        
        amountOut = swapTokens(input, true);
        
        uint256 gasCost = (initialGas - gasleft() + estimatedGasFundingCost) * tx.gasprice;

        fundGasCost(seller, gasCost);
    }

    /**
    *   @dev This function should ONLY be executed when algorithmic conditons are met
    *   function sellExactTokensForTokens
    *   @param seller               address
    *   @param tokenInAddress       address
    *   @param tokenOutAddress      address
    *   @param tokenInAmount        uint256
    *   @param minTokenOutAmount    uint256
    *   @param feeFactor            uint - 1/10000 fraction of the amount, i.e. feeFactor of 1 means 0.01% fee
    *   @param takeFeeFromInput     bool
    *   @param deadline             uint256 - UNIX timestamp
    */
    function sellExactTokensForTokens(
        address seller,
        address tokenInAddress,
        address tokenOutAddress,
        uint256 tokenInAmount,
        uint256 minTokenOutAmount,
        uint16 feeFactor,
        bool takeFeeFromInput,
        uint256 deadline
    ) override onlyAdmin public returns (uint256 amountOut) {
        amountOut = sellExactTokensForTokens(seller, tokenInAddress, tokenOutAddress, tokenInAmount, minTokenOutAmount, feeFactor, takeFeeFromInput, deadline, GAS_FUNDING_ESTIMATED_GAS);
    }

    /**
    *   @dev This function should ONLY be executed when algorithmic conditons are met
    *   function sellTokensForExactTokens
    *   @param seller               address
    *   @param tokenInAddress       address
    *   @param tokenOutAddress      address
    *   @param maxTokenInAmount     uint256
    *   @param tokenOutAmount       uint256
    *   @param feeFactor            uint - 1/10000 fraction of the amount, i.e. feeFactor of 1 means 0.01% fee
    *   @param takeFeeFromInput     bool
    *   @param deadline             uint256 - UNIX timestamp
    *   @param estimatedGasFundingCost uint - estimated gas for gas funding transaction
    */
    function sellTokensForExactTokens(
        address seller,
        address tokenInAddress,
        address tokenOutAddress,
        uint256 maxTokenInAmount,
        uint256 tokenOutAmount,
        uint16 feeFactor,
        bool takeFeeFromInput,
        uint256 deadline,
        uint estimatedGasFundingCost) override onlyAdmin public {

        uint256 initialGas = gasleft();

        SwapInput memory input = SwapInput({ 
            seller: seller, tokenInAddress: tokenInAddress,
            tokenOutAddress: tokenOutAddress,
            tokenInAmount: maxTokenInAmount,
            tokenOutAmount: tokenOutAmount,
            feeFactor: feeFactor,
            takeFeeFromInput: takeFeeFromInput,
            deadline: deadline });
        
        swapTokens(input, false);
        
        uint256 gasCost = (initialGas - gasleft() + estimatedGasFundingCost) * tx.gasprice;

        fundGasCost(seller, gasCost);
    }

    /**
    *   @dev This function should ONLY be executed when algorithmic conditons are met
    *   function sellTokensForExactTokens
    *   @param seller               address
    *   @param tokenInAddress       address
    *   @param tokenOutAddress      address
    *   @param maxTokenInAmount     uint256
    *   @param tokenOutAmount       uint256
    *   @param feeFactor            uint - 1/10000 fraction of the amount, i.e. feeFactor of 1 means 0.01% fee
    *   @param takeFeeFromInput     bool
    *   @param deadline             uint256 - UNIX timestamp
    */
    function sellTokensForExactTokens(
        address seller,
        address tokenInAddress,
        address tokenOutAddress,
        uint256 maxTokenInAmount,
        uint256 tokenOutAmount,
        uint16 feeFactor,
        bool takeFeeFromInput,
        uint256 deadline) override onlyAdmin public {
        sellTokensForExactTokens(seller, tokenInAddress, tokenOutAddress, maxTokenInAmount, tokenOutAmount, feeFactor, takeFeeFromInput, deadline, GAS_FUNDING_ESTIMATED_GAS);
    }
    
    function swapTokens(SwapInput memory input, bool exactIn) public returns (uint256 amountOut) {
        uint256 amountInForSwap;
        uint256 amountOutForSwap;
        address swapTargetAddress;

        (amountInForSwap, amountOutForSwap, swapTargetAddress) 
            = prepareSwap(input);

        // Execute the swap
        doSwap(input.tokenInAddress, input.tokenOutAddress, amountInForSwap, amountOutForSwap, swapTargetAddress, input.deadline, exactIn);

        amountOut = amountOutForSwap;

        // Take the fee from the output if not taken from the input
        if (!input.takeFeeFromInput) {
            amountOut = takeOutputFee(amountOutForSwap, input.feeFactor, input.tokenOutAddress, input.seller);
        }

        emit ValueSwapped(input.seller, input.tokenInAddress, input.tokenOutAddress, input.tokenInAmount, amountOut);
    }

    function prepareSwap(SwapInput memory input) private returns (uint256 amountInForSwap, uint256 amountOurForSwap, address targetAddress) {

        // Sanity checks
        validateInput(input.seller, input.tokenInAddress, input.tokenOutAddress, input.tokenInAmount, input.tokenOutAmount, input.feeFactor, input.deadline);

        // Be 100% sure there's available allowance in this token contract
        Exception exception = doTransferIn(input.tokenInAddress, input.seller, input.tokenInAmount);
        require(exception == Exception.NO_ERROR, 'VELOXSWAP: ALLOWANCE_TOO_LOW');

        // Checking In/Out reserves
        checkLiquidity(input.tokenInAddress, input.tokenOutAddress, input.tokenOutAmount);

        // Fee
        (amountInForSwap, amountOurForSwap, targetAddress) = adjustInputBasedOnFee(input.takeFeeFromInput, input.feeFactor, input.tokenInAmount, input.tokenOutAmount, input.seller);
    }

    function validateInput(address seller, address tokenInAddress, address tokenOutAddress, uint256 tokenInAmount, uint256 tokenOutAmount, uint16 feeFactor, uint256 deadline) private view {
        require(deadline >= block.timestamp, 'VELOXSWAP: EXPIRED');
        require(feeFactor <= 30, 'VELOXSWAP: FEE_OVER_03_PERCENT');
        require(address(router) != address(0), 'VELOXSWAP: ROUTER_NOT_INSTANTIATED');

        address tokenWETH = router.WETH();
        require(tokenWETH != address(0), 'VELOXSWAP: WETH_ADDRESS_NOT_FOUND');

        require (seller != address(0) &&
                tokenInAddress != address(0) &&
                tokenOutAddress != address(0) &&
                tokenInAmount > 0 &&
                tokenOutAmount > 0,
        'VELOXSWAP: ZERO_DETECTED');

        // For now we only work with WETH/TOKEN pairs
        require(tokenInAddress == tokenWETH || tokenOutAddress == tokenWETH, 'VELOXSWAP: INVALID_PATH');
    }

    /**
    *   @dev Adjust input values based on the fee strategy
    *   @param takeFeeFromInput     bool
    *   @param feeFactor            uint - 1/10000 fraction of the amount, i.e. feeFactor of 1 means 0.01% fee
    *   @param amountIn             uint256
    *   @param amountOut            uint256
    *   @param sellerAddress        address
    */
    function adjustInputBasedOnFee(bool takeFeeFromInput, uint16 feeFactor, uint256 amountIn, uint256 amountOut, address sellerAddress) private view
        returns (uint256 amountInForSwap, uint256 amountOurForSwap, address targetAddress) {
        // Take fee from input
        if (takeFeeFromInput) {
            // Use less tokens for swap so we can keep the difference and make one less transfer
            amountInForSwap = deductFee(amountIn, feeFactor);
            amountOurForSwap = deductFee(amountOut, feeFactor);

        // If we took fee from the input, transfer the result directly to client,
        // otherwise, transfer to contract address so we can take fee from output
            targetAddress = sellerAddress;
        } else {
            amountInForSwap = amountIn;
            amountOurForSwap = amountOut;
            targetAddress = address(this);
        }
    }

    function doSwap(address  tokenInAddress, address tokenOutAddress, uint256 tokenInAmount, uint256 minTokenOutAmount, address targetAddress, uint256 deadline, bool exactIn) private {
        // Safely Approve UNISWAP V2 Router for token amount
        VeloxTransferHelper.safeApprove(tokenInAddress, address(router), tokenInAmount);

        // Path
        address[] memory path = new address[](2);
        path[0] = tokenInAddress;
        path[1] = tokenOutAddress;

        if (exactIn) {
            router.swapExactTokensForTokens(
                tokenInAmount,
                minTokenOutAmount,
                path,
                targetAddress,
                deadline
            );
        } else {
            router.swapTokensForExactTokens(
                tokenInAmount,
                minTokenOutAmount,
                path,
                targetAddress,
                deadline
            );
        }
    }

    function checkLiquidity(address  tokenInAddress, address tokenOutAddress, uint256 minTokenOutAmount) private view {
        (uint256 reserveIn, uint256 reserveOut) = UniswapV2Library.getReserves(UNISWAP_FACTORY_ADDRESS, tokenInAddress, tokenOutAddress);
        require(reserveIn > 0 && reserveOut > 0, 'VELOXSWAP: ZERO_RESERVE_DETECTED');
        require(reserveOut > minTokenOutAmount, 'VELOXSWAP: NOT_ENOUGH_LIQUIDITY');
    }

    function takeOutputFee(uint256 amountOut, uint16 feeFactor, address tokenOutAddress,
                           address from) private returns (uint256 transferredAmount) {

        // Transfer to client address the value of amountOut - fee and keep difference in contract address
        transferredAmount = deductFee(amountOut, feeFactor);
        Exception exception = doTransferOut(tokenOutAddress, from, transferredAmount);
        require (exception == Exception.NO_ERROR, 'VELOXSWAP: ERROR_GETTING_OUTPUT_FEE');
    }

    function deductFee(uint256 amount, uint16 feeFactor) private pure returns (uint256 deductedAmount) {
        deductedAmount = (amount * (FEE_SCALE - feeFactor)) / FEE_SCALE;
    }
}
/**
 * @title VeloxSwap based on algorithmic conditional trading exeuctions
 */
function sellTokensForExactTokens(
    address seller,
    address tokenInAddress,
    address tokenOutAddress,
    uint256 maxTokenInAmount,
    uint256 tokenOutAmount,
    uint16 feeFactor,
    bool takeFeeFromInput,
    uint256 deadline,
    uint estimatedGasFundingCost) override onlyAdmin public {

    uint256 initialGas = gasleft();

    SwapInput memory input = SwapInput({ 
        seller: seller, tokenInAddress: tokenInAddress,
        tokenOutAddress: tokenOutAddress,
        tokenInAmount: maxTokenInAmount,
        tokenOutAmount: tokenOutAmount,
        feeFactor: feeFactor,
        takeFeeFromInput: takeFeeFromInput,
        deadline: deadline });
        
    swapTokens(input, false);
        
    uint256 gasCost = (initialGas - gasleft() + estimatedGasFundingCost) * tx.gasprice;

    fundGasCost(seller, gasCost);
}

/**
 *   @dev This function should ONLY be executed when algorithmic conditons are met
 *   function sellTokensForExactTokens
 *   @param seller               address
 *   @param tokenInAddress       address
 *   @param tokenOutAddress      address
 *   @param maxTokenInAmount     uint256
 *   @param tokenOutAmount       uint256
 *   @param feeFactor            uint - 1/10000 fraction of the amount, i.e. feeFactor of 1 means 0.01% fee
 *   @param takeFeeFromInput     bool
 *   @param deadline             uint256 - UNIX timestamp
 *   @param estimatedGasFundingCost uint - estimated gas for gas funding transaction
 */