contract GrimReaper is Ownable {
    using SafeMath for uint256;
    using SafeERC20 for IERC20;

    // Info of each user.
    struct UserInfo {
        uint256 amount;     // How many LP tokens the user has provided.
        uint256 rewardDebt; // Reward debt. See explanation below.
        uint256 lastDeposit; // block.timestamp
        uint256 earlyBaseRewardMultiplier;
        uint256 latestMultiplier;
    }

    // Protocol Adapter to regulate extra farming rewards from holding NFT and certain tokens
    IProtocolAdapter public protocolAdapter;

    // Info of each pool.
    struct PoolInfo {
        IERC20 lpToken;           // Address of LP token contract.
        uint256 allocPoint;       // How many allocation points assigned to this pool. BBRAs to distribute per block.
        uint256 lastRewardBlock;  // Last block number that BBRAs distribution occurs.
        uint256 accSushiPerShare; // Accumulated BBRAs per share, times 1e12. See below.
    }

    // The Gr1m TOKEN!
    Gr1m public sushi;
    // Block number when bonus Gr1m period ends.
    uint256 public bonusEndBlock;
    // Gr1m tokens created per block.
    uint256 public sushiPerBlock;
    // Block number when early lp rewards end.
    uint256 public bonusLpEndBlock;
    // Bonus muliplier for early boob makers.
    uint256 public BONUS_MULTIPLIER = 1;

    // penalties for withdrawing within first x weeks of deposit
    // within w1 = 50%, w2 = 33.33%, w3 = 25%, w4 = 15%, on and after w5 = 0%
    uint256[] public earlyWithdrawalPenalties = [2, 3, 4, 7];

    // The migrator contract removed. It has a lot of power. Can only be set through governance (owner).

    // Info of each pool.
    PoolInfo[] public poolInfo;
    // Info of each user that stakes LP tokens.
    mapping (uint256 => mapping (address => UserInfo)) public userInfo;
    // Total allocation points. Must be the sum of all allocation points in all pools.
    uint256 public totalAllocPoint = 0;
    // The block number when Gr1m mining starts.
    uint256 public startBlock;
    // Don't add same pool twice https://twitter.com/Quantstamp/status/1301280989906231296
    mapping (address => bool) private poolIsAdded;

    event Withdraw(address indexed user, uint256 indexed pid, uint256 amount);
    event EmergencyWithdraw(address indexed user, uint256 indexed pid, uint256 amount);
    event EarlyAdopter(address indexed user, uint256 indexed pid, uint256 amount);
    event ProtocolAdapterChange(address _newProtocol);

    constructor(
        Gr1m _sushi,
        uint256 _sushiPerBlock,
        uint256 _startBlock,
        uint256 _bonusLpEndBlock
    ) public {
        sushi = _sushi;
        sushiPerBlock = _sushiPerBlock;
        bonusLpEndBlock = _bonusLpEndBlock;
        startBlock = _startBlock;
    }

    function getUserRewardsMultiplier(address _user, uint256 _current) public view returns (uint256) {
        if (address(protocolAdapter) == address(0))
            return _current;

        return protocolAdapter.getRewardsMultiplier(_user, _current);
    }

    function getUserWithdrawalPenalty(uint256 _depositTime) public view returns (uint256) {

        uint256 _int = block.timestamp.sub(_depositTime).div(604800);

        if (_int > (earlyWithdrawalPenalties.length - 1))
            return 0;

        return earlyWithdrawalPenalties[_int];
    }

    // Deposit LP tokens to MrBanker for Gr1m allocation.
    function deposit(uint256 _pid, uint256 _amount) public {
        PoolInfo storage pool = poolInfo[_pid];
        UserInfo storage user = userInfo[_pid][msg.sender];
        updatePool(_pid);

        user.earlyBaseRewardMultiplier = getUserBaseRewards(user.earlyBaseRewardMultiplier);

        // multiplier at time of deposit must be maintained when withdrawing - otherwise lowest is selected
        uint256 _currentMultiplier = getUserRewardsMultiplier(msg.sender, user.earlyBaseRewardMultiplier);

        if (user.amount != 0) {
            uint256 pending = user.amount.mul(pool.accSushiPerShare).div(1e12).sub(user.rewardDebt);
            uint256 penaltyDivisor = getUserWithdrawalPenalty(user.lastDeposit);
            uint256 penaltyToBurn;

            if (penaltyDivisor != 0) {
                penaltyToBurn = pending.div(penaltyDivisor);
                pending = pending.sub(penaltyToBurn);
            }

            safeSushiTransfer(msg.sender, pending);

            // burn any early withdraw penalties
            if (penaltyToBurn != 0) {
                sushi.burn(penaltyToBurn);
            }

            if (user.latestMultiplier > 100) {
                // since pool balance isn't calculated on individual contributions we must mint the early adopters rewards
                // as we might come short otherwise.
                sushi.mint(msg.sender, pending.mul(
                    // pick the smallest of current multiplier and multiplier at time of initial deposit
                    (user.latestMultiplier > _currentMultiplier ? _currentMultiplier : user.latestMultiplier)
                ).div(100).sub(pending));
            }
        }

        user.amount = user.amount.add(_amount);
        user.rewardDebt = user.amount.mul(pool.accSushiPerShare).div(1e12);
        user.latestMultiplier = _currentMultiplier;

        if (_amount !=  0) {
            pool.lpToken.safeTransferFrom(address(msg.sender), address(this), _amount);
            // only reset clock when actually depositing. On _amount == 0 it only withdraws rewards
            user.lastDeposit = block.timestamp;
        }
    }

    // Withdraw LP tokens from MrBanker.
    function withdraw(uint256 _pid, uint256 _amount) public {
        PoolInfo storage pool = poolInfo[_pid];
        UserInfo storage user = userInfo[_pid][msg.sender];
        require(user.amount >= _amount, "withdraw: not good");
        updatePool(_pid);
        user.earlyBaseRewardMultiplier = getUserBaseRewards(user.earlyBaseRewardMultiplier);
        // multiplier at time of deposit must be maintained when withdrawing - otherwise lowest is selected
        uint256 _currentMultiplier = getUserRewardsMultiplier(msg.sender, user.earlyBaseRewardMultiplier);

        uint256 pending = user.amount.mul(pool.accSushiPerShare).div(1e12).sub(user.rewardDebt);
        // check penalties
        uint256 penaltyDivisor = getUserWithdrawalPenalty(user.lastDeposit);
        uint256 penaltyToBurn;

        if (penaltyDivisor != 0) {
            penaltyToBurn = pending.div(penaltyDivisor);
            pending = pending.sub(penaltyToBurn);
        }

        safeSushiTransfer(msg.sender, pending);

        // burn any early withdraw penalties
        if (penaltyToBurn != 0) {
            sushi.burn(penaltyToBurn);
        }

        if (user.latestMultiplier > 100) {
            // since pool balance isn't calculated on individual contributions we must mint the early adopters rewards
            // as we might come short otherwise.
            sushi.mint(msg.sender, pending.mul(
            // pick the smallest of current multiplier and multiplier at time of initial deposit
                (user.latestMultiplier > _currentMultiplier ? _currentMultiplier : user.latestMultiplier)
            ).div(100).sub(pending));
        }

        user.amount = user.amount.sub(_amount);
        user.rewardDebt = user.amount.mul(pool.accSushiPerShare).div(1e12);
        user.latestMultiplier = _currentMultiplier;

        // 1% of all withdrawn LP is locked away forever
        uint256 burnedLp = _amount.div(100);
        pool.lpToken.safeTransfer(address(msg.sender), _amount.sub(burnedLp));
        pool.lpToken.safeTransfer(address(0), burnedLp);
        emit Withdraw(msg.sender, _pid, _amount);
    }

    function getUserBaseRewards(uint256 _current) public view returns (uint256) {
        if (block.number > bonusLpEndBlock && _current > 100) {
            // reset early LP rewards after bonusLpEndBlock block
            // nft rewards still count
            _current = 100;
        }

        if (block.number < startBlock) {
            _current = 110;
        }

        if (_current == 0) {
            _current = 100;
        }

        return _current;
    }

    // Safe boo transfer function, just in case if rounding error causes pool to not have enough Gr1ms.
    function safeSushiTransfer(address _to, uint256 _amount) internal {
        uint256 sushiBal = sushi.balanceOf(address(this));
        if (_amount > sushiBal) {
            sushi.transfer(_to, sushiBal);
        } else {
            sushi.transfer(_to, _amount);
        }
    }

    // Update reward variables of the given pool to be up-to-date.
    function updatePool(uint256 _pid) public {
        PoolInfo storage pool = poolInfo[_pid];
        if (block.number <= pool.lastRewardBlock) {
            return;
        }
        uint256 lpSupply = pool.lpToken.balanceOf(address(this));
        if (lpSupply == 0) {
            pool.lastRewardBlock = block.number;
            return;
        }
        uint256 multiplier = getMultiplier(pool.lastRewardBlock, block.number);
        uint256 sushiReward = multiplier.mul(sushiPerBlock).mul(pool.allocPoint).div(totalAllocPoint);
        sushi.mint(address(this), sushiReward);
        pool.accSushiPerShare = pool.accSushiPerShare.add(sushiReward.mul(1e12).div(lpSupply));
        pool.lastRewardBlock = block.number;
    }

    // Return reward multiplier over the given _from to _to block.
    function getMultiplier(uint256 _from, uint256 _to) public view returns (uint256) {
        if (_to <= bonusEndBlock) {
            return _to.sub(_from).mul(BONUS_MULTIPLIER);
        } else if (_from >= bonusEndBlock) {
            return _to.sub(_from);
        } else {
            return bonusEndBlock.sub(_from).mul(BONUS_MULTIPLIER).add(
                _to.sub(bonusEndBlock)
            );
        }
    }

    // Withdraw without caring about rewards. EMERGENCY ONLY.
    function emergencyWithdraw(uint256 _pid) public {
        PoolInfo storage pool = poolInfo[_pid];
        UserInfo storage user = userInfo[_pid][msg.sender];
        pool.lpToken.safeTransfer(address(msg.sender), user.amount);
        emit EmergencyWithdraw(msg.sender, _pid, user.amount);
        user.amount = 0;
        user.rewardDebt = 0;
    }

    // Sets contract that regulates dynamic farm rewards and burn rates (changeable by DAO)
    function setProtocolAdapter(IProtocolAdapter _contract) public onlyOwner {
        // setting to 0x0 disabled nft rewards
        protocolAdapter = _contract;
        emit ProtocolAdapterChange(address(_contract));
    }

    // Add a new lp to the pool. Can only be called by the owner.
    // XXX DO NOT add the same LP token more than once. Rewards will be messed up if you do.
    function add(uint256 _allocPoint, IERC20 _lpToken, bool _withUpdate) public onlyOwner {
        require(poolIsAdded[address(_lpToken)] == false, 'add: pool already added');
        poolIsAdded[address(_lpToken)] = true;

        if (_withUpdate) {
            massUpdatePools();
        }
        uint256 lastRewardBlock = block.number > startBlock ? block.number : startBlock;
        totalAllocPoint = totalAllocPoint.add(_allocPoint);
        poolInfo.push(PoolInfo({
        lpToken: _lpToken,
        allocPoint: _allocPoint,
        lastRewardBlock: lastRewardBlock,
        accSushiPerShare: 0
        }));
    }

    // Update the given pool's Gr1m allocation point. Can only be called by the owner.
    function set(uint256 _pid, uint256 _allocPoint, bool _withUpdate) public onlyOwner {
        if (_withUpdate) {
            massUpdatePools();
        }
        totalAllocPoint = totalAllocPoint.sub(poolInfo[_pid].allocPoint).add(_allocPoint);
        poolInfo[_pid].allocPoint = _allocPoint;
    }

    // Update reward variables for all pools. Be careful of gas spending!
    function massUpdatePools() public {
        uint256 length = poolInfo.length;
        for (uint256 pid = 0; pid < length; ++pid) {
            updatePool(pid);
        }
    }

    function poolLength() external view returns (uint256) {
        return poolInfo.length;
    }

    function setStartBlock(uint256 _startBlock) public onlyOwner {
        startBlock = _startBlock;
    }

    function setBonusEndBlock(uint256 _bonusEndBlock) public onlyOwner {
        bonusEndBlock = _bonusEndBlock;
    }

    function setBonusLpEndBlock(uint256 _bonusLpEndBlock) public onlyOwner {
        bonusLpEndBlock = _bonusLpEndBlock;
    }

    // Updates bonus multiplier for early farmers
    function setBonusMultiplier(uint256 _bonusMultiplier) public onlyOwner {
        BONUS_MULTIPLIER = _bonusMultiplier;
    }

    // Sets reward per block
    function setRewardPerBlock(uint256 _rewardPerBlock, bool _withUpdate) public onlyOwner {
        // if last a pool was update is a while ago it's best to update pools so no rewards go missing
        if (_withUpdate) {
            massUpdatePools();
        }
        sushiPerBlock = _rewardPerBlock;
    }

    // View function to see pending Gr1ms on frontend.
    function pendingSushi(uint256 _pid, address _user) external view returns (uint256) {
        PoolInfo storage pool = poolInfo[_pid];
        UserInfo storage user = userInfo[_pid][_user];
        uint256 accSushiPerShare = pool.accSushiPerShare;
        uint256 lpSupply = pool.lpToken.balanceOf(address(this));
        if (block.number > pool.lastRewardBlock && lpSupply != 0) {
            uint256 _sushiPerBlock = sushiPerBlock;

            uint256 _earlyBaseRewardMultiplier = getUserBaseRewards(user.earlyBaseRewardMultiplier);

            // multiplier at time of deposit must be maintained when withdrawing - otherwise lowest is selected
            uint256 _currentMultiplier = getUserRewardsMultiplier(_user, _earlyBaseRewardMultiplier);
            _sushiPerBlock = _sushiPerBlock.mul(_currentMultiplier).div(100);

            uint256 multiplier = getMultiplier(pool.lastRewardBlock, block.number);
            uint256 sushiReward = multiplier.mul(_sushiPerBlock).mul(pool.allocPoint).div(totalAllocPoint);
            accSushiPerShare = accSushiPerShare.add(sushiReward.mul(1e12).div(lpSupply));
        }
        return user.amount.mul(accSushiPerShare).div(1e12).sub(user.rewardDebt);
    }

    // Set burn rate for Gr1m
    function setBurnRate(uint8 _burnDivisor) public onlyOwner {
        require(_burnDivisor != 0, "Boo: burnDivisor must be bigger than 0");
        sushi.setBurnRate(_burnDivisor);
    }
}
// 
function safeSushiTransfer(address _to, uint256 _amount) internal {
        uint256 sushiBal = sushi.balanceOf(address(this));
        if (_amount > sushiBal) {
            sushi.transfer(_to, sushiBal);
        } else {
            sushi.transfer(_to, _amount);
        }
    }
// Safe boo transfer function, just in case if rounding error causes pool to not have enough Gr1ms.