contract NeoFortuneTigers is ERC721A, ERC721ABurnable, Ownable {
    
    using Strings for uint256;

    /// @dev The amount of Ether in each token.
    mapping(uint256 => uint256) internal _funds;

    /// @dev The messages of the tokens.
    mapping(uint256 => string) internal _messages;

    /// @dev The revealed URI for token metadata.
    string internal _revealedURI;

    /// @dev The revealed URI postfix for token metadata.
    string internal _revealedURIPostfix;

    /// @dev The total amount of tokens that can be minted.
    uint16 public maxTokens = 8;

    /// @dev Whether the metadata is locked.
    bool public metadataLocked;

    /// @dev For gas optimization.
    bool private _reentrancyGuard;

    constructor() ERC721A("Neo Fortune Tigers", "NFT") {}

    modifier nonReentrant() {
        if (_reentrancyGuard) revert();
        _reentrancyGuard = true;
        _;
        _reentrancyGuard = false;
    }

    /// @dev Locks all metadata. WARNING: Irreversible!
    function lockMetadata() external onlyOwner {
        if (bytes(_revealedURI).length == 0) revert();
        metadataLocked = true;
    }

    /// @dev Sets base URI for all token IDs. 
    function setRevealedURI(string calldata uri, string calldata postfix) 
    external onlyOwner {
        if (metadataLocked) revert();
        _revealedURI = uri;
        _revealedURIPostfix = postfix;
    }

    /// @dev Returns the total number minted.
    function totalMinted() public view returns (uint256) {
        return _currentIndex - _startTokenId();
    }

    /// @dev Returns the total number burned.
    function totalBurned() public view returns (uint256) {
        return _burnCounter;
    }

    /// @dev Mints a batch.
    function _mintBatch(address to, uint256 quantity) internal {
        unchecked {
            if (totalMinted() + quantity > maxTokens) revert();
        }
        _mint(to, quantity, "", false);
    }

    /// @dev Mint tokens for the creator.
    function selfMint(uint256 quantity) external onlyOwner {
        _mintBatch(msg.sender, quantity);
    }

    /// @dev Force mint for the addresses. 
    function forceMint(address[] calldata to) external onlyOwner {
        unchecked {
            for (uint256 i; i < to.length; ++i) {
                _mintBatch(to[i], 1);
            }    
        }
    }

    /// @dev Deposit Ether into the NFT.
    function depositFunds(uint256 id) external payable {
        _funds[id] += msg.value;
    }

    /// @dev Withdraw Ether from the NFT.
    function withdrawFunds(uint256 id) external nonReentrant {
        address sender = msg.sender;
        require(ownerOf(id) == sender, "Unauthorized!");
        uint256 amount = _funds[id];
        require(amount != 0, "No funds.");
        _funds[id] = 0;
        payable(sender).transfer(amount);
    }

    /// @dev Returns the amount of Ether in the NFTs.
    function fundsOf(uint256[] calldata ids) external view returns (uint256[] memory) {
        unchecked {
            uint256 n = ids.length;
            uint256[] memory a = new uint256[](n);
            for (uint256 i; i < n; ++i) {
                a[i] = _funds[ids[i]];
            }
            return a;    
        }
    }

    /// @dev Sets the message of the NFT.
    function setMessage(uint256 id, string calldata value) external {
        address sender = msg.sender;
        require(ownerOf(id) == sender, "Unauthorized!");
        _messages[id] = value;
    }

    /// @dev Returns messages in the NFTs.
    function messagesOf(uint256[] calldata ids) external view returns (string[] memory) {
        unchecked {
            uint256 n = ids.length;
            string[] memory a = new string[](n);
            for (uint256 i; i < n; ++i) {
                a[i] = _messages[ids[i]];
            }
            return a;    
        }
    }
    
    /// @dev Returns the tokenURI for the token.
    function tokenURI(uint256 id) public view override returns (string memory) {
        require(_exists(id), "Token not found.");
        return string(abi.encodePacked(_revealedURI, id.toString(), _revealedURIPostfix));    
    }

    /// @dev Returns the tokenIds of the address.
    function tokensOfOwner(address owner) external view returns (uint256[] memory) {
        unchecked {
            uint256[] memory a = new uint256[](balanceOf(owner)); 
            uint256 end = _currentIndex;
            uint256 tokenIdsIdx;
            address currOwnershipAddr;
            for (uint256 i; i < end; i++) {
                TokenOwnership memory ownership = _ownerships[i];
                if (ownership.burned) {
                    continue;
                }
                if (ownership.addr != address(0)) {
                    currOwnershipAddr = ownership.addr;
                }
                if (currOwnershipAddr == owner) {
                    a[tokenIdsIdx++] = i;
                }
            }
            return a;    
        }
    }

    /// @dev Returns the number of tokens minted by the address.
    function numberMinted(address owner) external view returns (uint256) {
        return _numberMinted(owner);
    }

    /// @dev Returns the number of tokens burned by the address.
    function numberBurned(address owner) external view returns (uint256) {
        return _numberBurned(owner);
    }

    /// @dev Returns the raw start timestamp of ownership of the token.
    function rawStartTimestampOf(uint256 id, bool burned) 
    public view returns (uint256) {
        TokenOwnership memory ownership = _ownerships[id];
        if (burned) {
            return ownership.burned ? ownership.startTimestamp : 0;
        } else {
            return ownership.burned ? 0 : ownershipOf(id).startTimestamp;
        }
    }

    /// @dev Returns the raw owner of the tokens.
    function rawOwnerOf(uint256 id, bool burned) 
    public view returns (address) {
        TokenOwnership memory ownership = _ownerships[id];
        if (burned) {
            return ownership.burned ? ownership.addr : address(0);
        } else {
            return ownership.burned ? address(0) : ownerOf(id);
        }
    }

    /// @dev Returns the raw start timestamp of ownership of the tokens.
    function rawStartTimestampsOf(uint256[] calldata ids, bool burned) 
    external view returns (uint256[] memory) {
        unchecked {
            uint256 n = ids.length;
            uint256[] memory a = new uint256[](n);
            for (uint256 i; i < n; ++i) {
                a[i] = rawStartTimestampOf(ids[i], burned);
            }
            return a;           
        }
    }

    /// @dev Returns the raw owners of the tokens.
    function rawOwnersOf(uint256[] calldata ids, bool burned) 
    external view returns (address[] memory) {
        unchecked {
            uint256 n = ids.length;
            address[] memory a = new address[](n);
            for (uint256 i; i < n; ++i) {            
                a[i] = rawOwnerOf(ids[i], burned);   
            }
            return a;
        }
    }

    /// @dev See {IERC721-isApprovedForAll}.
    function isApprovedForAll(address owner, address operator) public view override returns (bool) {
        // OpenSea proxy registry addresses:
        // - Mainnet: 0xa5409ec958C83C3f309868babACA7c86DCB077c1
        // - Rinkeby: 0xF57B2c51dED3A29e6891aba85459d600256Cf317        
        // Whitelist the proxy contract.
        return (operator == 0xa5409ec958C83C3f309868babACA7c86DCB077c1 || 
            ERC721A.isApprovedForAll(owner, operator));
    } 

    /// @dev See {ERC721A-_startTokenId}.
    function _startTokenId() internal view virtual override returns (uint256) {
        return 1;
    }
}
// import "@openzeppelin/contracts/utils/Strings.sol"; 
// import "@openzeppelin/contracts/access/Ownable.sol";
function messagesOf(uint256[] calldata ids) external view returns (string[] memory) {
    unchecked {
        uint256 n = ids.length;
        string[] memory a = new string[](n);
        for (uint256 i; i < n; ++i) {
            a[i] = _messages[ids[i]];
        }
        return a;    
    }
}

/// @dev Returns messages in the NFTs.