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

    IERC20 public token;

    IChiToken public constant chi = IChiToken(0x0000000000004946c0e9F43F4Dee607b0eF1fA1c);


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

    uint256 public loanFee = 1;
    uint256 public loanFeeMax = 10000;
    bool public loanEnabled = true;

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

    mapping(address => bool) keepers;

    event FlashLoan(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"
        );
        _;
    }

    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;
    }

    function getName() public view returns(string memory){
        return "mmVaultV2";
    }

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

    function setMin(uint256 _min) external {
        require(msg.sender == governance, "!governance");
        min = _min;
    }

    function setGovernance(address _governance) public {
        require(msg.sender == governance, "!governance");
        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 {
        require(msg.sender == governance, "!governance");
        loanFee = _loanFee;
    }

    function setLoanEnabled(bool _loanEnabled) public {
        require(msg.sender == governance, "!governance");
        loanEnabled = _loanEnabled;
    }

    function addKeeper(address _keeper) public {
        require(
            msg.sender == governance,
            "!governance"
        );
        keepers[_keeper] = true;
    }

    function removeKeeper(address _keeper) public {
        require(
            msg.sender == governance,
            "!governance"
        );
        keepers[_keeper] = false;
    }

    // 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 {
        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 {
        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 flashLoan(address _receiver, uint256 _amount, bytes memory _data) public nonReentrant discountCHI{
        require(loanEnabled == true, "!loanEnabled");
        require(_amount > 0, "amount too small!");
        uint256 beforeBalance = token.balanceOf(address(this));
        require(beforeBalance > _amount, "balance not enough!");

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

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

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

        //execute user's logic
        IFlashLoanReceiver(_receiver).mushroomsFlashloan(address(token), _amount, _fee, _data);

        uint256 afterBalance = token.balanceOf(address(this));

        require(afterBalance == beforeBalance.add(_fee), "payback amount incorrect!");

        emit FlashLoan(address(token), _receiver, _amount, _fee);
    }

}
// https://github.com/iearn-finance/vaults/blob/master/contracts/vaults/yVault.sol
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 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);
//}