library UserDataOps {
    function getUserData(uint256 userData) internal pure returns (UserData memory) {
        return
            UserData({
                balance: UserDataOps.balance(userData),
                stakeStart: UserDataOps.stakeStart(userData),
                lastClaimed: UserDataOps.lastClaimed(userData),
                numStaked: UserDataOps.numStaked(userData),
                roleBalances: UserDataOps.roleBalances(userData),
                uniqueRoleCount: UserDataOps.uniqueRoleCount(userData),
                levelBalances: UserDataOps.levelBalances(userData),
                specialGuestIndex: UserDataOps.specialGuestIndex(userData),
                rarityPoints: UserDataOps.rarityPoints(userData),
                OGCount: UserDataOps.OGCount(userData),
                boostStart: UserDataOps.boostStart(userData)
            });
    }

    function balance(uint256 userData) internal pure returns (uint256) {
        return userData & 0xFFFFF;
    }

    function increaseBalance(uint256 userData, uint256 amount) internal pure returns (uint256) {
        unchecked {
            return userData + amount;
        }
    }

    function decreaseBalance(uint256 userData, uint256 amount) internal pure returns (uint256) {
        unchecked {
            return userData - amount;
        }
    }

    function numMinted(uint256 userData) internal pure returns (uint256) {
        return (userData >> 20) & 0xFFFFF;
    }

    function increaseNumMinted(uint256 userData, uint256 amount) internal pure returns (uint256) {
        unchecked {
            return userData + (amount << 20);
        }
    }

    function stakeStart(uint256 userData) internal pure returns (uint256) {
        return (userData >> 40) & 0xFFFFFFFFFF;
    }

    function setStakeStart(uint256 userData, uint256 timestamp) internal pure returns (uint256) {
        return (userData & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000FFFFFFFFFF) | (timestamp << 40);
    }

    function lastClaimed(uint256 userData) internal pure returns (uint256) {
        return (userData >> 80) & 0xFFFFFFFFFF;
    }

    function setLastClaimed(uint256 userData, uint256 timestamp) internal pure returns (uint256) {
        return (userData & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000FFFFFFFFFFFFFFFFFFFF) | (timestamp << 80);
    }

    function numStaked(uint256 userData) internal pure returns (uint256) {
        return (userData >> 120) & 0xFF;
    }

    function increaseNumStaked(uint256 userData, uint256 amount) internal pure returns (uint256) {
        unchecked {
            return userData + (amount << 120);
        }
    }

    function decreaseNumStaked(uint256 userData, uint256 amount) internal pure returns (uint256) {
        unchecked {
            return userData - (amount << 120);
        }
    }

    function roleBalances(uint256 userData) internal pure returns (uint256[5] memory balances) {
        balances = [
            (userData >> (128 + 0)) & 0xFF,
            (userData >> (128 + 8)) & 0xFF,
            (userData >> (128 + 16)) & 0xFF,
            (userData >> (128 + 24)) & 0xFF,
            (userData >> (128 + 32)) & 0xFF
        ];
    }

    // trait counts are set through hook in madmouse contract (MadMouse::_beforeStakeDataTransform)
    function uniqueRoleCount(uint256 userData) internal pure returns (uint256) {
        unchecked {
            return (toUInt256((userData >> (128)) & 0xFF > 0) +
                toUInt256((userData >> (128 + 8)) & 0xFF > 0) +
                toUInt256((userData >> (128 + 16)) & 0xFF > 0) +
                toUInt256((userData >> (128 + 24)) & 0xFF > 0) +
                toUInt256((userData >> (128 + 32)) & 0xFF > 0));
        }
    }

    function levelBalances(uint256 userData) internal pure returns (uint256[3] memory balances) {
        balances = [(userData >> (168 + 0)) & 0xFF, (userData >> (168 + 8)) & 0xFF, (userData >> (168 + 16)) & 0xFF];
    }

    // depends on the levels of the staked tokens (also set in hook MadMouse::_beforeStakeDataTransform)
    // counts the base reward, depending on the levels of staked ids
    function baseReward(uint256 userData) internal pure returns (uint256) {
        unchecked {
            return (((userData >> (168)) & 0xFF) +
                (((userData >> (168 + 8)) & 0xFF) << 1) +
                (((userData >> (168 + 16)) & 0xFF) << 2));
        }
    }

    function rarityPoints(uint256 userData) internal pure returns (uint256) {
        return (userData >> 200) & 0x3FF;
    }

    function specialGuestIndex(uint256 userData) internal pure returns (uint256) {
        return (userData >> 192) & 0xFF;
    }

    function setSpecialGuestIndex(uint256 userData, uint256 index) internal pure returns (uint256) {
        return (userData & ~uint256(0xFF << 192)) | (index << 192);
    }

    function boostStart(uint256 userData) internal pure returns (uint256) {
        return (userData >> 218) & 0xFFFFFFFFFF;
    }

    function setBoostStart(uint256 userData, uint256 timestamp) internal pure returns (uint256) {
        return (userData & ~(uint256(0xFFFFFFFFFF) << 218)) | (timestamp << 218);
    }

    function OGCount(uint256 userData) internal pure returns (uint256) {
        return (userData >> 210) & 0xFF;
    }

    //  (should start at 128, 168; but role/level start at 1...)
    function updateUserDataStake(uint256 userData, uint256 tokenData) internal pure returns (uint256) {
        unchecked {
            uint256 role = TokenDataOps.role(tokenData);
            if (role > 0) {
                userData += uint256(1) << (120 + (role << 3)); // roleBalances
                userData += TokenDataOps.rarity(tokenData) << 200; // rarityPoints
            }
            if (TokenDataOps.mintAndStake(tokenData)) userData += uint256(1) << 210; // OGCount
            userData += uint256(1) << (160 + (TokenDataOps.level(tokenData) << 3)); // levelBalances
            return userData;
        }
    }

    function updateUserDataUnstake(uint256 userData, uint256 tokenData) internal pure returns (uint256) {
        unchecked {
            uint256 role = TokenDataOps.role(tokenData);
            if (role > 0) {
                userData -= uint256(1) << (120 + (role << 3)); // roleBalances
                userData -= TokenDataOps.rarity(tokenData) << 200; // rarityPoints
            }
            if (TokenDataOps.mintAndStake(tokenData)) userData -= uint256(1) << 210; // OG-count
            userData -= uint256(1) << (160 + (TokenDataOps.level(tokenData) << 3)); // levelBalances
            return userData;
        }
    }

    function increaseLevelBalances(uint256 userData, uint256 tokenData) internal pure returns (uint256) {
        unchecked {
            return userData + (uint256(1) << (160 + (TokenDataOps.level(tokenData) << 3)));
        }
    }

    function decreaseLevelBalances(uint256 userData, uint256 tokenData) internal pure returns (uint256) {
        unchecked {
            return userData - (uint256(1) << (160 + (TokenDataOps.level(tokenData) << 3)));
        }
    }
}
// @note: many of these are unchecked, because safemath wouldn't be able to guard
// overflows while updating bitmaps unless custom checks were to be implemented
function uniqueRoleCount(uint256 userData) internal pure returns (uint256) {
    unchecked {
        return (toUInt256((userData >> (128)) & 0xFF > 0) +
            toUInt256((userData >> (128 + 8)) & 0xFF > 0) +
            toUInt256((userData >> (128 + 16)) & 0xFF > 0) +
            toUInt256((userData >> (128 + 24)) & 0xFF > 0) +
            toUInt256((userData >> (128 + 32)) & 0xFF > 0));
    }
}

// trait counts are set through hook in madmouse contract (MadMouse::_beforeStakeDataTransform)