contract SparkleMuffinz is SparkleMuffinzERC721 {
    using Address for address;
    using Strings for uint256;

    struct MuffinInfo {
        string name;
        string description;
        bool locked;
        uint256 tier;
    }

    event UpdateName(uint256 indexed tokenId, string name);
    event UpdateDescription(uint256 indexed tokenId, string description);
    event UpdateLockStatus(uint256 indexed tokenId, bool lockStatus);
    event UpdateTier(uint256 indexed tokenId, uint256 previousTier, uint256 newTier);
    event MassUnlock();

    IWeb public Web;

    uint256 public constant MAX_TIER = 3;

    uint256 public updateNamePrice = 50 ether;
    uint256 public updateDescriptionPrice = 50 ether;
    uint256 public bakePrice = 100 ether;

    mapping(uint256 => MuffinInfo) public muffinInfo;

    constructor(string memory unrevealedTokenURI, bytes32 root) SparkleMuffinzERC721(unrevealedTokenURI, root) {}

    // MODIFIERS

    modifier onlyTokenOwner(uint256 tokenId) {
        require(ownerOf(tokenId) == msg.sender, "Sender is not the token owner");
        _;
    }

    // TOKENURI

    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        string memory baseURI = _baseURI();
        if (revealed) {
            bool locked = this.getLockStatus(tokenId);
            if (locked) {
                return string(abi.encodePacked(baseURI, tokenId.toString(), "-locked"));
            } else {
                uint256 tier = this.getTier(tokenId);
                return string(abi.encodePacked(baseURI, tokenId.toString(), "-", tier.toString()));
            }
        } else {
            return unrevealedTokenURI;
        }
    }

    // ADMIN FUNCTIONS

    function setWebAddress(address web) public onlyOwner {
        Web = IWeb(web);
    }

    function setUpdateNamePrice(uint256 price) public onlyOwner {
        updateNamePrice = price;
    }

    function setUpdateDescriptionPrice(uint256 price) public onlyOwner {
        updateDescriptionPrice = price;
    }

    function setBakePrice(uint256 price) public onlyOwner {
        bakePrice = price;
    }

    function massUnlock() public onlyOwner {
        for (uint256 i = 0; i < _tokenIds; i++) {
            muffinInfo[i].locked = false;
        }
        emit MassUnlock();
    }

    // VIEW FUNCTIONS

    function getName(uint256 tokenId) external view returns (string memory) {
        return muffinInfo[tokenId].name;
    }

    function getDescription(uint256 tokenId) external view returns (string memory) {
        return muffinInfo[tokenId].description;
    }

    function getLockStatus(uint256 tokenId) external view returns (bool) {
        return muffinInfo[tokenId].locked;
    }

    function getTier(uint256 tokenId) external view returns (uint256) {
        return muffinInfo[tokenId].tier;
    }

    // UPDATE FUNCTIONS

    function updateName(uint256 tokenId, string calldata name) public onlyTokenOwner(tokenId) {
        require(address(Web) != address(0), "No token contract set");

        bytes memory n = bytes(name);
        require(n.length > 0 && n.length < 25, "Invalid name length");
        require(sha256(n) != sha256(bytes(muffinInfo[tokenId].name)), "New name is same as current name");

        Web.burn(msg.sender, updateNamePrice);

        muffinInfo[tokenId].name = name;
        emit UpdateName(tokenId, name);
    }

    function updateDescription(uint256 tokenId, string calldata description) public onlyTokenOwner(tokenId) {
        require(address(Web) != address(0), "No token contract set");

        bytes memory d = bytes(description);
        require(d.length > 0 && d.length < 280, "Invalid description length");
        require(
            sha256(bytes(d)) != sha256(bytes(muffinInfo[tokenId].description)),
            "New description is same as current description"
        );

        Web.burn(msg.sender, updateDescriptionPrice);

        muffinInfo[tokenId].description = description;
        emit UpdateDescription(tokenId, description);
    }

    // override transferFrom to update WEB token reward balances
    function transferFrom(
        address from,
        address to,
        uint256 tokenId
    ) public override nonReentrant {
        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved");

        if (address(Web) != address(0)) {
            Web.update(from, to);
        }

        ERC721.transferFrom(from, to, tokenId);
    }

    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId,
        bytes memory data
    ) public override nonReentrant {
        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved");

        if (address(Web) != address(0)) {
            Web.update(from, to);
        }

        ERC721.safeTransferFrom(from, to, tokenId, data);
    }

    function bake(uint256 tokenId) public onlyTokenOwner(tokenId) {
        require(address(Web) != address(0), "No token contract set");
        require(!this.getLockStatus(tokenId), "Token is locked");

        uint256 currentTier = this.getTier(tokenId);
        require(currentTier < MAX_TIER, "Token already max tier");

        Web.burn(msg.sender, bakePrice);

        uint256 pseudoRandomNumber = _genPseudoRandomNumber(tokenId);
        if (pseudoRandomNumber == 0) {
            muffinInfo[tokenId].locked = true;
            muffinInfo[tokenId].tier = 0;
            emit UpdateLockStatus(tokenId, true);
            emit UpdateTier(tokenId, currentTier, 0);
        } else {
            muffinInfo[tokenId].tier = currentTier + 1;
            emit UpdateTier(tokenId, currentTier, currentTier + 1);
        }
    }

    function _genPseudoRandomNumber(uint256 tokenId) private view returns (uint256) {
        uint256 pseudoRandomHash = uint256(keccak256(abi.encodePacked(block.difficulty, block.timestamp, tokenId)));
        return pseudoRandomHash % 2;
    }
}
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
    string memory baseURI = _baseURI();
    if (revealed) {
        bool locked = this.getLockStatus(tokenId);
        if (locked) {
            return string(abi.encodePacked(baseURI, tokenId.toString(), "-locked"));
        } else {
            uint256 tier = this.getTier(tokenId);
            return string(abi.encodePacked(baseURI, tokenId.toString(), "-", tier.toString()));
        }
    } else {
        return unrevealedTokenURI;
    }
}

// TOKENURI