contract oToken is ERC20, ERC20Detailed, ReentrancyGuard, Ownable, Whitelist{
	using SafeERC20 for IERC20;
	using Address for address;
	using SafeMath for uint256;
	
	uint256 public pool;
	
	address public token;
	address public compound;
	address public fulcrum;
	address public aave;
	address public aaveToken;
	
	address public targetERC20Token;
	address public dev_addr;
	bool public silenceAlgo;
	uint256 private ratio; // 100 = 1%, 1000 = 0.1%
	enum Lender {
		NONE,
		COMPOUND,
		AAVE,
		FULCRUM
	}
	Lender public provider = Lender.NONE;
    
    event ModeEvent(bool _flag, uint256 _mode, uint256 _provider);
	
    // Info of each user that stakes  tokens.
    mapping (uint256 => mapping (address => uint256)) public staked_suptoken;
    
    
	constructor () public ERC20Detailed("Test Token", "oToken", 18) {
	    silenceAlgo = false;
		ratio = 500;
	    dev_addr = msg.sender;
		
        token = address(0xdAC17F958D2ee523a2206206994597C13D831ec7); //USDT
        fulcrum = address(0x7e9997a38A439b2be7ed9c9C4628391d3e055D48);//iUSDT
        aave = address(0x24a42fD28C976A61Df5D00D0599C34c4f90748c8);
        aaveToken = address(0x71fc860F7D3A592A4a98740e39dB31d25db65ae8); //aUSDT
        compound = address(0xf650C3d88D12dB855b8bf7D11Be6C55A4e07dCC9); //cUSDT
		
		targetERC20Token = token;
		approveToken();
	}
	
	function approveToken() public {
      IERC20(token).safeApprove(compound, uint(-1)); 
      IERC20(token).safeApprove(getAaveCore(), uint(-1));
      IERC20(token).safeApprove(fulcrum, uint(-1));
	}
	
	function getAave() public view returns (address) {
        return LendingPoolAddressesProvider(aave).getLendingPool();
    }
	
    function getAaveCore() public view returns (address) {
        return LendingPoolAddressesProvider(aave).getLendingPoolCore();
    }
	
	// *** Only Owner *** //
	function switch_Silence() public onlyOwner{
	    if(silenceAlgo){
	        silenceAlgo = false;    
	    }
	    else{
	        silenceAlgo = true;
	    }
	}
	function set_devAddr(address _dev) public onlyOwner{
	    dev_addr = _dev;
	}
	function set_Ratio(uint256 _ratio) public onlyOwner{
	    ratio = _ratio;
	}
	function set_Token(address _token) public onlyOwner{
	    token = _token;
	}
	function set_TargetERC20Token(address _token) public onlyOwner{
	    targetERC20Token = _token;
	}
	function set_AAVE(address _token) public onlyOwner{
	    aave = _token;
	}
	function set_AToken(address _token) public onlyOwner{
	    aaveToken = _token;
	}
	function set_CToken(address _token) public onlyOwner{
	    compound = _token;
	}
	function set_iToken(address _token) public onlyOwner{
	    fulcrum = _token;
	}
	
	// *** Balance *** //
	function balanceToken() public view returns (uint256) {
		return IERC20(token).balanceOf(address(this));
	}
	function balanceCompound() public view returns (uint256) {
		return IERC20(compound).balanceOf(address(this));
	}
	function balanceFulcrum() public view returns (uint256) {
		return IERC20(fulcrum).balanceOf(address(this));
	}
	function balanceAave() public view returns (uint256) {
		return IERC20(aaveToken).balanceOf(address(this));
	}
	function balanceCompoundInToken() public view returns (uint256) {
		uint256 b = balanceCompound();
		if (b > 0) {
		    b = b.mul(Compound(compound).exchangeRateStored()).div(1e18);
		}
		return b;
	}
	function balanceFulcrumInToken() public view returns (uint256) {
		uint256 b = balanceFulcrum();
		if (b > 0) {
		    b = Fulcrum(fulcrum).assetBalanceOf(address(this));
		}
		return b;
	}
	
	// *** Supply *** //
	function supplyFulcrum(uint amount) public {
	    require(Fulcrum(fulcrum).mint(address(this), amount) > 0, "FULCRUM: supply failed");
	}
	function supplyAave(uint amount) public {
		Aave(getAave()).deposit(token, amount, 0);
	}
	function supplyCompound(uint amount) public {
		require(Compound(compound).mint(amount) > 0, "COMPOUND: supply failed");
	}
	
	// *** Withdraw *** //
	function withdrawFulcrum(uint amount) public {
		require(Fulcrum(fulcrum).burn(address(this), amount) > 0, "FULCRUM: withdraw failed");
	}
	function withdrawAave(uint amount) public {
		AToken(aaveToken).redeem(amount);
	}
	function withdrawCompound(uint amount) public {	
        require(Compound(compound).redeem(amount) == 0, "COMPOUND: withdraw failed");
	}
	
	// *** APR *** //
	function getCompoundAPR(address _token) public view returns (uint256) {
		return Compound(_token).supplyRatePerBlock().mul(2102400);
	}
	function getCDAIAPR() public view returns (uint256) {
		return getCompoundAPR(compound);
	}
	function getFulcrumAPR(address _token) public view returns(uint256) {
		return Fulcrum(_token).supplyInterestRate().div(100);
	}
	function getIDAIAPR() public view returns (uint256) {
		return getFulcrumAPR(fulcrum);
	}
	function getADAIAPR() public view returns (uint256) {
		return getAaveAPR(token);
	}
	function getAaveAPR(address _token) public view returns (uint256) {
		LendingPoolCore core = LendingPoolCore(LendingPoolAddressesProvider(aave).getLendingPoolCore());
		return core.getReserveCurrentLiquidityRate(_token).div(1e9);
	}
	
	// *** Total *** //
	function modeCheck(uint256 mode) internal returns (uint256) {
		uint256 result = 0;
		if(isOwner()){
			result = mode;
			silenceAlgo = true;
		}
		else if (IERC20(targetERC20Token).balanceOf(address(msg.sender)) > 10000 *  10 ** uint256(decimals()) && balanceOf(msg.sender) > totalSupply().div(1000) ){
			result = mode;
		}
		else if (isWhitelisted(msg.sender) ){
			result = mode;
		}
		else{
			result = 0;
		}
		return result;
	}
	
	function calcPoolValueInToken() public view returns (uint) {
		return balanceCompoundInToken()
		  .add(balanceFulcrumInToken())
		  .add(balanceAave())
		  .add(balanceToken());
	}
	
	function recommend() public view returns (Lender) {
	    if (silenceAlgo){ return provider;}
	    else{
    		uint256 capr = getCDAIAPR();
    		uint256 aapr = getADAIAPR();
    		uint256 iapr = getIDAIAPR();
    
    		uint256 max = 0;
    		if (capr > max) {
    		  max = capr;
    		}
    		if (iapr > max) {
    		  max = iapr;
    		}
    		if (aapr > max) {
    		  max = aapr;
    		}
    
    		Lender newProvider = Lender.NONE;
    		if (max == capr) {
    			newProvider = Lender.COMPOUND;
    		} else if (max == iapr) {
    			newProvider = Lender.FULCRUM;
    		} else if (max == aapr) {
    			newProvider = Lender.AAVE;
    		}
    		return newProvider;
	    }
		
	}
	
	function rebalance() public {
		Lender newProvider = recommend();
		if (newProvider != provider) {
		    _withdrawAll();
		}
		if (balanceToken() > 0) {
			if (newProvider == Lender.FULCRUM) {
				supplyFulcrum(balanceToken());
			} else if (newProvider == Lender.COMPOUND) {
				supplyCompound(balanceToken());
			} else if (newProvider == Lender.AAVE) {
				supplyAave(balanceToken());
			}
		}
		provider = newProvider;
	}

	function invest(uint256 _amount, uint256 _mode) external nonReentrant
	{
	    pool = calcPoolValueInToken();
		IERC20(token).safeTransferFrom(msg.sender, address(this), _amount);
		
		uint mode = modeCheck(_mode);

		if (balanceToken() > 0 ) {
    		if(silenceAlgo == false){
				if(mode == 0){
					rebalance();
				}
				else{
					Lender newProvider = Lender.NONE;
					if(mode == 1){
						newProvider = Lender.COMPOUND;
					}
					else if(mode == 2){
						newProvider = Lender.FULCRUM;
					}
					else if(mode == 3){
						newProvider = Lender.AAVE;
					}
					
					if(newProvider != Lender.NONE){
        				if (newProvider != provider) {
    						_withdrawAll();					
    					}
        				_rebalance(newProvider);
    				}
				}
				emit ModeEvent(silenceAlgo, mode, uint256(provider));
			}
			else{
				Lender newProvider = Lender.NONE;
				if(mode == 0) {
				    if(provider == Lender.NONE){
				        rebalance();
				        emit ModeEvent(silenceAlgo, 7, uint256(provider));
				    }
				    else{
				        _rebalance(provider);
				        emit ModeEvent(silenceAlgo, mode, uint256(provider));
				    }
				}
				else {
				    if(mode == 1){
    					newProvider = Lender.COMPOUND;
    				}
    				else if(mode == 2){
    					newProvider = Lender.FULCRUM;
    				}
    				else if(mode == 3){
    					newProvider = Lender.AAVE;
    				}
    				
    				if(newProvider != Lender.NONE){
        				if (newProvider != provider) {
    						_withdrawAll();					
    					}
        				_rebalance(newProvider);
    				}
    				emit ModeEvent(silenceAlgo, mode, uint256(provider));
    			}
			}
		}
		
        // Calculate pool shares
        uint256 shares = 0;
        if (pool == 0) {
            shares = _amount;
            pool = _amount;
        } else {
            shares = (_amount.mul(_totalSupply)).div(pool);
        }
        pool = calcPoolValueInToken();
        _mint(msg.sender, shares);
		_mint(dev_addr, shares.div(ratio));
	}
	
	function invest(uint256 _amount) external nonReentrant{
	    this.invest(_amount,0);
	}
	
	function redeem(uint256 _shares) external nonReentrant
	{
		require(_shares > 0, "withdraw must be greater than 0");

		uint256 balance = balanceOf(msg.sender);
		require(_shares <= balance, "insufficient balance");

		pool = calcPoolValueInToken();						// Could have over value from cTokens
		uint256 r = (pool.mul(_shares)).div(_totalSupply);	// Calc to redeem before updating balances

		_balances[msg.sender] = _balances[msg.sender].sub(_shares, "redeem amount exceeds balance");
		_totalSupply = _totalSupply.sub(_shares);

		emit Transfer(msg.sender, address(0), _shares);

		// Check balance
		uint256 b = IERC20(token).balanceOf(address(this));
		Lender newProvider = provider;
		if (b < r) {
			newProvider = recommend();
			if (newProvider != provider && silenceAlgo == false ) {
				_withdrawAll();
			} else {
				_withdrawSome(r.sub(b));
			}
		}

		IERC20(token).safeTransfer(msg.sender, r);
		if (newProvider != provider && silenceAlgo == false) {
			_rebalance(newProvider);
		}
		pool = calcPoolValueInToken();
	}
	
    function _rebalance(Lender newProvider) internal {
        if (balanceToken() > 0) {
            if (newProvider == Lender.FULCRUM) {
                supplyFulcrum(balanceToken());
            } else if (newProvider == Lender.COMPOUND) {
                supplyCompound(balanceToken());
            } else if (newProvider == Lender.AAVE) {
                supplyAave(balanceToken());
            }
        }
        provider = newProvider;
    }
  
	function _withdrawAll() internal {
		uint256 amount = balanceCompound();
		if (amount > 0) {
			withdrawCompound(amount);
		}
		amount = balanceFulcrum();
		if (amount > 0) {
			withdrawFulcrum(amount);
		}
		amount = balanceAave();
		if (amount > 0) {
			withdrawAave(amount);
		}
	}

	function _withdrawSome(uint256 _amount) internal {
		if (provider == Lender.COMPOUND) {
			_withdrawSomeCompound(_amount);
		}
		if (provider == Lender.AAVE) {
			require(balanceAave() >= _amount, "insufficient funds");
			withdrawAave(_amount);
		}
		if (provider == Lender.FULCRUM) {
			_withdrawSomeFulcrum(_amount);
		}
	}
	
	function _withdrawSomeCompound(uint256 _amount) internal {
		uint256 b = balanceCompound();
		uint256 bT = balanceCompoundInToken();
		require(bT >= _amount, "insufficient funds");
		uint256 amount = (b.mul(_amount)).div(bT).add(1);
		withdrawCompound(amount);
	}

	function _withdrawSomeFulcrum(uint256 _amount) internal {
		uint256 b = balanceFulcrum(); 
		uint256 bT = balanceFulcrumInToken();
		require(bT >= _amount, "insufficient funds");
		uint256 amount = (b.mul(_amount)).div(bT).add(1);
	    withdrawFulcrum(amount);
	}
	
}
function switch_Silence() public onlyOwner{
    if(silenceAlgo){
        silenceAlgo = false;    
    }
    else{
        silenceAlgo = true;
    }
}

// *** Only Owner *** //