// Contract: DigitalArtists
// Target function: function toggleCollectionAuthorization(
1Enumerable-tokenOfOwnerByIndex}.
     * @param owner_ owner address
     * @param idx_ index of token
     */
    function tokenOfOwnerByIndex(address owner_, uint256 idx_)
        public
        view
        returns (uint256)
    {
        require(idx_ < balanceOf(owner_), "Index out of bounds");
        return _ownedTokens[owner_][idx_];
    }

    /**
     * @notice Returns the total amount of tokens stored by the contract.
     * @dev See {IERC721Enumerable-totalSupply}.
     */
    function totalSupply() public view returns (uint256) {
        return _allTokens.length;
    }

    /**
     * @notice Returns the total amount of tokens in collection.
     * @param collectionId_ checked collection ID.
     */
    function totalSupplyByCollectionId(uint256 collectionId_)
        public
        view
        returns (uint256)
    {
        unchecked {
            return
                _collections[collectionId_].tokenId -
                _collections[collectionId_].tokenIdFrom;
        }
    }

    /**
     * @notice Returns a token ID at a given `index` of all the tokens stored by the contract.
     * @dev See {IERC721Enumerable-tokenByIndex}.
     * @param idx_ desired index
     */
    function tokenByIndex(uint256 idx_) public view returns (uint256) {
        require(idx_ < totalSupply(), "Index out of bounds");
        return _allTokens[idx_];
    }

    function name() public view returns (string memory) {
        return _name;
    }

    function symbol() public view returns (string memory) {
        return _symbol;
    }

    /**
     * @notice Returns address and royatly amount from given token id and price.
     * Different collections could have different royatlies.
     * @param tokenId_ token id
     * @param price_ price against royatly is calculated
     */
    function royaltyInfo(uint256 tokenId_, uint256 price_)
        public
        view
        returns (address, uint256)
    {
        require(_exists(tokenId_), "Nonexistent token");
        uint256 collectionId = _tokenIdToCollectionId[tokenId_];

        unchecked {
            return (
                _collections[collectionId].royaltyAddr == address(0)
                    ? _collections[collectionId].artistAddr
                    : _collections[collectionId].royaltyAddr,
                (price_ * _collections[collectionId].royaltyBasis) / 10000
            );
        }
    }

    /**
     * @notice Returns token uri from given token id.
     * @param tokenId_ token id
     */
    function tokenURI(uint256 tokenId_) public view returns (string memory) {
        require(_exists(tokenId_), "Nonexistent token");

        if (bytes(_tokenUri[tokenId_]).length > 0) {
            // Explicit token uri
            return _tokenUri[tokenId_];
        }

        string memory baseUri = bytes(
            _collections[_tokenIdToCollectionId[tokenId_]].baseUri
        ).length > 0
            ? _collections[_tokenIdToCollectionId[tokenId_]].baseUri
            : _baseUri;

        return string(abi.encodePacked(baseUri, Strings.toString(tokenId_)));
    }

    /**
     * @notice Allows collection's artist or contract owner to explicitly set URI for token.
     * @param tokenId_ updated token id
     * @param uri_ explicit token uri
     */
    function updateTokenUri(uint256 tokenId_, string memory uri_)
        public
        onlyOwner
    {
        _tokenUri[tokenId_] = uri_;
    }

    /**
     * @notice Returns the count of the owned tokens by address.
     * @param owner_ checked addess
     */
    function balanceOf(address owner_) public view returns (uint256) {
        return _balances[owner_];
    }

    /**
     * @notice Returns the address of token id owner or zero address if non-existing token id is specified.
     * @param tokenId_ checked token id
     */
    function ownerOf(uint256 tokenId_) public view returns (address) {
        return _owners[tokenId_];
    }

    /**
     * @notice Returns boolean value indicating if the checked interface is supported.
     * @param interfaceId_ checked interface id
     */
    function supportsInterface(bytes4 interfaceId_) public pure returns (bool) {
        return
            interfaceId_ == type(IERC165).interfaceId ||
            interfaceId_ == type(IERC2981).interfaceId ||
            interfaceId_ == type(IERC721).interfaceId ||
            interfaceId_ == type(IERC721Metadata).interfaceId ||
            interfaceId_ == type(IERC721Enumerable).interfaceId;
    }

    /**
     * @notice Allows contract owner to update default percentages in case of public mint is organized.
     * @param percentArtist_ artist percentage
     * @param percentPlatform_ platform percentage
     */
    function updateDefaultPercentages(
        uint16 percentArtist_,
        uint16 percentPlatform_
    ) public onlyOwner {
        unchecked {
            require(
                percentArtist_ + percentPlatform_ == 10000,
                "Invalid percentage"
            );
        }

        _percentArtist = percentArtist_;
        _percentPlatform = percentPlatform_;
    }

    /**
     * @notice Allows contract owner to specify explicit split percentage for collection if public mint is organized.
     * @param collectionId_ collection ID
     * @param percentArtist_ artist percentage
     * @param percentPlatform_ platform percentage
     */
    function updateCollectionPercentages(
        uint256 collectionId_,
        uint16 percentArtist_,
        uint16 percentPlatform_
    ) public onlyOwner {
        if (percentArtist_ == 1) {
            // Artist percetage is explicitly set to 0, 1 means 0 percentage, see withdraw function
            _collections[collectionId_].percentage[0] = 1;
            _collections[collectionId_].percentage[1] = 10000;
        } else if (percentPlatform_ == 1) {
            // Platform percetage is explicitly set to 0, 1 means 0 percentage, see withdraw function
            _collections[collectionId_].percentage[0] = 10000;
            _collections[collectionId_].percentage[1] = 1;
        } else {
            unchecked {
                require(
                    percentArtist_ + percentPlatform_ == 10000,
                    "Invalid percentage"
                );
            }
            _collections[collectionId_].percentage[0] = percentArtist_;
            _collections[collectionId_].percentage[1] = percentPlatform_;
        }
    }

    /**
     * @notice Returns default split percentages - artists and platform respectively.
     */
    function defaultPercentages() public view returns (uint16, uint16) {
        return (_percentArtist, _percentPlatform);
    }

    /**
     * @notice Allows contract owner to specify default base URI.
     * @param uri_ new base URI
     */
    function setBaseUri(string memory uri_) public onlyOwner {
        _baseUri = uri_;
    }

    /**
     * @notice Returns the default base URI
     */
    function defaultBaseUri() public view returns (string memory) {
        return _baseUri;
    }

    /**
     * @notice Allows contract owner to change createCollection access level.
     * @param level_ access level - public or known artists only
     */
    function updateCreateCollectionAccess(uint8 level_) public onlyOwner {
        // Everything greater than 0 will allow public access to createCollection
        // otherwise onlyOwner and _knownArtists have access
        _isCreateCollectionPublic = level_;
    }

    /**
     * @notice Returns a boolean flag indicating if createColletion function is publicly accessible.
     */
    function isCreateCollectionPublic() public view returns (bool) {
        return _isCreateCollectionPublic > 0;
    }

    /**
     * @notice Allows contract owner to add addresses to the known artists white list.
     * @param addresses_ array of addresses
     */
    function addKnownArtists(address[] memory addresses_) public onlyOwner {
        for (uint256 i = 0; i < addresses_.length; i++) {
            _knownArtists[addresses_[i]] = true;
        }
    }

    /**
     * @notice Allows contract owner to remove addresses from the known artists white list.
     * @param addresses_ array of addresses
     */
    function removeKnownArtists(address[] memory addresses_) public onlyOwner {
        for (uint256 i = 0; i < addresses_.length; i++) {
            _knownArtists[addresses_[i]] = false;
        }
    }

    /**
     * @notice Returns boolean flag indicating whether address is a known artist.
     * @param addr_ checked address
     */
    function isKnownArtist(address addr_) public view returns (bool) {
        return _knownArtists[addr_];
    }

    /**
     * @notice Allows contract owner to set base URI for specific collection.
     * @param collectionId_ collection ID
     * @param baseUri_ base URI
     */
    function setCollectionBaseUri(uint256 collectionId_, string memory baseUri_)
        public
        onlyOwner
    {
        require(
            _collections[collectionId_].baseUriLocked == 0,
            "baseURI locked"
        );
        _collections[collectionId_].baseUri = baseUri_;
    }

    /**
     * @notice Allows contract owner to lock base URI from being changed. Once locked it cannot be unlocked.
     * @param collectionId_ collection ID
     */
    function lockCollectionBaseUri(uint256 collectionId_) public onlyOwner {
        _collections[collectionId_].baseUriLocked = 1;
    }

    /**
     * @notice Returns collection's base URI, if there is any.
     * @param collectionId_ collection ID
     */
    function collectionBaseUri(uint256 collectionId_)
        public
        view
        virtual
        returns (string memory)
    {
        return _collections[collectionId_].baseUri;
    }

    /**
     * @notice Returns boolean flag indicating whether collection's base URI is locked
     * @param collectionId_ collection ID
     */
    function isCollectionBaseUriLocked(uint256 collectionId_)
        public
        view
        virtual
        returns (bool)
    {
        return _collections[collectionId_].baseUriLocked > 0;
    }

    /**
     * @notice Allows collection authorized or contract owner to update max supply and premint max supply
     * @param collectionId_ collection ID
     * @param shouldUpdateMaxSupply_ flag indicating whether max supply value should be updated
     * @param maxSupply_ new max supply
     * @param shouldUpdatePremintMaxSupply_ flag indicating whether premint max supply value should be updated
     * @param premintMaxSupply_ new premint max supply
     */
    function updateCollectionSupplies(
        uint256 collectionId_,
        uint8 shouldUpdateMaxSupply_,
        uint32 maxSupply_,
        uint8 shouldUpdatePremintMaxSupply_,
        uint32 premintMaxSupply_
    ) public onlyAuthorized(collectionId_) {
        if (shouldUpdateMaxSupply_ != 0) {
            unchecked {
                require(
                    totalSupplyByCollectionId(collectionId_) <= maxSupply_ &&
                        _collections[collectionId_].maxSupply + 10000 >
                        maxSupply_,
                    "Invalid max supply"
                );
            }

            _collections[collectionId_].maxSupply = maxSupply_;
        }

        if (shouldUpdatePremintMaxSupply_ != 0) {
            unchecked {
                require(
                    totalSupplyByCollectionId(collectionId_) <=
                        premintMaxSupply_ &&
                        _collections[collectionId_].maxSupply + 10000 >
                        premintMaxSupply_,
                    "Invalid premint max supply"
                );
            }
            _collections[collectionId_].premintMaxSupply = premintMaxSupply_;
        }
    }

    /**
     * @notice Returns collection supplies - max supply, premint max supply and current total supply
     * @param collectionId_ collection ID
     */
    function collectionSupplies(uint256 collectionId_)
        public
        view
        returns (
            uint32,
            uint32,
            uint256
        )
    {
        return (
            _collections[collectionId_].maxSupply,
            _collections[collectionId_].premintMaxSupply,
            totalSupplyByCollectionId(collectionId_)
        );
    }

    /**
     * @notice Allows collection authorized or contract owner to change state
     * @param collectionId_ collection ID
     * @param state_ state - 1(premint), 2(public mint), everything else is considered not active
     */
    function updateCollectionState(uint256 collectionId_, uint8 state_)
        public
        onlyAuthorized(collectionId_)
    {
        uint8 oldState = _collections[collectionId_].state;
        _collections[collectionId_].state = state_;
        emit CollectionStateUpdated(collectionId_, state_, oldState);
    }

    /**
     * @notice Returns collection state - 1(premint), 2(public mint), everything else is considered not active
     * @param collectionId_ collection ID
     */
    function collectionState(uint256 collectionId_)
        public
        view
        returns (uint8)
    {
        return _collections[collectionId_].state;
    }

    /**
     * @notice Returns how many NFTs give address minted on premint and public mint for specific collection
     * @param collectionId_ collection ID
     * @param addr_ checked address
     */
    function collectionAddressMintedStats(uint256 collectionId_, address addr_)
        public
        view
        returns (uint32, uint32)
    {
        return (
            _collections[collectionId_].minted[addr_],
            _collections[collectionId_].preminted[addr_]
        );
    }

    /**
     * @notice Returns generated balance from mint campaign of collection
     * @param collectionId_ collection ID
     */
    function collectionBalance(uint256 collectionId_)
        public
        view
        returns (uint256)
    {
        return _collections[collectionId_].balance;
    }

    /**
     * @notice Returns boolean flag indicating if address is collection authorized
     * @param collectionId_ collection ID
     * @param addr_ checked address
     */
    function isCollectionAuthorized(uint256 collectionId_, address addr_)
        public
        view
        returns (bool)
    {
        return _collections[collectionId_].authorized[addr_];
    }

    /**
     * @notice Returns collection bounds - starting token id, last token id, current token id
     * @param collectionId_ collection ID
     */
    function collectionBounds(uint256 collectionId_)
        public
        view
        returns (
            uint256,
            uint256,
            uint256
        )
    {
        unchecked {
            return (
                _collections[collectionId_].tokenIdFrom + 1,
                _collections[collectionId_].tokenIdFrom +
                    _collections[collectionId_].maxSupply,
                _collections[collectionId_].tokenId
            );
        }
    }

    /**
     * @notice Returns info about collection - collection name, artist name, base URI, artist address, max supply
     * @param collectionId_ collection ID
     */
    function collectionInfo(uint256 collectionId_)
        public
        view
        returns (
            string memory,
            string memory,
            string memory,
            address,
            uint32
        )
    {
        return (
            string(abi.encodePacked(_collections[collectionId_].name)),
            string(
                abi.encodePacked(
                    _artists[_collections[collectionId_].artistAddr].name
                )
            ),
            string(abi.encodePacked(_collections[collectionId_].baseUri)),
            _collections[collectionId_].artistAddr,
            _collections[collectionId_].maxSupply
        );
    }

    /**
     * @notice Allows collection authorized or contract owner to retrive split payments information
     * @param collectionId_ collection ID
     */
    function collectionPaymentsInfo(uint256 collectionId_)
        public
        view
        onlyAuthorized(collectionId_)
        returns (
            uint256,
            uint256,
            uint16,
            uint16
        )
    {
        return (
            _collections[collectionId_].withdrawn[0],
            _collections[collectionId_].withdrawn[1],
            _collections[collectionId_].percentage[0],
            _collections[collectionId_].percentage[1]
        );
    }

    /**
     * @notice Allows contract owner or known artists to create new collectins on this contract
     * @param artistAddr_ collection artists address (required)
     * @param maxSupply_ collection max supply (required)
     * @param premintMaxSupply_ collection premint max supply (optional)
     */
    function createCollection(
        address artistAddr_,
        uint32 maxSupply_,
        uint32 premintMaxSupply_
    ) public {
        _nonZeroAddress(artistAddr_);
        require(maxSupply_ > 0 && maxSupply_ <= 5000000, "Invalid supply");

        if (_isCreateCollectionPublic == 0) {
            // Leave an option to publicly allow this operation
            require(
                _msgSender() == owner() || _knownArtists[_msgSender()],
                "Unauthorized"
            );
        }

        Collection storage c = _collections[++_lastCollectionId];
        c.artistAddr = artistAddr_;
        c.maxSupply = maxSupply_;

        if (premintMaxSupply_ > 0) {
            _collections[_lastCollectionId]
                .premintMaxSupply = premintMaxSupply_;
        }

        uint256 prevIdx;
        unchecked {
            prevIdx = _lastCollectionId - 1;
            _artists[artistAddr_].collectionsCount++;
        }

        // Every new collection's tokenId will start from the previous end + 10000(gap)
        c.tokenId =
            _collections[prevIdx].tokenIdFrom +
            _collections[prevIdx].maxSupply +
            10000;
        c.tokenIdFrom = c.tokenId;
    }

    /**
     * @notice Returns boolean flag indicating whether collection exists
     * @param collectionId_ collection ID
     */
    function collectionExists(uint256 collectionId_)
        public
        view
        returns (bool)
    {
        return _collections[collectionId_].artistAddr != address(0);
    }