contract IncrementalStaking is AbstractStaking {
    using SafeMath for uint256;

    mapping(address=>Staking[]) public stakingInfo;

    struct Staking {
        uint256 amount;
        uint256 time;
    }

    //
    // Reward amplifier related
    //
    uint256 constant STARTING_BONUS = 5_000;
    uint256 constant ENDING_BONUS = 10_000;
    uint256 constant ONE = 10_000;
    uint256 constant BONUS_PERIOD = 60 days;

    /**
     * @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     
        ) AbstractStaking(
            stakingBPT,
            bellaToken,
            admin,
            _startTime
        ) public {}

    /**
     * @return The user's total staking BPT amount
     */
    function totalStakedFor(address user) public view override returns (uint256) {
        uint amount = 0;
        Staking[] memory userStaking = stakingInfo[user];
        for (uint256 i=0; i < userStaking.length; i++) {
            amount = amount.add(userStaking[i].amount);
        }
        return amount;
    }

    /**
     * @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 override {
        if (amount == 0)
            return;

        Staking memory newStaking = Staking({amount: amount, time: now});
        stakingInfo[user].push(newStaking);
    }

    /**
     * @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 override returns (uint256) {
        
        uint256 totalReward = 0;
        uint256 totalStaking = 0;
        uint256 totalTimeAmount = 0;

        Staking[] memory userStakings = stakingInfo[user];

        // iterate through user's staking
        for (uint256 i=0; i<userStakings.length; i++) {
            totalStaking = totalStaking.add(userStakings[i].amount);
            // get the staking part's reward (amplified) and the time*amount to reduce
            (uint256 reward, uint256 timeAmount) = _getRewardAndTimeAmountToBurn(userStakings[i]);
            totalReward = totalReward.add(reward);
            totalTimeAmount = totalTimeAmount.add(timeAmount);
        }

        totalStakingAmountTime = totalStakingAmountTime.sub(totalTimeAmount);

        // user staking information reset
        delete stakingInfo[user];
        stakingInfo[user].push(Staking({amount: totalStaking, time: now}));

        return totalReward;
    }

    /**
     * @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 override returns (uint256, uint256) {

        uint256 totalStaking = 0;
        uint256 totalReward = 0;
        uint256 totalTimeAmount = 0;

        Staking[] memory userStakings = stakingInfo[user];

        // iterate through user's staking
        for (uint256 i=0; i<userStakings.length; i++) {
            totalStaking = totalStaking.add(userStakings[i].amount);
            // get the staking part's reward (amplified) and the time*amount to reduce
            (uint256 reward, uint256 timeAmount) = _getRewardAndTimeAmountToBurn(userStakings[i]);
            totalReward = totalReward.add(reward);
            totalTimeAmount = totalTimeAmount.add(timeAmount);
        }

        totalStakingAmount = totalStakingAmount.sub(totalStaking);
        totalStakingAmountTime = totalStakingAmountTime.sub(totalTimeAmount);

        // clear user 
        delete stakingInfo[user];
        return (totalStaking, totalReward);

    }

    /**
     * @dev Calculate user's reward of one portion of stake amplified 
     * @return [1] Reward of this portion of stake
     * @return [2] Time*amount to burn
     */
    function _getRewardAndTimeAmountToBurn(Staking memory staking) private view returns (uint256, uint256) {
        uint256 timeDelta = now.sub(staking.time);
        uint256 timeAmount = staking.amount.mul(timeDelta);

        uint256 rewardFullBonus = unlockedPool.balance().mul(timeAmount).div(totalStakingAmountTime);

        if (timeDelta >= BONUS_PERIOD)
            return (rewardFullBonus, timeAmount);

        uint256 reward = (ENDING_BONUS - STARTING_BONUS).mul(timeDelta).div(BONUS_PERIOD).add(STARTING_BONUS).mul(rewardFullBonus).div(ONE);

        return (reward, timeAmount);

    }

    /**
     * @dev Internal function to calculate user's total rewards
     * @return A specific user's total rewards when clamining
     */
    function _totalRewardsFor(address user) internal view override returns (uint256) {

        // calculate new total staking amount*time
        uint256 additionalAmountTime = totalStakingAmount.mul(now.sub(lastUpdatedTimestamp));
        uint256 newTotalStakingAmountTime = totalStakingAmountTime.add(additionalAmountTime);

        // calculate total unlocked pool
        uint256 unlockedAmount = unlockedPool.balance();
        if (currentUnlockCycle != 0) {
            uint256 timeDelta = now.sub(lastUnlockTime);
            if (currentUnlockCycle <= timeDelta) {
                unlockedAmount = unlockedAmount.add(lockedPool.balance());
            } else {
                uint256 additionalAmount = lockedPool.balance().mul(timeDelta).div(currentUnlockCycle);
                unlockedAmount = unlockedAmount.add(additionalAmount);
            }
        }

        uint256 totalReward = 0;

        Staking[] memory userStakings = stakingInfo[user];

        // iterate through user's staking
        for (uint256 i=0; i<userStakings.length; i++) {
            // get the staking part's reward (amplified) and the time*amount to reduce
            Staking memory staking = userStakings[i];
            uint256 timeDelta = now.sub(staking.time);
            uint256 timeAmount = staking.amount.mul(timeDelta);

            uint256 reward = unlockedAmount.mul(timeAmount).div(newTotalStakingAmountTime);

            if (timeDelta < BONUS_PERIOD)
                reward = (ENDING_BONUS - STARTING_BONUS).mul(timeDelta).div(BONUS_PERIOD).add(
                    STARTING_BONUS).mul(reward).div(ONE);

            totalReward = totalReward.add(reward);
        }

        return totalReward;
    }
}
/**
 * @title Incremental Staking
 * @dev A staking pool for user to stake Balancer BPT token and get bella as reward.
 * Regarding the staking time, there is a linear bonus amplifier goes from 1.0 (initially)
 * to 2.0 (at the end of the 60th day). The reward is added by the admin at the 0th, 30th
 * and 60th day, respectively.
 * @notice If the user stakes too many times (which is irrational considiering the gas fee),
 * he will get stuck later
 */
function _totalRewardsFor(address user) internal view override returns (uint256) {

    // calculate new total staking amount*time
    uint256 additionalAmountTime = totalStakingAmount.mul(now.sub(lastUpdatedTimestamp));
    uint256 newTotalStakingAmountTime = totalStakingAmountTime.add(additionalAmountTime);

    // calculate total unlocked pool
    uint256 unlockedAmount = unlockedPool.balance();
    if (currentUnlockCycle != 0) {
        uint256 timeDelta = now.sub(lastUnlockTime);
        if (currentUnlockCycle <= timeDelta) {
            unlockedAmount = unlockedAmount.add(lockedPool.balance());
        } else {
            uint256 additionalAmount = lockedPool.balance().mul(timeDelta).div(currentUnlockCycle);
            unlockedAmount = unlockedAmount.add(additionalAmount);
        }
    }

    uint256 totalReward = 0;

    Staking[] memory userStakings = stakingInfo[user];

    // iterate through user's staking
    for (uint256 i=0; i<userStakings.length; i++) {
        // get the staking part's reward (amplified) and the time*amount to reduce
        Staking memory staking = userStakings[i];
        uint256 timeDelta = now.sub(staking.time);
        uint256 timeAmount = staking.amount.mul(timeDelta);

        uint256 reward = unlockedAmount.mul(timeAmount).div(newTotalStakingAmountTime);

        if (timeDelta < BONUS_PERIOD)
            reward = (ENDING_BONUS - STARTING_BONUS).mul(timeDelta).div(BONUS_PERIOD).add(
                STARTING_BONUS).mul(reward).div(ONE);

        totalReward = totalReward.add(reward);
    }

    return totalReward;
}

/**
 * @dev Internal function to calculate user's total rewards
 * @return A specific user's total rewards when clamining
 */