contract RADIOACTIVEPUNKS is ERC721Burnable, ERC721Pausable, ERC721Enumerable, HasSecondarySaleFees, ERC2981, Ownable {

  event Frozen();

  struct Unit {
      string logIn;
  }

  uint256 public activationTime = 1633536000; // THE CRYPTOPOCALYPSE
  address payable public PAYMENT_SPLITTER = payable(0xFc9961d08Ef8e04DB145b9fb3d48cf8DbA96116D ); // PaymentSplitter
  string public API_BASE_URL = '';
  string public METADATA_PROVENANCE_HASH = '';
  uint256 public constant punkPrice = 25000000000000000; // 0.025 ETH
  uint private constant maxPunksAtOnce = 5;
  uint256 private MAX_PUNKS = 3000;
  uint256 public PUNKS_LEFT = 2999;
  uint256 private PRIME = 197;
  uint256 private COPRIME = 474569;
  bool public frozen = false;

  string private constant EIP712_DOMAIN  = "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)";
  string private constant UNIT_TYPE = "Unit(string logIn)";
  bytes32 private constant EIP712_DOMAIN_TYPEHASH = keccak256(abi.encodePacked(EIP712_DOMAIN));
  bytes32 private constant UNIT_TYPEHASH = keccak256(abi.encodePacked(UNIT_TYPE));

  bytes32 private DOMAIN_SEPARATOR = keccak256(abi.encode(
    EIP712_DOMAIN_TYPEHASH,
    keccak256("RADIOACTIVEPUNKS"),
    keccak256("1"), // version
    1,
    0xFc9961d08Ef8e04DB145b9fb3d48cf8DbA96116D // PaymentSplitter
  ));

  constructor() ERC721("Radioactive Punks", "RPUNK") {}

  function withdraw() public onlyOwner {
    uint256 balance = address(this).balance;
    PAYMENT_SPLITTER.transfer(balance);
  }

  function setPaymentSplitter(address payable newAddress) external onlyOwner {
    PAYMENT_SPLITTER = newAddress;
  }

  function activated() public view returns (bool) {
    return block.timestamp >= activationTime;
  }

  function setActivationTime(uint256 timestampInSeconds) external onlyOwner {
    activationTime = timestampInSeconds;
  }

  /*
   * @dev Used in pseudorandom tokenId
   */
  function setCoprimes(uint256 primeNumber, uint256 coprimeNumber) external onlyOwner {
    PRIME = primeNumber;
    COPRIME = coprimeNumber;
  }

  /*
   * Once executed, metadata for NFTs can never be changed.
   */
  function freezeMetadata() external onlyOwner {
    frozen = true;
    emit Frozen();
  }

  function setAPIBaseURL(string memory URI) external onlyOwner {
    require(frozen == false, "Frozen");
    API_BASE_URL = URI;
  }

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

  function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
    require(_exists(tokenId), "Token DNE");

    string memory baseURI = _baseURI();
    return bytes(baseURI).length > 0
      ? string(abi.encodePacked(baseURI, uint2str(tokenId), '.json'))
      : '';
  }

  /*
   * Allows the owner to mint X tokens as a way
   * of reserving them for our core supporters.
   */
  function reservePunks(uint256 numberOfTokens) public onlyOwner {
    require(numberOfTokens <= PUNKS_LEFT, "Exceeds max supply");

    // for every tokent they want
    for(uint i = 0; i < numberOfTokens; i++) {
      // reduce the total pop after minting
      if (PUNKS_LEFT >= 0) {
        uint randomUnusedInt = ((PRIME * PUNKS_LEFT) + COPRIME) % MAX_PUNKS;
        _safeMint(msg.sender, randomUnusedInt);
        PUNKS_LEFT--;
      }
    }
  }

  /*
   * For minting up to five punks at once - tokenIds pseudorandomly generated.
   */
  function mintPunks(uint256 numberOfTokens, uint8 sigV, bytes32 sigR, bytes32 sigS) public payable {
    require(activated(), 'You are too early');
    require(numberOfTokens <= maxPunksAtOnce, "Exceeds max mint rate");
    require(numberOfTokens <= PUNKS_LEFT, "Exceeds max supply");
    require(punkPrice * numberOfTokens == msg.value, "Ether value incorrect");
    require(balanceOf(msg.sender) < 100, "Owns max amount");
    require(verify(msg.sender, sigV, sigR, sigS) == true, "No cheaters!");

    if (block.timestamp <= activationTime + 600) {
      require(balanceOf(msg.sender) + numberOfTokens <= 10, "Only 10 for first 10m");
    }

    // for every token they want
    for(uint i = 0; i < numberOfTokens; i++) {
      // reduce the total pop after minting
      if (PUNKS_LEFT >= 0) {
        uint randomUnusedInt = ((PRIME * PUNKS_LEFT) + COPRIME) % MAX_PUNKS;
        _safeMint(msg.sender, randomUnusedInt);
        PUNKS_LEFT--;
      }
    }
  }

  /*
   * @dev Leads to a big file on IPFS containing a list of all
   *      tokenIDs and their corresponding metadata etc.
   */
  function setProvenanceHash(string memory hash) external onlyOwner {
    METADATA_PROVENANCE_HASH = hash;
  }

  /*
   * @dev For returning ERC20s we dont know what to do with
   *      if people have issues they can contact owner/devs
   *      and we can send back their tokens.
   */
  function forwardERC20s(IERC20 _token, uint256 _amount) external onlyOwner {
    _token.transfer(address(this), _amount);
  }

  function pause() external onlyOwner {
    _pause();
  }

  function unpause() external onlyOwner{
    _unpause();
  }

  /*
   * @dev Required for ERC721Pausable
   */
  function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal override(ERC721, ERC721Pausable, ERC721Enumerable) {
    super._beforeTokenTransfer(from, to, tokenId);
  }

  /*
   * Rarible/Foundation Royalties Protocol
   */
  function getFeeRecipients(uint256 id) public view override returns (address payable[] memory) {
    require(_exists(id), "DNE");
    address payable[] memory result = new address payable[](1);
    result[0] = PAYMENT_SPLITTER;
    return result;
  }

  /*
   * Rarible/Foundation Royalties Protocol
   */
  function getFeeBps(uint256 id) public view override returns (uint[] memory) {
    require(_exists(id), "DNE");
    uint[] memory result = new uint[](1);
    result[0] = 1000; // 10%
    return result;
  }

  /*
   * ERC2981 Royalties Standard
   */
  function royaltyInfo(uint256 _tokenId, uint256 _value, bytes calldata _data) external view override returns (address _receiver, uint256 _royaltyAmount, bytes memory _royaltyPaymentData) {
    return (PAYMENT_SPLITTER, _value / 10, _data); // 10%
  }

  function supportsInterface(bytes4 interfaceId) public view override(ERC721, HasSecondarySaleFees, ERC721Enumerable, ERC2981) returns (bool) {
    return super.supportsInterface(interfaceId);
  }

  function uint2str(uint256 _i) internal pure returns (string memory str) {
    if (_i == 0)
    {
      return "0";
    }
    uint256 j = _i;
    uint256 length;
    while (j != 0)
    {
      length++;
      j /= 10;
    }
    bytes memory bstr = new bytes(length);
    uint256 k = length;
    j = _i;
    while (j != 0)
    {
      bstr[--k] = bytes1(uint8(48 + j % 10));
      j /= 10;
    }
    str = string(bstr);
  }

  /*
   * Generates the hash representation of the struct Unit for EIP-718 usage
   */
  function hashUnit(Unit memory unitobj) private view returns (bytes32) {
    return keccak256(abi.encodePacked(
        "\x19\x01",
        DOMAIN_SEPARATOR,
        keccak256(abi.encode(
            UNIT_TYPEHASH,
            keccak256(bytes(unitobj.logIn))
        ))
    ));
  }

  /*
   * EIP-718 signature check
   */
  function verify(address signer, uint8 sigV, bytes32 sigR, bytes32 sigS) private view returns (bool) {
    Unit memory msgObj = Unit({
       logIn: 'Log In To Radioactive Punks!'
    });
    return signer == ecrecover(hashUnit(msgObj), sigV, sigR, sigS);
  }
}
function verify(address signer, uint8 sigV, bytes32 sigR, bytes32 sigS) private view returns (bool) {
  Unit memory msgObj = Unit({
     logIn: 'Log In To Radioactive Punks!'
  });
  return signer == ecrecover(hashUnit(msgObj), sigV, sigR, sigS);
}

/*
 * EIP-718 signature check
 */