contract StakingPoolV2 is LPTokenWrapperV2, Ownable {
    using Address for address;
    using SafeMath for uint256;
    using SafeERC20 for IERC20;

    struct UserInfo {
        uint256 poolId;
        uint256 stakingTime;
        uint256 lastRewardPerToken;
        uint256 pendingReward;
        uint256 lastRewardPerToken4Pool;
        uint256 pendingReward4Pool;
    }

    struct PoolInfo {
        address owner;
        address beneficiary;
        uint256 announceTime;
        uint256 amount; // How many tokens staked in the pool
        uint256 profit;
    }

    enum PoolAnnounceStatusCode {OK, WRONG_PID, POOL_HAS_OWNER, NEED_MORE_DEPOSIT, USER_IN_ANOTHER_POOL}

    IERC20 public rewardToken = IERC20(0xA56Ed2632E443Db5f93e73C89df399a081408Cc4);

    uint256 public constant SECONDS_PER_DAY = 1 days;
    uint256 public constant REWARD_DURATION = 7 * SECONDS_PER_DAY;

    // pool related constant
    uint256 public constant MAX_POOL_NUMBER = 108;
    uint256 public constant MIN_POOL_ANNOUNCE_AMOUNT = 50 * 1e18;
    uint256 public constant MIN_POOL_DEPOSIT_AMOUNT = 100 * 1e18;
    uint256 public constant MIN_POOL_ANNOUNCE_DURATION = 3 * SECONDS_PER_DAY;

    PoolInfo[] private poolInfo;
    mapping(address => UserInfo) public userInfo;
    mapping(address => uint256) public poolRewards;
    
    // settle state
    mapping(address => uint256) public pendingAmount;
    mapping(address => uint256) public settlementDate;
    
    // settle period
    uint256 public minWithdrawDuration = 3 * SECONDS_PER_DAY;

    uint256 public periodFinish;
    uint256 public lastRewardUpdateTime = 0;
    uint256 public rewardPerTokenStored = 0;
    uint256 public rewardPerTokenStored4Pool = 0;

    uint256 public userRewardRatePerDay = 40;
    uint256 public poolRewardRatePerDay = 40;
    uint256 public constant REWARD_RATE_BASE = 10000;

    bool public breaker = false;
    mapping(address => bool) public whiteList;

    /* ========== EVENTS ========== */
    event Deposited(address indexed user, uint256 amount, uint256 pid);
    event Withdrawn(address indexed user, uint256 amount, uint256 pid);
    event Settled(address indexed user, uint256 amount);

    event Reinvested(address indexed user, uint256 amount, uint256 pid);
    event RewardPaidToOwner(address indexed owner, uint256 reward, uint256 pid);

    event Announced(address indexed owner, uint256 pid);
    event Renounced(address indexed owner, uint256 pid);
    event BeneficiaryTransferred(address indexed previousBeneficiary, address indexed newBeneficiary, uint256 pid);

    event RewardsDurationUpdated(uint256 newDuration);
    event Recovered(address indexed token, uint256 amount);
    event EmergencyWithdraw(address indexed user, uint256 amount);

    constructor(address token) public LPTokenWrapperV2(token) {
        rewardToken = IERC20(token);
    }

    function createPool(uint256 count) external onlyOwner {
        require(poolInfo.length.add(count) <= MAX_POOL_NUMBER, "too much pools");
        for (uint256 i = 0; i < count; i++) {
            poolInfo.push(
                PoolInfo({owner: address(0), beneficiary: address(0), announceTime: 0, amount: 0, profit: 0})
            );
        }
    }

    function notifyRewardAmount() external onlyOwner updateReward(address(0)) {
        require(currentTime() > periodFinish, "not finish");
        lastRewardUpdateTime = currentTime();
        periodFinish = currentTime().add(REWARD_DURATION);
        emit RewardsDurationUpdated(periodFinish);
    }

    modifier updateReward(address account) {
        rewardPerTokenStored = calcUserRewardPerToken();
        rewardPerTokenStored4Pool = calcPoolRewardPerToken();
        lastRewardUpdateTime = lastTimeRewardApplicable();
        if (account != address(0)) {
            UserInfo storage user = userInfo[account];

            user.pendingReward = _earned(account);
            user.lastRewardPerToken = rewardPerTokenStored;

            user.pendingReward4Pool = _earned4Pool(account);
            user.lastRewardPerToken4Pool = rewardPerTokenStored4Pool;
        }
        _;
    }

    function calcUserRewardPerToken() public view returns (uint256) {
        return rewardPerTokenStored.add(lastTimeRewardApplicable().sub(lastRewardUpdateTime).mul(userRewardRatePerDay));
    }

    function calcPoolRewardPerToken() public view returns (uint256) {
        return
            rewardPerTokenStored4Pool.add(
                lastTimeRewardApplicable().sub(lastRewardUpdateTime).mul(poolRewardRatePerDay)
            );
    }

    function lastTimeRewardApplicable() public view returns (uint256) {
        return Math.min(block.timestamp, periodFinish);
    }

    function reinvest(uint256 pid) public updateReward(msg.sender) {
        require(pid < poolInfo.length, "Invalid pool id");

        UserInfo storage user = userInfo[msg.sender];

        if (balanceOf(msg.sender) > 0) {
            require(user.poolId == pid, "Wrong pid");
        }

        uint256 userEarned = _earned(msg.sender);
        user.pendingReward = 0;

        uint256 poolEarned = _earned4Pool(msg.sender);
        user.pendingReward4Pool = 0;

        PoolInfo storage pool = poolInfo[pid];
        if (poolEarned > 0 && pool.owner != address(0) && pool.amount >= MIN_POOL_DEPOSIT_AMOUNT) {
            poolRewards[pool.beneficiary] = poolRewards[pool.beneficiary].add(poolEarned);
            pool.profit = pool.profit.add(poolEarned);

            emit RewardPaidToOwner(pool.beneficiary, poolEarned, pid);
        }

        uint256 rewardFromPool = poolRewards[msg.sender];
        if (rewardFromPool > 0) {
            poolRewards[msg.sender] = 0;
            userEarned = userEarned.add(rewardFromPool);
        }

        if (userEarned == 0) {
            return;
        }

        super._stakeFromReward(msg.sender, userEarned);

        user.stakingTime = currentTime();
        user.poolId = pid;

        pool.amount = pool.amount.add(userEarned);

        emit Reinvested(msg.sender, userEarned, pid);
    }

    function deposit(uint256 pid, uint256 amount) external updateReward(msg.sender) {
        require(amount > 0, "Cannot deposit 0");
        require(pid < poolInfo.length, "Invalid pool id");

        UserInfo storage user = userInfo[msg.sender];

        if (balanceOf(msg.sender) > 0) {
            require(pid == user.poolId, "Can deposit in only one pool");
        }

        super._stake(amount);

        user.stakingTime = currentTime();
        user.poolId = pid;

        PoolInfo storage pool = poolInfo[pid];
        pool.amount = pool.amount.add(amount);

        emit Deposited(msg.sender, amount, pid);
    }

    function withdraw(uint256 amount) public updateReward(msg.sender) {
        require(amount > 0, "Cannot withdraw 0");
        require(balanceOf(msg.sender) >= amount, "Not enough");

        UserInfo memory user = userInfo[msg.sender];

        PoolInfo storage pool = poolInfo[user.poolId];
        if (pool.owner == msg.sender) {
            require(balanceOf(msg.sender) >= amount.add(MIN_POOL_ANNOUNCE_AMOUNT), "Cannot withdraw");
        }

        pool.amount = pool.amount.sub(staked(msg.sender));

        pendingAmount[msg.sender] = amount;
        settlementDate[msg.sender] = currentTime();

        pool.amount = pool.amount.add(staked(msg.sender));

        emit Withdrawn(msg.sender, amount, user.poolId);
    }

    function settle() external {
        require(currentTime() >= settlementDate[msg.sender].add(minWithdrawDuration), "too early");
        uint256 amount = pendingAmount[msg.sender];
        if (amount > 0) {
            pendingAmount[msg.sender] = 0;
            
            super._withdraw(amount);

            emit Settled(msg.sender, amount);
        }
    }

    function exit() external {
        UserInfo memory user = userInfo[msg.sender];
        reinvest(user.poolId);
        withdraw(balanceOf(msg.sender));
    }

    function settleableDate(address account) external view returns (uint256) {
        return settlementDate[account].add(minWithdrawDuration);
    }

    function earned(address account) external view returns (uint256) {
        uint256 userEarned = _earned(account);
        return userEarned;
    }

    function staked(address account) public view returns (uint256) {
        return balanceOf(account).sub(pendingAmount[account]);
    }

    function announce(uint256 pid) external {
        require(!address(msg.sender).isContract() || whiteList[msg.sender], "Not welcome");

        require(staked(msg.sender) >= MIN_POOL_ANNOUNCE_AMOUNT, "deposit more to announce");

        PoolAnnounceStatusCode status = checkAnnounceable(pid, msg.sender);
        require(status == PoolAnnounceStatusCode.OK, "Check Status Code");

        PoolInfo storage pool = poolInfo[pid];
        pool.owner = msg.sender;
        pool.beneficiary = msg.sender;
        pool.announceTime = currentTime();
        pool.profit = 0;

        emit Announced(msg.sender, pid);
    }

    function renounce(uint256 pid) external {
        PoolInfo storage pool = poolInfo[pid];
        require(pool.owner == msg.sender, "Must be owner");
        require(pool.announceTime + MIN_POOL_ANNOUNCE_DURATION < currentTime(), "Cannot renounce now");

        pool.owner = address(0);
        pool.beneficiary = address(0);
        pool.announceTime = 0;

        emit Renounced(msg.sender, pid);
    }

    function setBeneficiary(uint256 _pid, address _beneficiary) external {
        require(_beneficiary != address(0), "!_beneficiary");
        PoolInfo storage pool = poolInfo[_pid];
        require(pool.owner == msg.sender, "Must be owner");
        address preBeneficiary = pool.beneficiary;
        pool.beneficiary = _beneficiary;
        emit BeneficiaryTransferred(preBeneficiary, pool.beneficiary, _pid);
    }

    function setBreaker(bool _breaker) external onlyOwner {
        breaker = _breaker;
    }

    function setWhiteList(address addr, bool status) external onlyOwner {
        require(addr != address(0), "!addr");
        whiteList[addr] = status;
    }

    function setRewardRatePerDay(uint256 userRewardRate, uint256 poolRewardRate) external onlyOwner {
        require(currentTime() > periodFinish, "not finish");
        require(userRewardRate <= REWARD_RATE_BASE, "!wrong user rate");
        require(poolRewardRate <= REWARD_RATE_BASE, "!wrong pool rate");
        userRewardRatePerDay = userRewardRate;
        poolRewardRatePerDay = poolRewardRate;
    }

    function setMinWithdrawDuration(uint256 duration) external onlyOwner {
        require(duration >= SECONDS_PER_DAY, "!at least one day");
        minWithdrawDuration = duration;
    }

    // Added to support recovering LP Rewards from other systems such as BAL to be distributed to holders
    function recoverERC20(address tokenAddress, uint256 tokenAmount) external onlyOwner {
        require(
            tokenAddress != address(lp) && tokenAddress != address(rewardToken),
            "Cannot withdraw the staking or rewards tokens"
        );
        IERC20(tokenAddress).safeTransfer(owner(), tokenAmount);
        emit Recovered(tokenAddress, tokenAmount);
    }

    // withdraw extra reward tokens after staking period
    function withdrawAll() external onlyOwner {
        require(currentTime() > periodFinish, "period not finished");
        uint256 bal = rewardToken.balanceOf(address(this));
        uint256 amount = bal.sub(totalSupply());
        IERC20(rewardToken).safeTransfer(owner(), amount);
    }

    // Withdraw without caring about rewards. EMERGENCY ONLY.
    function emergencyWithdraw() external {
        require(breaker, "!breaker");

        UserInfo storage user = userInfo[msg.sender];
        user.pendingReward = 0;
        user.pendingReward4Pool = 0;
        poolRewards[msg.sender] = 0;
        pendingAmount[msg.sender] = 0;

        uint256 amount = balanceOf(msg.sender);
        super._withdraw(amount);
        emit EmergencyWithdraw(msg.sender, amount);
    }

    function maxWithdrawAmount(address account) public view returns (uint256) {
        uint256 maxAmount = balanceOf(account);
        UserInfo memory user = userInfo[account];
        PoolInfo memory pool = poolInfo[user.poolId];
        if (pool.owner == account) {
            return maxAmount.sub(MIN_POOL_ANNOUNCE_AMOUNT);
        }
        return maxAmount;
    }

    function queryPoolInfo(uint256 pid, address account)
        public
        view
        returns (
            bool hasOwner,
            bool isOwner,
            uint256 announceableStatus,
            uint256 totalAmount,
            uint256 announceTime,
            uint256 poolProfit,
            address beneficiary
        )
    {
        PoolInfo memory pool = poolInfo[pid];
        if (pool.owner != address(0)) {
            hasOwner = true;
            if (pool.owner == address(account)) {
                isOwner = true;
            }
        }
        announceableStatus = uint256(checkAnnounceable(pid, account));
        totalAmount = pool.amount;
        if (hasOwner) {
            announceTime = pool.announceTime;
            poolProfit = pool.profit;
        }
        if (isOwner) {
            beneficiary = pool.beneficiary;
        }
    }

    function poolCount() public view returns (uint256) {
        return uint256(poolInfo.length);
    }

    function _earned(address account) internal view returns (uint256) {
        UserInfo memory user = userInfo[account];
        return
            staked(account)
                .mul(calcUserRewardPerToken().sub(user.lastRewardPerToken))
                .div(REWARD_RATE_BASE)
                .div(SECONDS_PER_DAY)
                .add(user.pendingReward);
    }

    function _earned4Pool(address account) internal view returns (uint256) {
        UserInfo memory user = userInfo[account];
        return
            staked(account)
                .mul(calcPoolRewardPerToken().sub(user.lastRewardPerToken4Pool))
                .div(REWARD_RATE_BASE)
                .div(SECONDS_PER_DAY)
                .add(user.pendingReward4Pool);
    }

    function checkAnnounceable(uint256 pid, address account) internal view returns (PoolAnnounceStatusCode) {
        // check pid
        if (pid >= poolInfo.length) {
            return PoolAnnounceStatusCode.WRONG_PID;
        }
        // check owner
        PoolInfo memory pool = poolInfo[pid];
        if (pool.owner != address(0)) {
            return PoolAnnounceStatusCode.POOL_HAS_OWNER;
        }
        // check user
        UserInfo memory user = userInfo[account];
        if (balanceOf(account) > 0 && pid != user.poolId) {
            return PoolAnnounceStatusCode.USER_IN_ANOTHER_POOL;
        }
        // check the minimum deposit requirement
        if (staked(account) < MIN_POOL_ANNOUNCE_AMOUNT) {
            return PoolAnnounceStatusCode.NEED_MORE_DEPOSIT;
        }
        return PoolAnnounceStatusCode.OK;
    }

    function currentTime() internal view returns (uint256) {
        return block.timestamp;
    }
}
function recoverERC20(address tokenAddress, uint256 tokenAmount) external onlyOwner {
    require(
        tokenAddress != address(lp) && tokenAddress != address(rewardToken),
        "Cannot withdraw the staking or rewards tokens"
    );
    IERC20(tokenAddress).safeTransfer(owner(), tokenAmount);
    emit Recovered(tokenAddress, tokenAmount);
}

// Added to support recovering LP Rewards from other systems such as BAL to be distributed to holders