// Contract: CryptoBlobs
// Target function: function safeTransferFrom(
, bytes memory returndata) = payable(account).call{value: amount}(data);
        // If not successful.
        if (!success) {
            // If no return data.
            if (returndata.length == 0) {
                // Revert with a generic error.
                if (amount > 0) {
                    revert UNABLE_TO_TRANSFER_ETHER();
                } else {
                    revert UNABLE_TO_INTERACT_WITH_CONTRACT();
                }
            } else {
                // Revert with the error from the contract.
                assembly {
                    revert(add(32, returndata), mload(returndata))
                }
            }
        }
    }

    /**
        @notice Transfers ETH to another address.
        @param account Address to transfer ETH to.
        @param amount Amount of ETH to transfer.
    */
    function _transferEther(address account, uint256 amount) internal {
        _interaction(account, amount, "");
    }

    /**
        @notice Convert index to token ID.
    */
    function _getTokenId(uint256 index) internal pure returns (uint256 tokenId) {
        unchecked {
            if (index >= _maxSupply) revert INVALID_CRYPTOBLOB_ID();
            if ((index / _sections) % 2 == 0) {
                return (_distance * (index % _sections)) + ((_distance * index) / _maxSupply) + 1;
            } else {
                return _maxSupply - ((_distance * (index % _sections)) + ((_distance * index) / _maxSupply));
            }
        }
    }

    /**
        @notice Convert token ID to index.
    */
    function _getTokenIndex(uint256 tokenId) internal pure returns (uint256 index) {
        unchecked {
            if (tokenId > _maxSupply || tokenId < 1) revert INVALID_CRYPTOBLOB_ID();
            uint256 base = tokenId % _distance;
            if (base == 0) base = _distance;
            if (base % 2 != 0) { tokenId--; } else { tokenId = _maxSupply - tokenId; }
            return (((tokenId % _distance) * _sections) + (tokenId / _distance));
        }
    }
}


/**
    @title CryptoBlobs
    @author SuperCollectiv
    @dev Only contains write methods. See [SuperERC721] for read methods.
*/
contract CryptoBlobs is SuperERC721 {

    /**
        @notice Mint CryptoBlobs by claiming them for free or purchasing them on the Live Market.
        @dev Purchasing CryptoBlobs has an estimated gas fee of 66,200 + 2,050 GWEI/mint.
        @param key Each account has a unique key to claim free CryptoBlobs. Enter 0 if making a purchase.
        @param account When purchasing = referrer's address or zero address if none. When claiming = contract address of NFT or zero address if none.
        @param number When purchasing = amount to purchase (limit 100/tx). When claiming = token ID of the NFT or 0 if none.
    */
    function mint(uint256 key, address account, uint256 number) external payable nonReentrant {
        unchecked {

            // Initialize.
            uint256 tokensMintedPurchase = _tokensMintedPurchase; // Get amount purchased.
            uint256 tokensMintedFree = _tokensMintedFree; // Get amount claimed for free.
            uint32 safeBlockTimestamp = _safeBlockTimestamp(); // Get timestmap.

            if (key == 0) {

                // account = referrer
                // number = amountPurchasing

                // Get price and total.
                (
                    uint256 tokenPrice,,
                    uint256 multiplier,,
                    uint256 bogo
                ) = _marketPrice(tokensMintedPurchase, tokensMintedFree);

                // Calculate total price.
                uint256 salePrice = tokenPrice * number;

                // If the value sent is insufficient, the contract will attempt to 
                // prevent a revert by minting the maximum amount with the value sent.
                // Purchase one at a time if you do not want this feature.
                if (msg.value < salePrice) { // If the amount sent is insufficient.

                    if (msg.value >= tokenPrice) { // If at least one can be purchased with the amount sent.
                        // New amount cannot be higher than the old amount because msg.value < salePrice.
                        number = msg.value / tokenPrice; // Calculate max amount that can be minted.
                        salePrice = tokenPrice * number; // Calculate new total price.
                    }

                    // Revert if the value sent is still insufficient.
                    if (msg.value < salePrice) revert INSUFFICIENT_FUNDS_SENT_PRICE_MAY_HAVE_INCREASED(salePrice, msg.value);
                }

                // Revert if minting too many (>100).
                if (number > _purchaseLimit) revert PURCHASE_LIMIT_IS_100_PER_TRANSACTION();

                // Issue refund. Refund is always issued.
                // Cannot underflow because transaction reverts if msg.value < salePrice.
                _transferEther(msg.sender, (msg.value - salePrice));

                // Add to total account purchased. If price is 0 ETH, it will still count as a purchase.
                _account[msg.sender].mintedPurchase += uint16(number);

                // Initialize hourly drops if 5 are purchased. Drops at 00:00AM UTC each day. See {generalData} for countdown.
                if (
                    _account[msg.sender].mintedPurchase >= _hourlyDropThreshold && // If amount purchased surpasses threshold.
                    _account[msg.sender].timestampHourlyDropLastClaimed == 0 // If hourly drop not initialized.
                ) {
                    _account[msg.sender].timestampHourlyDropLastClaimed = safeBlockTimestamp; // Initialize.
                }

                // BOGO deal
                uint256 bogoReward;
                if (number >= bogo) { // If purchased enough for BOGO deal reward.
                    if (msg.sender == tx.origin) { // If not contract.
                        uint256 mintedFree = _accountToMintedFree(msg.sender);
                        if (mintedFree < _maxFreePerAccount) { // If account can claim more.
                            bogoReward = number / bogo; // Calculate amount earned.
                            if (mintedFree + bogoReward > _maxFreePerAccount) { // Correct amount if claiming too many.
                                // Grant remaining amount. Overflow check above (mintedFree < _maxFreePerAccount).
                                bogoReward = _maxFreePerAccount - mintedFree;
                            }
                            _account[msg.sender].mintedBogos += uint8(bogoReward); // Add to mintedBogos total.
                        }
                    }
                    // Update demand multiplier.
                    // Paid mints increases the multiplier by 0.01% and 0.03% after 20,000 for increased volatility.
                    // Free mints decreases the multiplier by 0.01% and 0.03% after 20,000 for increased volatility for every 2.
                    if (tokensMintedPurchase + tokensMintedFree < _increasedVolatilityThreshold) {
                        _temporaryDemandMultiplier = uint32(multiplier + ((number - (bogoReward / 2)) * _volatilityBase)); // Increase.
                    } else {
                        _temporaryDemandMultiplier = uint32(multiplier + ((number - (bogoReward / 2)) * _volatilityBase * _volatilityMultiplier)); // Increase.
                    }
                } else {
                    // Update demand multiplier.
                    // Paid mints increases the multiplier by 0.01% and 0.03% after 20,000 for increased volatility.
                    // Free mints decreases the multiplier by 0.01% and 0.03% after 20,000 for increased volatility for every 2.
                    if (tokensMintedPurchase + tokensMintedFree < _increasedVolatilityThreshold) {
                        _temporaryDemandMultiplier = uint32(multiplier + (number * _volatilityBase)); // Increase.
                    } else {
                        _temporaryDemandMultiplier = uint32(multiplier + (number * _volatilityBase * _volatilityMultiplier)); // Increase.
                    }
                }

                // Referral program
                if (account != address(0)) { // If referred.
                    if (
                        _account[msg.sender].timestampReferralLinkUsed == 0 && // If not previously referred.
                        account != msg.sender && // If not self.
                        msg.sender == tx.origin // If not contract.
                    ) {
                        _account[msg.sender].timestampReferralLinkUsed = safeBlockTimestamp; // Timestamp referred.
                        _account[account].referralPurchases += uint16(number); // Add to referralPurchases total.
                    } else {
                        account = address(0); // Remove referral.
                    }
                }

                // Update timestamp of latest mint.
                _purchaseTimestamp = safeBlockTimestamp;

                // Mint token(s).
                _mint(
                    msg.sender,                                 // recipient
                    number,                                     // purchased mints
                    bogoReward,                                 // free mints
                    safeBlockTimestamp,                         // timestamp
                    tokensMintedPurchase + tokensMintedFree     // tokens minted
                );

                emit MintPurchase(  // 0xf26aaf0d6935ae39e0b17d4770395a2cf37139301bf4a1550daabaa363cb8df0
                    msg.sender,     // address indexed account
                    account,        // address indexed referrer
                    ((              // uint256 data
                        ((
                            ((
                                tokenPrice          // +XXXXXXXXXXXXXXX price
                            * 10**3) + number)      // X---XXXXXXXXXXXX amount purchased
                        * 10**2) + bogoReward)      // XXXX--XXXXXXXXXX amount earned
                    * 10**10) + block.timestamp)    // XXXXXX---------- timestamp
                );

            } else {
                
                // account = contractAddress
                // number = tokenId

                // Initialize claim
                if (tx.origin != msg.sender || address(uint160(type(uint160).max - key)) != msg.sender) revert RESTRICTED_ACCESS();

                // Get claimable amounts.
                (
                    ,
                    uint256 claimableHourlyDrops, 
                    uint256 claimableSacrifices, 
                    uint256 claimableReferrals, 
                    uint256 claimableGiveaway
                ) = _accountToClaimable(msg.sender);

                // Claim hourly drops.
                if (claimableHourlyDrops > 0) {
                    _account[msg.sender].mintedHourlyDrops += uint8(claimableHourlyDrops);
                    _account[msg.sender].timestampHourlyDropLastClaimed = safeBlockTimestamp; // Timestamp claimed.
                }

                // Claim sacrifices.
                if (claimableSacrifices > 0) {
                    _account[msg.sender].mintedSacrifices += uint8(claimableSacrifices);
                }

                // Claim referrals.
                if (claimableReferrals > 0) {
                    _account[msg.sender].mintedReferrals += uint8(claimableReferrals);
                }

                // Claim giveaway.
                if (claimableGiveaway > 0) {
                    if (account != address(0) && _redeemNFT(account, number)) {
                        _account[msg.sender].mintedGiveaway += uint8(claimableGiveaway);
                    } else {
                        delete claimableGiveaway;
                        delete account;
                        delete number;
                    }
                } else {
                    delete account;
                    delete number;
                }

                // Revert if nothing to claim.
                uint256 claimableTotal = claimableHourlyDrops + claimableSacrifices + claimableReferrals + claimableGiveaway;
                if (claimableTotal == 0) revert ACCOUNT_HAS_NOTHING_TO_CLAIM();

                // Update demand multiplier.
                // Paid mints increases the multiplier by 0.01% and 0.03% after 20,000 for increased volatility.
                // Free mints decreases the multiplier by 0.01% and 0.03% after 20,000 for increased volatility for every 2.
                if (claimableTotal >= 2) { // If claiming two or more.
                    uint256 multiplierIncrement;
                    if (tokensMintedPurchase + tokensMintedFree < _increasedVolatilityThreshold) {
                        multiplierIncrement = (claimableTotal / 2) * _volatilityBase; // Increase.
                    } else {
                        multiplierIncrement = (claimableTotal / 2) * _volatilityBase * _volatilityMultiplier; // Increase.
                    }
                    if (_temporaryDemandMultiplier > multiplierIncrement) {
                        _temporaryDemandMultiplier -= uint32(multiplierIncrement); // Decrease.
                    } else {
                        delete _temporaryDemandMultiplier; // Cannot be negative.
                    }
                }

                // Mint token(s).
                _mint(
                    msg.sender,                                 // recipient
                    0,                                          // purchased mints
                    claimableTotal,                             // free mints
                    safeBlockTimestamp,                         // timestamp
                    tokensMintedPurchase + tokensMintedFree     // tokens minted
                );

                emit MintFree(      // 0x7236fe0badcff1706816875cc9df7b9b481c3fb939efe1e7967099499db621d9
                    msg.sender,     // address indexed account
                    account,        // address indexed contractAddress
                    number,         // uint256 tokenId
                    ((              // uint256 data
                        ((
                            ((
                                claimableHourlyDrops            // -XXXXXXXXXXXXXX hourly drops claimed (max 1 per tx)
                            * 10**2) + claimableSacrifices)     // X--XXXXXXXXXXXX sacrifices claimed (max 10 per tx)
                        * 10**2) + claimableReferrals)          // XXX--XXXXXXXXXX referrals claimed (max 10 per tx)
                    * 10**10) + block.timestamp)                // XXXXX---------- timestamp
                );
            }
        }
    }

    /**
        @notice Approves `to` to be a token operator of `tokenId`.
        @dev You must be the owner of `tokenId` or an account operator of the owner.
        @dev Only a single account can be approved at a time.
        @dev Approve the zero address to clear the current approval.
        @dev The approval is cleared when the token is transferred.
        @param to The address of the account to approve.
        @param tokenId The token ID of the CryptoBlob to manage.
    */
    function approve(address to, uint256 tokenId) public virtual override {
        // Get the owner of `tokenId`.
        address owner = ownerOf(tokenId);

        // Cannot approve the current owner.
        if (to == owner) revert CANNOT_APPROVE_THIS_ADDRESS();

        // Revert if not the owner and not approved for all.
        if (msg.sender != owner && !isApprovedForAll(owner, msg.sender)) revert RESTRICTED_ACCESS();

        // Approve `to` to manage `tokenId`.
        _token[tokenId].approval = to;

        emit Approval(  // 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925
            owner,      // address indexed owner
            to,         // address indexed approved
            tokenId     // uint256 indexed tokenId
        );
    }

    /**
        @notice Approves `to` to be an account operator of your account.
        @dev Account operators can do everything the owner can do expect sign messages and manage their account operators.
        @dev Preapproved services: OpenSea, X2Y2, LooksRare, Rarible, 0x Protocol v4, Element, SudoSwap, NFTX, and NFTTrader.
        @dev You can revoke these services using {setApprovalForAll} or visit CryptoBlobs.com for a batch tx to revoke all at once.
        @dev SuperCollectiv will permanently disable preapproved services if any issues are discovered with these contracts.
        @dev Services that list your account operators will not detect that these addresses have
        @dev been approved unless you reapprove each service with {setApprovalForAll} or {batch}.
        @param operator The address of the account to update the operator status of.
        @param approved The approval status to set `operator` to.
    */
    function setApprovalForAll(address operator, bool approved)
        public
        virtual
        override
    {
        unchecked {
            if (msg.sender == operator) revert CANNOT_APPROVE_THIS_ADDRESS();

            // Numbers are used to enable the preapproved services feature. Works similarly to an enum.
            // Values:
            // 0 = default
            // 1 = revoked
            // 2 = approved

            if (approved) {
                _account[msg.sender].approvals[operator] = 2; // Approve.
            } else {
                _account[msg.sender].approvals[operator] = 1; // Revoked.
            }

            emit ApprovalForAll(    // 0x17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31
                msg.sender,         // address indexed owner
                operator,           // address indexed operator
                approved            // bool approved
            );
        }
    }

    /**
        @notice Transfers `tokenId` from `from` to `to`.
        @param from The address of the account that owns `tokenId`.
        @param to The address of the account to transfer `tokenId` to.
        @param tokenId The token ID of the CryptoBlob to transfer.
    */
    function transferFrom(
        address from,
        address to,
        uint256 tokenId
    ) public virtual override {
        // Reverts if the sender does not have access.
        // The owner's address is returned if the sender does have access.
        address owner = _revertIfNoAccess(msg.sender, tokenId);
        if (owner != from) revert FROM_ADDRESS_DOES_NOT_MATCH_THE_OWNERS_ADDRESS();
        // Transfer the CryptoBlob to the new owner.
        _transfer(owner, to, tokenId);
    }