// Contract: CryptoBlobs
// Target function: function approve(address to, uint256 tokenId) public virtual override
verts if an account does not have access to manage a CryptoBlob ID.
        @param account The address of the account to check the access status of.
        @param tokenId The token ID of the CryptoBlob.
        @return owner The address of the account that owns `tokenId`.
    */
    function _revertIfNoAccess(address account, uint256 tokenId) internal view returns (address owner) {
        (bool hasAccess, address _owner) = _hasAccess(account, tokenId);
        if (!hasAccess) revert RESTRICTED_ACCESS();
        return _owner;
    }

    /**
        @notice Returns the total number of CryptoBlobs minted.
        @return minted The amount of CryptoBlobs minted.
    */
    function _tokensMinted() internal view returns (uint256 minted) {
        unchecked {
            return _tokensMintedPurchase + _tokensMintedFree; // Cannot overflow.
        }
    }

    /**
        @notice Returns TRUE if a CryptoBlob has been sacrificed.
        @param tokenId The token ID of the CryptoBlob.
        @return burned If `tokenId` has been sacrificed or not.
    */
    function _isTokenSacrificed(uint256 tokenId) internal view returns (bool burned) {
        unchecked {
            return (_token[tokenId].burnTimestamp != 0);
        }
    }

    /**
        @notice Reverts if the token ID does not exist.
        @param tokenId The token ID of the CryptoBlob.
    */
    function _revertIfTokenIsInvalid(uint256 tokenId) internal view {
        unchecked {
            if (_getTokenIndex(tokenId) >= _tokensMinted()) revert INVALID_CRYPTOBLOB_ID();
            if (_isTokenSacrificed(tokenId)) revert CRYPTOBLOB_HAS_BEEN_SACRIFICED();
        }
    }

    /**
        @notice Safely get the UNIX time for a unit32 number.
        @dev The contract does not use uint256 timestamps to save gas.
        @dev The maximum date/time the contract supports is Feb 7, 2106 6:28:15 AM.
        @dev Refer to past logs for uint256 timestamps.
        @return _seconds The current timestamp, which stops counting at uint32's limit.
    */
    function _safeBlockTimestamp() internal view returns (uint32 _seconds) {
        unchecked {
            if (block.timestamp < type(uint32).max) {
                return uint32(block.timestamp);
            } else {
                return type(uint32).max;
            }
        }
    }

    /**
        @notice If an address is a contract past construction.
        @dev Returns FALSE if accessed in a constructor.
    */
    function _isContractPastConstruction(address account) internal view returns (bool) {
        return account.code.length > 0;
    }
    
    /**
        @notice Converts a uint256 number to a padded number string, up to 5-digit numbers.
    */
    function _toPaddedString(uint256 value) internal pure returns (string memory) {
        uint256 temp = value;
        uint256 digits;
        while (temp != 0) {
            digits++; // Counts how many digits the number has.
            temp /= 10; // If value is 12345, then temp will be 1234, 123, 12, 1, 0.
        }
        bytes memory padding;
        for (uint256 i; i < 5 - digits; i++) {
            padding = abi.encodePacked(padding, "0"); // Create padding.
        }
        bytes memory buffer = new bytes(digits);
        while (value != 0) {
            buffer[--digits] = bytes1(uint8(48 + value % 10)); // Convert.
            value /= 10; // Same thing as temp above.
        }
        return string(abi.encodePacked(string(padding), string(buffer))); // Merge padding and number.
    }

    /**
        @notice Interacts with another contract and/or transfers ETH.
        @param account Address to interact with.
        @param amount Amount of ETH to transfer.
        @param data Data to send to the contract.
    */
    function _interaction(address account, uint256 amount, bytes memory data) internal {
        if (amount > address(this).balance) revert INSUFFICIENT_CONTRACT_BALANCE(); // Check balance.
        (bool success, 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
                );
            }
        }
    }