// Contract: Mooncake
// Target function: function tokenURIWithLevel(uint256 tokenId, uint256 tokenLevel) public view returns (string memory)
        uint256 tokenId
    ) internal virtual override {
        super._beforeTokenTransfer(from, to, tokenId);

        if (from == address(0)) {
            _addTokenToAllTokensEnumeration(tokenId);
        } else if (from != to) {
            _removeTokenFromOwnerEnumeration(from, tokenId);
        }
        if (to == address(0)) {
            _removeTokenFromAllTokensEnumeration(tokenId);
        } else if (to != from) {
            _addTokenToOwnerEnumeration(to, tokenId);
        }
    }

    /**
     * @dev Private function to add a token to this extension's ownership-tracking data structures.
     * @param to address representing the new owner of the given token ID
     * @param tokenId uint256 ID of the token to be added to the tokens list of the given address
     */
    function _addTokenToOwnerEnumeration(address to, uint256 tokenId) private {
        uint256 length = ERC721.balanceOf(to);
        _ownedTokens[to][length] = tokenId;
        _ownedTokensIndex[tokenId] = length;
    }

    /**
     * @dev Private function to add a token to this extension's token tracking data structures.
     * @param tokenId uint256 ID of the token to be added to the tokens list
     */
    function _addTokenToAllTokensEnumeration(uint256 tokenId) private {
        _allTokensIndex[tokenId] = _allTokens.length;
        _allTokens.push(tokenId);
    }

    /**
     * @dev Private function to remove a token from this extension's ownership-tracking data structures. Note that
     * while the token is not assigned a new owner, the `_ownedTokensIndex` mapping is _not_ updated: this allows for
     * gas optimizations e.g. when performing a transfer operation (avoiding double writes).
     * This has O(1) time complexity, but alters the order of the _ownedTokens array.
     * @param from address representing the previous owner of the given token ID
     * @param tokenId uint256 ID of the token to be removed from the tokens list of the given address
     */
    function _removeTokenFromOwnerEnumeration(address from, uint256 tokenId) private {
        // To prevent a gap in from's tokens array, we store the last token in the index of the token to delete, and
        // then delete the last slot (swap and pop).

        uint256 lastTokenIndex = ERC721.balanceOf(from) - 1;
        uint256 tokenIndex = _ownedTokensIndex[tokenId];

        // When the token to delete is the last token, the swap operation is unnecessary
        if (tokenIndex != lastTokenIndex) {
            uint256 lastTokenId = _ownedTokens[from][lastTokenIndex];

            _ownedTokens[from][tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token
            _ownedTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index
        }

        // This also deletes the contents at the last position of the array
        delete _ownedTokensIndex[tokenId];
        delete _ownedTokens[from][lastTokenIndex];
    }

    /**
     * @dev Private function to remove a token from this extension's token tracking data structures.
     * This has O(1) time complexity, but alters the order of the _allTokens array.
     * @param tokenId uint256 ID of the token to be removed from the tokens list
     */
    function _removeTokenFromAllTokensEnumeration(uint256 tokenId) private {
        // To prevent a gap in the tokens array, we store the last token in the index of the token to delete, and
        // then delete the last slot (swap and pop).

        uint256 lastTokenIndex = _allTokens.length - 1;
        uint256 tokenIndex = _allTokensIndex[tokenId];

        // When the token to delete is the last token, the swap operation is unnecessary. However, since this occurs so
        // rarely (when the last minted token is burnt) that we still do the swap here to avoid the gas cost of adding
        // an 'if' statement (like in _removeTokenFromOwnerEnumeration)
        uint256 lastTokenId = _allTokens[lastTokenIndex];

        _allTokens[tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token
        _allTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index

        // This also deletes the contents at the last position of the array
        delete _allTokensIndex[tokenId];
        _allTokens.pop();
    }
}

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);
    }