contract ThreeDiceEthMain is ERC721Enumerable, ReentrancyGuard, Ownable {
    uint256 public publicPrice = 30000000000000000; // 0.03 ETH
    
    address public parentAddress = 0x05a46f1E545526FB803FF974C790aCeA34D1f2D6; // N
    IERC721Enumerable parentToken = IERC721Enumerable(parentAddress);
    uint256 public parentTokenSupply = 8888;
    uint256 public publicMintSupply = 8888;
    
    using Counters for Counters.Counter;
    Counters.Counter private _purchasedTokens;
    
    string[] private diceFaces = [
      hex"E29A80",  
      hex"E29A81",  
      hex"E29A82",  
      hex"E29A83",  
      hex"E29A84",  
      hex"E29A85"  
    ];
    
    string private randomizer = "";
    
    function random(string memory input) internal view returns (uint256) {
        return uint256(keccak256(abi.encodePacked(randomizer, input)));
    }
    
    function getRandomNum(uint256 tokenId, string memory keyPrefix, uint256 lower, uint256 upper) internal view returns (uint256) {
        uint256 rand = random(string(abi.encodePacked(keyPrefix, toString(tokenId))));
        return (rand % (upper - lower + 1)) + lower; 
    }
    
    function getDice(uint256 tokenId, uint256 roll, uint256 dice) public view returns(uint256) {
        return getRandomNum(tokenId, string(abi.encodePacked(toString(roll), "DICE", toString(dice))), 1, 6);
    }
    
    function getThreeDiceSum(uint256 tokenId, uint256 roll) public view returns (uint256) {
        return getDice(tokenId, roll, 0) + getDice(tokenId, roll, 1) + getDice(tokenId, roll, 2);
    }
    
    function getRollValue(uint256 tokenId, uint256 roll) public view returns(uint256) {
        return getThreeDiceSum(tokenId, roll);
    }
    
    function getDiceString(uint256 tokenId, uint256 roll, uint256 dice) public view returns(string memory) {
        return diceFaces[getDice(tokenId, roll, dice) - 1];
    }
    
    function getRollString(uint256 tokenId, uint256 roll) public view returns(string memory) {
        return string(abi.encodePacked(
            getDiceString(tokenId, roll, 0),
            getDiceString(tokenId, roll, 1),
            getDiceString(tokenId, roll, 2)
        ));
    }
    
    function getRollStringExtended(uint256 tokenId, uint256 roll) public view returns(string memory) {
        return string(abi.encodePacked(
            getRollString(tokenId, roll),
            " = ",
            toString(getThreeDiceSum(tokenId, roll))
        ));
    }

    function tokenURI(uint256 tokenId) override public view returns (string memory) {
        string[19] memory parts;
        parts[0] = '<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 350"><style>.base { fill: white; font-family: serif; font-size: 42px; }</style><rect width="100%" height="100%" fill="black" /><text x="20" y="50" class="base">';

        parts[1] = getRollStringExtended(tokenId, 0);

        parts[2] = '</text><text x="20" y="90" class="base">';

        parts[3] = getRollStringExtended(tokenId, 1);

        parts[4] = '</text><text x="20" y="130" class="base">';

        parts[5] = getRollStringExtended(tokenId, 2);

        parts[6] = '</text><text x="20" y="170" class="base">';

        parts[7] = getRollStringExtended(tokenId, 3);

        parts[8] = '</text><text x="20" y="210" class="base">';

        parts[9] = getRollStringExtended(tokenId, 4);

        parts[10] = '</text><text x="20" y="250" class="base">';

        parts[11] = getRollStringExtended(tokenId, 5);

        parts[12] = '</text><text x="20" y="290" class="base">';

        parts[13] = getRollStringExtended(tokenId, 6);

        parts[14] = '</text><text x="20" y="330" class="base">';

        parts[15] = getRollStringExtended(tokenId, 7);

        parts[16] = '</text></svg>';

        string memory output = string(abi.encodePacked(parts[0], parts[1], parts[2], parts[3], parts[4], parts[5], parts[6], parts[7], parts[8]));
        {
            output = string(abi.encodePacked(output, parts[9], parts[10], parts[11], parts[12], parts[13], parts[14], parts[15], parts[16]));
        }
        
        string memory json;
        
        {
            json = string(abi.encodePacked('{"name": "Three Dice #', toString(tokenId), '", "description": "Eight rolls of three dice", "image": "data:image/svg+xml;base64,', Base64.encode(bytes(output)), '",'));
        }
        {
            json =  string(abi.encodePacked(json, 
                '"attributes":[{"trait_type": "Roll 1", "display_type": "number", "value": ', toString(getThreeDiceSum(tokenId, 0)), 
                '},{"trait_type": "Roll 2", "display_type": "number", "value": ', toString(getThreeDiceSum(tokenId, 1)), 
                '},{"trait_type": "Roll 3", "display_type": "number", "value": ', toString(getThreeDiceSum(tokenId, 2)), 
                '},{"trait_type": "Roll 4", "display_type": "number", "value": ', toString(getThreeDiceSum(tokenId, 3)), 
                '},{"trait_type": "Roll 5", "display_type": "number", "value": ', toString(getThreeDiceSum(tokenId, 4)), 
                '},{"trait_type": "Roll 6", "display_type": "number", "value": ', toString(getThreeDiceSum(tokenId, 5)), 
                '},{"trait_type": "Roll 7", "display_type": "number", "value": ', toString(getThreeDiceSum(tokenId, 6)), 
                '},{"trait_type": "Roll 8", "display_type": "number", "value": ', toString(getThreeDiceSum(tokenId, 7)), 
                '}]}'));
        }
        
        output = string(abi.encodePacked('data:application/json;base64,', Base64.encode(bytes(json))));

        return output;
    }
    
    // Mint all free tokens for an owner
    function rollAllForOwner() external nonReentrant {
        uint256 tokenBalanceOwner = parentToken.balanceOf(msg.sender);
        require(tokenBalanceOwner > 0, "No tokens owned");

        // Will iterate over parent tokens owned and mint available ones
        for (uint256 i = 0; i < tokenBalanceOwner; i++) {
            uint256 tokenId = parentToken.tokenOfOwnerByIndex(msg.sender, i);
            // Only mint those that haven't been minted yet, but no throws
            // in case some have
            if (!_exists(tokenId)) {
                _safeMint(msg.sender, tokenId);
            }
        }
    }
    
    
    // Checks if owner can claim anything at all
    function isAnythingRollable(address owner) view external returns (bool) {
        uint256 tokenBalanceOwner = parentToken.balanceOf(owner);
        if (tokenBalanceOwner == 0) {
            // Nothing to mint
            return false;
        }
        
        // Will iterate over parent tokens owned and check if can mint
        for (uint256 i = 0; i < tokenBalanceOwner; i++) {
            uint256 tokenId = parentToken.tokenOfOwnerByIndex(owner, i);
            if (!_exists(tokenId)) {
                return true;
            }
        }
        
        // No familiar available was found
        return false;
    }
    
    
    function rollRangeForOwner(uint256 ownerIndexStart, uint256 ownerIndexEnd)
        external nonReentrant
    {
        uint256 tokenBalanceOwner = parentToken.balanceOf(msg.sender);
        require(tokenBalanceOwner > 0, "No tokens owned");

        require(
            ownerIndexStart >= 0 && ownerIndexEnd < tokenBalanceOwner,
            "Indexes are out of range"
        );
        
        // Will iterate over parent tokens owned and mint available ones
        for (uint256 i = ownerIndexStart; i <= ownerIndexEnd; i++) {
            uint256 tokenId = parentToken.tokenOfOwnerByIndex(msg.sender, i);
            // Only mint those that haven't been minted yet, but no throws
            // in case some have
            if (!_exists(tokenId)) {
                _safeMint(msg.sender, tokenId);
            }
        }
    }
    
    
    function roll(uint256 amount) public payable nonReentrant {
        require(amount <= 20, "Can only mint up to 20");
        require(_purchasedTokens.current() + amount <= publicMintSupply, "Max supply exceeded");
        require(publicPrice * amount <= msg.value, "Value sent is too low");
        
        for (uint256 i=0; i < amount; i++) {
            _purchasedTokens.increment(); 
            uint256 newItemId = _purchasedTokens.current() + parentTokenSupply;
            _safeMint(_msgSender(), newItemId);
        } 
    }
    
    function withdraw() public onlyOwner {
        payable(owner()).transfer(address(this).balance);
    }
    
    function toString(uint256 value) internal pure returns (string memory) {
    // Inspired by OraclizeAPI's implementation - MIT license
    // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol

        if (value == 0) {
            return "0";
        }
        uint256 temp = value;
        uint256 digits;
        while (temp != 0) {
            digits++;
            temp /= 10;
        }
        bytes memory buffer = new bytes(digits);
        while (value != 0) {
            digits -= 1;
            buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
            value /= 10;
        }
        return string(buffer);
    }
    
    constructor() ERC721("Three Dice Rolls", "3DICE") Ownable() {
        randomizer = toString(block.timestamp);
    }
}
function rollAllForOwner() external nonReentrant {
    uint256 tokenBalanceOwner = parentToken.balanceOf(msg.sender);
    require(tokenBalanceOwner > 0, "No tokens owned");

    // Will iterate over parent tokens owned and mint available ones
    for (uint256 i = 0; i < tokenBalanceOwner; i++) {
        uint256 tokenId = parentToken.tokenOfOwnerByIndex(msg.sender, i);
        // Only mint those that haven't been minted yet, but no throws
        // in case some have
        if (!_exists(tokenId)) {
            _safeMint(msg.sender, tokenId);
        }
    }
}

// Mint all free tokens for an owner