contract VeveTokenV2 is
    Initializable,
    ERC721Upgradeable,
    ERC721EnumerableUpgradeable,
    PausableUpgradeable,
    AccessControlUpgradeable,
    UUPSUpgradeable
{
    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
    bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");

    event EnabledImx(address account);
    event DisabledImx(address account);
    modifier whenImxEnabled() {
        require(imxEnabled, "Imx L2 is disabled");
        _;
    }
    modifier whenImxDisabled() {
        require(!imxEnabled, "Imx L2 is enabled");
        _;
    }
    bool public imxEnabled;
    event UpdatedImx(address imx);
    modifier onlyIMX() {
        require(msg.sender == imx, "Function can only be called by IMX");
        _;
    }
    address public imx;

    string public baseURI;
    mapping(bytes => uint256) private tokenBlueprintToTokenId;
    mapping(uint256 => string) private tokenNames;
    mapping(uint256 => uint256) private tokenIssueNumbers;

    function initialize(string memory baseURI_, address imx_)
        public
        initializer
    {
        __ERC721_init("Veve Token", "VEVE");
        __ERC721Enumerable_init();
        __Pausable_init();
        __AccessControl_init();
        __UUPSUpgradeable_init();

        _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _setupRole(PAUSER_ROLE, msg.sender);
        _setupRole(MINTER_ROLE, msg.sender);
        _setupRole(BURNER_ROLE, msg.sender);
        _setupRole(UPGRADER_ROLE, msg.sender);

        baseURI = baseURI_;
        imxEnabled = true;
        imx = imx_;
    }

    function version() public pure returns (string memory) {
        return "2";
    }

    /**
     *  Management
     */

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

    function setBaseURI(string memory baseURI_)
        public
        onlyRole(DEFAULT_ADMIN_ROLE)
    {
        baseURI = baseURI_;
    }

    function pause() public onlyRole(PAUSER_ROLE) {
        _pause();
    }

    function unpause() public onlyRole(PAUSER_ROLE) {
        _unpause();
    }

    function _authorizeUpgrade(address newImplementation)
        internal
        override
        onlyRole(UPGRADER_ROLE)
    {}

    /**
     *  ImmutableX
     */

    function setImx(address _imx) public onlyRole(DEFAULT_ADMIN_ROLE) {
        imx = _imx;
        emit UpdatedImx(imx);
    }

    function enableImx() public whenImxDisabled onlyRole(DEFAULT_ADMIN_ROLE) {
        imxEnabled = true;
        emit EnabledImx(_msgSender());
    }

    function disableImx() public whenImxEnabled onlyRole(DEFAULT_ADMIN_ROLE) {
        imxEnabled = false;
        emit DisabledImx(_msgSender());
    }

    /**
     *  Token Helpers
     */

    function exists(uint256 tokenId) public view returns (bool) {
        return _exists(tokenId);
    }

    function getDetailsByBlueprint(bytes memory blueprint)
        public
        view
        returns (uint256 tokenId, string memory tokenURI)
    {
        uint256 _tokenId = tokenBlueprintToTokenId[blueprint];
        return (_tokenId, super.tokenURI(_tokenId));
    }

    function getDetails(uint256 tokenId)
        public
        view
        returns (
            string memory name,
            uint256 issueNumber,
            string memory tokenURI
        )
    {
        return (
            tokenNames[tokenId],
            tokenIssueNumbers[tokenId],
            super.tokenURI(tokenId)
        );
    }

    /**
     *  Minting
     */

    function safeMint(
        address to,
        uint256 tokenId,
        bytes calldata mintingBlob
    ) public onlyRole(MINTER_ROLE) {
        (
            uint256 _tokenId,
            bytes memory blueprint,
            bytes memory name,
            uint256 issueNumber
        ) = BlueprintV2.deserialize(mintingBlob);
        require(tokenId == _tokenId, "Token ids do not match");
        require(
            tokenBlueprintToTokenId[blueprint] == 0,
            "Token blueprint already minted"
        );
        _safeMint(to, _tokenId);
        tokenBlueprintToTokenId[blueprint] = _tokenId;
        tokenNames[_tokenId] = string(name);
        tokenIssueNumbers[_tokenId] = issueNumber;
    }

    function mint(
        address to,
        uint256 tokenId,
        bytes calldata mintingBlob
    ) public onlyRole(MINTER_ROLE) {
        (
            uint256 _tokenId,
            bytes memory blueprint,
            bytes memory name,
            uint256 issueNumber
        ) = BlueprintV2.deserialize(mintingBlob);
        require(tokenId == _tokenId, "Token ids do not match");
        require(
            tokenBlueprintToTokenId[blueprint] == 0,
            "Token blueprint already minted"
        );
        _mint(to, _tokenId);
        tokenBlueprintToTokenId[blueprint] = _tokenId;
        tokenNames[_tokenId] = string(name);
        tokenIssueNumbers[_tokenId] = issueNumber;
    }

    function mintFor(
        address to,
        uint256 quantity,
        bytes calldata mintingBlob
    ) public whenImxEnabled onlyIMX {
        require(quantity == 1, "Can only mint one NFT at a time");
        (
            uint256 _tokenId,
            bytes memory blueprint,
            bytes memory name,
            uint256 issueNumber
        ) = BlueprintV2.deserialize(mintingBlob);
        require(
            tokenBlueprintToTokenId[blueprint] == 0,
            "Token blueprint already minted"
        );
        _mint(to, _tokenId);
        tokenBlueprintToTokenId[blueprint] = _tokenId;
        tokenNames[_tokenId] = string(name);
        tokenIssueNumbers[_tokenId] = issueNumber;
    }

    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    )
        internal
        override(ERC721Upgradeable, ERC721EnumerableUpgradeable)
        whenNotPaused
    {
        super._beforeTokenTransfer(from, to, tokenId);
    }

    /**
     *  Burning
     */

    function burn(uint256 tokenId) public onlyRole(BURNER_ROLE) {
        string memory name = tokenNames[tokenId];
        uint256 issueNumber = tokenIssueNumbers[tokenId];
        bytes memory tokenBlueprint = BlueprintV2.serialize(name, issueNumber);
        require(
            tokenBlueprintToTokenId[tokenBlueprint] == tokenId,
            "Token id does not match metadata"
        );
        super._burn(tokenId);
        delete tokenBlueprintToTokenId[tokenBlueprint];
        delete tokenNames[tokenId];
        delete tokenIssueNumbers[tokenId];
    }

    // The following functions are overrides required by Solidity.

    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(
            ERC721Upgradeable,
            ERC721EnumerableUpgradeable,
            AccessControlUpgradeable
        )
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}
function _baseURI() internal view override returns (string memory) {
    return baseURI;
}

/**
 *  Management
 */