contract MMVault is ReentrancyGuard, ERC20Permit, IERC3156FlashLender  {
    using SafeERC20 for IERC20;
    using Address for address;
    using SafeMath for uint256;

    IERC20 public immutable token;

    IChiToken public constant chi = IChiToken(0x0000000000004946c0e9F43F4Dee607b0eF1fA1c);

    uint256 public min = 9900;
    uint256 public constant max = 10000;

    uint256 public loanFee = 1;
    uint256 public constant loanFeeMax = 10000;
    bool public loanEnabled = false;

    address public governance;
    address public timelock;
    address public controller;

    mapping(address => bool) public keepers;
    mapping(address => bool) public reentrancyWhitelist;

    uint256 public constant DAY_SECONDS = 86400;
    uint256 public constant lockWindowBuffer = DAY_SECONDS/4;

    uint256 public lockStartTime;
    uint256 public lockWindow;
    uint256 public withdrawWindow;
    uint256 public earnedTimestamp;
    bool public lockEnabled = false;
    bool public earnOnceEnabled = false;

    event FlashLoan(address _initiator, address _token, address _receiver, uint256 _amount, uint256 _loanFee);

    modifier discountCHI() {
        uint256 gasStart = gasleft();
        _;
        uint256 gasSpent = 21000 + gasStart - gasleft() + 16 * msg.data.length;

        if(chi.balanceOf(msg.sender) > 0) {
            chi.freeFromUpTo(msg.sender, (gasSpent + 14154) / 41947);
        }
    }

    modifier onlyKeepers {
        require(
            keepers[msg.sender] ||
            msg.sender == governance,
            "!keepers"
        );
        _;
    }

    modifier onlyGovernance(){
        require(msg.sender == governance, "!governance");
        _;
    }

    modifier canEarn {
        if(lockEnabled){
            require(
                block.timestamp > lockStartTime,
                "!earnTime"
            );
            if(earnOnceEnabled){
                require(
                    block.timestamp.sub(earnedTimestamp) > lockWindow,
                     "!earnTwice");
                if(earnedTimestamp != 0){
                    lockStartTime = getLockCycleEndTime();
                }
                earnedTimestamp = block.timestamp;
            }
        }
        _;
    }

    modifier canWithdraw(uint256 _shares) {
        if(lockEnabled){
            //withdraw locker not work when withdraw amount less than available balance
            if(!withdrawableWithoutLock(_shares)){
                uint256 withdrawStartTimestamp = lockStartTime.add(lockWindow);
                require(
                    block.timestamp > withdrawStartTimestamp &&
                    block.timestamp < withdrawStartTimestamp.add(withdrawWindow),
                    "!withdrawTime"
                );
            }
        }
        _;
    }
	
    modifier nonReentrantWithWhitelist() {
        // only check if NOT in whitelist
        if (!reentrancyWhitelist[msg.sender]){
            // On the first call to nonReentrantWithWhitelist, _notEntered will be true
            require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
            // Any calls to nonReentrantWithWhitelist after this point will fail
            _status = _ENTERED;
        }

        _;	
        
        if (!reentrancyWhitelist[msg.sender]){
            // By storing the original value once again, a refund is triggered (see https://eips.ethereum.org/EIPS/eip-2200)
            _status = _NOT_ENTERED;	
        }
    }

    constructor(address _token, address _governance, address _timelock, address _controller)
        public
        ERC20Permit(
            string(abi.encodePacked("mushrooming ", ERC20(_token).name())),
            string(abi.encodePacked("m", ERC20(_token).symbol()))
        )
    {
        _setupDecimals(ERC20(_token).decimals());
        token = IERC20(_token);
        governance = _governance;
        timelock = _timelock;
        controller = _controller;

        //time line: [lockStartTime]|----lockWindow----|----withdrawWindow----|[lockStartTime]|----lockWindow---|........
        lockWindow = (14 * DAY_SECONDS) + lockWindowBuffer;
        withdrawWindow = DAY_SECONDS/2;
        lockStartTime = block.timestamp.add(withdrawWindow);
    }

    function getName() public pure returns(string memory){
        return "mmVaultV2.1";
    }

    function balance() public view returns (uint256) {
        return token.balanceOf(address(this)).add(IController(controller).balanceOf(address(token)));
    }

    function setMin(uint256 _min) external onlyGovernance{
        min = _min;
    }

    function setGovernance(address _governance) public onlyGovernance{
        governance = _governance;
    }

    function setTimelock(address _timelock) public {
        require(msg.sender == timelock, "!timelock");
        timelock = _timelock;
    }

    function setController(address _controller) public {
        require(msg.sender == timelock, "!timelock");
        controller = _controller;
    }

    function setLoanFee(uint256 _loanFee) public onlyGovernance{
        loanFee = _loanFee;
    }

    function setLoanEnabled(bool _loanEnabled) public onlyGovernance{
        loanEnabled = _loanEnabled;
    }

    function addKeeper(address _keeper) public onlyGovernance{
        keepers[_keeper] = true;
    }

    function removeKeeper(address _keeper) public onlyGovernance{
        keepers[_keeper] = false;
    }

    function addReentrancyWhitelist(address _whitelist) public onlyGovernance{
        reentrancyWhitelist[_whitelist] = true;
    }

    function removeReentrancyWhitelist(address _whitelist) public onlyGovernance{
        reentrancyWhitelist[_whitelist] = false;
    }

    function setLockWindow(uint256 _lockWindow) public onlyGovernance {
        lockWindow = _lockWindow.add(lockWindowBuffer);
    }

    function setWithdrawWindow(uint256 _withdrawWindow) public onlyGovernance {
        withdrawWindow = _withdrawWindow;
    }

    function setLockEnabled(bool _enabled) public onlyGovernance {
        lockEnabled = _enabled;
    }

    function setEarnOnceEnabled(bool _earnOnce) public onlyGovernance {
        earnOnceEnabled = _earnOnce;
    }

    function resetLockStartTime(uint256 _lockStartTime) public onlyGovernance{
        require(lockEnabled, "!lockEnabled");

        uint256 withdrawEndTime = getLockCycleEndTime();
        require(block.timestamp >= withdrawEndTime, "Last lock cycle not end");
        require(_lockStartTime > block.timestamp, "!_lockStartTime");
        lockStartTime = _lockStartTime;
    }

    function getLockCycleEndTime() public view returns (uint256){
        return lockStartTime.add(lockWindow).add(withdrawWindow);
    }

    function withdrawableWithoutLock(uint256 _shares) public view returns(bool){
        uint256 _withdrawAmount = (balance().mul(_shares)).div(totalSupply());
        return _withdrawAmount <= token.balanceOf(address(this));
    }

    // Custom logic in here for how much the vaults allows to be borrowed
    // Sets minimum required on-hand to keep small withdrawals cheap
    function available() public view returns (uint256) {
        return token.balanceOf(address(this)).mul(min).div(max);
    }

    function earn() public nonReentrant onlyKeepers canEarn {
        uint256 _bal = available();
        token.safeTransfer(controller, _bal);
        IController(controller).earn(address(token), _bal);
    }

    function depositAll() external {
        deposit(token.balanceOf(msg.sender));
    }

    function deposit(uint256 _approveAmount, uint256 _amount, uint256 _deadline, uint8 v, bytes32 r, bytes32 s) public {
        require(_approveAmount >= _amount, "!_approveAmount");
        IERC2612(address(token)).permit(msg.sender, address(this), _approveAmount, _deadline, v, r, s);
        deposit(_amount);
    }

    function deposit(uint256 _amount) public nonReentrant {
        uint256 _pool = balance();
        uint256 _before = token.balanceOf(address(this));
        token.safeTransferFrom(msg.sender, address(this), _amount);
        uint256 _after = token.balanceOf(address(this));
        _amount = _after.sub(_before); // Additional check for deflationary tokens
        uint256 shares = 0;
        if (totalSupply() == 0) {
            shares = _amount;
        } else {
            shares = (_amount.mul(totalSupply())).div(_pool);
        }
        _mint(msg.sender, shares);
    }

    function withdrawAll() external {
        withdraw(balanceOf(msg.sender));
    }

    // Used to swap any borrowed reserve over the debt limit to liquidate to 'token'
    function harvest(address reserve, uint256 amount)  external nonReentrant {
        require(msg.sender == controller, "!controller");
        require(reserve != address(token), "token");
        IERC20(reserve).safeTransfer(controller, amount);
    }

    // No rebalance implementation for lower fees and faster swaps
    function withdraw(uint256 _shares) public nonReentrant canWithdraw(_shares) {
        uint256 r = (balance().mul(_shares)).div(totalSupply());
        _burn(msg.sender, _shares);

        // Check balance
        uint256 b = token.balanceOf(address(this));
        if (b < r) {
            uint256 _withdraw = r.sub(b);
            IController(controller).withdraw(address(token), _withdraw);
            uint256 _after = token.balanceOf(address(this));
            uint256 _diff = _after.sub(b);
            if (_diff < _withdraw) {
                r = b.add(_diff);
            }
        }

        token.safeTransfer(msg.sender, r);
    }

    function getRatio() public view returns (uint256) {
        return balance().mul(1e18).div(totalSupply());
    }

    function maxFlashLoan(
        address _token
    ) external override view returns (uint256){
        require(address(_token) == address(token), "!_token");
        return token.balanceOf(address(this));
    }

    function flashFee(
        address _token,
        uint256 amount
    ) external override view returns (uint256){
        require(address(_token) == address(token), "!_token");
        return loanFee.mul(amount).div(loanFeeMax);
    }

    // https://eips.ethereum.org/EIPS/eip-3156#lender-specification
    function flashLoan(IERC3156FlashBorrower _receiver, address _token, uint256 _amount, bytes memory _data) public override nonReentrantWithWhitelist discountCHI returns (bool){
        require(loanEnabled == true, "!loanEnabled");
        require(_amount > 0, "amount too small!");
        require(address(_token) == address(token), "!_token");
        uint256 beforeBalance = token.balanceOf(address(this));
        require(beforeBalance > _amount, "balance not enough!");
        
        // keep track of Vault balance for sanity check
        uint256 _totalBalBefore = balance();

        //loanFee
        uint256 _fee = _amount.mul(loanFee).div(loanFeeMax);

        require(_fee > 0, "fee too small");

        //transfer token to _receiver
        token.safeTransfer(address(_receiver), _amount);

        //execute user's logic
        IERC3156FlashBorrower(_receiver).onFlashLoan(address(msg.sender), address(token), _amount, _fee, _data);

        // pull repayment from _receiver
        uint256 _totalDue = _amount.add(_fee);
        token.safeTransferFrom(address(_receiver), address(this), _totalDue);
		
        // ensure we got correct balance after flashloan
        uint256 _totalBalAfter = balance();
        require(_totalBalAfter == _totalBalBefore.add(_fee), "payback amount incorrect!");

        emit FlashLoan(address(msg.sender), address(token), address(_receiver), _amount, _fee);
        return true;
    }

}
// https://github.com/iearn-finance/vaults/blob/master/contracts/vaults/yVault.sol
function available() public view returns (uint256) {
    return token.balanceOf(address(this)).mul(min).div(max);
}

// Custom logic in here for how much the vaults allows to be borrowed
// Sets minimum required on-hand to keep small withdrawals cheap