contract NtentArtX is ERC721A {
    using Ints for uint256;
    using Strings for string;

    event Mint(
        uint256 indexed _startTokenId,
        uint256 indexed _endTokenId,
        uint256 indexed _projectId
    );

    event TokenBurned(address indexed _tokenOwner, uint256 indexed _tokenId);

    struct Project {
        string name;
        string artist;
        string description;
        string website;
        string license;
        address purchaseContract;
        address dataContract;
        address tokenUriContract;
        address transferContract;
        bool acceptsMintPass;
        uint256 mintPassProjectId;
        bool dynamic;
        string projectBaseURI;
        string projectBaseIpfsURI;
        uint256 invocations;
        uint256 maxInvocations;
        string scriptJSON;
        mapping(uint256 => string) scripts;
        uint256 scriptCount;
        string ipfsHash;
        bool useHashString;
        bool useIpfs;
        bool active;
        bool locked;
        bool paused;
    }

    uint256 constant ONE_MILLION = 1_000_000;
    mapping(uint256 => Project) projects;

    //All financial functions are stripped from struct for visibility
    mapping(uint256 => address) public projectIdToArtistAddress;
    mapping(uint256 => uint256) public projectIdToPricePerTokenInWei;

    address public ntentAddress;
    uint256 public ntentPercentage = 10;

    mapping(uint256 => string) public staticIpfsImageLink;

    address public admin;
    mapping(address => bool) public isRainbowlisted;
    mapping(address => bool) public isMintRainbowlisted;

    uint256 public nextProjectId = 3;
    uint256 public startProjectId = 3;

    uint256 private _royaltyBps;
    address payable private _royaltyRecipient;
    bytes4 private constant _INTERFACE_ID_ROYALTIES_CREATORCORE = 0xbb3bafd6;
    bytes4 private constant _INTERFACE_ID_ROYALTIES_EIP2981 = 0x2a55205a;
    bytes4 private constant _INTERFACE_ID_ROYALTIES_RARIBLE = 0xb7799584;

    mapping(address => bool) proxyToApproved;

    modifier onlyValidTokenId(uint256 _tokenId) {
        require(_exists(_tokenId), "Token ID not exists");
        _;
    }

    modifier onlyUnlocked(uint256 _projectId) {
        require(!projects[_projectId].locked, "Only if unlocked");
        _;
    }

    modifier onlyArtist(uint256 _projectId) {
        require(
            msg.sender == projectIdToArtistAddress[_projectId],
            "Only artist"
        );
        _;
    }

    modifier onlyAdmin() {
        require(msg.sender == admin, "Only admin");
        _;
    }

    modifier onlyRainbowlisted() {
        require(isRainbowlisted[msg.sender], "Only Rainbowlisted");
        _;
    }

    modifier onlyArtistOrRainbowlisted(uint256 _projectId) {
        require(
            isRainbowlisted[msg.sender] ||
                msg.sender == projectIdToArtistAddress[_projectId],
            "Only artist or Rainbowlisted"
        );
        _;
    }

    constructor(string memory _tokenName, string memory _tokenSymbol)
        ERC721A(_tokenName, _tokenSymbol)
    {
        admin = msg.sender;
        isRainbowlisted[msg.sender] = true;
        ntentAddress = msg.sender;
    }

    function supportsInterface(bytes4 interfaceId)
        public
        view
        virtual
        override(ERC721A)
        returns (bool)
    {
        return
            interfaceId == type(IERC721).interfaceId ||
            ERC721A.supportsInterface(interfaceId) ||
            interfaceId == type(IERC721Metadata).interfaceId ||
            interfaceId == type(IERC721Enumerable).interfaceId ||
            interfaceId == _INTERFACE_ID_ROYALTIES_CREATORCORE ||
            interfaceId == _INTERFACE_ID_ROYALTIES_EIP2981 ||
            interfaceId == _INTERFACE_ID_ROYALTIES_RARIBLE;
    }

    function _exists(uint256 tokenId) internal view override returns (bool) {
        uint256 projectId = tokenIdToProjectId(tokenId);
        return
            (tokenId <
                ((projectId * ONE_MILLION) +
                    projects[projectId].invocations)) &&
            (tokenId >= (projectId * ONE_MILLION) && !isBurned(tokenId));
    }

    function mint(
        address _to,
        uint256 _projectId,
        uint256 quantity,
        address _by
    ) external returns (uint256 _tokenId) {
        require(
            isMintRainbowlisted[msg.sender],
            "Must mint from Rainbowlisted minter"
        );
        require(
            projects[_projectId].invocations + quantity <=
                projects[_projectId].maxInvocations,
            "Exceeds max invocations"
        );
        require(
            projects[_projectId].active ||
                _by == projectIdToArtistAddress[_projectId],
            "Proj must exist and be active"
        );
        require(
            !projects[_projectId].paused ||
                _by == projectIdToArtistAddress[_projectId],
            "Purchases are paused"
        );

        uint256 tokenId = _mintTokens(_to, _projectId, quantity);

        return tokenId;
    }

    function _mintTokens(
        address _to,
        uint256 _projectId,
        uint256 quantity
    ) internal returns (uint256 _tokenId) {
        uint256 nextStartTokenId = (_projectId * ONE_MILLION) +
            projects[_projectId].invocations;

        projects[_projectId].invocations =
            projects[_projectId].invocations +
            quantity;

        _safeMint(_to, nextStartTokenId, quantity);

        emit Mint(nextStartTokenId, nextStartTokenId + quantity, _projectId);

        return nextStartTokenId;
    }

    function burn(address ownerAddress, uint256 tokenId)
        external
        returns (uint256 _tokenId)
    {
        require(
            isMintRainbowlisted[msg.sender],
            "Must burn from Rainbowlisted minter"
        );
        _burn(tokenId);
        emit TokenBurned(ownerAddress, tokenId);
        return tokenId;
    }

    function updateNtentAddress(address _ntentAddress) public onlyAdmin {
        ntentAddress = _ntentAddress;
    }

    function updateNtentPercentage(uint256 _ntentPercentage) public onlyAdmin {
        require(_ntentPercentage <= 50, "Max of 50%");
        ntentPercentage = _ntentPercentage;
    }

    function addRainbowlisted(address _address) public onlyAdmin {
        isRainbowlisted[_address] = true;
    }

    function removeRainbowlisted(address _address) public onlyAdmin {
        isRainbowlisted[_address] = false;
    }

    function addMintRainbowlisted(address _address) public onlyAdmin {
        isMintRainbowlisted[_address] = true;
    }

    function removeMintRainbowlisted(address _address) public onlyAdmin {
        isMintRainbowlisted[_address] = false;
    }

    function getPricePerTokenInWei(uint256 _projectId)
        public
        view
        returns (uint256 price)
    {
        return projectIdToPricePerTokenInWei[_projectId];
    }

    function toggleProjectIsLocked(uint256 _projectId)
        public
        onlyRainbowlisted
        onlyUnlocked(_projectId)
    {
        projects[_projectId].locked = true;
    }

    function toggleProjectIsActive(uint256 _projectId)
        public
        onlyRainbowlisted
    {
        projects[_projectId].active = !projects[_projectId].active;
    }

    function updateProjectArtistAddress(
        uint256 _projectId,
        address _artistAddress
    ) public onlyArtistOrRainbowlisted(_projectId) {
        projectIdToArtistAddress[_projectId] = _artistAddress;
    }

    function toggleProjectIsPaused(uint256 _projectId)
        public
        onlyArtistOrRainbowlisted(_projectId)
    {
        projects[_projectId].paused = !projects[_projectId].paused;
    }

    function addProject(
        string memory _projectName,
        address _artistAddress,
        uint256 _pricePerTokenInWei,
        address _purchaseContract,
        bool _acceptsMintPass,
        uint256 _mintPassProjectId,
        bool _dynamic
    ) public onlyRainbowlisted {
        uint256 projectId = nextProjectId;
        projectIdToArtistAddress[projectId] = _artistAddress;
        projects[projectId].name = _projectName;
        projects[projectId].purchaseContract = _purchaseContract;
        projects[projectId].acceptsMintPass = _acceptsMintPass;
        projects[projectId].mintPassProjectId = _mintPassProjectId;
        projectIdToPricePerTokenInWei[projectId] = _pricePerTokenInWei;
        projects[projectId].paused = true;
        projects[projectId].dynamic = _dynamic;
        projects[projectId].maxInvocations = ONE_MILLION;
        if (!_dynamic) {
            projects[projectId].useHashString = false;
        } else {
            projects[projectId].useHashString = true;
        }
        nextProjectId = nextProjectId + 1;
    }

    function updateProjectPricePerTokenInWei(
        uint256 _projectId,
        uint256 _pricePerTokenInWei
    ) public onlyArtist(_projectId) {
        projectIdToPricePerTokenInWei[_projectId] = _pricePerTokenInWei;
    }

    function updateProjectName(uint256 _projectId, string memory _projectName)
        public
        onlyUnlocked(_projectId)
        onlyArtistOrRainbowlisted(_projectId)
    {
        projects[_projectId].name = _projectName;
    }

    function updateProjectArtistName(
        uint256 _projectId,
        string memory _projectArtistName
    ) public onlyUnlocked(_projectId) onlyArtistOrRainbowlisted(_projectId) {
        projects[_projectId].artist = _projectArtistName;
    }

    function updateProjectPurchaseContractInfo(
        uint256 _projectId,
        address _projectPurchaseContract,
        bool _acceptsMintPass,
        uint256 _mintPassProjectId
    ) public onlyUnlocked(_projectId) onlyRainbowlisted {
        projects[_projectId].purchaseContract = _projectPurchaseContract;
        projects[_projectId].acceptsMintPass = _acceptsMintPass;
        projects[_projectId].mintPassProjectId = _mintPassProjectId;
    }

    function updateProjectDataContractInfo(
        uint256 _projectId,
        address _projectDataContract
    ) public onlyUnlocked(_projectId) onlyRainbowlisted {
        projects[_projectId].dataContract = _projectDataContract;
    }

    function updateTransferContractInfo(
        uint256 _projectId,
        address _projectTransferContract
    ) public onlyUnlocked(_projectId) onlyRainbowlisted {
        projects[_projectId].transferContract = _projectTransferContract;
    }

    function updateProjectTokenUriContractInfo(
        uint256 _projectId,
        address _projectTokenUriContract
    ) public onlyUnlocked(_projectId) onlyRainbowlisted {
        projects[_projectId].tokenUriContract = _projectTokenUriContract;
    }

    function updateProjectDescription(
        uint256 _projectId,
        string memory _projectDescription
    ) public onlyArtist(_projectId) {
        projects[_projectId].description = _projectDescription;
    }

    function updateProjectWebsite(
        uint256 _projectId,
        string memory _projectWebsite
    ) public onlyArtist(_projectId) {
        projects[_projectId].website = _projectWebsite;
    }

    function updateProjectLicense(
        uint256 _projectId,
        string memory _projectLicense
    ) public onlyUnlocked(_projectId) onlyArtistOrRainbowlisted(_projectId) {
        projects[_projectId].license = _projectLicense;
    }

    function updateProjectMaxInvocations(
        uint256 _projectId,
        uint256 _maxInvocations
    ) public onlyArtist(_projectId) {
        require(
            (!projects[_projectId].locked ||
                _maxInvocations < projects[_projectId].maxInvocations),
            "Only if unlocked"
        );
        require(
            _maxInvocations > projects[_projectId].invocations,
            "Max invocations exceeds current"
        );
        require(_maxInvocations <= ONE_MILLION, "Cannot exceed 1000000");
        projects[_projectId].maxInvocations = _maxInvocations;
    }

    function toggleProjectUseHashString(uint256 _projectId)
        public
        onlyUnlocked(_projectId)
        onlyArtistOrRainbowlisted(_projectId)
    {
        require(
            projects[_projectId].invocations == 0,
            "Cannot modify after token is minted."
        );
        projects[_projectId].useHashString = !projects[_projectId]
            .useHashString;
    }

    function addProjectScript(uint256 _projectId, string memory _script)
        public
        onlyUnlocked(_projectId)
        onlyArtistOrRainbowlisted(_projectId)
    {
        projects[_projectId].scripts[
            projects[_projectId].scriptCount
        ] = _script;
        projects[_projectId].scriptCount = projects[_projectId].scriptCount + 1;
    }

    function updateProjectScript(
        uint256 _projectId,
        uint256 _scriptId,
        string memory _script
    ) public onlyUnlocked(_projectId) onlyArtistOrRainbowlisted(_projectId) {
        require(
            _scriptId < projects[_projectId].scriptCount,
            "scriptId out of range"
        );
        projects[_projectId].scripts[_scriptId] = _script;
    }

    function removeProjectLastScript(uint256 _projectId)
        public
        onlyUnlocked(_projectId)
        onlyArtistOrRainbowlisted(_projectId)
    {
        require(
            projects[_projectId].scriptCount > 0,
            "there are no scripts to remove"
        );
        delete projects[_projectId].scripts[
            projects[_projectId].scriptCount - 1
        ];
        projects[_projectId].scriptCount = projects[_projectId].scriptCount + 1;
    }

    function updateProjectScriptJSON(
        uint256 _projectId,
        string memory _projectScriptJSON
    ) public onlyUnlocked(_projectId) onlyArtistOrRainbowlisted(_projectId) {
        projects[_projectId].scriptJSON = _projectScriptJSON;
    }

    function updateProjectIpfsHash(uint256 _projectId, string memory _ipfsHash)
        public
        onlyUnlocked(_projectId)
        onlyArtistOrRainbowlisted(_projectId)
    {
        projects[_projectId].ipfsHash = _ipfsHash;
    }

    function updateProjectBaseURI(uint256 _projectId, string memory _newBaseURI)
        public
        onlyArtist(_projectId)
    {
        projects[_projectId].projectBaseURI = _newBaseURI;
    }

    function updateProjectBaseIpfsURI(
        uint256 _projectId,
        string memory _projectBaseIpfsURI
    ) public onlyArtist(_projectId) {
        projects[_projectId].projectBaseIpfsURI = _projectBaseIpfsURI;
    }

    function toggleProjectUseIpfsForStatic(uint256 _projectId)
        public
        onlyArtistOrRainbowlisted(_projectId)
    {
        require(
            !projects[_projectId].dynamic,
            "can only set static IPFS hash for static projects"
        );
        projects[_projectId].useIpfs = !projects[_projectId].useIpfs;
    }

    function toggleProjectIsDynamic(uint256 _projectId)
        public
        onlyUnlocked(_projectId)
        onlyArtistOrRainbowlisted(_projectId)
    {
        require(
            projects[_projectId].invocations == 0,
            "Can not switch after a token is minted."
        );
        if (projects[_projectId].dynamic) {
            projects[_projectId].useHashString = false;
        } else {
            projects[_projectId].useHashString = true;
        }
        projects[_projectId].dynamic = !projects[_projectId].dynamic;
    }

    function overrideTokenDynamicImageWithIpfsLink(
        uint256 _tokenId,
        string memory _ipfsHash
    ) public onlyArtistOrRainbowlisted(tokenIdToProjectId(_tokenId)) {
        staticIpfsImageLink[_tokenId] = _ipfsHash;
    }

    function clearTokenIpfsImageUri(uint256 _tokenId)
        public
        onlyArtistOrRainbowlisted(tokenIdToProjectId(_tokenId))
    {
        delete staticIpfsImageLink[tokenIdToProjectId(_tokenId)];
    }

    function projectDetails(uint256 _projectId)
        public
        view
        returns (
            string memory projectName,
            string memory artist,
            string memory description,
            string memory website,
            string memory license,
            bool dynamic
        )
    {
        projectName = projects[_projectId].name;
        artist = projects[_projectId].artist;
        description = projects[_projectId].description;
        website = projects[_projectId].website;
        license = projects[_projectId].license;
        dynamic = projects[_projectId].dynamic;
    }

    function projectTokenInfo(uint256 _projectId)
        public
        view
        returns (
            address artistAddress,
            uint256 pricePerTokenInWei,
            uint256 invocations,
            uint256 maxInvocations,
            bool active,
            address purchaseContract,
            address dataContract,
            address tokenUriContract,
            address transferContract,
            bool acceptsMintPass,
            uint256 mintPassProjectId
        )
    {
        artistAddress = projectIdToArtistAddress[_projectId];
        pricePerTokenInWei = projectIdToPricePerTokenInWei[_projectId];
        invocations = projects[_projectId].invocations;
        maxInvocations = projects[_projectId].maxInvocations;
        active = projects[_projectId].active;
        purchaseContract = projects[_projectId].purchaseContract;
        dataContract = projects[_projectId].dataContract;
        tokenUriContract = projects[_projectId].tokenUriContract;
        transferContract = projects[_projectId].transferContract;
        acceptsMintPass = projects[_projectId].acceptsMintPass;
        mintPassProjectId = projects[_projectId].mintPassProjectId;
    }

    function projectBurnedTokens(uint256 _projectId)
        public
        view
        returns (uint256[] memory)
    {
        uint256 burnedIndex;
        uint256 burnedTokenId;
        uint256 projectBurnedCount = projectBurnedTokensCount(_projectId);
        uint256[] memory burnedTokenIds = new uint256[](projectBurnedCount);

        uint256 burnedTokenLength = burnedTokens.length;
        for (uint256 i; i < burnedTokenLength; i++) {
            burnedTokenId = burnedTokens[i];
            if (tokenIdToProjectId(burnedTokenId) == _projectId) {
                burnedTokenIds[burnedIndex] = burnedTokenId;
            }
        }
        return burnedTokenIds;
    }

    function projectScriptInfo(uint256 _projectId)
        public
        view
        returns (
            string memory scriptJSON,
            uint256 scriptCount,
            bool useHashString,
            string memory ipfsHash,
            bool locked,
            bool paused
        )
    {
        scriptJSON = projects[_projectId].scriptJSON;
        scriptCount = projects[_projectId].scriptCount;
        useHashString = projects[_projectId].useHashString;
        ipfsHash = projects[_projectId].ipfsHash;
        locked = projects[_projectId].locked;
        paused = projects[_projectId].paused;
    }

    function projectScriptByIndex(uint256 _projectId, uint256 _index)
        public
        view
        returns (string memory)
    {
        return projects[_projectId].scripts[_index];
    }

    function projectURIInfo(uint256 _projectId)
        public
        view
        returns (
            string memory projectBaseURI,
            string memory projectBaseIpfsURI,
            bool useIpfs
        )
    {
        projectBaseURI = projects[_projectId].projectBaseURI;
        projectBaseIpfsURI = projects[_projectId].projectBaseIpfsURI;
        useIpfs = projects[_projectId].useIpfs;
    }

    function transferFrom(
        address _from,
        address _to,
        uint256 _tokenId
    ) public override {
        _transfer(_from, _to, _tokenId);
    }

    function _beforeTokenTransfers(
        address _from,
        address _to,
        uint256 _tokenId,
        uint256 _quantity
    ) internal view override {
        //check if custom transfer contract, if so, send call.
        if (
            projects[tokenIdToProjectId(_tokenId)].transferContract !=
            address(0)
        ) {
            NtentTransfer ntentTransfer = NtentTransfer(
                projects[tokenIdToProjectId(_tokenId)].transferContract
            );
            ntentTransfer.onTransfer(_from, _to, _tokenId, _quantity);
        }
    }

    function bulkTransfer(
        address[] calldata _addressList,
        uint256[] calldata _tokenList
    ) public onlyAdmin {
        uint256 addressLength = _addressList.length;
        uint256 tokenLength = _tokenList.length;
        require(addressLength > 0, "Address quantity must greater than zero");
        require(
            addressLength == tokenLength,
            "Token quantity must equal address quantity"
        );

        for (uint256 i; i < addressLength; i++) {
            address _toAddress = _addressList[i];
            uint256 _tokenId = _tokenList[i];
            safeTransferFrom(msg.sender, _toAddress, _tokenId);
        }
    }

    function projectShowAllTokens(uint256 _projectId)
        public
        view
        returns (uint256[] memory)
    {
        return projectIdToTokenIds(_projectId);
    }

    function projectIdToTokenIds(uint256 _projectId)
        public
        view
        returns (uint256[] memory)
    {
        uint256 tId;
        uint256 tokenId;
        uint256 projTokenLength = projects[_projectId].invocations;
        uint256 projectBurnedCount = projectBurnedTokensCount(_projectId);
        uint256[] memory projectTokens = new uint256[](
            projTokenLength - projectBurnedCount
        );

        for (uint256 i; i < projTokenLength; i++) {
            tokenId = (_projectId * ONE_MILLION) + i;
            if (isBurned(tokenId) == false) {
                projectTokens[tId] = tokenId;
                tId++;
            }
        }
        return projectTokens;
    }

    function projectBurnedTokensCount(uint256 _projectId)
        public
        view
        returns (uint256)
    {
        uint256 tokenId;
        uint256 count;
        uint256 burnedTokenLength = burnedTokens.length;
        for (uint256 i; i < burnedTokenLength; i++) {
            tokenId = burnedTokens[i];
            if (tokenIdToProjectId(tokenId) == _projectId) {
                count++;
            }
        }
        return count;
    }

    //example input : 1000004
    function tokenIdToProjectId(uint256 _tokenId)
        public
        pure
        returns (uint256)
    {
        //convert to string and reverse
        string memory reversed = _tokenId.toString().reverse();
        //chop off reversed project id from end, reverse back
        string memory strProjectId = reversed
            .substring(6, reversed.stringLength())
            .reverse();

        return strProjectId.str2int();
    }

    function tokensOfOwner(address owner)
        public
        view
        returns (uint256[] memory)
    {
        uint256 ownerBalance = balanceOf(owner);
        uint256 foundCount;
        address currOwnershipAddr;

        uint256[] memory ownerTokenIds = new uint256[](ownerBalance);

        // Counter overflow is impossible as the loop breaks when uint256 i is equal to another uint256 numMintedSoFar.
        unchecked {
            for (uint256 i = startProjectId; i < nextProjectId; i++) {
                uint256[] memory projectTokens = projectIdToTokenIds(i);

                for (uint256 ii; ii < projectTokens.length; ii++) {
                    uint256 tokenId = projectTokens[ii];

                    TokenOwnership memory ownership = _ownerships[tokenId];
                    if (ownership.addr != address(0)) {
                        currOwnershipAddr = ownership.addr;
                    }
                    if (currOwnershipAddr == owner) {
                        ownerTokenIds[foundCount] = tokenId;
                        foundCount++;
                    }
                    if (foundCount == ownerBalance) {
                        return ownerTokenIds;
                    }
                }
            }
        }

        revert UnableGetTokenOwnerByIndex();
    }

    function tokenURI(uint256 _tokenId)
        public
        view
        override
        onlyValidTokenId(_tokenId)
        returns (string memory)
    {
        //check if custom tokenUri contract, if so, use that.
        if (
            projects[tokenIdToProjectId(_tokenId)].tokenUriContract !=
            address(0)
        ) {
            NtentTokenUri ntentTokenUri = NtentTokenUri(
                projects[tokenIdToProjectId(_tokenId)].tokenUriContract
            );
            //if returned tokenUri is not empty, else continue
            string memory tUri = ntentTokenUri.tokenUri(_tokenId);
            if(bytes(tUri).length > 0)
                return tUri;
        }

        //check if tokenId has a specified image link
        if (bytes(staticIpfsImageLink[_tokenId]).length > 0) {
            return
                projects[tokenIdToProjectId(_tokenId)]
                    .projectBaseIpfsURI
                    .concatenate(staticIpfsImageLink[_tokenId]);
        }

        //check if the project has a single overall token Uri (mintpass, etc)
        if (
            !projects[tokenIdToProjectId(_tokenId)].dynamic &&
            projects[tokenIdToProjectId(_tokenId)].useIpfs
        ) {
            return
                projects[tokenIdToProjectId(_tokenId)]
                    .projectBaseIpfsURI
                    .concatenate(
                        projects[tokenIdToProjectId(_tokenId)].ipfsHash
                    );
        }

        return
            projects[tokenIdToProjectId(_tokenId)].projectBaseURI.concatenate(
                _tokenId.toString()
            );
    }

    /**
     * Royalties
     */

    function updateRoyalties(address payable recipient, uint256 bps)
        external
        onlyRainbowlisted
    {
        _royaltyRecipient = recipient;
        _royaltyBps = bps;
    }

    function getRoyalties(uint256)
        external
        view
        returns (address payable[] memory recipients, uint256[] memory bps)
    {
        if (_royaltyRecipient != address(0x0)) {
            recipients = new address payable[](1);
            recipients[0] = _royaltyRecipient;
            bps = new uint256[](1);
            bps[0] = _royaltyBps;
        }
        return (recipients, bps);
    }

    function getFeeRecipients(uint256)
        external
        view
        returns (address payable[] memory recipients)
    {
        if (_royaltyRecipient != address(0x0)) {
            recipients = new address payable[](1);
            recipients[0] = _royaltyRecipient;
        }
        return recipients;
    }

    function getFeeBps(uint256) external view returns (uint256[] memory bps) {
        if (_royaltyRecipient != address(0x0)) {
            bps = new uint256[](1);
            bps[0] = _royaltyBps;
        }
        return bps;
    }

    function royaltyInfo(uint256, uint256 value)
        external
        view
        returns (address, uint256)
    {
        return (_royaltyRecipient, (value * _royaltyBps) / 10000);
    }

    /**
     * Proxy 
     */

    function flipProxyState(address proxyAddress) public onlyRainbowlisted{
        proxyToApproved[proxyAddress] = !proxyToApproved[proxyAddress];
    }

    //proxy registration
    function isApprovedForAll(address _owner, address _operator) public view override returns (bool){
        if(proxyToApproved[_operator]) return true;
        return super.isApprovedForAll(_owner, _operator);
    }

    /**
     * @dev See {IERC721Enumerable-tokenByIndex}.
     * This read function is O(totalSupply). If calling from a separate contract, be sure to test gas first.
     * It may also degrade with extremely large collection sizes (e.g >> 10000), test for your use case.
     */
    function tokenByIndex(uint256 index) public view override returns (uint256) {

        uint256 tokenIdsIdx;

                // Counter overflow is impossible as the loop breaks when uint256 i is equal to another uint256 numMintedSoFar.
        unchecked {
            for (uint256 i = startProjectId; i < nextProjectId; i++) {
                uint256[] memory projectTokens = projectIdToTokenIds(i);

                for (uint256 ii; ii < projectTokens.length; ii++) {
                    uint256 tokenId = projectTokens[ii];

                    TokenOwnership memory ownership = _ownerships[tokenId];
                    if (!ownership.burned) {
                        if (tokenIdsIdx == index) {
                            return tokenId;
                        }
                        tokenIdsIdx++;
                    }
                }
            }
        }

        revert UnableGetTokenOwnerByIndex();
    }

    function tokenOfOwnerByIndex(address owner, uint256 index) public override view returns (uint256) {
        require(index < balanceOf(owner), "ERC721Enumerable: owner index out of bounds");
        return tokensOfOwner(owner)[index];
    }
}
function tokenIdToProjectId(uint256 _tokenId)
    public
    pure
    returns (uint256)
{
    //convert to string and reverse
    string memory reversed = _tokenId.toString().reverse();
    //chop off reversed project id from end, reverse back
    string memory strProjectId = reversed
        .substring(6, reversed.stringLength())
        .reverse();

    return strProjectId.str2int();
}

//example input : 1000004