contract StabinolStaker is Ownable, ReentrancyGuard {
    using SafeMath for uint256;
    using SafeERC20 for IERC20;
    
    // variables
    address public stbzAddress; // The address for the STBZ tokens
    address public stolAddress; // The address for STOL
    address public lpAddress; // The address for the lp
    address public claimerAddress = address(0); // This can be updated by governance
    uint256 public minLPStakeTime = 24 hours; // This is the minimum time the user must wait to unstake LP after a previous claim
    uint256 public totalSTBZ;
    uint256 public totalLP;
    
    uint256 constant DIVISION_FACTOR = 100000;
    
    mapping(address => UserInfo) private allUsersInfo;
    
    // Structs
    struct UserInfo {
        uint256 stbzAmount; // How many stbz the user has in contract
        uint256 lpAmount; // How many LP tokens the user has in contract
        uint256 lastDeposit; // Time at last deposit action (either staking LP or stbz)
        uint256 lastWithdraw; // Time at last withdraw action (either staking LP or stbz)
    }

    // Events
    event StakedSTBZ(address indexed user, uint256 amount);
    event StakedLP(address indexed user, uint256 amount);
    event WithdrawnSTBZ(address indexed user, uint256 amount);
    event WithdrawnLP(address indexed user, uint256 amount);
    
    constructor(
        address _stbz,
        address _stol,
        address _lp
    ) public {
        stbzAddress = _stbz;
        stolAddress = _stol;
        lpAddress = _lp;
    }
    
    // functions
    function getSTOLInLP(address _user) external view returns (uint256){
        if(IERC20(lpAddress).totalSupply() == 0) { return 0; }
        uint256 stolInLP = IERC20(stolAddress).balanceOf(lpAddress);
        return stolInLP.mul(allUsersInfo[_user].lpAmount).div(IERC20(lpAddress).totalSupply());
    }

    function getLPBalance(address _user) external view returns (uint256){
        return allUsersInfo[_user].lpAmount;
    }
    
    function getSTBZBalance(address _user) external view returns (uint256){
        return allUsersInfo[_user].stbzAmount;
    }
    
    function getDepositTime(address _user) external view returns (uint256){
        return allUsersInfo[_user].lastDeposit;
    }
    
    function getWithdrawTime(address _user) external view returns (uint256){
        return allUsersInfo[_user].lastWithdraw;
    }

    // User can force update her/his balance if it increases, extends claim window though
    function updateMyBalance() external nonReentrant
    {
        uint256 gasAllocated = gasleft().mul(tx.gasprice); // Factor in the gas allocated for this transaction, it affects the eth balance
        require(claimerAddress != address(0), "Claimer not set yet");
        allUsersInfo[_msgSender()].lastDeposit = now; // Reset the claim time
        Claimer(claimerAddress).stakerUpdateBalance(_msgSender(), gasAllocated); // Update the eth balance of the user in claim contract
    }

    // Stake functions
    function stakeSTBZ() external nonReentrant
    {
        uint256 gasAllocated = gasleft().mul(tx.gasprice); // Factor in the gas allocated for this transaction, it affects the eth balance
        require(claimerAddress != address(0), "Claimer not set yet");
        uint256 min = Claimer(claimerAddress).getMinSTBZStake(); // This will return the minimum amount of STBZ needed to stake
        require(min > allUsersInfo[_msgSender()].stbzAmount, "Already staked enough STBZ in contract");
        uint256 amount = min.sub(allUsersInfo[_msgSender()].stbzAmount); // This is the amount needed to top off account
        allUsersInfo[_msgSender()].lastDeposit = now;
        Claimer(claimerAddress).stakerUpdateBalance(_msgSender(), gasAllocated); // Update the eth balance of the user in claim contract
        
        IERC20 _token = IERC20(stbzAddress); // 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); // Some tokens lose amount (transfer fee) upon transfer can happen
        
        totalSTBZ = totalSTBZ.add(amount);
        allUsersInfo[_msgSender()].stbzAmount = allUsersInfo[_msgSender()].stbzAmount.add(amount);
        require(allUsersInfo[_msgSender()].stbzAmount >= min, "Not enough tokens staked");
        emit StakedSTBZ(_msgSender(), amount);
    }
    
    function stakeSTOLLP(uint256 amount) external nonReentrant
    {
        uint256 gasAllocated = gasleft().mul(tx.gasprice); // Factor in the gas allocated for this transaction, it affects the eth balance
        require(claimerAddress != address(0), "Claimer not set yet");
        allUsersInfo[_msgSender()].lastDeposit = now;
        Claimer(claimerAddress).stakerUpdateBalance(_msgSender(), gasAllocated); // Update the eth balance of the user in claim contract
        
        IERC20 _token = IERC20(lpAddress); // 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);
        
        totalLP = totalLP.add(amount);
        allUsersInfo[_msgSender()].lpAmount = allUsersInfo[_msgSender()].lpAmount.add(amount);
        emit StakedLP(_msgSender(), amount);
    }
    
    // Unstake functions
    // This will unstake the entire STBZ amount staked, user can unstake STBZ at any time
    // It takes into account the STBZ burn fee that is taken from the sender by implementing a withdraw fee
    function unstakeSTBZ() external nonReentrant
    {
        require(allUsersInfo[_msgSender()].stbzAmount > 0, "There is no STBZ balance for this user");
        uint256 feeRate = STBZToken(stbzAddress).burnRate();
        if(feeRate > 0){
            feeRate = (feeRate * DIVISION_FACTOR) / (DIVISION_FACTOR + feeRate); // Unique formula to calcuate fee upon sending
        }
        uint256 feeAmount = allUsersInfo[_msgSender()].stbzAmount.mul(feeRate).div(DIVISION_FACTOR);
        uint256 sendAmount = allUsersInfo[_msgSender()].stbzAmount.sub(feeAmount);
        totalSTBZ = totalSTBZ.sub(allUsersInfo[_msgSender()].stbzAmount);
        allUsersInfo[_msgSender()].stbzAmount = 0;
        allUsersInfo[_msgSender()].lastWithdraw = now;
        IERC20(stbzAddress).safeTransfer(_msgSender(), sendAmount);
        emit WithdrawnSTBZ(_msgSender(), sendAmount.add(feeAmount));
    }
    
    // User can unstake only after timelock time
    function unstakeLP(uint256 amount) external nonReentrant
    {
        require(allUsersInfo[_msgSender()].lpAmount > 0, "There is no LP balance for this user");
        require(claimerAddress != address(0), "Claimer not set yet");
        uint256 lastClaim = Claimer(claimerAddress).getLastClaimTime(_msgSender());
        require(now >= lastClaim + minLPStakeTime, "Last claim too recent to unstake LP");
        totalLP = totalLP.sub(amount);
        allUsersInfo[_msgSender()].lpAmount = allUsersInfo[_msgSender()].lpAmount.sub(amount);
        allUsersInfo[_msgSender()].lastWithdraw = now;
        IERC20(lpAddress).safeTransfer(_msgSender(), amount);
        emit WithdrawnLP(_msgSender(), amount);
    }
    
    // Governance only functions
    
    // 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;
    uint256 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
        if(claimerAddress != address(0)){
            // Timelock is only required after tokens burned into contract
            require(now >= _timelockStart + TIMELOCK_DURATION + minLPStakeTime, "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 contract
    // --------------------
    function startClaimerChange(address _address) external onlyGovernance {
        _timelockStart = now;
        _timelockType = 2;
        _timelock_address = _address;       
    }
    
    function finishClaimerChange() external onlyGovernance timelockConditionsMet(2) {
        claimerAddress = _timelock_address;
    }
    // --------------------
    
    // Change the LP lockup time since last claim time
    // --------------------
    function startChangeLPTimeLock(uint256 _time) external onlyGovernance {
        _timelockStart = now;
        _timelockType = 3;
        _timelock_data = _time;
    }
    
    function finishChangeLPTimeLock() external onlyGovernance timelockConditionsMet(3) {
        minLPStakeTime = _timelock_data;
    }
    // --------------------
   
}
function stakeSTBZ() external nonReentrant
{
    uint256 gasAllocated = gasleft().mul(tx.gasprice); // Factor in the gas allocated for this transaction, it affects the eth balance
    require(claimerAddress != address(0), "Claimer not set yet");
    uint256 min = Claimer(claimerAddress).getMinSTBZStake(); // This will return the minimum amount of STBZ needed to stake
    require(min > allUsersInfo[_msgSender()].stbzAmount, "Already staked enough STBZ in contract");
    uint256 amount = min.sub(allUsersInfo[_msgSender()].stbzAmount); // This is the amount needed to top off account
    allUsersInfo[_msgSender()].lastDeposit = now;
    Claimer(claimerAddress).stakerUpdateBalance(_msgSender(), gasAllocated); // Update the eth balance of the user in claim contract
        
    IERC20 _token = IERC20(stbzAddress); // 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); // Some tokens lose amount (transfer fee) upon transfer can happen
        
    totalSTBZ = totalSTBZ.add(amount);
    allUsersInfo[_msgSender()].stbzAmount = allUsersInfo[_msgSender()].stbzAmount.add(amount);
    require(allUsersInfo[_msgSender()].stbzAmount >= min, "Not enough tokens staked");
    emit StakedSTBZ(_msgSender(), amount);
}

// Stake functions