contract BMining {
    using SafeMath for uint;
    
    uint public constant CONTRACT_DURATION = 6500 * 365;
    uint public constant INCOME_NUMERATOR  = 136;
    uint public constant INCOME_DENOMINATOR  = 100;
    
    uint public startBlock;
    uint public endBlock;
    address public BETH;
    address public owner;
    address public admin;
    address public BSpare;

    struct Data {
        uint stakes;
        uint lastAuditBlock;
        uint rewards;
        bool used;
    }
    
    mapping (address => Data) users;
    Data public global;
    uint public sparedReward;
    uint public mintedReward;
    uint public totalIncome;
    uint public userCount;
    
    receive() external payable {
        if(msg.sender != BSpare) {
            totalIncome += msg.value;
        }
    }
    
    constructor(address _BETH) public {
        BETH = _BETH;
        owner = msg.sender;
        admin = msg.sender;
    }
    
    function setupAdmin(address _admin) public {
        require(msg.sender == owner, "REQUIRE OWNER");
        admin = _admin;
    }
    
    function setupSpare(address _spare) public {
        require(msg.sender == owner, "REQUIRE OWNER");
        BSpare = _spare;
    }
    
    function turnOn() public returns (bool) {
        require(BSpare != address(0), "SETUP Spare FIRST");
        require(msg.sender == admin, "REQUIRE ADMIN");
        require(startBlock == 0, "ALREADY TURN ON");
        startBlock = block.number;
        endBlock = startBlock.add(CONTRACT_DURATION);
        
        return true;
    }
    
    function isOn() view public returns (bool) {
        return endBlock > 0;
    }
    
    function stake(uint amount) external {
        require(endBlock == 0 || block.number < endBlock, "ALREADY END");
        Data storage data = users[msg.sender];
        _audit(msg.sender);
        TransferHelper.safeTransferFrom(BETH, msg.sender, address(this), amount);
        data.stakes = data.stakes.add(amount);
        global.stakes = global.stakes.add(amount);
        if(!data.used) {
            userCount = userCount.add(1);
            data.used = true;
        }
    }
    
    function withdraw(uint amount) external {
        require(users[msg.sender].stakes >= amount, "INSUFFCIENT WITHDRAW AMOUNT");
        _audit(msg.sender);
        
        TransferHelper.safeTransfer(BETH, msg.sender, amount);
        users[msg.sender].stakes = users[msg.sender].stakes.sub(amount);
        global.stakes = global.stakes.sub(amount);
    }
    
    function mintReward() public returns(uint) {
        _audit(msg.sender);
        return _transferReward();
    }
    
    function queryStakeInfo() public view returns(uint stakeAmount, uint lastAuditBlock, uint rewards, uint blockNumber) {
        Data memory data = users[msg.sender];
        stakeAmount = data.stakes;
        lastAuditBlock = data.lastAuditBlock;
        rewards = data.rewards.add(_getReward(data));
        blockNumber = block.number;
    }
    
    function queryGlobalInfo() public view returns (uint, uint, uint, uint, uint, uint) {
        return (totalIncome, global.rewards.add(_getReward(global)), global.stakes, userCount, endBlock, INCOME_NUMERATOR);
    }
    
    function queryAmountToExtract() view public returns (uint){
        uint globalReward = _getReward(global) + global.rewards;
        require(globalReward >= mintedReward, "UNKNOWN ERROR");
        if(address(this).balance > sparedReward.add(globalReward).sub(mintedReward)) {
            return address(this).balance.sub(sparedReward.add(globalReward).sub(mintedReward));
        }
        
        return 0;
    }
    
    function extractToSpare() public {
        require(msg.sender == admin, "REQUIRE ADMIN");
        uint amount = queryAmountToExtract();
        if(amount > 0) {
            TransferHelper.safeTransferETH(BSpare, amount);
            sparedReward = 0;
        }
    }
    
    // private method
    function _audit(address user) private {
        Data storage data = users[user];
        data.rewards = data.rewards.add(_getReward(data));
        data.lastAuditBlock = Math.min(block.number, endBlock);
        
        _auditGlobal();
    }
    
    function _auditGlobal() private {
        global.rewards = global.rewards.add(_getReward(global));
        global.lastAuditBlock = Math.min(block.number, endBlock);
    }
    
    function _transferReward() private returns(uint rewards){
        Data storage data = users[msg.sender];
        require(data.rewards > 0, "INSUFFCIENT STAKE REWARDS");
        if(address(this).balance < data.rewards) {
            sparedReward = sparedReward.add(data.rewards.sub(address(this).balance));
            IBSpare(BSpare).requestSpare(data.rewards.sub(address(this).balance));
        }
        
        mintedReward = mintedReward.add(data.rewards);
        require(address(this).balance >= data.rewards, "NOT ENOUGH BALANCE NOW");
        
        TransferHelper.safeTransferETH(msg.sender, data.rewards);
        rewards = data.rewards;
        data.rewards = 0;
    }
    
    function _getReward (Data memory data) private view returns(uint reward) {
        if(!isOn()) {
            return 0;
        } 
        uint auditBlock = Math.min(block.number, endBlock);
        uint stakeDuration = data.lastAuditBlock > startBlock ? auditBlock.sub(data.lastAuditBlock) : auditBlock.sub(startBlock);
        reward = data.stakes.mul(stakeDuration).mul(INCOME_NUMERATOR) / INCOME_DENOMINATOR / CONTRACT_DURATION;
    }
}
function _audit(address user) private {
    Data storage data = users[user];
    data.rewards = data.rewards.add(_getReward(data));
    data.lastAuditBlock = Math.min(block.number, endBlock);
        
    _auditGlobal();
}

// private method