// Contract: Worlds
// Target function: function findPtr(
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 Worlds is ERC721Enumerable, ReentrancyGuard, Ownable {
    using strings for string;
    using strings for strings.slice;

    string private constant numbers =
        "ZERO,ONE,TWO,THREE,FOUR,FIVE,SIX,SEVEN,EIGHT,NINE";
    uint256 private constant numbersLength = 10;

    string private constant worldTypes =
        "TERRESTRIAL,OCEAN,ICE,GAS,RARE,RARER,RAREST";
    uint256 private constant worldTypesLength = 7;

    string private constant terrestrialWorlds =
        "Small Hot Terrestrial,Small Temperate Terrestrial,Small Cool Terrestrial,Medium Hot Terrestrial,Medium Temperate Terrestrial,Medium Cool Terrestrial,Large Hot Terrestrial,Large Temperate Terrestrial,Large Cool Terrestrial";
    uint256 private constant terrestrialWorldsLength = 9;

    string private constant oceanWorlds =
        "Small H2O Ocean World,Medium H2O Ocean World,Large H2O Ocean World";
    uint256 private constant oceanWorldsLength = 3;

    string private constant iceWorlds =
        "Small H2O Ice World,Medium H2O Ice World,Large H2O Ice World,Small CO2 NH3 Ice World,Medium CO2 NH3 Ice World,Large CO2 NH3 Ice World,Small CH4 Ice World,Medium CH4 Ice World,Large CH4 Ice World";
    uint256 private constant iceWorldsLength = 9;

    string private constant gasWorlds =
        "Small Type I Gas Giant,Medium Type I Gas Giant,Large Type I Gas Giant,Massive Type I Gas Giant,Super Massive Type I Gas Giant,Small Type II Gas Giant,Medium Type II Gas Giant,Large Type II Gas Giant,Massive Type II Gas Giant,Super Massive Type II Gas Giant,Small Type III Gas Giant,Medium Type III Gas Giant,Large Type III Gas Giant,Massive Type III Gas Giant,Super Massive Type III Gas Giant,Small Type IV Gas Giant,Medium Type IV Gas Giant,Large Type IV Gas Giant,Massive Type IV Gas Giant,Super Massive Type IV Gas Giant,Small Type V Gas Giant,Medium Type V Gas Giant,Large Type V Gas Giant,Massive Type V Gas Giant,Super Massive Type V Gas Giant";
    uint256 private constant gasWorldsLength = 25;

    string private constant rareWorlds =
        "Small Lava World,Medium Lava World,Large Lava World,Small Chthonian World,Medium Chthonian World,Large Chthonian World,Small Desert World,Medium Desert World,Large Desert World,Small Hydrocarbon Ocean World,Medium Hydrocarbon Ocean World,Large Hydrocarbon Ocean World";
    uint256 private constant rareWorldsLength = 12;

    string private constant rarerWorlds =
        "Small Orbital Sphere,Medium Orbital Sphere,Large Orbital Sphere,Small Orbital Ring,Medium Orbital Ring,Large Orbital Ring";
    uint256 private constant rarerWorldsLength = 6;

    string private constant rarestWorlds =
        "Small Shellworld,Medium Shellworld,Large Shellworld,Dyson Sphere";
    uint256 private constant rarestWorldsLength = 4;

    function random(string memory input) internal pure returns (uint256) {
        return uint256(keccak256(abi.encodePacked(input)));
    }

    function getNumberOfWorlds(uint256 tokenId) public pure returns (uint8) {
        uint256 rand = random(
            string(
                abi.encodePacked("NUMBER_OF_WORLDS", Strings.toString(tokenId))
            )
        );
        uint8 numWorlds = 9;

        uint256 chance = rand & 101;

        if (chance < 11) {
            numWorlds = 5;
        } else if (chance < 31) {
            numWorlds = 6;
        } else if (chance < 59) {
            numWorlds = 7;
        } else if (chance < 87) {
            numWorlds = 8;
        }

        return numWorlds;
    }

    function getSystemId(uint256 tokenId) public pure returns (string memory) {
        string memory systemNumber = Strings.toString(tokenId);
        if (tokenId < 10) {
            systemNumber = string(abi.encodePacked("000", systemNumber));
        } else if (tokenId < 100) {
            systemNumber = string(abi.encodePacked("00", systemNumber));
        } else if (tokenId < 1000) {
            systemNumber = string(abi.encodePacked("0", systemNumber));
        }

        return string(abi.encodePacked("SN-", systemNumber));
    }

    function getWorld(
        uint256 tokenId,
        uint256 worldNumberIndex,
        uint256 worldTypeIndex,
        string memory sourceCSV,
        uint256 sourceCSVLength,
        uint256 inhabitedThreshold,
        uint256 abandonedThreshold
    ) public pure returns (string memory) {
        uint256 rand = random(
            string(
                abi.encodePacked(
                    getItemFromCSV(worldTypes, worldTypeIndex),
                    Strings.toString(tokenId),
                    getItemFromCSV(numbers, worldNumberIndex)
                )
            )
        );

        string memory world = getItemFromCSV(sourceCSV, rand % sourceCSVLength);

        uint256 chance = rand % 101;

        if (chance < inhabitedThreshold) {
            world = string(abi.encodePacked("Inhabited ", world));
        } else if (chance < abandonedThreshold) {
            world = string(abi.encodePacked("Abandoned ", world));
        }

        return world;
    }

    function getWorlds(uint256 tokenId) public pure returns (string[] memory) {
        uint8 numWorlds = getNumberOfWorlds(tokenId);

        string[] memory worlds = new string[](numWorlds);

        for (uint256 worldIndex = 0; worldIndex < numWorlds; worldIndex++) {
            uint256 rand = random(
                string(
                    abi.encodePacked(
                        getItemFromCSV(numbers, worldIndex),
                        Strings.toString(tokenId)
                    )
                )
            );

            uint256 chance = rand % 101;

            string memory world;

            if (chance < 33) {
                world = getWorld(
                    tokenId,
                    worldIndex,
                    0,
                    terrestrialWorlds,
                    terrestrialWorldsLength,
                    41,
                    71
                );
            } else if (chance < 46) {
                world = getWorld(
                    tokenId,
                    worldIndex,
                    1,
                    oceanWorlds,
                    oceanWorldsLength,
                    21,
                    36
                );
            } else if (chance < 63) {
                world = getWorld(
                    tokenId,
                    worldIndex,
                    2,
                    iceWorlds,
                    iceWorldsLength,
                    9,
                    20
                );
            } else if (chance < 93) {
                world = getWorld(
                    tokenId,
                    worldIndex,
                    3,
                    gasWorlds,
                    gasWorldsLength,
                    3,
                    6
                );
            } else if (chance < 97) {
                world = getWorld(
                    tokenId,
                    worldIndex,
                    4,
                    rareWorlds,
                    rareWorldsLength,
                    7,
                    23
                );
            } else if (chance < 100) {
                world = getWorld(
                    tokenId,
                    worldIndex,
                    5,
                    rarerWorlds,
                    rarerWorldsLength,
                    71,
                    101
                );
            } else if (chance == 100) {
                world = getWorld(
                    tokenId,
                    worldIndex,
                    6,
                    rarestWorlds,
                    rarestWorldsLength,
                    71,
                    101
                );
            }

            if (
                keccak256(bytes(world)) ==
                keccak256(bytes("Abandoned Dyson Sphere"))
            ) {
                string[] memory onlyDyson = new string[](1);
                onlyDyson[0] = "Abandoned Dyson Sphere";
                return onlyDyson;
            }

            if (
                keccak256(bytes(world)) ==
                keccak256(bytes("Inhabited Dyson Sphere"))
            ) {
                string[] memory onlyDyson = new string[](1);
                onlyDyson[0] = "Inhabited Dyson Sphere";
                return onlyDyson;
            }

            worlds[worldIndex] = world;
        }

        return worlds;
    }

    function tokenURI(uint256 tokenId)
        public
        pure
        override
        returns (string memory)
    {
        string[] memory worlds = getWorlds(tokenId);

        string[2] memory parts;

        parts[
            0
        ] = '<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 350"><style>.base { fill: #EEEEFF; font-family: sans-serif; font-size: 14px; }</style><rect width="100%" height="100%" fill="black" /><text x="10" y="20" class="base">';

        string memory systemId = getSystemId(tokenId);

        parts[1] = string(
            abi.encodePacked(
                systemId,
                '</text><text x="10" y="40" class="base">'
            )
        );

        string memory output = string(abi.encodePacked(parts[0], parts[1]));

        for (uint8 i = 0; i < worlds.length; i++) {
            string memory world = worlds[i];
            uint256 multiplier = i + 3;
            uint256 yValue = 20 * multiplier;
            string memory yString = Strings.toString(yValue);
            string memory closing = (i != (worlds.length - 1))
                ? string(
                    abi.encodePacked(
                        '</text><text x="10" y="',
                        yString,
                        '" class="base">'
                    )
                )
                : "</text></svg>";
            string memory line = string(abi.encodePacked(world, closing));
            output = string(abi.encodePacked(output, line));
        }

        string memory json = Base64.encode(
            bytes(
                string(
                    abi.encodePacked(
                        '{"name": "System #',
                        Strings.toString(tokenId),
                        '", "description": "Worlds is randomly generated star systems, often inhabited, or once inhabited, created and stored on chain. Stats, images, and other functionality are intentionally omitted for others to interpret. Feel free to use Worlds 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 < 9901, "Token ID invalid");
        _safeMint(_msgSender(), tokenId);
    }

    function ownerClaim(uint256 tokenId) public nonReentrant onlyOwner {
        require(tokenId > 9900 && tokenId < 10001, "Token ID invalid");
        _safeMint(owner(), tokenId);
    }

    function getItemFromCSV(string memory str, uint256 index)
        internal
        pure
        returns (string memory)
    {
        strings.slice memory strSlice = str.toSlice();
        string memory separatorStr = ",";
        strings.slice memory separator = separatorStr.toSlice();
        strings.slice memory item;
        for (uint256 i = 0; i <= index; i++) {
            item = strSlice.split(separator);
        }
        return item.toString();
    }

    constructor() ERC721("Worlds", "WORLDS") Ownable() {}
}

/// [MIT License]
/// @title Base64
/// @notice Provides a function for encoding some bytes in base64
/// @author Brecht Devos <brecht@loopring.org>
library Base64 {
    bytes internal constant TABLE =
        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

    /// @notice Encodes some bytes to the base64 representation
    function encode(bytes memory data) internal pure returns (string memory) {
        uint256 len = data.length;
        if (len == 0) return "";

        // multiply by 4/3 rounded up
        uint256 encodedLen = 4 * ((len + 2) / 3);

        // Add some extra buffer at the end
        bytes memory result = new bytes(encodedLen + 32);

        bytes memory table = TABLE;

        assembly {
            let tablePtr := add(table, 1)
            let resultPtr := add(result, 32)

            for {
                let i := 0
            } lt(i, len) {

            } {
                i := add(i, 3)
                let input := and(mload(add(data, i)), 0xffffff)

                let out := mload(add(tablePtr, and(shr(18, input), 0x3F)))
                out := shl(8, out)
                out := add(
                    out,
                    and(mload(add(tablePtr, and(shr(12, input), 0x3F))), 0xFF)
                )
                out := shl(8, out)
                out := add(
                    out,
                    and(mload(add(tablePtr, and(shr(6, input), 0x3F))), 0xFF)
                )
                out := shl(8, out)
                out := add(
                    out,
                    and(mload(add(tablePtr, and(input, 0x3F))), 0xFF)
                )
                out := shl(224, out)

                mstore(resultPtr, out)

                resultPtr := add(resultPtr, 4)
            }

            switch mod(len, 3)
            case 1 {
                mstore(sub(resultPtr, 2), shl(240, 0x3d3d))
            }
            case 2 {
                mstore(sub(resultPtr, 1), shl(248, 0x3d))
            }

            mstore(result, encodedLen)
        }

        return string(result);
    }
}

/*
 * @title String & slice utility library for Solidity contracts.
 * @author Nick Johnson <arachnid@notdot.net>
 *
 * @dev Functionality in this library is largely implemented using an
 *      abstraction called a 'slice'. A slice represents a part of a string -
 *      anything from the entire string to a single character, or even no
 *      characters at all (a 0-length slice). Since a slice only has to specify
 *      an offset and a length, copying and manipulating slices is a lot less
 *      expensive than copying and manipulating the strings they reference.
 *
 *      To further reduce gas costs, most functions on slice that need to return
 *      a slice modify the original one instead of allocating a new one; for
 *      instance, `s.split(".")` will return the text up to the first '.',
 *      modifying s to only contain the remainder of the string after the '.'.
 *      In situations where you do not want to modify the original slice, you
 *      can make a copy first with `.copy()`, for example:
 *      `s.copy().split(".")`. Try and avoid using this idiom in loops; since
 *      Solidity has no memory management, it will result in allocating many
 *      short-lived slices that are later discarded.
 *
 *      Functions that return two slices come in two versions: a non-allocating
 *      version that takes the second slice as an argument, modifying it in
 *      place, and an allocating version that allocates and returns the second
 *      slice; see `nextRune` for example.
 *
 *      Functions that have to copy string data will return strings rather than
 *      slices; these can be cast back to slices for further processing if
 *      required.
 *
 *      For convenience, some functions are provided with non-modifying
 *      variants that create a new slice and return both; for instance,
 *      `s.splitNew('.')` leaves s unmodified, and returns two values
 *      corresponding to the left and right parts of the string.
 */

pragma solidity ^0.8.9;

library strings {
    struct slice {
        uint256 _len;
        uint256 _ptr;
    }

    function memcpy(
        uint256 dest,
        uint256 src,
        uint256 len
    ) private pure {
        // Copy word-length chunks while possible
        for (; len > 32; len -= 32) {
            assembly {
                mstore(dest, mload(src))
            }
            dest += 32;
            src += 32;
        }

        // Copy remaining bytes
        uint256 mask = 256**(32 - len) - 1;
        assembly {
            let srcpart := and(mload(src), not(mask))
            let destpart := and(mload(dest), mask)
            mstore(dest, or(destpart, srcpart))
        }
    }

    /*
     * @dev Returns a slice containing the entire string.
     * @param self The string to make a slice from.
     * @return A newly allocated slice containing the entire string.
     */
    function toSlice(string memory self) internal pure returns (slice memory) {
        uint256 ptr;
        assembly {
            ptr := add(self, 0x20)
        }
        return slice(bytes(self).length, ptr);
    }

    /*
     * @dev Copies a slice to a new string.
     * @param self The slice to copy.
     * @return A newly allocated string containing the slice's text.
     */
    function toString(slice memory self) internal pure returns (string memory) {
        string memory ret = new string(self._len);
        uint256 retptr;
        assembly {
            retptr := add(ret, 32)
        }

        memcpy(retptr, self._ptr, self._len);
        return ret;
    }