contract BitMonsters is IBitMonsters, ERC721Enumerable, Ownable {
    uint256 constant public SUPPLY_LIMIT = 6666;
    using RngLibrary for Rng;

    mapping (uint256 => BitMonster) public tokenIdToBitMonster;

    Brainz public brainz;
    Mutator public mutator;
    Minter public minter;
    Metadata public metadata;
    mapping (address => bool) private admins;

    bool private initialized;

    /**
     * @param whitelistedAddrs The addresses that are allowed to mint when the mintingState is WhiteListOnly.
                               The owner of the contract is automatically whitelisted, so the owning address doesn't need to be given.
     */
    constructor(address[] memory whitelistedAddrs) ERC721("Bit Monsters", unicode"💰🧟") {
        brainz = new Brainz();
        mutator = new Mutator(brainz);
        minter = new Minter(payable(msg.sender), whitelistedAddrs);
        metadata = new Metadata();
        address[5] memory a = [msg.sender, address(brainz), address(mutator), address(minter), address(metadata)];
        for (uint i = 0; i < a.length; ++i) {
            admins[a[i]] = true;
        }
    }

    function isAdmin(address addr) public view override returns (bool) {
        return owner() == addr || admins[addr];
    }

    modifier onlyAdmin() {
        require(isAdmin(msg.sender), "admins only");
        _;
    }

    function addAdmin(address addr) external onlyAdmin {
        admins[addr] = true;
    }

    function removeAdmin(address addr) external onlyAdmin {
        admins[addr] = false;
    }

    /**
     * Initializes the sub contracts so they're ready for use.
     * @notice IMPORTANT: This must be called before any other contract functions.
     *
     * @dev This can't be done in the constructor, because the contract doesn't have an address until the transaction is mined.
     */
    function initialize() external onlyAdmin {
        if (initialized) {
            return;
        }
        initialized = true;

        admins[address(this)] = true;
        brainz.setBitMonstersContract(this);
        metadata.setBitMonstersContract(this);
        mutator.setBitMonstersContract(this);
        minter.setBitMonstersContract(this);
    }

    /**
     * Returns the metadata of the Bit Monster corresponding to the given tokenId as a base64-encoded JSON object. Meant for use with OpenSea.
     *
     * @dev This function can take a painful amount of time to run, sometimes exceeding 9 minutes in length. Use getBitMonster() instead for frontends.
     */
    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        require(_exists(tokenId), "the token doesn't exist");

        string memory metadataRaw = metadata.getMetadataJson(tokenId);
        string memory metadataB64 = Base64.encode(bytes(metadataRaw));

        return string(abi.encodePacked(
            "data:application/json;base64,",
            metadataB64
        ));
    }

    /**
     * Returns the internal representation of the Bit Monster corresponding to the given tokenId.
     */
    function getBitMonster(uint256 tokenId) external view override returns (BitMonster memory) {
        return tokenIdToBitMonster[tokenId];
    }

    function setBitMonster(uint256 tokenId, BitMonster memory bm) public override onlyAdmin {
        tokenIdToBitMonster[tokenId] = bm;
    }

    function createBitMonster(BitMonster memory bm, address owner) external override onlyAdmin {
        uint total = totalSupply();
        require(total <= SUPPLY_LIMIT, "Supply limit reached");

        uint tid = total + 1;
        _mint(owner, tid);
        setBitMonster(tid, bm);

        brainz.register(tid);
    }
}
/**
 * @title The Bit Monsters contract. This is where all of the magic happens.
 */
function initialize() external onlyAdmin {
        if (initialized) {
            return;
        }
        initialized = true;

        admins[address(this)] = true;
        brainz.setBitMonstersContract(this);
        metadata.setBitMonstersContract(this);
        mutator.setBitMonstersContract(this);
        minter.setBitMonstersContract(this);
    }
/**
 * Initializes the sub contracts so they're ready for use.
 * @notice IMPORTANT: This must be called before any other contract functions.
 *
 * @dev This can't be done in the constructor, because the contract doesn't have an address until the transaction is mined.
 */