contract Degen is Ownable {
    using TransferHelper for address;
    enum OrderType {EthForTokens, TokensForEth, TokensForTokens, EthForEth}
    
    IUni public Uni = IUni(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); //mainnet network address for uniswap (valid for Ropsten as well)
    IUni public Sushi = IUni(0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F); // Mainnet network address for sushiswap
    //address public _sushi = address(0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506); // Ropsten network address for sushiswap
    //address public USDT = address(0x47A530f3Fa882502344DC491549cA9c058dbC7Da); // Ropsten test net USDT test token
    address public USDT = address(0xdAC17F958D2ee523a2206206994597C13D831ec7); // USDT Token Address
    address public system;
    uint256 public processingFee = 0 ;
    
    uint256 private deadlineLimit = 20*60;      // 20 minutes by default 
    
    uint256 private collectedFees = 1; // amount of collected fee (starts from 1 to avoid additional gas usage)
    address public feeReceiver; // address which receive the fee (by default is validator)


    IReimbursement public reimbursementContract;      // reimbursement contract address

    address public companyVault;    // the vault address of our company registered in reimbursement contract

    ISwapFactory public swapFactory;
   
    modifier onlySystem() {
        require(msg.sender == system || owner() == msg.sender,"Caller is not the system");
        _;
    }
    
    constructor(address _swapFactory, address _system) 
    {
        swapFactory = ISwapFactory(_swapFactory);
        system = _system;
    }
    

    function setCompanyVault(address _comapnyVault) external onlyOwner {
        companyVault = _comapnyVault;
    }

    function setReimbursementContract(address _reimbursementContarct) external onlyOwner {
        reimbursementContract = IReimbursement(_reimbursementContarct);
    }

    function setProcessingFee(uint256 _processingFees) external onlySystem {
        processingFee = _processingFees;
    }

    function setSwapFactory(address _swapFactory) external onlyOwner {
        swapFactory = ISwapFactory(_swapFactory);
    }
    
    function setSystem(address _system) external onlyOwner {
        system = _system;
    }
    
    function setFeeReceiver(address _addr) external onlyOwner {
        feeReceiver = _addr;
    }
    
    function getDeadlineLimit() public view returns(uint256) {
        return deadlineLimit;
    }
    
    function setDeadlineLimit(uint256 limit) external onlyOwner {
        deadlineLimit = limit;
    }

    // get amount of collected fees that can be claimed
    function getColletedFees() external view returns (uint256) {
        // collectedFees starts from 1 to avoid additional gas usage to initiate storage (when collectedFees = 0)
        return collectedFees - 1;
    }

    // claim fees by feeReceiver
    function claimFee() external returns (uint256 feeAmount) {
        require(msg.sender == feeReceiver, "This fee can be claimed only by fee receiver!!");
        feeAmount = collectedFees - 1;
        collectedFees = 1;        
        TransferHelper.safeTransferETH(msg.sender, feeAmount);
    }
    
    
    // Call function processFee() at the end of main function for correct gas usage calculation.
    // txGas - is gasleft() on start of calling contract. Put `uint256 txGas = gasleft();` as a first command in function
    // feeAmount - fee amount that user paid
    // processing - processing fee (for cross-chain swaping)
    // licenseeVault - address that licensee received on registration and should provide when users comes from their site
    // user - address of user who has to get reimbursement (usually msg.sender)

    function processFee(uint256 txGas, uint256 feeAmount, uint256 processing, address licenseeVault, address user) internal {
        if (address(reimbursementContract) == address(0)) {
            payable(user).transfer(feeAmount); // return fee to sender if no reimbursement contract
            return;
        }
        
        uint256 licenseeFeeAmount;
        if (licenseeVault != address(0)) {
            uint256 companyFeeRate = reimbursementContract.getLicenseeFee(companyVault, address(this));
            uint256 licenseeFeeRate = reimbursementContract.getLicenseeFee(licenseeVault, address(this));
            if (licenseeFeeRate != 0)
                licenseeFeeAmount = (feeAmount * licenseeFeeRate)/(licenseeFeeRate + companyFeeRate);
            if (licenseeFeeAmount != 0) {
                address licenseeFeeTo = reimbursementContract.requestReimbursement(user, licenseeFeeAmount, licenseeVault);
                if (licenseeFeeTo == address(0)) {
                    payable(user).transfer(licenseeFeeAmount);    // refund to user
                } else {
                    payable(licenseeFeeTo).transfer(licenseeFeeAmount);  // transfer to fee receiver
                }
            }
        }
        feeAmount -= licenseeFeeAmount; // company's part of fee
        collectedFees += feeAmount; 
        
        if (processing != 0) 
            payable(system).transfer(processing);  // transfer to fee receiver
        
        txGas -= gasleft(); // get gas amount that was spent on Licensee fee
        txGas = txGas * tx.gasprice;
        // request reimbursement for user
        reimbursementContract.requestReimbursement(user, feeAmount+txGas+processing, companyVault);
    }
    
    
    function _swap( 
        OrderType orderType, 
        address[] memory path, 
        uint256 assetInOffered,
        uint256 minExpectedAmount, 
        address to,
        uint256 dexId,
        uint256 deadline
    ) internal returns(uint256 amountOut) {
         
        require(dexId < 2, "Invalid DEX Id!");
        require(deadline >= block.timestamp, "EXPIRED: Deadline for transaction already passed.");

       if(dexId == 0){
            uint[] memory swapResult;
            if(orderType == OrderType.EthForTokens) {
                 path[0] = Uni.WETH();
                 swapResult = Uni.swapExactETHForTokens{value:assetInOffered}(0, path, to,block.timestamp);
            }
            else if (orderType == OrderType.TokensForEth) {
                path[path.length-1] = Uni.WETH();
                TransferHelper.safeApprove(path[0], address(Uni), assetInOffered);
                swapResult = Uni.swapExactTokensForETH(assetInOffered, 0, path,to, block.timestamp);
            }
            else if (orderType == OrderType.TokensForTokens) {
                TransferHelper.safeApprove(path[0], address(Uni), assetInOffered);
                swapResult = Uni.swapExactTokensForTokens(assetInOffered, minExpectedAmount, path, to, block.timestamp);
            }
            amountOut = swapResult[swapResult.length - 1];
        } else if(dexId == 1) {
            uint[] memory swapResult;
            if(orderType == OrderType.EthForTokens) {
                 path[0] = Sushi.WETH();
                 swapResult = Sushi.swapExactETHForTokens{value:assetInOffered}(minExpectedAmount, path, to, block.timestamp);
            }
            else if (orderType == OrderType.TokensForEth) {
                path[path.length-1] = Sushi.WETH();
                TransferHelper.safeApprove(path[0], address(Sushi), assetInOffered);
                swapResult = Sushi.swapExactTokensForETH(assetInOffered, minExpectedAmount, path, to, block.timestamp);
            }
            else if (orderType == OrderType.TokensForTokens) {
                TransferHelper.safeApprove(path[0], address(Sushi), assetInOffered);
                swapResult = Sushi.swapExactTokensForTokens(assetInOffered, minExpectedAmount, path, to, block.timestamp);
            }
            amountOut = swapResult[swapResult.length - 1];
        }
    }
    
    function executeSwap(
        OrderType orderType, 
        address[] memory path, 
        uint256 assetInOffered, 
        uint256 fees, 
        uint256 minExpectedAmount,
        address licenseeVault,
        uint256 dexId,
        uint256 deadline
    ) external payable {
        uint256 gasA = gasleft();
        uint256 receivedFees = 0;
        if(deadline == 0) {
            deadline = block.timestamp + deadlineLimit;
        }
        
        if(orderType == OrderType.EthForTokens){
            require(msg.value >= (assetInOffered + fees), "Payment = assetInOffered + fees");
            receivedFees = receivedFees + msg.value - assetInOffered;
        } else {
            require(msg.value >= fees, "fees not received");
            receivedFees = receivedFees + msg.value;
            TransferHelper.safeTransferFrom(path[0], msg.sender, address(this), assetInOffered);
        }
        
        _swap(orderType, path, assetInOffered, minExpectedAmount, msg.sender, dexId, deadline);
   
        processFee(gasA, receivedFees, 0, licenseeVault, msg.sender);
    }
    
    function executeCrossExchange(
        address[] memory path, 
        OrderType orderType,
        uint256 crossOrderType,
        uint256 assetInOffered,
        uint256 fees, 
        uint256 minExpectedAmount,
        address licenseeVault,
        uint256[3] memory dexId_deadline // dexId_deadline[0] - native swap dexId, dexId_deadline[1] - foreign swap dexId, dexId_deadline[2] - deadline
    ) public payable {
        uint256[2] memory feesPrice;
        feesPrice[0] = gasleft();       // equivalent to gasA
        feesPrice[1] = 0;               // processing fees
        
        if (dexId_deadline[2] == 0) {   // if deadline == 0, set deadline to deadlineLimit
            dexId_deadline[2] = block.timestamp + deadlineLimit;
        }

        if(orderType == OrderType.EthForTokens){
            require(msg.value >= (assetInOffered + fees + processingFee), "Payment = assetInOffered + fees + processingFee");
            feesPrice[1] = msg.value - assetInOffered - fees;
        } else {
            require(msg.value >= (fees + processingFee), "fees not received");
            feesPrice[1] = msg.value - fees;
            TransferHelper.safeTransferFrom(path[0], msg.sender, address(this), assetInOffered);
        }
        
        if(path[0] == USDT) {
            TransferHelper.safeApprove(USDT, address(swapFactory), assetInOffered);
            swapFactory.swap(USDT, path[path.length-1], assetInOffered, msg.sender, crossOrderType, dexId_deadline[1], dexId_deadline[2]);
        }
        else {
            address tokenB = path[path.length-1];
            path[path.length-1] = USDT;
            uint256 minAmountExpected = _swap(orderType, path, assetInOffered, minExpectedAmount, address(this), dexId_deadline[0], dexId_deadline[2]);
                
            TransferHelper.safeApprove(USDT, address(swapFactory),minAmountExpected);
            swapFactory.swap(USDT, tokenB, minAmountExpected, msg.sender, crossOrderType, dexId_deadline[1], dexId_deadline[2]);
        }        

        processFee(feesPrice[0], fees, feesPrice[1], licenseeVault, msg.sender);
    }

    function callbackCrossExchange( 
        uint256 orderType, 
        address[] memory path, 
        uint256 assetInOffered, 
        address user,
        uint256 dexId,
        uint256 deadline
    ) public returns(bool) {
        require(msg.sender == address(swapFactory) , "Degen : caller is not SwapFactory");
        if(deadline==0) {
            deadline = block.timestamp + deadlineLimit;
        }
        _swap(OrderType(orderType), path, assetInOffered, uint256(0), user, dexId, deadline);
        return true;
    }
/*
    // user has to approve 1Inch router contract to transfer `assetInOffered` before calling this function 
    // when call `swap` API of 1Inch, you have to set:
    // `fromTokenAddress` - token user want to swap (if user swap ETH use address: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)
    // `toTokenAddress ` - token user want to receive
    // `amount` - amount of tokens user want to swap
    // `fromAddress` - the user's address. MAY NOT WORK
    // `slippage` - from user's setting
    function executeSwap1Inch(
        address[2] memory path, // path[0] - from token, path[1] - to token
        OrderType orderType, 
        uint256 assetInOffered, // token amount or value of ETH (Form API response).
        uint256 fees, 
        address licenseeVault,
        address to, // 1Inch router. Form API response.
        bytes memory data   // Form API response.
    ) external payable {
        uint256 gasA = gasleft();
        uint256 receivedFees;
        uint256 value;
        
        if(orderType == OrderType.EthForTokens){
            require(msg.value >= (assetInOffered + fees), "Payment = assetInOffered + fees");
            receivedFees = msg.value - assetInOffered;
            value = assetInOffered;
        } else {
            require(msg.value >= fees, "fees not received");
            receivedFees = msg.value;
            TransferHelper.safeTransferFrom(path[0], msg.sender, address(this), assetInOffered);
            TransferHelper.safeApprove(path[0],to,assetInOffered);
        }
        
        callTo(to, value, data);
   
        processFee(gasA, receivedFees, 0, licenseeVault, msg.sender);
    }
    
    // user has to approve 1Inch router contract to transfer `assetInOffered` before calling this function 
    // when call `swap` API of 1Inch, you have to set:
    // `fromTokenAddress` - token user want to swap (if user swap ETH use address: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)
    // `toTokenAddress ` - token user want to receive
    // `amount` - amount of tokens user want to swap
    // `fromAddress` - the user's address
    // `slippage` - from user's setting
    // `destReceiver` - Degen contract address
    function executeCrossExchange1Inch(
        address[2] memory path, // path[0] - from token, path[1] - to token
        OrderType crossOrderType, 
        uint256 assetInOffered, // token amount or value of ETH (Form API response).
        uint256 fees, 
        address licenseeVault,
        address to, // 1Inch router. Form API response.
        bytes memory data,   // Form API response.
        uint256 toDEX,  // destination dex that will be used on foreign chain.
        uint256 deadline
    ) external payable {
        uint256 gasA = gasleft();
        uint256 _crossOrderType = uint256(crossOrderType);
        uint256 value;
        
        if(deadline==0) {
            deadline = block.timestamp + deadlineLimit;
        }
        
        if(crossOrderType == OrderType.EthForTokens || crossOrderType == OrderType.EthForEth){
            require(msg.value >= (assetInOffered + fees), "Payment = assetInOffered + fees");
            fees = msg.value - assetInOffered;
            value = assetInOffered;
        } else {
            require(msg.value >= fees, "fees not received");
            fees = msg.value;
            TransferHelper.safeTransferFrom(path[0], msg.sender, address(this), assetInOffered);
            TransferHelper.safeApprove(path[0],to,assetInOffered);
        }
               
        uint256 balanceUSDT = IERC20(USDT).balanceOf(address(this));
        callTo(to, value, data);
        balanceUSDT = IERC20(USDT).balanceOf(address(this)) - balanceUSDT;  // USDT amount that received from 1Inch swap
        TransferHelper.safeApprove(USDT, address(swapFactory), balanceUSDT);
        
        swapFactory.swap(USDT, path[1], balanceUSDT, msg.sender, _crossOrderType, toDEX, deadline);

        processFee(gasA, fees, 0, licenseeVault, msg.sender);
    }

    function callTo(address to, uint256 value, bytes memory data) internal {
        (bool success,) = to.call{value: value}(data);
        require(success, "call to contract error");
    }
*/
    // If someone accidentally transfer tokens to this contract, the owner will be able to rescue it and refund sender.
    function rescueTokens(address _token) external onlyOwner {
        if (address(0) == _token) {
            payable(msg.sender).transfer(address(this).balance);
        } else {
            uint256 available = IERC20(_token).balanceOf(address(this));
            TransferHelper.safeTransfer(_token, msg.sender, available);
        }
    }

}
function getColletedFees() external view returns (uint256) {
    // collectedFees starts from 1 to avoid additional gas usage to initiate storage (when collectedFees = 0)
    return collectedFees - 1;
}

// get amount of collected fees that can be claimed