contract ReethLPFarm is Ownable, ReentrancyGuard {
    using SafeMath for uint256;
    using SafeERC20 for IERC20;
    using Address for address;
    
    IERC20 public rewardsToken;
    IERC20 public stakingToken;
    
    uint256 public duration = 7 days;
    uint256 public baseReward = 2e18; // 2 reeth per week (adjusts as supply increases or shrinks)

    uint256 public periodFinish = 0;
    uint256 public rewardRate = 0;
    uint256 public lastUpdateTime;
    uint256 public rewardPerTokenStored;

    bool public initialized = false;
    bool public rewardsStopped;

    mapping(address => uint256) public userRewardPerTokenPaid;
    mapping(address => uint256) public rewards;
    
    uint256 private _totalSupply;
    mapping(address => uint256) private _balances;

    event RewardAdded(uint256 reward);
    event Staked(address indexed user, uint256 amount);
    event Withdrawn(address indexed user, uint256 amount);
    event RewardPaid(address indexed user, uint256 reward);
    event RewardsDurationUpdated(uint256 newDuration);
    event BaseRewardChanged(uint256 newBase);
    
    constructor(
        address _rewardsToken,
        address _stakingToken
    ) public {
        rewardsToken = IERC20(_rewardsToken);
        stakingToken = IERC20(_stakingToken);
    }

    modifier updateReward(address account) {
        rewardPerTokenStored = rewardPerToken();
        lastUpdateTime = lastTimeRewardApplicable();
        if (account != address(0)) {
            rewards[account] = earned(account);
            userRewardPerTokenPaid[account] = rewardPerTokenStored;
        }
        _;
    }
    
    function poolSize() external view returns (uint256) {
        return _totalSupply;
    }

    function poolBalance(address account) external view returns (uint256) {
        return _balances[account];
    }

    function lastTimeRewardApplicable() public view returns (uint256) {
        return block.timestamp < periodFinish ? block.timestamp : periodFinish;
    }

    function rewardPerToken() public view returns (uint256) {
        if (_totalSupply == 0) {
            return rewardPerTokenStored;
        }
        return
            rewardPerTokenStored.add(
                lastTimeRewardApplicable()
                    .sub(lastUpdateTime)
                    .mul(rewardRate)
                    .mul(1e18)
                    .div(_totalSupply)
            );
    }

    function earned(address account) public view returns (uint256) {
        return
            _balances[account]
                .mul(rewardPerToken().sub(userRewardPerTokenPaid[account]))
                .div(1e18)
                .add(rewards[account]);
    }
    
    function earnedScaled(address account) public view returns (uint256) {
        // This represents the actual return
        uint256 reward =
            _balances[account]
                .mul(rewardPerToken().sub(userRewardPerTokenPaid[account]))
                .div(1e18)
                .add(rewards[account]);
        uint256 scalingFactor = REETH(address(rewardsToken)).reethScalingFactor();
        return reward.mul(scalingFactor).div(1e18);
    }
    
    function rewardRateScaled() public view returns (uint256) {
        uint256 scalingFactor = REETH(address(rewardsToken)).reethScalingFactor();
        return rewardRate.mul(scalingFactor).div(1e18);
    }

    function stake(uint256 amount) external nonReentrant updateReward(_msgSender()) checkNewPeriod {
        require(amount > 0, "Cannot stake 0");
        _totalSupply = _totalSupply.add(amount);
        _balances[_msgSender()] = _balances[_msgSender()].add(amount);
        stakingToken.safeTransferFrom(_msgSender(), address(this), amount);
        emit Staked(_msgSender(), amount);
    }

    function withdraw(uint256 amount) public nonReentrant updateReward(_msgSender()) {
        require(amount > 0, "Cannot withdraw 0");
        _totalSupply = _totalSupply.sub(amount);
        _balances[_msgSender()] = _balances[_msgSender()].sub(amount);
        stakingToken.safeTransfer(_msgSender(), amount);
        emit Withdrawn(_msgSender(), amount);
    }

    function exit(uint256 amount) external {
        withdraw(amount);
        getReward();
    }

    function getReward() public nonReentrant updateReward(_msgSender()) checkNewPeriod {
        uint256 reward = rewards[_msgSender()];
        if (reward > 0) {
            rewards[_msgSender()] = 0;
            uint256 scalingFactor = REETH(address(rewardsToken)).reethScalingFactor();
            uint256 trueReward = reward.mul(scalingFactor).div(1e18);
            uint256 contractBalance = rewardsToken.balanceOf(address(this));
            if(contractBalance < trueReward){
                trueReward = contractBalance; // Prevents contract from locking up
            }
            rewardsToken.safeTransfer(_msgSender(), trueReward);
            emit RewardPaid(_msgSender(), trueReward);
        }
    }

    modifier checkNewPeriod() {
        if (rewardsStopped == true) {
          // do nothing
        } else if (block.timestamp >= periodFinish && initialized) {
            // It will be the base reward released
            uint256 scalingFactor = REETH(address(rewardsToken)).reethScalingFactor();
            uint256 newRewards = baseReward.mul(scalingFactor).div(1e18);
            REETH(address(rewardsToken)).mint(address(this), newRewards);
            lastUpdateTime = block.timestamp;
            rewardRate = baseReward.div(duration);
            periodFinish = block.timestamp.add(duration);
            emit RewardAdded(newRewards);
        }
        _;
    }
    
    function startRewardDistribution() external onlyGovernance updateReward(address(0)) {
        // increased buffer for scaling factor
        require(baseReward < uint256(-1) / 10**22, "rewards too large, would lock");
        require(!initialized, "already initialized");
        initialized = true;
        
        uint256 scalingFactor = REETH(address(rewardsToken)).reethScalingFactor();
        uint256 newRewards = baseReward.mul(scalingFactor).div(1e18);
        REETH(address(rewardsToken)).mint(address(this), newRewards);
        lastUpdateTime = block.timestamp;
        rewardRate = baseReward.div(duration);
        periodFinish = block.timestamp.add(duration);
        emit RewardAdded(newRewards);        
    }
    
    // Governance 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 _timelockDuration = 86400; // Timelock is 24 hours
    
    // Reusable timelock variables
    address private _timelock_address;
    uint256 private _timelock_data;
    bool private _timelock_bool;
    
    modifier timelockConditionsMet(uint256 _type) {
        require(_timelockType == _type, "Timelock not acquired for this function");
        _timelockType = 0; // Reset the type once the timelock is used
        if(stakingToken.balanceOf(address(this)) > 0){ // Timelock only applies when balance exists
            require(now >= _timelockStart + _timelockDuration, "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 time to get the reward
    // --------------------
    function startChangeRewardDuration(uint256 _duration) external onlyGovernance {
        require(_duration > 60,"Cannot set duration to less than 1 minute");
        _timelockStart = now;
        _timelockType = 2;
        _timelock_data = _duration;
    }
    
    function finishChangeRewardDuration() external onlyGovernance timelockConditionsMet(2) {
        duration = _timelock_data;
        emit RewardsDurationUpdated(duration);
    }
    // --------------------
    
    // Change the amount of reward
    // --------------------
    function startChangeRewardAmount(uint256 _amount) external onlyGovernance {
        _timelockStart = now;
        _timelockType = 3;
        _timelock_data = _amount;
    }
    
    function finishChangeRewardAmount() external onlyGovernance timelockConditionsMet(3) {
        baseReward = _timelock_data;
        emit BaseRewardChanged(baseReward);
    }
    // --------------------
    
    // Change if rewards are activated
    // --------------------
    function startChangeRewardStopped(bool _stopped) external onlyGovernance {
        _timelockStart = now;
        _timelockType = 4;
        _timelock_bool = _stopped;
    }
    
    function finishChangeRewardStopped() external onlyGovernance timelockConditionsMet(4) {
        rewardsStopped = _timelock_bool;
    }
    // --------------------

}
function startChangeRewardDuration(uint256 _duration) external onlyGovernance {
    require(_duration > 60,"Cannot set duration to less than 1 minute");
    _timelockStart = now;
    _timelockType = 2;
    _timelock_data = _duration;
}

// --------------------
// Change the time to get the reward
// --------------------