contract EaselyMinter is ERC721Enumerable, Ownable {    

    // The marketplace this contract directly lists to for minted NFTs
    address public marketplaceAddress;

    uint256 private nextTokenId;

    // mapping to see if a given token is claimable or not
    mapping(uint256 => bool) private _tokenIdToClaimable;
    // If populated, this mapping holds the info backing a newly minted token's URI 
    mapping(uint256 => string) private _tokenIdToPostClaimURI;

    // Event that is emitted when a tokenId is made claimable by the contract owner
    event ClaimableCreated(uint256 _tokenId);
    // Event that is emitted when a tokenId is claimed by the token owner
    event ClaimableClaimed(address _claimer, uint256 _claimedTokenId, string _claimedHash);
    // Event that is emitted when a tokenId is claimed by the token owner and what newTokenId was created
    event ClaimableClaimedWithNew(address _claimer, uint256 _claimedTokenId, uint256 _newTokenId, string _claimedHash);

    /**
     * @dev Constructor function
     */
    constructor(
        string memory name_, 
        string memory symbol_, 
        address marketplaceAddress_
    ) ERC721(name_, symbol_) {
        nextTokenId = 0;
        marketplaceAddress = marketplaceAddress_;

        setApprovalForAll(marketplaceAddress, true);
    }

    /**
     * @dev Base URI for computing {tokenURI}. If set, the resulting URI for each
     * token will be the concatenation of the `baseURI` and the `tokenId`. Empty
     * by default, can be overriden in child contracts.
     */
    function _baseURI() internal view virtual override returns (string memory) {
        return "ipfs://ipfs/";
    }

    function _isClaimable(uint256 tokenId) internal view virtual returns (bool) {
        return _tokenIdToClaimable[tokenId];
    }

    /**
     * @dev _safeMint wrapper that stores the IPFS metadata into the token URI 
     * it also uses and increments nextTokenId.
     */
    function _createNFT(address collectorAddress, string memory metadataIPFSHash) internal returns (uint256) {
        // mint token
        _safeMint(collectorAddress, nextTokenId);
        _setTokenURI(nextTokenId, metadataIPFSHash);
        
        nextTokenId = nextTokenId + 1;
        return nextTokenId - 1;
    }
    
    /**
     * @dev _createNFT wrapper that also sets up claimable. If postClaimIPFSHash is
     * exactly "" then no NFT will be created when the token is claimied.
     */
    function _createClaimableNFT(
        address collectorAddress, 
        string memory metadataIPFSHash, 
        string memory postClaimIPFSHash
    ) internal returns (uint256) {
        // mint token
        uint256 tokenId = _createNFT(collectorAddress, metadataIPFSHash);
        
        // seetup claimable
        _tokenIdToClaimable[tokenId] = true;
        _tokenIdToPostClaimURI[tokenId] = postClaimIPFSHash;

        emit ClaimableCreated(tokenId);

        return tokenId;
    }

    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return interfaceId == type(IERC721Enumerable).interfaceId 
            || interfaceId == type(Ownable).interfaceId
            || super.supportsInterface(interfaceId);
    }

    /**
     * @dev see _createNFT.
     */
    function createNFT(address collectorAddress, string memory metadataIPFSHash) external onlyOwner {
        _createNFT(collectorAddress, metadataIPFSHash);
    }

    /**
     * @dev Takes an existing token with no claimable content and adds to it. If postClaimIPFSHash is
     * exactly "" then no NFT will be created when the token is claimied.
     */
    function addClaimable(uint256 tokenId, string memory postClaimIPFSHash) external onlyOwner {
        require(_exists(tokenId), "Cannot create claimable for nonexistent token");
        require(!_isClaimable(tokenId), "Token already is claimable");

        _tokenIdToClaimable[tokenId] = true;
        _tokenIdToPostClaimURI[tokenId] = postClaimIPFSHash;

        emit ClaimableCreated(tokenId);
    }

    /**
     * @dev see _createClaimableNFT.
     */
    function createClaimableNFT(
        address collectorAddress, 
        string memory metadataIPFSHash, 
        string memory postClaimIPFSHash
    ) external onlyOwner {
        _createClaimableNFT(collectorAddress, metadataIPFSHash, postClaimIPFSHash);
    }

    /**
     * @dev Follows _createNFT, but also lists the token for sale in the marketplace
     * at the given price for the current owner.
     */
    function createAndSellNFT(
        string memory metadataIPFSHash, 
        uint256 price
    ) external onlyOwner {
        uint256 tokenId = _createNFT(owner(), metadataIPFSHash);

        uint256[] memory tokenIds = new uint256[](1);
        tokenIds[0] = tokenId;

        ISimpleMarketplace(marketplaceAddress).sellBundle(address(this), owner(), tokenIds, price);
    }

    /**
     * @dev Follows _createClaimableNFT, but also lists the token for sale in the marketplace
     * at the given price for the current owner.
     */
    function createAndSellClaimableNFT(
        string memory metadataIPFSHash, 
        string memory postClaimIPFSHash,
        uint256 price
    ) external onlyOwner {
        uint256 tokenId = _createClaimableNFT(owner(), metadataIPFSHash, postClaimIPFSHash);

        uint256[] memory tokenIds = new uint256[](1);
        tokenIds[0] = tokenId;

        ISimpleMarketplace(marketplaceAddress).sellBundle(address(this), owner(), tokenIds, price);
    }

    /**
     * @dev Similar to createAndSellClaimableNFT but can create and batch multiple tokens
     * for sale in one bundle in the marketplace.
     */
    function createAndSellClaimableBundle(
        string[] memory metadataIPFSHashes, 
        string[] memory postClaimIPFSHashes,
        uint256 price
    ) external onlyOwner {
        require(metadataIPFSHashes.length == postClaimIPFSHashes.length, "Mismatched arrays");

        uint256 numHashes = metadataIPFSHashes.length;
        uint256[] memory tokenIds = new uint256[](numHashes);
        for (uint256 i = 0; i < numHashes; i++) {
            tokenIds[i] = _createClaimableNFT(owner(), metadataIPFSHashes[i], postClaimIPFSHashes[i]);
        }

        ISimpleMarketplace(marketplaceAddress).sellBundle(address(this), owner(), tokenIds, price);
    }

    /**
     * @dev Similar to createAndSellNFT but can create and batch multiple tokens
     * for sale in one bundle in the marketplace.
     */
    function createAndSellBundle(
        string[] memory metadataIPFSHashes, 
        uint256 price
    ) external onlyOwner {
        uint256 numHashes = metadataIPFSHashes.length;
        uint256[] memory tokenIds = new uint256[](numHashes);
        for (uint256 i = 0; i < numHashes; i++) {
            tokenIds[i] = _createNFT(owner(), metadataIPFSHashes[i]);
        }

        ISimpleMarketplace(marketplaceAddress).sellBundle(address(this), owner(), tokenIds, price);
    }

    /**
     * @dev For already created tokens that the owner still owns to be sold in a bundle
     * in the marketplace.
     */
    function sellBundle(uint256[] memory tokenIds, uint256 price) external onlyOwner {
        for (uint256 i = 0; i < tokenIds.length; i++) {
            require(ownerOf(tokenIds[i]) == owner(), "You must own these tokens");      
        }

        ISimpleMarketplace(marketplaceAddress).sellBundle(address(this), owner(), tokenIds, price);
    }

    /**
     * @dev For any bundle already created in the marketplace that the owner wants to
     * recind and prevent people from buying.
     */
    function withdrawBundle(uint256 bundleId) external onlyOwner {        
        ISimpleMarketplace(marketplaceAddress).withdrawBundle(bundleId);
    }

    /**
     * @dev For any user who owns a token that is claimable. If the token has an
     * associated post claim hash then the claimer will get a newly minted token
     * with that hash after claiming. 
     *
     * Claimed tokens that refer to off-chain benefits will be facillitated
     * by Easely, but are the responsibility of the contract creator to 
     * deliver on the promises.
     */
    function claimToken(uint256 tokenId) external {
        require(_exists(tokenId), "Can't claim nonexistent claimable token");
        require(_isClaimable(tokenId), "Token is not claimable");
        require(ownerOf(tokenId) == _msgSender(), "Only current tokenOwner can claim");

        // lock the token from being claimed and thus also from being transferred
        _tokenIdToClaimable[tokenId] = false;

        // If URI is set, mint new token which is unclaimable
        if (bytes(_tokenIdToPostClaimURI[tokenId]).length > 0) {
            _safeMint(_msgSender(), nextTokenId);
            _setTokenURI(nextTokenId, _tokenIdToPostClaimURI[tokenId]);
            
            emit ClaimableClaimedWithNew(_msgSender(), tokenId, nextTokenId, tokenURI(tokenId));
            nextTokenId = nextTokenId + 1;

            // once we set the URI of the new NFT we can delete it from the map 
            delete _tokenIdToPostClaimURI[tokenId];
        } else {
            emit ClaimableClaimed(_msgSender(), tokenId, tokenURI(tokenId));
        }

        _burn(tokenId);
    }

    /**
     * @dev 
     * @param tokenId uint256 ID of the claimed token to query
     */
    function isClaimable(uint256 tokenId) external view returns (bool) {
        return _tokenIdToClaimable[tokenId];
    }
}
/**
 * @dev This implements two things on top of the standard ERC extensions.
 * 1. The ability for the current token owner to claim it by burning it and emiting an event
 * 2. The ability to mint multiple and immediately list with a price
 */
function createAndSellClaimableBundle(
    string[] memory metadataIPFSHashes, 
    string[] memory postClaimIPFSHashes,
    uint256 price
) external onlyOwner {
    require(metadataIPFSHashes.length == postClaimIPFSHashes.length, "Mismatched arrays");

    uint256 numHashes = metadataIPFSHashes.length;
    uint256[] memory tokenIds = new uint256[](numHashes);
    for (uint256 i = 0; i < numHashes; i++) {
        tokenIds[i] = _createClaimableNFT(owner(), metadataIPFSHashes[i], postClaimIPFSHashes[i]);
    }

    ISimpleMarketplace(marketplaceAddress).sellBundle(address(this), owner(), tokenIds, price);
}

/**
 * @dev Similar to createAndSellClaimableNFT but can create and batch multiple tokens
 * for sale in one bundle in the marketplace.
 */