contract WorkshopOrigin is ERC721Enumerable, Pausable, Ownable {
  using SafeERC20 for IERC20;

  // Represents a Workshop Item, e.g., Power Glove, Shurinken
  struct Item {
    uint16 max;
    uint16 minted;
    uint96 madCost;
    uint96 erc721Cost;
  }

  // If frozen, baseTokenURI cannot be updated
  bool public frozen;

  // Tracks tokenId
  uint88 private _tokenIdTracker;

  // Workshop currency $MAD
  IERC20 public immutable madToken;

  // Address where the mint fees are deposited
  address public multiSigOwner;

  // Tracks tokenId => groupId
  mapping(uint256 => uint256) public tokenToGroup;

  // Maps eligible contracts to their respective merkle roots
  mapping(address => bytes32) public contractToMerkleRoot;

  // Wallet Address => (Contract Address => Boolean)
  // Indicates if a user has claimed workshop items
  // against an eligible contract
  mapping(address => mapping(address => bool)) public claimed;

  // Cannot be updated if contract is frozen
  string public baseTokenURI;

  // Max mint per tx
  uint256 public constant MAX_MINT_AMOUNT = 30;

  // List of all workshop items
  Item[] public items;

  event Forged(uint256 indexed id);

  /**
   * @notice Sanity checks before minting
   * @param to Address where tokens will be minted to
   * @param count Number of tokens to be minted
   * @param groupId Index of relevant workshop item in `items`
   */
  modifier validateMintParameters(
    address to,
    uint16 count,
    uint8 groupId
  ) {
    require(to != address(0), "Cannot mint to empty address");
    require(groupId < items.length, "Invalid group id");
    Item storage item = items[groupId];

    uint256 available = item.max - item.minted;

    require(count <= available, "Special Items sold out");
    require(count <= MAX_MINT_AMOUNT, "Max mint exceeded");
    _;
  }

  constructor(
    string memory baseURI,
    address _madToken,
    address cyberLootToken,
    bytes32 cyberLootMerkleRoot,
    address _multiSigOwner
  ) ERC721("WorkshopOrigin", "WSO") {
    
    setBaseURI(baseURI);

    madToken = IERC20(_madToken);
    contractToMerkleRoot[cyberLootToken] = cyberLootMerkleRoot;
    multiSigOwner = _multiSigOwner;
  }

  /**
   * @notice Mint using $MAD
   * @param to Address where tokens will be minted to
   * @param count Number of tokens to be minted
   * @param groupId Index of relevant workshop item in `items`
   */
  function mint(
    address to,
    uint16 count,
    uint8 groupId
  ) external validateMintParameters(to, count, groupId) {
    Item storage item = items[groupId];
    uint256 cost = item.madCost * count;

    require(madToken.balanceOf(msg.sender) >= cost, "Insufficient $MAD tokens");
    madToken.safeTransferFrom(msg.sender, multiSigOwner, cost);
    item.minted += count;

    for (uint256 i = 0; i < count; i++) {
      _mintOneItem(to, groupId);
    }
  }

  /**
   * @notice Mint against balanceOf a contract in `eligibleContracts`
   * @param to Address where tokens will be minted to
   * @param count Number of tokens to be minted
   * @param groupId Index of relevant workshop item in `items`
   * @param eligibleContract Should be present in `eligibleContracts`
   */
  function claim(
    address to,
    uint16 count,
    uint8 groupId,
    uint96 erc721Budget,
    address eligibleContract,
    bytes32[] calldata merkleProof
  ) external validateMintParameters(to, count, groupId) {
    require(contractToMerkleRoot[eligibleContract] != 0, "Ineligible contract");
    require(claimed[msg.sender][eligibleContract] == false, "Already claimed");

    bytes32 node = keccak256(abi.encodePacked(msg.sender, erc721Budget));
    require(
      MerkleProof.verify(
        merkleProof,
        contractToMerkleRoot[eligibleContract],
        node
      ),
      "Could not verify merkleProof"
    );

    Item storage item = items[groupId];
    require(erc721Budget >= item.erc721Cost * count, "Not enough budget");

    claimed[msg.sender][eligibleContract] = true;
    item.minted += count;

    for (uint256 i = 0; i < count; i++) {
      _mintOneItem(to, groupId);
    }
  }

  /**
   * @notice Add a contract to contractToMerkleRoot
   * @param _contract Address to be added
   */
  function addContract(address _contract, bytes32 merkleRoot)
    external
    onlyOwner
  {
    contractToMerkleRoot[_contract] = merkleRoot;
  }

  /**
   * @notice Remove a contract from contractToMerkleRoot
   * @param _contract Address to be removed
   */
  function removeContract(address _contract) external onlyOwner {
    contractToMerkleRoot[_contract] = 0;
  }

  /**
   * @notice Add special items to the workshop
   * @param _items List of items to be added
   */
  function addItems(Item[] memory _items) external onlyOwner {
    for (uint256 i = 0; i < _items.length; i++) {
      items.push(
        Item({
          max: _items[i].max,
          minted: _items[i].minted,
          erc721Cost: _items[i].erc721Cost,
          madCost: _items[i].madCost
        })
      );
    }
  }

  /**
   * @notice Modify existing item.
   * Cannot modify number of minted items
   * @param _item New parameters to be updated
   */
  function modifyItem(Item memory _item, uint256 groupId) external onlyOwner {
    require(groupId < items.length, "Invalid group id");

    Item storage item = items[groupId];

    item.max = _item.max;
    item.erc721Cost = _item.erc721Cost;
    item.madCost = _item.madCost;
  }

  function _mintOneItem(address to, uint256 groupId) private {
    _tokenIdTracker += 1;

    uint256 tokenId = _tokenIdTracker;

    tokenToGroup[tokenId] = groupId;

    _safeMint(to, tokenId);

    emit Forged(tokenId);
  }

  function tokenURI(uint256 tokenId)
    public
    view
    override
    returns (string memory)
  {
    return
      string(
        abi.encodePacked(baseTokenURI, Strings.toString(tokenId), ".json")
      );
  }

  /**
   * @notice Updates baseURI if contract is not frozen
   */
  function setBaseURI(string memory baseURI) public onlyOwner {
    require(!frozen, "Contract is frozen");
    baseTokenURI = baseURI;
  }

  /**
   * @notice Cannot be undone.
   */
  function freeze() public onlyOwner {
    frozen = true;
  }

  function pause(bool val) public onlyOwner {
    if (val) {
      _pause();
      return;
    }
    _unpause();
  }

  function setMultisigOwner(address _multiSigOwner) external onlyOwner {
    multiSigOwner = _multiSigOwner;
  }

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

  /**
   * @notice Rescue any $MAD sent to the contract by mistake
   *
   */
  function rescueTokens() external onlyOwner {
    uint256 balance = madToken.balanceOf(address(this));
    require(balance > 0);
    madToken.safeTransfer(multiSigOwner, balance);
  }
}
/**
 * @title The Workshop: Origin
 * @notice
 */
function setBaseURI(string memory baseURI) public onlyOwner {
  require(!frozen, "Contract is frozen");
  baseTokenURI = baseURI;
}

/**
 * @notice Updates baseURI if contract is not frozen
 */