contract Yfin is ERC20, ERC20Detailed, ReentrancyGuard {
  using SafeERC20 for IERC20;
  using Address for address;
  using SafeMath for uint256;
  
    uint256 public feeRateDivision;
    address public feeAccount;
    
    struct UnderlyingToken {
        string tokenName;
        address tokenAddress;
        uint256 ratioMultiplier; 
        uint256 feeBalance;
    }  
    
    // index of tokens has to match with index of yieldfarmaddresses
    // they are parallel array
    uint8 public numTokens = 4;
    UnderlyingToken[4] public tokens;
    IYieldFarm[4] public farms;
    
    event DepositUnderlyingToken(address token, address indexed from, uint256 amount);
    event WithdrawUnderlyingToken(address token, address indexed to, uint256 amount);
    event FeeAccountTransfered(address feeAccount);
    event FeeRateChanged(uint256 rate);
    event YieldFarmAddressChange(address oldFarm, address newFarm);
    
    
    constructor () public  ERC20Detailed("Y Financial", "YFIN", 18) {
        /**
         * Fee is in terms of division, for example 100 means 1%
         * All token has to be 18 digits
         * Right now only breakdown token has fee
         */ 
    
        _totalSupply = 0;
        
        feeRateDivision = 100;
        feeAccount = _msgSender();
        
        tokens[0] = UnderlyingToken("pickle", address(0x429881672B9AE42b8EbA0E26cD9C73711b891Ca5), 3, 0);
        tokens[1] = UnderlyingToken("sushi", address(0x6B3595068778DD592e39A122f4f5a5cF09C90fE2), 30, 0);
        tokens[2] = UnderlyingToken("dai", address(0x6B175474E89094C44Da98b954EedeAC495271d0F), 10, 0);
        tokens[3] = UnderlyingToken("lua", address(0xB1f66997A5760428D3a87D68b90BfE0aE64121cC), 300, 0);
        
        // yield farm address
        farms[0] = IYieldFarm(address(0x0000000000000000000000000000000000000000));
        farms[1] = IYieldFarm(address(0x0000000000000000000000000000000000000000));
        farms[2] = IYieldFarm(address(0x0000000000000000000000000000000000000000));
        farms[3] = IYieldFarm(address(0x0000000000000000000000000000000000000000));
    }

    function depositAndMint(uint256 amount) external nonReentrant returns (bool success) {
        // convenient method to get YFIN right away
        // remember to get approve for all underlying tokens before calling this function
        for (uint i = 0; i < numTokens; i++) {
            IERC20(tokens[i].tokenAddress).safeTransferFrom(_msgSender(), address(this), amount.mul(tokens[i].ratioMultiplier));
        }
         _mint(_msgSender(), amount);
        return success;
    }
    
    function _withdrawUnderlyingToken(uint index, uint256 amount) internal {
        UnderlyingToken storage token = tokens[index];
    
        // need to withdraw from pool if token not enough
        uint tokenBalance = getInternalUnderlying(index);
        if (tokenBalance < amount) {
            _yieldFarmUnstake(index, amount.sub(tokenBalance));
            // worse case got whatever unstaked from the pool
            tokenBalance = getInternalUnderlying(index); 
        }
        
        uint256 withdrawAmount;
        if (_msgSender() == feeAccount) {
            if (tokenBalance < amount) {
                withdrawAmount = tokenBalance;
            } else {
                withdrawAmount = amount;
            }
        } else {
            uint256 fee = amount.div(feeRateDivision);
            withdrawAmount = amount.sub(fee);
            // this happen when pool withdraw fee greater than yfin burn fee
            if (tokenBalance <= withdrawAmount) {
                withdrawAmount = tokenBalance;
            } else {
                if (tokenBalance.sub(withdrawAmount) >= fee) {
                    token.feeBalance = token.feeBalance.add(fee);
                } else {
                    token.feeBalance = token.feeBalance.add(tokenBalance.sub(withdrawAmount));
                }
            }
        }
        
        IERC20(token.tokenAddress).safeTransfer(_msgSender(), withdrawAmount);
        emit WithdrawUnderlyingToken(token.tokenAddress, _msgSender(), withdrawAmount);
    }
    
    function pushFeeForToken(uint index, uint amount) public nonReentrant {
        require (amount <= tokens[index].feeBalance, "YFIN: requested more than available fees");
        tokens[index].feeBalance = tokens[index].feeBalance.sub(amount);
        IERC20(tokens[index].tokenAddress).safeTransfer(feeAccount, amount);
    }
    
    function pushAllFeeForToken(uint index) public {
        pushFeeForToken(index, tokens[index].feeBalance);
    }
    
    function pushAllFeeForAllTokens() external {
        for (uint i = 0; i < numTokens; i++) {
            pushAllFeeForToken(i);
        }
    }
    
    function breakdownAndWithdrawUnderlying(uint256 amount) public nonReentrant returns (bool success) {
        require (balanceOf(_msgSender()) >= amount, "YFIN: Breakdown amount excess balance");
        _burn(_msgSender(), amount);
        
        for (uint i = 0; i < numTokens; i++) {
            _withdrawUnderlyingToken(i, amount.mul(tokens[i].ratioMultiplier));
        }
        return true;
    }
    
    function getExternalUnderlying(uint index ) public view returns (uint256 balance) {
        return farms[index].totalStaked();
    }
    
    function getInternalUnderlying (uint index ) public view returns (uint256 balance) {
        return IERC20(tokens[index].tokenAddress).balanceOf(address(this)).sub(tokens[index].feeBalance);
    }
    
    function balanceOfUnderlying(uint index) public view returns (uint256 balance) {
         return getInternalUnderlying(index).add(getExternalUnderlying(index));
    }
    
    function changeFeeRate(uint rate) external onlyOwner returns (bool success) {
        feeRateDivision = rate;
        emit FeeRateChanged(rate);
        return true;
    }
    
    function changeYieldFarmAddress(uint index, address newModuleAddress) external onlyOwner returns (bool success) {
        address old = address(farms[index]);
        farms[index] = IYieldFarm(newModuleAddress);
        emit YieldFarmAddressChange(old, newModuleAddress);
        
        // approve the token with the same index
        _approveModule(tokens[index].tokenAddress, newModuleAddress);
        return true;
    }

    function _approveModule(address tokenAddress, address approveAddress) internal returns (bool success) {
        // note that this is approve for unlimited amount
        IERC20(tokenAddress).safeApprove(approveAddress, uint256(-1)); //also add to constructor
        return true;
     }
  
    function disapproveModule(address tokenAddress, address approveAddress) external onlyOwner returns (bool success) {
        // note that this is approve for unlimited amount
        IERC20(tokenAddress).safeApprove(approveAddress, 0); //also add to constructor
        return true;
     }
     
    function approveModule(address tokenAddress, address approveAddress) external onlyOwner returns (bool success) {
        _approveModule(tokenAddress, approveAddress);
        return true;
     }
     
    function yieldFarmStake(uint index, uint256 amount) public onlyOwner nonReentrant returns (bool success) {
        require(getInternalUnderlying(index) >= amount, "YFIN: Balance not enough");
        farms[index].stake(amount);
        return true;
    }
    
    function yieldFarmStakeAll(uint index) external onlyOwner returns (bool success) {
        require(getInternalUnderlying(index) > 0, 'YFIN: you have nothing to stake');
        yieldFarmStake(index, getInternalUnderlying(index));
        return true;
    }
    
    function _yieldFarmUnstake(uint index, uint256 amount) internal returns (uint256) {
        require(getExternalUnderlying(index) >= amount, "YFIN: Balance not enough");
        return farms[index].unstake(amount); 
    }
    
    // if the yield farm address only has one token
    function yieldFarmUnstake(uint index, uint256 amount) external onlyOwner nonReentrant returns (uint256) {
        require(amount > 0, "YFIN: You can not unstake non-positive number");
        return _yieldFarmUnstake(index, amount);
    }

    function yieldFarmUnstakeAll(uint index) public onlyOwner returns (bool success) {
        require(getExternalUnderlying(index) > 0, 'YFIN: you have nothing to unstake');
        _yieldFarmUnstake(index, getExternalUnderlying(index));
        return true; 
    }
    
    function changeFeeAccount(address newFeeAccount) external onlyOwner returns (bool success) {
        feeAccount = newFeeAccount;
        emit FeeAccountTransfered(newFeeAccount);
        return true;
    }
}
function yieldFarmUnstake(uint index, uint256 amount) external onlyOwner nonReentrant returns (uint256) {
    require(amount > 0, "YFIN: You can not unstake non-positive number");
    return _yieldFarmUnstake(index, amount);
}

// if the yield farm address only has one token