contract Mooncake is ERC721Enumerable, ReentrancyGuard, Ownable {
    string[] public slot0 = [
        "Almond paste ",
        "Azuki bean paste ",
        unicode"Açaí coconut custard ",
        "Baileys custard ",
        "Baked sweet potato ",
        "Black sesame paste ",
        "Blackberry custard ",
        "Blueberry custard ",
        "Champagne ganache ",
        unicode"Chestnut purée ",
        "Chocolate peanut butter ",
        "Classic lava custard ",
        "Classic lotus paste double salted egg yolk ",
        "Classic red bean paste double salted egg yolk ",
        "Coco tiramisu ",
        "Cream custard ",
        "Crystal flower ",
        "Deluxe abalone ",
        unicode"Deluxe bird’s nest ",
        "Deluxe caviar and truffle ",
        "Deluxe foie gras ",
        "Deluxe matsutake mushroom ",
        "Egg custard ",
        "Ferrero Rocher chocolate ",
        "Five kernel with roast pork ",
        "Fruit and vegetable ",
        "Ginseng lotus paste ",
        "Golden lotus paste ",
        "Golden lotus paste double salted egg yolk ",
        "Golden lotus paste triple salted egg yolk ",
        "Golden lotus paste with salted egg yolk ",
        "Green tea ",
        "Green tea mung bean ",
        "Ham and nut ",
        "Hazelnut cream ",
        "Jelly grass custard ",
        "Lava caramel macchiato ",
        "Lava coffee custard ",
        "Lava custard salted egg yolk ",
        "Limited-edition Musang King durian with salted egg yolk ",
        "Limited-edition preserved eggs and pickled ginger ",
        "Limited-edition seafood medley ",
        "Spicy chicken floss ",
        "Lychee Martini custard ",
        "Lychee with lime marshmallow ",
        "Mango mousse ",
        "Mango pomelo sago ",
        "Matcha red bean ",
        "Matcha red bean with salted egg yolk ",
        "Mixed berry custard ",
        "Mixed nuts ",
        "Mountain hawthorn ",
        "Mung bean paste ",
        "Munk bean paste double salted egg yolk ",
        "Munk bean paste single salted egg yolk ",
        "Musang King durian ",
        "Mysterious preserved eggs and pickled ginger ",
        "Nutella lava ",
        "Pandan coconut ",
        "Pandan coconut single yolk ",
        "Peanut butter and jelly ",
        "Pineapple jam ",
        "Plum blossom ",
        "Purple sweet potato ",
        "Purple yam ",
        "Red bean chestnut ",
        "Red bean matcha ",
        "Red bean paste ",
        "Red bean paste double salted egg yolk ",
        "Red bean paste single salted egg yolk ",
        "Red date ",
        "Red date lotus seed paste ",
        "Roasted chestnut ",
        "Sago chia seeds jelly ",
        "Strawberry jelly ",
        "Sweet bean paste double salted egg yolk ",
        "Taro paste ",
        "Taro paste with salted egg yolk ",
        "Traditional five kernel ",
        "Traditional jujube paste ",
        "Traditional lotus paste ",
        "Traditional sweet bean paste ",
        "Walnut and fig flavored egg custard ",
        "Wasabi bean ",
        "White lotus paste ",
        "White lotus paste double salted egg yolk ",
        "White peach Bellini ",
        "Wisteria blossom "
    ];

    string[] public slot1 = ["diamond-shaped ", "doge-shaped ", "eth-shaped ", "gluten-free ", "goldfish-shaped ", "heart-shaped ", "lotus-shaped ", "low-fat ", "low-sugar ", "magnolia-shaped ", "vegan "];

    string[] public slot2 = ["mini-", "mochi-", "snow-skin ", "ice cream ", "cream cheese ", "extra large ", "flaky crust "];

    string public STRING_MOONCAKE = "mooncake ";

    string public STRING_PRE_SLOT3 = "sprinkled with ";

    string[] public slot3 = [
        "50-year dried tangerine peel ",
        "almond flakes ",
        "bonito flakes ",
        "candied bacon bits ",
        "candied cacao nibs ",
        "caramel bits ",
        "chocolate peppermint crackle ",
        "cinnamon and brown sugar ",
        "cocoa powder ",
        "crunched coffee bean ",
        "crunchy salted caramel bits ",
        "fresh fruits ",
        "fruit confit ",
        "24-karat edible gold flakes ",
        "lotus seeds of the year ",
        "onion confit ",
        "pink Himalayan salt flakes ",
        "tangy lemon crinkle ",
        "tangy pomegranate seeds ",
        "toasted coconut flakes ",
        "toasted pistachios ",
        "toasted sesame seeds ",
        "volcanic salt caramel crisps ",
        "white truffle shavings "
    ];

    string public STRING_PRE_SLOT4 = "accompanied with a cup of ";

    string[] public slot4 = [
        "aged Pu'er tea ",
        "chamomile flowers tea ",
        "Darjeeling tea ",
        "Dragon Well green tea ",
        "Matcha green tea ",
        "jasmine green tea ",
        "Keemun black tea ",
        "Lapsang Souchong black tea ",
        "Moroccan mint tea ",
        "Osmanthus oolong tea ",
        "pomegranate green tea ",
        "roasted barley tea ",
        "Silver Needle white tea ",
        "turmeric ginger herbal tea ",
        "vanilla chai tea ",
        "white peony tea "
    ];

    string public STRING_PRE_SLOT5 = "presented ";

    string[] public slot5 = [
        "in a bento lunch box ",
        "in a biofriendly edible carton ",
        "in a Chinese takeout box ",
        "in a decorative paper wrap with origami finish ",
        "in a lantern-shaped LED box ",
        "in a lavish deep red chest ",
        "in a luxury sterling silver chest ",
        "in a metallic turquoise box ",
        "in a non-fungible token ",
        "in a stainless steel box ",
        "in a Swarovski crystal case ",
        "in a wicker picnic basket ",
        "in an elaborate box with jeweled clasp ",
        "on a biodegradable palm leaf wooden plate ",
        "on a fine bone china dish ",
        "on a jumbo pearl oyster shell ",
        "on a Qing dynasty porcelain plate ",
        "on a textured blue ceramic tray ",
        "on a vintage French oyster plate ",
        "on a vintage stoneware tray ",
        "on a woven bamboo placemat ",
        "on an antique rosewood tray "
    ];

    mapping(uint256 => uint256) public tokenTransferCounts; // This is the level a token can be upgraded to
    mapping(uint256 => uint256) public tokenLevels; // This is the current level of a token

    uint256 public SALES_START_TIMESTAMP = 1631880900; // 2021-09-17 12:15:00 UTC
    uint256 public CLAIM_START_TIMESTAMP = 1631880000; // 2021-09-17 12:00:00 UTC

    uint256 public MINT_FEE = 0.01 ether;

    uint256 public maxNftSupply = 1508;
    uint256 public maxFreeMints = 888;

    uint256 public numFreeMints;
    bool public contractSealed;

    event TokenUpgraded(uint256 indexed tokenId, uint256 newLevel, uint256 oldLevel);
    event AffiliateMint(uint256 indexed tokenId, address affiliate);

    /**
     * @dev Track token transfers / upgrade potentials
     */
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal override {
        super._beforeTokenTransfer(from, to, tokenId);

        // Skip freshly minted tokens to save gas
        if (from != address(0)) {
            tokenTransferCounts[tokenId] += 1;
        }
    }

    /**
     * @dev Upgrade a token if it's ready
     */
    function upgradeToken(uint256 tokenId) external {
        require(_exists(tokenId), "Update nonexistent token");
        require(_msgSender() == ERC721.ownerOf(tokenId), "Must own token for upgrade");

        require(tokenLevels[tokenId] < tokenTransferCounts[tokenId], "Token is not ready for upgrade");
        uint256 oldLevel = tokenLevels[tokenId];
        uint256 newLevel = tokenTransferCounts[tokenId];

        tokenLevels[tokenId] = newLevel;

        emit TokenUpgraded(tokenId, newLevel, oldLevel);
    }

    /**
     * @dev Return the slot id for each text chunk. Useful for translations.
     */
    function getMooncakeTextSlotIds(uint256 tokenId, uint256 level) public view returns (uint8[6] memory) {
        uint8[6] memory slotIds;

        //generate a sequence of pseudo random numbers, seeded by id
        uint32[15] memory seq;
        uint256 r = tokenId + 20000;
        for (uint256 i = 0; i < 15; i++) {
            r = (r * 16807) % 2147483647; //advance PRNG
            seq[i] = uint32(r);
        }
        uint16 sp = 0;

        uint8[5] memory slotLevelDefaults = [60, 90, 100, 100, 100];
        uint8[5] memory slotLevels;

        // Starting with 2 slots (on top of slot0)
        uint256 maxSlots = 2;
        // Freshly minted token is at level 0
        if (level > 0) {
            maxSlots = 2 + level;
        }
        if (maxSlots > 5) {
            maxSlots = 5;
        }

        for (uint256 i = 0; i < maxSlots; i++) {
            uint256 slotNum = seq[sp++] % 5;
            while (slotLevels[slotNum] != 0) {
                slotNum = (slotNum + 1) % 5;
            }
            slotLevels[slotNum] = slotLevelDefaults[slotNum];
        }

        // Slot checks used up some random numbers
        sp = 5;
        uint16 f = 0;

        slotIds[0] = uint8(seq[0] % slot0.length);

        f = uint16(seq[sp++] % 100);
        if (f < slotLevels[0]) {
            slotIds[1] = uint8(seq[sp] % slot1.length);
        } else {
            slotIds[1] = 255;
        }

        f = uint16(seq[sp++] % 100);
        if (f < slotLevels[1]) {
            slotIds[2] = uint8(seq[sp] % slot2.length);
        } else {
            slotIds[2] = 255;
        }

        f = uint16(seq[sp++] % 100);
        if (f < slotLevels[2]) {
            slotIds[3] = uint8(seq[sp] % slot3.length);
        } else {
            slotIds[3] = 255;
        }

        f = uint16(seq[sp++] % 100);
        if (f < slotLevels[3]) {
            slotIds[4] = uint8(seq[sp] % slot4.length);
        } else {
            slotIds[4] = 255;
        }

        f = uint16(seq[sp++] % 100);
        if (f < slotLevels[4]) {
            slotIds[5] = uint8(seq[sp] % slot5.length);
        } else {
            slotIds[5] = 255;
        }

        return slotIds;
    }

    /**
     * @dev Return the mooncake text for a given tokenId and its level
     */
    function getMooncakeTextWithLevel(uint256 tokenId, uint256 level) public view returns (string memory) {
        uint8[6] memory slotIds = getMooncakeTextSlotIds(tokenId, level);
        string memory output = slot0[slotIds[0]];

        if (slotIds[1] < 255) {
            output = string(abi.encodePacked(output, slot1[slotIds[1]]));
        }

        if (slotIds[2] < 255) {
            output = string(abi.encodePacked(output, slot2[slotIds[2]]));
        }

        output = string(abi.encodePacked(output, STRING_MOONCAKE));

        if (slotIds[3] < 255) {
            output = string(abi.encodePacked(output, STRING_PRE_SLOT3, slot3[slotIds[3]]));
        }

        if (slotIds[4] < 255) {
            output = string(abi.encodePacked(output, STRING_PRE_SLOT4, slot4[slotIds[4]]));
        }

        if (slotIds[5] < 255) {
            output = string(abi.encodePacked(output, STRING_PRE_SLOT5, slot5[slotIds[5]]));
        }

        return output;
    }

    /**
     * @dev Return the mooncake text for a given tokenId
     */
    function getMooncakeText(uint256 tokenId) public view returns (string memory) {
        uint256 tokenLevel = tokenLevels[tokenId];
        return getMooncakeTextWithLevel(tokenId, tokenLevel);
    }

    /**
     * @dev Get token URI at a specified token level
     */
    function tokenURIWithLevel(uint256 tokenId, uint256 tokenLevel) public view returns (string memory) {
        string[17] memory parts;
        parts[
            0
        ] = '<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 350"><style>foreignObject { fill: black; font-family: "DM Sans", "Open Sans", "Calibri", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; font-size: 24px; }</style><rect width="100%" height="100%" fill="';

        if (tokenLevel == 0) {
            parts[1] = "#fef2d5";
        } else if (tokenLevel == 1) {
            parts[1] = "#feecc1";
        } else if (tokenLevel == 2) {
            parts[1] = "#fde5ae";
        } else {
            parts[1] = "#fccb2d";
        }

        parts[2] = '" /><foreignObject width="300" height="320" x="20" y="20" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">';
        parts[3] = getMooncakeTextWithLevel(tokenId, tokenLevel);
        parts[4] = "</foreignObject></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]));
        string memory json = Base64.encode(
            bytes(
                string(
                    abi.encodePacked(
                        '{"name": "Mooncake #',
                        toString(tokenId),
                        '", "description": "The Mooncake Gift is a collection of randomized mooncakes generated and stored onchain. The more times a mooncake is gifted, the tastier it becomes. Images are intentionally omitted for others to interpret. Feel free to use these mooncakes 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;
    }

    /**
     * @dev Retrieve the current tokenURI (embedded SVG)
     */
    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        uint256 tokenLevel = tokenLevels[tokenId];
        return tokenURIWithLevel(tokenId, tokenLevel);
    }

    /**
     * @dev Mint tokens
     */
    function mintWithAffiliate(uint256 numberOfNfts, address affiliate) public payable nonReentrant {
        // Checks
        require(!contractSealed, "Contract sealed");

        require(block.timestamp >= SALES_START_TIMESTAMP, "Not started");

        require(numberOfNfts > 0, "Cannot mint 0 NFTs");
        require(numberOfNfts <= 50, "Cannot mint more than 50 in 1 tx");

        uint256 total = totalSupply();
        require(total + numberOfNfts <= maxNftSupply, "Sold out");

        require(msg.value == numberOfNfts * MINT_FEE, "Ether value sent is incorrect");

        // Effects
        for (uint256 i = 0; i < numberOfNfts; i++) {
            uint256 tokenId = total + i;

            _safeMint(_msgSender(), tokenId, "");
            if (affiliate != address(0)) {
                emit AffiliateMint(tokenId, affiliate);
            }
        }
    }

    /**
     * @dev Mint tokens
     */
    function mintNfts(uint256 numberOfNfts) external payable {
        mintWithAffiliate(numberOfNfts, address(0));
    }

    /**
     * @dev Claim a free one
     */
    function claimOneFreeNft() external payable nonReentrant {
        // Checks
        require(!contractSealed, "Contract sealed");

        require(block.timestamp >= CLAIM_START_TIMESTAMP, "Not started");

        uint256 total = totalSupply();
        require(total + 1 <= maxNftSupply, "Sold out");
        require(numFreeMints + 1 <= maxFreeMints, "No more free tokens left");

        require(balanceOf(_msgSender()) == 0, "You already have a mooncake. Enjoy it!");

        // Effects
        numFreeMints += 1;
        _safeMint(_msgSender(), total, "");
    }

    /**
     * @dev Return detail information about an owner's token (tokenId, current level, potential level, uri)
     */
    function tokenDetailOfOwnerByIndex(address _owner, uint256 index)
        public
        view
        returns (
            uint256,
            uint256,
            uint256,
            string memory
        )
    {
        uint256 tokenId = tokenOfOwnerByIndex(_owner, index);
        uint256 tokenLevel = tokenLevels[tokenId];
        uint256 tokenTransferCount = tokenTransferCounts[tokenId];
        string memory uri = tokenURI(tokenId);
        return (tokenId, tokenLevel, tokenTransferCount, uri);
    }

    /**
     * @dev Reserve tokens and gift tokens
     */
    function deployerMintMultiple(address[] calldata recipients) external payable onlyDeployer {
        require(!contractSealed, "Contract sealed");

        uint256 total = totalSupply();
        require(total + recipients.length <= maxNftSupply, "Sold out");

        for (uint256 i = 0; i < recipients.length; i++) {
            require(recipients[i] != address(0), "Can't mint to null address");

            _safeMint(recipients[i], total + i);
        }
    }

    /**
     * @dev Deployer parameters
     */
    function deployerSetParam(uint256 key, uint256 value) external onlyDeployer {
        require(!contractSealed, "Contract sealed");

        if (key == 0) {
            SALES_START_TIMESTAMP = value;
        } else if (key == 1) {
            CLAIM_START_TIMESTAMP = value;
        } else if (key == 2) {
            MINT_FEE = value;
        } else if (key == 3) {
            maxNftSupply = value;
        } else if (key == 4) {
            maxFreeMints = value;
        } else {
            revert();
        }
    }

    /**
     * @dev Deployer withdraws ether from this contract
     */
    function deployerWithdraw(uint256 amount) external onlyDeployer {
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed.");
    }

    /**
     * @dev Deployer withdraws ERC20s
     */
    function deployerWithdraw20(IERC20 token) external onlyDeployer {
        if (address(token) == 0x0000000000000000000000000000000000000000) {
            payable(owner()).transfer(address(this).balance);
            (bool success, ) = owner().call{value: address(this).balance}("");
            require(success, "Transfer failed.");
        } else {
            token.transfer(owner(), token.balanceOf(address(this)));
        }
    }

    /**
     * @dev Seal this contract
     */
    function deployerSealContract() external onlyDeployer {
        contractSealed = true;
    }

    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("Mooncake", "MOONCAKE") Ownable() {}
}
function tokenDetailOfOwnerByIndex(address _owner, uint256 index)
    public
    view
    returns (
        uint256,
        uint256,
        uint256,
        string memory
    )
{
    uint256 tokenId = tokenOfOwnerByIndex(_owner, index);
    uint256 tokenLevel = tokenLevels[tokenId];
    uint256 tokenTransferCount = tokenTransferCounts[tokenId];
    string memory uri = tokenURI(tokenId);
    return (tokenId, tokenLevel, tokenTransferCount, uri);
}

/**
 * @dev Return detail information about an owner's token (tokenId, current level, potential level, uri)
 */