pragma solidity ^0.8.11;
// SPDX-License-Identifier: UNLICENSED

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Pausable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Royalty.sol";

contract CrypteesToken is ERC721, ERC721Pausable, ERC721Royalty, Ownable {
    event TokenCreated(string uuid, address to);
    event BaseURISet(string uri);

    address public proxyMinter; // only account that can call mintFor()
    string private _baseTokenURI;
    mapping (uint256 => bytes32) private tokenIdToUuid;  // Maps token id to UUID
    mapping (bytes32 => uint256) private uuidToTokenId;  // Maps UUID to token id
    mapping (uint256 => string) private tokenURIMap;

    constructor(string memory baseTokenURI) ERC721("Cryptees", "Cryptees") {
        _baseTokenURI = baseTokenURI;
        proxyMinter = _msgSender();
        setDefaultRoyalty(_msgSender(), 1000);
    }

    function setTokenRoyalty(uint256 tokenId, address recipient, uint96 feeNumerator) public onlyOwner {
        _setTokenRoyalty(tokenId, recipient, feeNumerator);
    }

    function setDefaultRoyalty(address recipient, uint96 fraction) public onlyOwner {
        _setDefaultRoyalty(recipient, fraction);
    }

    function deleteDefaultRoyalty() public onlyOwner {
        _deleteDefaultRoyalty();
    }

    function createToken(uint256 tokenId, string memory uuid, address to) public onlyOwner {
        bytes32 uuidBytes32 = UUIDStringToBytes32(uuid);
        createTokenWithId(tokenId, uuidBytes32, to);
    }

    function burn(uint256 tokenId) public onlyOwner {
        _burn(tokenId);
    }

    function mintFor(address to, uint256 amount, bytes memory mintingBlob) public {
        require(proxyMinter == _msgSender(), "Caller is not the proxy minter.");
        (uint256[] memory tokenIds, bytes32[] memory uuids) = parseMintingBlob(mintingBlob, amount);
        for (uint i; i < tokenIds.length; i++) {
            createTokenWithId(tokenIds[i], uuids[i], to);
        }
    }

    function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721Royalty, ERC721) returns (bool) {
        return super.supportsInterface(interfaceId);
    }

    function getTokenIdOfUUID(string memory uuid) public view returns (uint256) {
        bytes32 uuid32 = UUIDStringToBytes32(uuid);
        require(uuidExists(uuid32), "UUID does not exist.");
        return uuidToTokenId[uuid32];
    }

    function getUUIDOfTokenId(uint256 tokenId) public view returns (string memory) {
        require(_exists(tokenId), "Token does not exist.");
        return bytes32ToUUIDString(tokenIdToUuid[tokenId]);
    }

    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
        if (bytes(tokenURIMap[tokenId]).length > 0) {
            return tokenURIMap[tokenId];
        }
        string memory uuid = bytes32ToUUIDString(tokenIdToUuid[tokenId]);
        string memory baseURI = _baseURI();
        return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, uuid)) : '';
    }

    function setBaseTokenURI(string memory uri) public onlyOwner {
        _baseTokenURI = uri;
        emit BaseURISet(uri);
    }

    function setTokenURI(uint256 tokenId, string memory uri) public onlyOwner {
        tokenURIMap[tokenId] = uri;
    }

    function clearTokenURI(uint256 tokenId) public onlyOwner {
        delete tokenURIMap[tokenId];
    }

    function pause() public virtual onlyOwner {
        _pause();
    }

    function unpause() public virtual onlyOwner {
        _unpause();
    }

    function setProxyMinter(address proxy) public onlyOwner {
        proxyMinter = proxy;
    }

    // ------- Internal functions below here:
    function createTokenWithId(uint256 tokenId, bytes32 uuid, address to) internal {
        require(!_exists(tokenId), "Token already exists.");
        _safeMint(to, tokenId);

        uuidToTokenId[uuid] = tokenId;
        tokenIdToUuid[tokenId] = uuid;
        emit TokenCreated(bytes32ToUUIDString(uuid), to);
    }

    // Parses a mintingBlob with {tokenId1}:{uuid1}{tokenId2}:{uuid2} and then returns them
    // Example: {1}:{e9071858-e63b-4f27-92d7-0603235c0b8c}{2}:{f23a1b52-e63b-3f77-42d7-3603235c1b42}
    function parseMintingBlob(bytes memory mintingBlob, uint256 amount) internal pure returns (uint256[] memory, bytes32[] memory) {
        uint256 offset = 0;
        uint256[] memory tokenIds = new uint256[](amount);
        bytes32[] memory uuids = new bytes32[](amount);
        bool isTokenId = true;
        for (uint i; i < mintingBlob.length; i++) {
            if (mintingBlob[i] == "{") {
                if (isTokenId) {
                    (uint256 tokenId, uint256 nextItem) = parseMintingBlobTokenId(mintingBlob, i + 1);
                    tokenIds[offset] = tokenId;
                    isTokenId = false;
                    i = nextItem;
                } else {
                    (bytes32 uuid, uint256 nextItem) = parseMintingBlobUUID(mintingBlob, i + 1);
                    uuids[offset] = uuid;
                    isTokenId = true;
                    offset++;
                    i = nextItem;
                }
            }
        }
        return (tokenIds, uuids);
    }

    function parseMintingBlobTokenId(bytes memory mintingBlob, uint start) internal pure returns (uint256, uint256) {
        uint256 result = 0;
        for (uint256 i = start; i < mintingBlob.length; i++) {
            if (mintingBlob[i] == "}") {
                return (result, i);
            }
            result = (result * 10) + (uint8(mintingBlob[i]) - 48);
        }
        return (result, mintingBlob.length);
    }

    function parseMintingBlobUUID(bytes memory mintingBlob, uint start) internal pure returns (bytes32, uint256) {
        bytes memory temp = new bytes(32);
        bytes32 result;
        uint32 index;
        for (uint i = start; i < mintingBlob.length; i++) {
            if (mintingBlob[i] == "}") {
                assembly {
                    result := mload(add(temp, 32))
                }
                return (result, i);
            }
            if (mintingBlob[i] == "-") {
                continue;
            }
            temp[index] = mintingBlob[i];
            index++;
        }
        assembly {
            result := mload(add(temp, 32))
        }
        return (result, mintingBlob.length);
    }

    function uuidExists(bytes32 uuid) internal view returns (bool) {
        return uuidToTokenId[uuid] != 0;
    }

    function _baseURI() internal view virtual override returns (string memory) {
        return _baseTokenURI;
    }

    function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal virtual override(ERC721, ERC721Pausable) {
        super._beforeTokenTransfer(from, to, tokenId);
    }

    function _burn(uint256 tokenId) internal virtual override(ERC721Royalty, ERC721) {
        super._burn(tokenId);
        _resetTokenRoyalty(tokenId);
    }

    // Converts a bytes32 UUID to a UUID string with dashes
    // e9071858e63b4f2792d70603235c0b8c => e9071858-e63b-4f27-92d7-0603235c0b8c
    function bytes32ToUUIDString(bytes32 b) internal pure returns (string memory) {
        bytes memory bytesArray = new bytes(36);
        uint j;
        for (uint256 i; i < 32; i++) {
            bytesArray[j] = b[i];
            // Add back in dashes at correct positions
            if (i == 7 || i == 11 || i == 15 || i == 19) {
                j++;
                bytesArray[j] = "-";
            }
            j++;
        }
        return string(bytesArray);
    }

    // Converts a UUID string with dashes into a bytes32 UUID without dashes
    // e9071858-e63b-4f27-92d7-0603235c0b8c => e9071858e63b4f2792d70603235c0b8c
    function UUIDStringToBytes32(string memory s) internal pure returns (bytes32) {
        bytes memory bytesArray = bytes(s);
        bytes memory noDashes = new bytes(32);
        uint index;
        for (uint256 i; i < bytesArray.length; i++) {
            if (bytesArray[i] == "-") {
                continue;
            }
            noDashes[index] = bytesArray[i];
            index++;
        }
        bytes32 result;
        assembly {
            result := mload(add(noDashes, 32))
        }
        return result;
    }
}
[{"inputs":[{"internalType":"string","name":"baseTokenURI","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"approved","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":false,"internalType":"bool","name":"approved","type":"bool"}],"name":"ApprovalForAll","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"uri","type":"string"}],"name":"BaseURISet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"account","type":"address"}],"name":"Paused","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"uuid","type":"string"},{"indexed":false,"internalType":"address","name":"to","type":"address"}],"name":"TokenCreated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"account","type":"address"}],"name":"Unpaused","type":"event"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"approve","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"burn","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"clearTokenURI","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"string","name":"uuid","type":"string"},{"internalType":"address","name":"to","type":"address"}],"name":"createToken","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"deleteDefaultRoyalty","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"getApproved","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"uuid","type":"string"}],"name":"getTokenIdOfUUID","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"getUUIDOfTokenId","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"operator","type":"address"}],"name":"isApprovedForAll","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"bytes","name":"mintingBlob","type":"bytes"}],"name":"mintFor","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ownerOf","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"paused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"proxyMinter","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_tokenId","type":"uint256"},{"internalType":"uint256","name":"_salePrice","type":"uint256"}],"name":"royaltyInfo","outputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"bool","name":"approved","type":"bool"}],"name":"setApprovalForAll","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"uri","type":"string"}],"name":"setBaseTokenURI","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint96","name":"fraction","type":"uint96"}],"name":"setDefaultRoyalty","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"proxy","type":"address"}],"name":"setProxyMinter","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint96","name":"feeNumerator","type":"uint96"}],"name":"setTokenRoyalty","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"string","name":"uri","type":"string"}],"name":"setTokenURI","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"tokenURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"transferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"unpause","outputs":[],"stateMutability":"nonpayable","type":"function"}]