contract Metastonez is Ownable, ERC721Checkpointable, VRFConsumerBase {
  using Counters for Counters.Counter;
  using Strings for uint256;

  // Metastonez is structured in several batches.
  //
  // A batch represents a range of tokens, where each batch
  // contains metadata generated with different parameters from other batches.
  //
  // Metadata is shuffled locally within each batch using entropy set
  // during reveal of the tokens.
  //
  // All batches are revealed at the same time.
  struct Batch {
    // The starting token ID in this batch (inclusive)
    uint256 startIndex;
    // The ending token ID in this batch (exclusive)
    uint256 endIndex;
    // The hash of the metadata URI
    bytes32 metadataHash;
    // The metadata URI (where `keccak256(metadata) == metadataHash`)
    string metadata;
    // The price per token in the batch. Batches are priced differently
    // because they are generated with different parameters.
    uint256 unitPrice;
    // The merkle root of the whitelist of the batch.
    bytes32 merkleRoot;
    // A bitmap of spent already spent merkle leaves in the whitelist.
    mapping(uint256 => uint256) spentLeaves;
    // The entropy used to shuffle the tokens.
    // Once this is set, all tokens are revealed simultaneously,
    // given that the metadata URI of the batch has also
    // been revealed.
    uint256 entropy; 
  }

  // Metastonez has three initial batches:
  //
  // - The first one, "Genesis", contains a small set of Metastonez
  // - The second one, "Origins", contains a larger set of Metastonez
  // - The third one contains a set of 1/1s that will be auctioned off
  //   using a separate mechanism. This batch will be minted by the
  //   auction contract.
  //
  // It is possible for the owner to create more batches, e.g. for
  // collaboration tokens. The ownership of the contract is transferred
  // to the Metastonez DAO when minting is done.
  Batch[] public batches;
  // The batch that is currently (or about to be) minting
  uint256 public currentBatch = 0;

  enum MintState {
    Pause,
    Whitelist,
    Public
  }

  // The current state of minting
  //
  // The contract can be in three states:
  // - Pause: No one can mint
  // - Whitelist: Whitelist can mint
  // - Public: Anyone can mint
  //
  // The state can only transition in the following ways:
  //   Pause -> Whitelist -> Public
  //
  // After a cycle has been completed the contract is put
  // back into a paused state and the `currentBatch`
  // state variable is incremented.
  //
  // Accounts can only mint the tokens available in
  // the current batch (denoted by `currentBatch`).
  MintState public mintState = MintState.Pause;

  // The IPFS CID for pre-reveal metadata
  string public prerevealMetadata;

  // The address that ether used for mint is sent to.
  address public immutable beneficiary;

  // The Chainlink token used for VRF fees.
  IERC20 public immutable linkToken;

  // Chainlink VRF key hash
  bytes32 internal immutable keyHash;

  // A mapping of Chainlink request IDs to batch IDs
  mapping(bytes32 => uint256) internal entropyRequests;

  // Total supply tracking
  Counters.Counter private _totalSupply;

  // A list of authorized contracts.
  // The intention is that the controller of Metastonez will be able to create more
  // batches in the future. In the case that smart wallets and/or account abstraction
  // become a prevalent thing, we want to be able to give control of minting
  // to a separate contract.
  mapping(address => bool) public authorizedContracts;

  constructor(
    address _beneficiary,
    string memory _prerevealMetadata,
    bytes32 _linkKeyHash,
    address _linkAddress,
    address _linkVrfCoordinatorAddress
  )
    ERC721("MetaStonez", "MSZ")
    VRFConsumerBase(_linkVrfCoordinatorAddress, _linkAddress)
  {
    beneficiary = _beneficiary;
    prerevealMetadata = _prerevealMetadata;
    keyHash = _linkKeyHash;
    linkToken = IERC20(_linkAddress);
  }

  /**
   * @dev Creates a new batch of tokens with given supply, price and metadata.
   *
   * The metadata **SHOULD** be an IPFS CID and nothing else.
   *
   * Note: It is OK to create a batch without committing the metadata hash,
   * since the batch can only start minting once the hash is set (which can be set later).
   */
  function createBatch(
    uint256 supply,
    bytes32 metadataHash,
    uint256 unitPrice
  )
    external
    onlyOwner
    returns (uint256 batchId)
  {
    require(
      supply > 0,
      "Metastonez: BATCH_SIZE"
    );

    uint256 startIndex = 0;
    if (batches.length > 0) {
      startIndex = batches[batches.length - 1].endIndex;
    }

    Batch storage batch = batches.push();
    batch.startIndex = startIndex;
    batch.endIndex = startIndex + supply;
    batch.metadataHash = metadataHash;
    batch.unitPrice = unitPrice;

    return batches.length - 1;
  }

  /**
   * @dev Commits the hash of the metadata for the batch.
   *
   * This hash is used to prove that metadata was not shuffled
   * post-mint and must therefore be committed pre-mint.
   *
   * The metadata hash is committed upon batch creation, however
   * this method is intended to be a failsafe in case the committed
   * hash was wrong.
   */
  function commitBatchMetadata(
    uint256 batchId,
    bytes32 metadataHash
  )
    external
    onlyOwner
  {
    require(
      currentBatch < batchId || mintState == MintState.Pause,
      "Metastonez: BATCH_LOCKED"
    );
    Batch storage batch = batches[batchId];
    batch.metadataHash = metadataHash;
  }

  /**
   * @dev Reveals metadata for a given batch.
   *
   * The hash of the metadata *must* match the hash
   * committed to the batch before minting began.
   *
   * It is possible to reveal metadata _before_
   * minting begins, since metadata "leaks" are not
   * a concern, as in the case of a batch
   * that is put up for auction.
   */
  function revealBatchMetadata(
    uint256 batchId,
    string calldata metadata
  )
    internal
  {
    Batch storage batch = batches[batchId];
    require(
      keccak256(abi.encodePacked(metadata)) == batch.metadataHash,
      "Metastonez: INVALID_METADATA"
    );
    batch.metadata = metadata;
  }

  /**
   * @dev Reveals a batch by revealing the metadata
   * and requesting shuffling entropy from Chainlink.
   */
  function revealBatch(
    uint256 batchId,
    string calldata metadata,
    uint256 linkFee
  )
    public
    onlyOwner
    returns (bytes32 requestId)
  {
    require(linkToken.balanceOf(address(this)) >= linkFee, "Metastonez: INSUFFICIENT_LINK");
    require(batches[batchId].entropy == 0, "Metastonez: ENTROPY_ALREADY_SET");
    revealBatchMetadata(batchId, metadata);

    requestId = requestRandomness(keyHash, linkFee);
    entropyRequests[requestId] = batchId;

    return requestId;
  }

  /**
   * @dev Sets the whitelist merkle root for a batch.
   * Fails if it is already set.
   */
  function setBatchWhitelist(
    uint256 batchId,
    bytes32 merkleRoot
  )
    external
    onlyOwner
  {
    Batch storage batch = batches[batchId];
    require(
      batch.merkleRoot == 0,
      "Metastonez: WHITELIST_LOCKED"
    );
    batch.merkleRoot = merkleRoot;
  }

  function transitionState() external onlyOwner {
    if (mintState == MintState.Pause) {
      require(
        batches.length > 0,
        "Metastonez: TRANS_NO_BATCHES"
      );
      require(
        currentBatch <= batches.length - 1,
        "Metastonez: TRANS_NO_BATCH"
      );
      Batch storage batch = batches[currentBatch];
      require(
        batch.metadataHash != 0,
        "Metastonez: TRANS_METADATA_HASH"
      );

      mintState = MintState.Whitelist;
    } else if (mintState == MintState.Whitelist) {
      mintState = MintState.Public;
    } else {
      require(
        _totalSupply.current() == batches[currentBatch].endIndex,
        "Metastonez: TRANS_MINTING"
      );
      currentBatch = currentBatch + 1;
      mintState = MintState.Pause;
    }
  }

  /**
   * @dev Mints a token in the current batch for a
   * whitelisted user by submitting a merkle proof.
   *
   * Note that it is possible to perform the mint on
   * behalf of other accounts; given the proof is valid
   * the minted token will be sent to the account specified
   * in the parameter of the call.
   */
  function mintWhitelist(
    uint256 index,
    address account,
    uint256 amount,
    bytes32[] calldata proof
  )
    external
    payable
  {
    // Ensure that minting is enabled
    require(
      mintState != MintState.Pause,
      "Metastonez: MINT_PAUSED"
    );

    // Check that this leaf was not already claimed
    Batch storage batch = batches[currentBatch];
    require(
      !isClaimed(currentBatch, index),
      "Metastonez: MINT_ALREADY_CLAIMED"
    );

    // Validate the proof
    bytes32 node = keccak256(abi.encodePacked(index, account, amount));
    require(
      batch.merkleRoot != 0 && MerkleProof.verify(proof, batch.merkleRoot, node),
      "Metastonez: INVALID_PROOF"
    );

    // Validate the sent value
    require(msg.value == batch.unitPrice * amount, "Metastonez: INVALID_VALUE");

    // Validate that there are more tokens to mint
    require(_totalSupply.current() + amount <= batch.endIndex, "Metastonez: SUPPLY_EXHAUSTED");

    // Set the leaf as claimed and mint the token
    _setClaimed(batch, index);
    for (uint256 i = 0; i < amount; i++) {
      _safeMint(account, _totalSupply.current());
      _totalSupply.increment();
    }
  }

  /**
   * @dev Public mint
   */
  function mint() external payable {
    // Ensure that minting is enabled
    require(
      mintState == MintState.Public,
      "Metastonez: MINT_PAUSED"
    );

    // Ensure caller is either an EOA or an approved contract
    require(
      tx.origin == msg.sender || authorizedContracts[msg.sender],
      "Metastonez: SENDER_NOT_ALLOWED"
    );

    // Validate the sent value
    Batch storage batch = batches[currentBatch];
    require(msg.value == batch.unitPrice, "Metastonez: INVALID_VALUE");

    // Validate that there are more tokens to mint
    require(_totalSupply.current() + 1 <= batch.endIndex, "Metastonez: SUPPLY_EXHAUSTED");

    _safeMint(msg.sender, _totalSupply.current());
    _totalSupply.increment();
  }

  function fulfillRandomness(bytes32 requestId, uint256 entropy) internal override {
    uint256 batchId = entropyRequests[requestId];
    if (batches[batchId].entropy == 0) {
      batches[batchId].entropy = entropy;
      delete entropyRequests[requestId];
    }
  }

  function tokenURI(uint256 tokenId) public view override returns (string memory) {
    require(_exists(tokenId), "Metastonez: TOKEN_DOESNT_EXIST");

    for (uint256 i = 0; i < batches.length; i++) {
      Batch storage batch = batches[i];
      if (tokenId >= batch.startIndex && tokenId < batch.endIndex) {
        if (batch.entropy == 0 || bytes(batch.metadata).length == 0) {
          return string(abi.encodePacked("ipfs://", prerevealMetadata));
        }

        require(bytes(batch.metadata).length > 0, "Metastonez: METADATA_UNREVEALED");

        uint256 shuffledTokenId = computeShuffledIndex(
          uint64(tokenId - batch.startIndex),
          uint64(batch.endIndex - batch.startIndex),
          keccak256(abi.encode(batch.entropy))
        ) + batch.startIndex;

        return string(abi.encodePacked("ipfs://", batch.metadata, "/", shuffledTokenId.toString(), ".json")); 
      }
    }
 }

 function totalSupply() public view returns (uint256) {
   return _totalSupply.current();
 }

  /**
   * @dev An implementation of the swap-or-not algorithm based on
   * the implementation in the [Eth2 consesus spec](https://github.com/ethereum/consensus-specs/blob/v0.12.2/specs/phase0/beacon-chain.md#compute_shuffled_index)
   */
  function computeShuffledIndex(
    uint64 index,
    uint64 indexCount,
    bytes32 seed
  )
    public
    pure
    returns (uint64)
  {
    require(index < indexCount, "SwapOrNot: INVALID_PARAMS");

    for (
      uint8 currentRound = 0;
      currentRound < 24;
      currentRound++
    ) {
      uint64 pivot = uint64(uint(keccak256(abi.encodePacked(seed, currentRound)))) % indexCount;
      uint64 flip = (pivot + indexCount - index) % indexCount;
      uint64 position = index > flip ? index : flip;
      bytes32 source = keccak256(
        abi.encodePacked(seed, currentRound, position / 256)
      );
      uint8 byt = uint8(source[(position % 256) / 8]);
      uint8 bit = (byt >> (position % 8)) % 2;

      if (bit > 0) {
        index = flip;
      }
    }

    return index;
  }

  /**
   * @dev Checks if a whitelist spot has already been used for
   * a given batch ID and whitelist index.
   */
  function isClaimed(
    uint256 batchId,
    uint256 index
  )
    public
    view
    returns (bool)
  {
    uint256 claimedWordIndex = index / 256;
    uint256 claimedBitIndex = index % 256;
    uint256 claimedWord = batches[batchId].spentLeaves[claimedWordIndex];
    uint256 mask = (1 << claimedBitIndex);
    return claimedWord & mask == mask;
  }

  /**
   * @dev Mark a whitelist spot as claimed for a given batch.
   */
  function _setClaimed(
    Batch storage batch,
    uint256 index
  ) private {
    uint256 claimedWordIndex = index / 256;
    uint256 claimedBitIndex = index % 256;
    batch.spentLeaves[claimedWordIndex] = batch.spentLeaves[claimedWordIndex] | (1 << claimedBitIndex);
  }

  /**
   * @dev Authorize a contract to mint.
   */
  function authorizeContract(address contractAddress) public onlyOwner {
    authorizedContracts[contractAddress] = true;
  }

  /**
   * @dev Forbid a contract that has previously been authorized to mint.
   */
  function forbidContract(address contractAddress) public onlyOwner {
    authorizedContracts[contractAddress] = false;
  }

  /**
   * @dev Withdraw all eth from the contract
   */
  function withdraw() public {
    Address.sendValue(payable(beneficiary), address(this).balance);
  }

  /**
   * @dev Withdraw all of a token from the contract
   */
  function withdrawToken(address tokenAddress) public onlyOwner {
    IERC20 token = IERC20(tokenAddress);
    token.transferFrom(
      address(this),
      beneficiary,
      token.balanceOf(address(this))
    );
  }
}
// MMMMMMMNy+/:-......-:/+yNMMMMMMM
// MMMMMMd-....---::---....-mMMMMMM
// MMMMMMo.:/oossssssssoo/:.sMMMMMM
// MMMMMy//osssss+//+ssssso/+yMMMMM
// MMMMNs:..--://....//:--../sNMMMM
// MMMMds:......+..../-.....:sdMMMM
// MMMMhs:....../..../......:shMMMM
// MMMMys:......:://::......:syMMMM
// MMMMss:.....:osssso:.....:ssMMMM
// MMMNss:.....-:////:-.....:ssNMMM
// MMMMys:....-////////-..../syMMMM
// MMMMNd/...-osssssssso-...+dNMMMM
// MMMMMMo..................sMMMMMM
// MMMMMdo-................-sdMMMMM
// MMMMMysso+///::::::///+ossyMMMMM
// MMMMMNdhyyssssssssssssyyhdNMMMMM
function commitBatchMetadata(
  uint256 batchId,
  bytes32 metadataHash
)
  external
  onlyOwner
{
  require(
    currentBatch < batchId || mintState == MintState.Pause,
    "Metastonez: BATCH_LOCKED"
  );
  Batch storage batch = batches[batchId];
  batch.metadataHash = metadataHash;
}

/**
 * @dev Commits the hash of the metadata for the batch.
 *
 * This hash is used to prove that metadata was not shuffled
 * post-mint and must therefore be committed pre-mint.
 *
 * The metadata hash is committed upon batch creation, however
 * this method is intended to be a failsafe in case the committed
 * hash was wrong.
 */