contract ReethClaimPolicyV1 is Ownable, ReentrancyGuard {
    using SafeMath for uint256;
    
    // variables
    uint256 public minStakeTime = 5; // At least 5 seconds staked before being able to claim reeth
    uint256 public eFactor = 36; // The exponential factor that determines the early growth rate of claiming
    uint256 public eFactorLength = 14 days; // The length of period where the eFactor applies
    uint256 public maxEarlyAccumulation = 5200; // The most we can earn via the exponential early growth
    uint256 public maxAccumulatedClaim = 44800; // Total maximum accumulation claim back
    uint256 public dailyClaimRate = 500; // The amount of claim increases after the exponential growth phase
    uint256 public maxETHSpentClaimPercent = 4000; // Maximum amount of eth spent claimable
    uint256 public ethEquivalentAtMaxPercent = 10e18; // The ethereum amount that will yield this maximum percent
    
    address public reethAddress; // The address for the REETH tokens
    address public zsTokenAddress;
    address public stakerAddress; // The address for the staker
    address public priceOracleAddress; // The address of the price oracle

    uint256 constant DIVISION_FACTOR = 100000;
    address constant WETH_ADDRESS = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); // WETH address

    constructor(
        address _reeth,
        address _staker,
        address _oracle
    ) public {
        reethAddress = _reeth;
        stakerAddress = _staker;
        priceOracleAddress = _oracle;
    }
    
    modifier onlyStaker() {
        require(_msgSender() == stakerAddress, "Only staker can call this function");
        _;
    }
    
    // functions
    
    
    function getClaimBackPercent(address _user) public view returns (uint256) {
        // This function will calculate the max amount of claimback percent the user can expect based on accumulation time
        Staker _stake = Staker(stakerAddress);
        // First do sanity checks
        {
            uint256 _bal = _stake.getUserBalance(_user);
            if(_bal == 0){
                return 0;
            }
            uint256 _spent = _stake.getETHSpentSinceAction(_user);
            if(_spent == 0){
                return 0;
            }
        }
        uint256 lastTime = _stake.getLastActionTime(_user);
        if(lastTime == 0){
            return 0; // No deposits ever
        }
        if(now < lastTime + minStakeTime){
            return 0; // Too soon to claim
        }
        uint256 timeDiff = now - lastTime; // Will be at least minStakeTime
        
        // Complicated math stuff
        uint256 percent = eFactor.mul(timeDiff**2).div(1e10);
        if(percent > maxEarlyAccumulation){
            percent = maxEarlyAccumulation;
        }
        if(timeDiff > eFactorLength){
            // Add an additional percent up to a higher percent
            uint256 extra = dailyClaimRate.mul(timeDiff.sub(eFactorLength)).div(1 days);
            if(extra > maxAccumulatedClaim){
                extra = maxAccumulatedClaim;
            }
            percent = percent.add(extra);
        }
        return percent;
    }
    
    function getETHSpentClaimPercent(address _user) public view returns (uint256) {
        require(stakerAddress != address(0), "Staker not set yet");
        Staker _stake = Staker(stakerAddress);
        uint256 _value = _stake.getUserBalance(_user);
        if(_value == 0){
            return 0;
        }
        _value = calculateUserETHValue(_user);
        if(_value == 0){
            return 0;
        }
        if(_value >= ethEquivalentAtMaxPercent){
            return maxETHSpentClaimPercent;
        }
        uint256 percent = maxETHSpentClaimPercent.mul(_value).div(ethEquivalentAtMaxPercent);
        return percent;
    }
    
    // Claim call
    function getClaimable(address _user) external onlyStaker returns (uint256){
        require(priceOracleAddress != address(0), "Price oracle not set yet");
        PriceOracle(priceOracleAddress).updateREETHPrice(); // Update the price
        uint256 claimable = queryClaimable(_user);
        return claimable;
    }
    
    function queryClaimable(address _user) public view returns (uint256) {
        require(PriceOracle(priceOracleAddress).getLatestREETHPrice() > 0, "There is no price yet determined for REETH");
        require(stakerAddress != address(0), "Staker not set yet");
        require(zsTokenAddress != address(0), "ZS token not set yet");
        uint256 claimPercent = getClaimBackPercent(_user);
        if(claimPercent == 0){
            return 0;
        }
        uint256 stackValue = calculateUserETHValue(_user); // Stack value in eth
        uint256 spentAmount = Staker(stakerAddress).getETHSpentSinceAction(_user);
        if(spentAmount == 0 || stackValue == 0){
            return 0;
        }
        uint256 claimable = stackValue.mul(claimPercent).div(DIVISION_FACTOR); // Maximum amount claimable in ETH based on stack
        uint256 spentPercent = getETHSpentClaimPercent(_user);
        if(spentPercent == 0){
            return 0;
        }else{
            spentAmount = spentAmount.mul(spentPercent).div(DIVISION_FACTOR); // Reduce eligible spent percent by max amount
        }
        if(claimable > spentAmount){
            claimable = spentAmount; // Cannot claim more than spent
        }
        uint256 reethPrice = PriceOracle(priceOracleAddress).getLatestREETHPrice();
        // Convert claimable to reeth units
        claimable = claimable.mul(10**uint256(IERC20(reethAddress).decimals())).div(10**uint256(IERC20(WETH_ADDRESS).decimals()));
        // This is claimable in reeth
        claimable = claimable.mul(1e18).div(reethPrice);
        return claimable;
    }
    
    function calculateUserETHValue(address _user) public view returns (uint256) {
        // Now calculate the eth value of the user's strategy position
        // This returns how much eth the user's staked position is worth
        Staker _stake = Staker(stakerAddress);
        uint256 _bal = _stake.getUserBalance(_user); // Amount of ZS tokens
        if(_bal == 0){ return 0; }
        ZsTokenProtocol zsToken = ZsTokenProtocol(zsTokenAddress);
        address strategyAddress = zsToken.getCurrentStrategy(); 
        uint256 totalZS = IERC20(zsTokenAddress).totalSupply();
        uint256 totalETH = IERC20(WETH_ADDRESS).balanceOf(zsTokenAddress);
        uint256 totalREETH = IERC20(reethAddress).balanceOf(zsTokenAddress);
        if(strategyAddress != address(0)){
            totalETH += IERC20(WETH_ADDRESS).balanceOf(strategyAddress);
            totalREETH += IERC20(reethAddress).balanceOf(strategyAddress);
        }
        uint256 reethPrice = PriceOracle(priceOracleAddress).getLatestREETHPrice();
        // Convert reeth decimals to ETH
        totalREETH = totalREETH.mul(10**uint256(IERC20(WETH_ADDRESS).decimals())).div(10**uint256(IERC20(reethAddress).decimals()));
        totalETH += totalREETH.mul(reethPrice).div(1e18);
        return totalETH.mul(_bal).div(totalZS); // This will be the user's equivalent balance in eth worth
    }
    
    // Governance only functions
    
    // Timelock variables
    
    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;
    uint256[8] private _timelock_data;
    
    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 claimable staker
    // --------------------
    function startChangeStaker(address _address) external onlyGovernance {
        _timelockStart = now;
        _timelockType = 2;
        _timelock_address = _address;
        if(stakerAddress == address(0)){
            _timelockType = 0;
            internalChangeStaker(_timelock_address);
        }
    }
    
    function finishChangeStaker() external onlyGovernance timelockConditionsMet(2) {
        internalChangeStaker(_timelock_address);
    }
    
    function internalChangeStaker(address _addr) internal {
        stakerAddress = _addr;
    }
    // --------------------
    
    // Change the price oracle
    // --------------------
    function startChangePriceOracle(address _address) external onlyGovernance {
        _timelockStart = now;
        _timelockType = 3;
        _timelock_address = _address;
        if(priceOracleAddress == address(0)){
            _timelockType = 0;
            internalChangePriceOracle(_timelock_address);
        }
    }
    
    function finishChangePriceOracle() external onlyGovernance timelockConditionsMet(3) {
        internalChangePriceOracle(_timelock_address);
    }
    
    function internalChangePriceOracle(address _addr) internal {
        priceOracleAddress = _addr;
    }
    // --------------------
    
    // Change the zs token
    // --------------------
    function startChangeZSToken(address _address) external onlyGovernance {
        _timelockStart = now;
        _timelockType = 4;
        _timelock_address = _address;
        if(zsTokenAddress == address(0)){
            _timelockType = 0;
            internalChangeZSToken(_timelock_address);
        }
    }
    
    function finishChangeZSToken() external onlyGovernance timelockConditionsMet(4) {
        internalChangeZSToken(_timelock_address);
    }
    
    function internalChangeZSToken(address _addr) internal {
        zsTokenAddress = _addr;
    }
    // --------------------
    
    // Change the claim factors
    // --------------------
    function startChangeClaimFactors(uint256 _minStake, uint256 _eFac, uint256 _eFacLength, uint256 _maxEarly,
                                    uint256 _dailyPercent, uint256 _maxEnd, uint256 _maxSpentAmount, uint256 _maxSpentPercent) external onlyGovernance {
        _timelockStart = now;
        _timelockType = 5;
        _timelock_data[0] = _minStake;
        _timelock_data[1] = _eFac;
        _timelock_data[2] = _eFacLength;
        _timelock_data[3] = _maxEarly;
        _timelock_data[4] = _dailyPercent;
        _timelock_data[5] = _maxEnd;
        _timelock_data[6] = _maxSpentAmount;
        _timelock_data[7] = _maxSpentPercent;
    }
    
    function finishChangeClaimFactors() external onlyGovernance timelockConditionsMet(5) {
        minStakeTime = _timelock_data[0];
        eFactor = _timelock_data[1];
        eFactorLength = _timelock_data[2];
        maxEarlyAccumulation = _timelock_data[3];
        dailyClaimRate = _timelock_data[4];
        maxAccumulatedClaim = _timelock_data[5];
        ethEquivalentAtMaxPercent = _timelock_data[6];
        maxETHSpentClaimPercent = _timelock_data[7];
    }
    // --------------------
   
}
function startChangeStaker(address _address) external onlyGovernance {
    _timelockStart = now;
    _timelockType = 2;
    _timelock_address = _address;
    if(stakerAddress == address(0)){
        _timelockType = 0;
        internalChangeStaker(_timelock_address);
    }
}

// --------------------
// Change the claimable staker
// --------------------