contract ReethClaimableStaker is Ownable, ReentrancyGuard {
    using SafeMath for uint256;
    using SafeERC20 for IERC20;
    
    // variables
    address public zsTokenAddress; // The address for the strategy token
    address public ethSpentOracleAddress;
    address public claimPolicyAddress;
    address public reethAddress;
    uint256 public totalZSTokens;
    
    uint256 constant DIVISION_FACTOR = 100000;
    
    mapping(address => UserInfo) private allUsersInfo;
    
    // Structs
    struct UserInfo {
        uint256 depositAmount; // How many LP tokens the user has in contract
        uint256 lastActionTime; // Time at last deposit or claim action
        uint256 lastETHSpent; // The value from the eth spent oracle
    }

    // Events
    event StakedToken(address indexed user, uint256 amount);
    event UnstakedToken(address indexed user, uint256 amount);
    event ClaimedREETH(address indexed user, uint256 amount);
    
    constructor(
        address _reeth,
        address _ethOracle
    ) public {
        reethAddress = _reeth;
        ethSpentOracleAddress = _ethOracle;
    }
    
    // functions
    function getUserBalance(address _user) external view returns (uint256){
        return allUsersInfo[_user].depositAmount;
    }
    
    function getLastActionTime(address _user) external view returns (uint256){
        return allUsersInfo[_user].lastActionTime;
    }

    function getLastETHSpent(address _user) external view returns (uint256){
        return allUsersInfo[_user].lastETHSpent;
    }

    function getETHSpentSinceAction(address _user) external view returns (uint256) {
        if(ethSpentOracleAddress == address(0)){
            return 0;
        }
        // Will return the amount of ETH spent since last call
        uint256 total = SpentOracle(ethSpentOracleAddress).getUserETHSpent(_user);
        if(total > allUsersInfo[_user].lastETHSpent){
            return total.sub(allUsersInfo[_user].lastETHSpent);
        }else{
            // Possibly switched to a new iteration
            return 0;
        }
    }

    // Stake functions
    function stakeZSToken(uint256 _amount) external nonReentrant
    {
        uint256 gasUsed = gasleft(); // Start calculate gas spent
        require(_amount > 0, "Cannot stake 0");
        require(claimPolicyAddress != address(0), "Claim policy not set yet");
        allUsersInfo[_msgSender()].lastActionTime = now;

        IERC20 _token = IERC20(zsTokenAddress); // Trusted token
        uint256 _before = _token.balanceOf(address(this));
        _token.safeTransferFrom(_msgSender(), address(this), _amount); // Transfer token to this address
        _amount = _token.balanceOf(address(this)).sub(_before);
        
        totalZSTokens = totalZSTokens.add(_amount);
        allUsersInfo[_msgSender()].depositAmount = allUsersInfo[_msgSender()].depositAmount.add(_amount);
        emit StakedToken(_msgSender(), _amount);
        if(ethSpentOracleAddress != address(0)){
            // Factor this gas usage to stake into the oracle
            SpentOracle oracle = SpentOracle(ethSpentOracleAddress);
            gasUsed = gasUsed.sub(gasleft()).mul(tx.gasprice); // The amount of ETH used for this transaction
            oracle.addUserETHSpent(_msgSender(), gasUsed);
        }
    }
    
    // Unstake & claim functions
    function claimOnly() external nonReentrant
    {
        internalClaimReward(_msgSender());
    }
    
    function claimAndExit(uint256 _amount) external nonReentrant
    {
        internalClaimReward(_msgSender());
        internalUnstake(_msgSender(), _amount);
    }
    
    function exitOnly(uint256 _amount) external nonReentrant
    {
        internalUnstake(_msgSender(), _amount);
    }
    
    function internalClaimReward(address _user) internal {
        uint256 gasUsed = gasleft(); // Start calculate gas spent, user is reimbursed for gas spent claiming possibly
        require(claimPolicyAddress != address(0), "Claim policy not set yet");
        require(allUsersInfo[_user].depositAmount > 0, "There is no balance for this user");
        require(allUsersInfo[_user].lastActionTime > 0, "User has never deposited");
        Claimer policy = Claimer(claimPolicyAddress);
        uint256 mintable = policy.getClaimable(_user); // This updates the oracle and queries the mintable amount
        allUsersInfo[_user].lastActionTime = now;
        if(mintable > 0){
            // User is eligible for Reeth, mint some for user
            ReethToken reeth = ReethToken(reethAddress);
            reeth.mint(_user, mintable);
            emit ClaimedREETH(_user, mintable);
            if(ethSpentOracleAddress != address(0)){
                // Factor this gas usage to stake into the oracle
                SpentOracle oracle = SpentOracle(ethSpentOracleAddress);
                allUsersInfo[_user].lastETHSpent = oracle.getUserETHSpent(_user); // Get oracle information before updating oracle
                gasUsed = gasUsed.sub(gasleft()).mul(tx.gasprice); // The amount of ETH used for this transaction
                oracle.addUserETHSpent(_user, gasUsed);
            }            
        }
    }
    
    function internalUnstake(address _user, uint256 _amount) internal {
        require(allUsersInfo[_user].depositAmount > 0, "There is no balance for this user");
        allUsersInfo[_user].lastActionTime = now;
        totalZSTokens = totalZSTokens.sub(_amount);
        allUsersInfo[_user].depositAmount = allUsersInfo[_user].depositAmount.sub(_amount);
        IERC20(zsTokenAddress).safeTransfer(_user, _amount);
        emit UnstakedToken(_user, _amount);
    }
    
    // Governance only functions
    function governanceSetZSToken(address _addr) external onlyGovernance {
        require(zsTokenAddress == address(0), "Token is already set");
        zsTokenAddress = _addr;
    }
    
    // Timelock variables
    // Timelock doesn't activate until claimer address set
    
    uint256 private _timelockStart; // The start of the timelock to change governance variables
    uint256 private _timelockType; // The function that needs to be changed
    uint256 constant TIMELOCK_DURATION = 86400; // Timelock is 24 hours
    
    // Reusable timelock variables
    address private _timelock_address;
    
    modifier timelockConditionsMet(uint256 _type) {
        require(_timelockType == _type, "Timelock not acquired for this function");
        _timelockType = 0; // Reset the type once the timelock is used
        require(now >= _timelockStart + TIMELOCK_DURATION, "Timelock time not met");
        _;
    }
    
    // Change the owner of the token contract
    // --------------------
    function startGovernanceChange(address _address) external onlyGovernance {
        _timelockStart = now;
        _timelockType = 1;
        _timelock_address = _address;       
    }
    
    function finishGovernanceChange() external onlyGovernance timelockConditionsMet(1) {
        transferGovernance(_timelock_address);
    }
    // --------------------
    
    // Change the claimer policy
    // --------------------
    function startChangeClaimerPolicy(address _address) external onlyGovernance {
        _timelockStart = now;
        _timelockType = 2;
        _timelock_address = _address;
        if(claimPolicyAddress == address(0)){
            _timelockType = 0;
            internalChangeClaimerPolicy(_timelock_address);
        }
    }
    
    function finishChangeClaimerPolicy() external onlyGovernance timelockConditionsMet(2) {
        internalChangeClaimerPolicy(_timelock_address);
    }
    
    function internalChangeClaimerPolicy(address _addr) internal {
        claimPolicyAddress = _addr;
    }
    // --------------------
    
    // Change the eth spent oracle
    // --------------------
    function startChangeSpentOracle(address _address) external onlyGovernance {
        _timelockStart = now;
        _timelockType = 3;
        _timelock_address = _address;
        if(ethSpentOracleAddress == address(0)){
            _timelockType = 0;
            internalChangeSpentOracle(_timelock_address);
        }
    }
    
    function finishChangeSpentOracle() external onlyGovernance timelockConditionsMet(3) {
        internalChangeSpentOracle(_timelock_address);
    }
    
    function internalChangeSpentOracle(address _addr) internal {
        ethSpentOracleAddress = _addr;
    }
    // --------------------
   
}
function claimOnly() external nonReentrant
    {
        internalClaimReward(_msgSender());
    }
// Unstake & claim functions