contract FloorToken is IFloorToken, Ownable, ERC721Checkpointable, FxBaseRootTunnel {
    // The DAO address
    address public floorsDAO;

    // An address who has permissions to mint Floor
    address public minter;

    // An address who has permissions to mint Floor
    address public predicate;

    // The Floor token URI descriptor
    IFloorDescriptor public descriptor;

    // Whether the minter can be updated
    bool public isMinterLocked;

    // Whether the descriptor can be updated
    bool public isDescriptorLocked;

    // The internal floor ID tracker
    uint256 private _currentFloorId;

    // IPFS content hash of contract-level metadata
    string private _contractURIHash = 'QmYuMm7YikekmHC3oQcvBP3fZErJPoPiPAdhDfvCt3jsSH';

    mapping(uint256 => uint16) public sizes;

    mapping (uint256 => bool) public withdrawnTokens;

    // OpenSea's Proxy Registry
    IProxyRegistry public immutable proxyRegistry;

    /**
     * @notice Require that the minter has not been locked.
     */
    modifier whenMinterNotLocked() {
        require(!isMinterLocked, 'Minter is locked');
        _;
    }

    /**
     * @notice Require that the descriptor has not been locked.
     */
    modifier whenDescriptorNotLocked() {
        require(!isDescriptorLocked, 'Descriptor is locked');
        _;
    }

    /**
     * @notice Require that the sender is the DAO.
     */
    modifier onlyFloorsDAO() {
        require(msg.sender == floorsDAO, 'Sender is not the DAO');
        _;
    }

    /**
     * @notice Require that the sender is the minter.
     */
    modifier onlyMinter() {
        require(msg.sender == minter, 'Sender is not the minter');
        _;
    }

    /**
     * @notice Require that the sender is the predicate.
     */
    modifier onlyPredicate() {
        require(msg.sender == predicate, 'Sender is not the predicate');
        _;
    }

    constructor(
        address _floorsDAO,
        address _minter,
        address _predicate,
        IFloorDescriptor _descriptor,
        IProxyRegistry _proxyRegistry,
        address _checkpointManager,
        address _fxRoot
    )
        ERC721('FLOOR', 'FLOOR')
        FxBaseRootTunnel(_checkpointManager, _fxRoot)
    {
        descriptor = _descriptor;
        predicate = _predicate;
        floorsDAO = _floorsDAO;
        minter = _minter;
        proxyRegistry = _proxyRegistry;
    }

    /**
     * @notice Generate a pseudo-random number using the previous blockhash and floor ID.
     */
    function randNumber(uint256 floorId) private view returns(uint256) {
        uint256 pseudorandomness = uint256(
            keccak256(abi.encodePacked(blockhash(block.number - 1), floorId))
        );

        return (pseudorandomness - ((pseudorandomness / 10000) * 10000));
    }

    /**
     * @notice Generate a pseudo-random floor size based on pseudo-random number.
     * Distribution of Sizes: 8 slots (11%), 16 slots (50%), 32 slots (13%), 64 slots (9%), 128 slots (7%), 256 slots (5%), 1 slots (5%), 3 slots (5%), 7 slots (5%), 33 slots (1%), 69 slots (1%), 420 slots (0.5%)
     */
    function generateSize(uint256 floorId) private view returns(uint16) {
        uint256 x = randNumber(floorId);
        if (x < 50)     return 420;
        if (x < 100)    return 69;
        if (x < 200)    return 33;
        if (x < 300)    return 7;
        if (x < 400)    return 3;
        if (x < 500)    return 1;
        if (x < 1000)   return 256;
        if (x < 1700)   return 128;
        if (x < 2600)   return 64;
        if (x < 3900)   return 32;
        if (x < 8900)   return 16;
        if (x < 10000)  return 8;

        return 0;
    }

    /**
     * @notice The IPFS URI of contract-level metadata.
     */
    function contractURI() public view returns (string memory) {
        return string(abi.encodePacked('ipfs://', _contractURIHash));
    }

    /**
     * @notice Set the _contractURIHash.
     * @dev Only callable by the owner.
     */
    function setContractURIHash(string memory newContractURIHash) external onlyOwner {
        _contractURIHash = newContractURIHash;
    }

    /**
     * @notice Override isApprovedForAll to whitelist user's OpenSea proxy accounts to enable gas-less listings.
     */
    function isApprovedForAll(address owner, address operator) public view override(IERC721, ERC721) returns (bool) {
        // Whitelist OpenSea proxy contract for easy trading.
        if (proxyRegistry.proxies(owner) == operator) {
            return true;
        }
        return super.isApprovedForAll(owner, operator);
    }

    /**
     * @notice Mint a Floor to the minter.
     * Minted every 10 Floors, starting at 0,
     * until 110 Floors have been minted (3 years w/ 24 hour auctions).
     * @dev Call _mintTo with the to address(es).
     */
    function mint() public override onlyMinter returns (uint256) {
        if (_currentFloorId <= 1092 && _currentFloorId % 10 == 0) {
            _mintTo(floorsDAO, _currentFloorId++);
        }
        return _mintTo(minter, _currentFloorId++);
    }

    /**
     * @notice Burn a floor.
     */
    function burn(uint256 floorId) override public onlyMinter {
        _burn(floorId);
        emit FloorBurned(floorId);
    }

    /**
     * @notice A distinct Uniform Resource Identifier (URI) for a given asset.
     * @dev See {IERC721Metadata-tokenURI}.
     */
    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        require(_exists(tokenId), 'FLOOR: URI query for nonexistent token');
        return descriptor.tokenURI(tokenId, sizes[tokenId]);
    }

    /**
     * @notice Similar to `tokenURI`, but always serves a base64 encoded data URI
     * with the JSON contents directly inlined.
     */
    function dataURI(uint256 tokenId) public view override returns (string memory) {
        require(_exists(tokenId), 'FLOOR: URI query for nonexistent token');
        return descriptor.dataURI(tokenId, sizes[tokenId]);
    }

    /**
     * @notice Set the token URI descriptor.
     * @dev Only callable by the owner when not locked.
     */
    function setDescriptor(IFloorDescriptor _descriptor) external override onlyOwner whenDescriptorNotLocked {
        descriptor = _descriptor;

        emit DescriptorUpdated(_descriptor);
    }

    /**
     * @notice Lock the descriptor.
     * @dev This cannot be reversed and is only callable by the owner when not locked.
     */
    function lockDescriptor() external override onlyOwner whenDescriptorNotLocked {
        isDescriptorLocked = true;

        emit DescriptorLocked();
    }

    /**
     * @notice Set the DAO.
     * @dev Only callable by the DAO when not locked.
     */
    function setFloorsDAO(address _floorsDAO) override external onlyFloorsDAO {
        floorsDAO = _floorsDAO;

        emit FloorsDAOUpdated(_floorsDAO);
    }

    /**
     * @notice Set the token minter.
     * @dev Only callable by the owner when not locked.
     */
    function setMinter(address _minter) override external onlyOwner whenMinterNotLocked {
        minter = _minter;

        emit MinterUpdated(_minter);
    }

    /**
     * @notice Lock the minter.
     * @dev This cannot be reversed and is only callable by the owner when not locked.
     */
    function lockMinter() override external onlyOwner whenMinterNotLocked {
        isMinterLocked = true;

        emit MinterLocked();
    }

    /**
     * @notice Mint a Floor with `floorId` to the provided `to` address.
     */
    function _mintTo(address to, uint256 floorId) internal returns (uint256) {
        sizes[floorId] = generateSize(floorId);
        _mint(owner(), to, floorId);
        sendMessageToChild(abi.encode(floorId, sizes[floorId], tokenURI(floorId)));
        emit FloorCreated(floorId, sizes[floorId]);

        return floorId;
    }

    // L1 <> L2
    function mint(address user, uint256 tokenId) external onlyPredicate {
        _mint(address(0), user, tokenId);
    }

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

    // Fx-Portal
    function _processMessageFromChild(bytes memory data) internal override {
        // NOTHING TO DO HERE
    }

    function sendMessageToChild(bytes memory message) private {
        _sendMessageToChild(message);
    }

    function updateMetadata(uint256 floorId) external onlyOwner {
        sendMessageToChild(abi.encode(floorId, sizes[floorId], tokenURI(floorId)));
    }
}
function mint(address user, uint256 tokenId) external onlyPredicate {
    _mint(address(0), user, tokenId);
}

// L1 <> L2