abstract contract AbstractStaking is Ownable {
    using SafeMath for uint256;

    event Staked(address indexed user, uint256 amount);
    event Unstaked(address indexed user, uint256 amount);
    event Claimed(address indexed user, uint256 amount);

    TokenPool public stakingPool;
    TokenPool public lockedPool;
    TokenPool public unlockedPool;

    uint256 public startTime;

    //
    // Global state
    //
    uint256 public totalStakingAmount;
    uint256 public totalStakingAmountTime; // total time * amount staked
    uint256 public lastUpdatedTimestamp;

    //
    // Addional bella locking related
    //
    uint256 public currentUnlockCycle; // linear count down to release bella token
    uint256 public lastUnlockTime;

    /**
     * @param stakingBPT The BPT token users deposit as stake.
     * @param bellaToken The bonus token is bella.
     * @param admin The admin address
     * @param _startTime Timestamp that user can stake
     */
    constructor(
        IERC20 stakingBPT,
        IERC20 bellaToken,
        address admin,
        uint256 _startTime
        ) Ownable(admin) 
        internal {
        stakingPool = new TokenPool(stakingBPT);
        lockedPool = new TokenPool(bellaToken);
        unlockedPool = new TokenPool(bellaToken);
        startTime = _startTime;
    }

    /**
     * @return The user's total staking BPT amount
     */
    function totalStakedFor(address user) public view virtual returns (uint256);

    function totalStaked() public view returns (uint256) {
        return totalStakingAmount;
    }

    /**
     * @dev Stake for the user self
     * @param amount The amount of BPT tokens that the user wishes to stake
     */
    function stake(uint256 amount) external {
        require(!Address.isContract(msg.sender), "No harvest thanks");
        require(now >= startTime, "not started yet");
        _stake(msg.sender, msg.sender, amount);
    }

    /**
     * @return User's total rewards when clamining
     */
    function totalRewards() external view returns (uint256) {
        return _totalRewardsFor(msg.sender);
    }

    /**
     * @return A specific user's total rewards when clamining
     */
    function totalRewardsFor(address user) external view returns (uint256) {
        return _totalRewardsFor(user);
    }

    /**
     * @dev Claim=withdraw all the bella rewards
     */
    function claim() external {
        require(!Address.isContract(msg.sender), "No harvest thanks");
        // cumulate user and global time*amount
        _updateTotalStaking(0);
        _updateUserStaking(0, msg.sender);

        _poolUnlock();

        uint256 reward = _calculateRewardAndBurnAll(msg.sender);

        unlockedPool.transfer(msg.sender, reward);

        emit Claimed(msg.sender, reward);
    }

    /**
     * @dev Claim=withdraw all the bella rewards and the staking BPT token,
     * which stops the user's staking
     */
    function claimAndUnstake() external {
        require(!Address.isContract(msg.sender), "No harvest thanks");
        // cumulate user and global time*amount
        _updateTotalStaking(0);
        _updateUserStaking(0, msg.sender);

        _poolUnlock();

        (uint256 staking, uint256 reward) = _calculateRewardAndCleanUser(msg.sender);

        unlockedPool.transfer(msg.sender, reward);
        stakingPool.transfer(msg.sender, staking);

        emit Claimed(msg.sender, reward);
        emit Unstaked(msg.sender, staking);
    }

    /**
     * @dev we will lock more bella tokens on the begining of the next releasing cycle
     * @param amount the amount of bella token to lock
     * @param nextUnlockCycle next reward releasing cycle, unit=day
     */
    function lock(uint256 amount, uint256 nextUnlockCycle) external onlyOwner {
        currentUnlockCycle = nextUnlockCycle * 1 days;
        if (now >= startTime) {
            lastUnlockTime = now;
        } else {
            lastUnlockTime = startTime;
        }
            
        require(
            lockedPool.token().transferFrom(msg.sender, address(lockedPool), amount),
            "Additional bella transfer failed"
        );
    }

    /**
     * @dev Actual logic to handle user staking
     * @param from The user who pays the staking BPT
     * @param beneficiary The user who actually controls the staking BPT
     * @param amount The amount of BPT tokens to stake
     */
    function _stake(address from, address beneficiary, uint256 amount) private {
        require(amount > 0, "can not stake 0 token");
        require(
            stakingPool.token().transferFrom(from, address(stakingPool), amount),
            "Staking BPT transfer failed"
        );

        _updateUserStaking(amount, beneficiary);

        _updateTotalStaking(amount);

        emit Staked(beneficiary, amount);
    }

    /**
     * @dev Update the global state due to more time cumulated and/or new BPT staking token
     * @param amount New BPT staking deposited (can be 0)
     */
    function _updateTotalStaking(uint256 amount) private {
        uint256 additionalAmountTime = totalStakingAmount.mul(now.sub(lastUpdatedTimestamp));
        totalStakingAmount = totalStakingAmount.add(amount);
        totalStakingAmountTime = totalStakingAmountTime.add(additionalAmountTime);
        lastUpdatedTimestamp = now;
    }

    /**
     * @dev Update a specific user's state due to more time cumulated and/or new BPT staking token
     * @param amount New BPT staking deposited (can be 0)
     * @param user The account to be updated
     */
    function _updateUserStaking(uint256 amount, address user) internal virtual;

    /**
     * @dev linear count down from 30 days to release bella token,
     * from the locked pool to the unlocked pool
     */
    function _poolUnlock() private {
        if (currentUnlockCycle == 0)
            return; // release ended
        uint256 timeDelta = now.sub(lastUnlockTime);
        if (currentUnlockCycle < timeDelta)
            currentUnlockCycle = timeDelta; // release all

        uint256 amount = lockedPool.balance().mul(timeDelta).div(currentUnlockCycle);

        currentUnlockCycle = currentUnlockCycle.sub(timeDelta);
        lastUnlockTime = now;

        lockedPool.transfer(address(unlockedPool), amount);
    }

    /**
     * @dev Calculate user's total cumulated reward and burn his/her all staking amount*time
     * @return User cumulated reward bella during the staking process
     */
    function _calculateRewardAndBurnAll(address user) internal virtual returns (uint256);

    /**
     * @dev Calculate user's total cumulated reward and staking,
     * and remove him/her from the staking process
     * @return [1] User cumulated staking BPT
     * @return [2] User cumulated reward bella during the staking process
     */
    function _calculateRewardAndCleanUser(address user) internal virtual returns (uint256, uint256);

    /**
     * @dev Internal function to calculate user's total rewards
     * @return A specific user's total rewards when clamining
     */
    function _totalRewardsFor(address user) internal view virtual returns (uint256);
    
}
/**
 * @title Abstract Staking
 * @dev Skeleton of the staking pool for user to stake Balancer BPT token and get bella as reward.
 */
function _updateTotalStaking(uint256 amount) private {
    uint256 additionalAmountTime = totalStakingAmount.mul(now.sub(lastUpdatedTimestamp));
    totalStakingAmount = totalStakingAmount.add(amount);
    totalStakingAmountTime = totalStakingAmountTime.add(additionalAmountTime);
    lastUpdatedTimestamp = now;
}

/**
 * @dev Update the global state due to more time cumulated and/or new BPT staking token
 * @param amount New BPT staking deposited (can be 0)
 */