contract Dice is ERC721Enumerable, ReentrancyGuard, Ownable {

    uint256 private maxSides = 20;
    
    string[] private dice = [
        "Lumpy Dice",
        "Holy Dice",
        "Ornate Dice",
        "Heavy Dice",
        "Ivory Dice",
        "Demon's Dice",
        "Weighted Dice",
        "Wooden Dice",
        "Cursed Dice",
        "Divine Dice",
        "Ancient Dice",
        "Flaming Dice",
        "Bone Dice"
    ];
    
    string[] private suffixes = [
        "of Power",
        "of Giants",
        "of Titans",
        "of Skill",
        "of Perfection",
        "of Brilliance",
        "of Enlightenment",
        "of Protection",
        "of Anger",
        "of Rage",
        "of Fury",
        "of Vitriol",
        "of the Fox",
        "of Detection",
        "of Reflection",
        "of the Twins"
    ];
    
    string[] private namePrefixes = [
        "Agony", "Apocalypse", "Armageddon", "Beast", "Behemoth", "Blight", "Blood", "Bramble", 
        "Brimstone", "Brood", "Carrion", "Cataclysm", "Chimeric", "Corpse", "Corruption", "Damnation", 
        "Death", "Demon", "Dire", "Dragon", "Dread", "Doom", "Dusk", "Eagle", "Empyrean", "Fate", "Foe", 
        "Gale", "Ghoul", "Gloom", "Glyph", "Golem", "Grim", "Hate", "Havoc", "Honour", "Horror", "Hypnotic", 
        "Kraken", "Loath", "Maelstrom", "Mind", "Miracle", "Morbid", "Oblivion", "Onslaught", "Pain", 
        "Pandemonium", "Phoenix", "Plague", "Rage", "Rapture", "Rune", "Skull", "Sol", "Soul", "Sorrow", 
        "Spirit", "Storm", "Tempest", "Torment", "Vengeance", "Victory", "Viper", "Vortex", "Woe", "Wrath",
        "Light's", "Shimmering"  
    ];
    
    string[] private nameSuffixes = [
        "Bane",
        "Root",
        "Bite",
        "Song",
        "Roar",
        "Grasp",
        "Instrument",
        "Glow",
        "Bender",
        "Shadow",
        "Whisper",
        "Shout",
        "Growl",
        "Tear",
        "Peak",
        "Form",
        "Sun",
        "Moon"
    ];
    
    function random(string memory input) internal pure returns (uint256) {
        return uint256(keccak256(abi.encodePacked(input)));
    }
    
    function getName(uint256 tokenId) public view returns (string memory) {
        return pluck(tokenId);
    }
    
    function getSideWeight(uint256 tokenId, uint256 side) public pure returns (uint256) {
        // Special dice
        if (tokenId > 8000 && tokenId <= 8888) {
            return 20 + side;
        }
        return 20 + getVariation(tokenId, side);
    }
    
    function getVariation(uint256 tokenId, uint256 side) public pure returns (uint256) {
        return random(string(abi.encodePacked(side, toString(tokenId)))) % 5;
    }
    
    // Roll the n sided die from your set
    // Seed can be an additional sources of entropy
    function roll(uint256 tokenId, uint256 sideCount, uint256 seed) public view returns (uint256) {
        require(_msgSender() == ownerOf(tokenId));
        require(sideCount == 4 ||
                sideCount == 6 ||
                sideCount == 8 ||
                sideCount == 10 ||
                sideCount == 12 ||
                sideCount == 20);
        uint256 pseudoRandom = uint256(keccak256(abi.encodePacked(seed, block.basefee, blockhash(block.number-1), msg.sender, address(this))));
        uint256 total = 0;
        // Add up all side weights for sides considered
        for (uint256 i = 1; i <= sideCount; i++) {
            total = total + getSideWeight(tokenId, i);
        }
        pseudoRandom = pseudoRandom % total + 1;
        for (uint256 i = 1; i <= sideCount; i++) {
            uint256 sideWeight = getSideWeight(tokenId, i);
            if (pseudoRandom <= sideWeight) {
                return i;
            }
            pseudoRandom = pseudoRandom - sideWeight;
        }
        revert();
    }
    
    function pluck(uint256 tokenId) internal view returns (string memory) {
        uint256 rand = random(string(abi.encodePacked("DICE", toString(tokenId))));
        string memory output = dice[rand % dice.length];

        // Special dice
        if (tokenId > 8000 && tokenId <= 8888) {
            output = "Lucky Dice";
        }

        uint256 greatness = rand % 21;
        if (greatness > 14) {
            output = string(abi.encodePacked(output, " ", suffixes[rand % suffixes.length]));
        }
        if (greatness >= 19) {
            string[2] memory name;
            name[0] = namePrefixes[rand % namePrefixes.length];
            name[1] = nameSuffixes[rand % nameSuffixes.length];
            if (greatness == 19) {
                output = string(abi.encodePacked('"', name[0], ' ', name[1], '" ', output));
            } else {
                output = string(abi.encodePacked('"', name[0], ' ', name[1], '" ', output, " +1"));
            }
        }
        return output;
    }

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

        parts[1] = getName(tokenId);

        parts[2] = '</text><text x="10" y="40" class="stats">Probability Distribution</text>';

        for (uint256 i = 1; i <= maxSides; i++) {
            parts[2+i] = string(abi.encodePacked('<text x="10" y="', toString(40 + 20 * i),'" class="stats">', toString(i), '</text><rect x="25" y="', toString((40 + 20 * i) - 10), '" width="', toString(getSideWeight(tokenId, i)*10), '" height="15" stroke="black" stroke-width="6" fill="white"/>'));
        }

        parts[maxSides + 3] = '</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]));
        output = string(abi.encodePacked(output, parts[17], parts[18], parts[19], parts[20], parts[21], parts[22], parts[23], parts[24]));
        
        string memory json = Base64.encode(bytes(string(abi.encodePacked('{"name": "Dice Set #', toString(tokenId), '", "description": "Every adventurer needs Dice. Feel free to use Dice in any way you want.", "image": "data:image/svg+xml;base64,', Base64.encode(bytes(output)), '"}'))));
        output = string(abi.encodePacked('data:application/json;base64,', json));

        return output;
    }

    function claim(uint256 tokenId) public nonReentrant {
        require(tokenId > 0 && tokenId < 7778, "Token ID invalid");
        _safeMint(_msgSender(), tokenId);
    }
    
    function ownerClaim(uint256 tokenId) public nonReentrant onlyOwner {
        require(tokenId > 7777 && tokenId < 8001, "Token ID invalid");
        _safeMint(owner(), tokenId);
    }

    // Requires an existing dice to claim
    function claimSpecial(uint256 tokenId, uint256 existingTokenId) public nonReentrant {
        require(roll(existingTokenId, 20, tokenId) >= 18);
        require(tokenId > 8000 && tokenId <= 8888, "Token ID invalid");
        _safeMint(_msgSender(), tokenId);
    }
    
    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("Dice", "DICE") Ownable() {}
}
function claimSpecial(uint256 tokenId, uint256 existingTokenId) public nonReentrant {
    require(roll(existingTokenId, 20, tokenId) >= 18);
    require(tokenId > 8000 && tokenId <= 8888, "Token ID invalid");
    _safeMint(_msgSender(), tokenId);
}

// Requires an existing dice to claim