contract ActualGenesis is ERC721, EIP712, Ownable {
  string public tokenBaseURI;
  bool public tokenURILocked;

  string public arweaveID;
  bool public arweaveIDLocked;

  string public tokenURISuffix = '';

  bool public saleLocked;
  uint256 public totalSupply;
  uint256 public maxNumberOfArtworks;
  uint256 public numberOfMintedArtworks;
  uint256 public pricePerArtwork;
  uint256 public artistProofs = 10;

  mapping(uint256 => bool) private tokenIdToIsArtworkPrivate;
  mapping(uint256 => address) private tokenIdToAddressArtworkIsLentTo;

  event Purchase(address indexed buyer, uint256 price, uint256 indexed tokenId);
  event Gift(address indexed receiver, uint256 indexed tokenId);
  event Lend(
    address indexed from,
    address indexed receiver,
    uint256 indexed tokenId
  );
  event ArweaveIdUpdated(string newArweaveId);

  constructor(
    string memory _tokenBaseURI,
    bool _saleLocked,
    uint256 _totalSupply,
    uint256 _maxNumberOfArtworks,
    uint256 _pricePerArtwork
  ) ERC721('Actual Genesis', 'ACTUAL') EIP712('Actual Genesis', '1.0.0') {
    tokenBaseURI = _tokenBaseURI;
    saleLocked = _saleLocked;
    totalSupply = _totalSupply;
    maxNumberOfArtworks = _maxNumberOfArtworks;
    pricePerArtwork = _pricePerArtwork;
  }

  /**
   * @dev Checks whether th eprovided `tokenId` exists.
   *
   * @param tokenId artwork token id
   *
   * @return true if artwork exists (has been minted
   * already), false otherwise
   */
  function exists(uint256 tokenId) public view returns (bool) {
    return _exists(tokenId);
  }

  /**
   * @dev Base URI for computing {tokenURI}. The resulting
   * tokenURI for each token will be the concatenation of
   * the `baseURI` and the `tokenId`.
   */
  function _baseURI() internal view override returns (string memory) {
    return tokenBaseURI;
  }

  /**
   * @dev Changes `baseURI`, useful when freezing the
   * contract.
   *
   * @notice that value cannot be changed if `tokenURILocked`
   * is locked.
   *
   * @param newBaseURI new URI replacing the old one
   */
  function setTokenBaseURI(string memory newBaseURI) public onlyOwner {
    if (tokenURILocked == true) revert TokenURILocked();
    tokenBaseURI = newBaseURI;
  }

  /**
   * @dev Overriden `tokenURI` with the ability to add a
   * suffix to URI. This is particularly helpful if
   * if metadata is "baked" as static .json files.
   *
   * @notice calls ERC721 `tokenURI` first and concatenates
   * the returned value with `tokenURISuffix`
   *
   * @param tokenId id of token to retrive URI for
   */
  function tokenURI(uint256 tokenId)
    public
    view
    override
    returns (string memory)
  {
    string memory tokenURIBasename = super.tokenURI(tokenId);

    return
      bytes(tokenURIBasename).length > 0
        ? string(abi.encodePacked(tokenURIBasename, tokenURISuffix))
        : '';
  }

  /**
   * @dev Lock the `baseURI` preventing any changes to
   * be committed to it. Useful when freezing the contract.
   */
  function lockTokenURI() public onlyOwner {
    tokenURILocked = true;
  }

  /**
   * @dev Changes `tokenURISuffix`, useful when freezing the
   * contract after all assets have been decentralized.
   *
   * @param newTokenURISuffix new suffix replacing the old one
   */
  function setTokenURISuffix(string memory newTokenURISuffix) public onlyOwner {
    tokenURISuffix = newTokenURISuffix;
  }

  /**
   * @dev Changes `arweaveID`, useful when freezing the
   * contract after all assets have been decentralized.
   *
   * @notice that value cannot be changed if `arweaveIDLocked`
   * is locked.
   *
   * @param newArweaveID new ID replacing the old one
   */
  function setArweaveID(string memory newArweaveID) public onlyOwner {
    if (arweaveIDLocked == true) revert ArweaveIDLocked();
    arweaveID = newArweaveID;
  }

  /**
   * @dev Lock the `arweaveID` preventing any changes to be
   * committed to it. Useful when freezing the contract.
   */
  function lockArweaveID() public onlyOwner {
    arweaveIDLocked = true;
  }

  /**
   * @dev Collects balance sum and transfers to sender.
   */
  function collectSum() public onlyOwner {
    (bool success, ) = payable(msg.sender).call{value: address(this).balance}(
      ''
    );
    if (success == false) revert WithdrawalFailed();
  }

  /**
   * @dev Permits lending of an artwork to an arbitrary
   * address 'addressToLendTo'. This functionality is to be
   * used in conjuction with setArtworkPublic/Private
   *
   * Requirements:
   * - `tokenID` must be owned by msg.sender
   * - `tokenID` must be already minted
   *
   * @param tokenID id of token that will be lent
   * @param addressToLendTo address of the borrower
   *
   * Emits a {Lend} event.
   */
  function lend(uint256 tokenID, address addressToLendTo) public {
    if (!_exists(tokenID)) revert ArtworkDoesNotExist();
    if (ownerOf(tokenID) != msg.sender) revert SenderIsDifferentFromOwner();

    emit Lend(ownerOf(tokenID), addressToLendTo, tokenID);
    tokenIdToAddressArtworkIsLentTo[tokenID] = addressToLendTo;
  }

  /**
   * @dev Checks the address to which `tokenID` is lent to.
   *
   * Requirements:
   * - `tokenID` must be already minted
   *
   * @param tokenID id of token that is lent
   * @return address of the borrower of `tokenID`
   */
  function getLentToAddress(uint256 tokenID) public view returns (address) {
    if (!_exists(tokenID)) revert ArtworkDoesNotExist();
    return tokenIdToAddressArtworkIsLentTo[tokenID];
  }

  /**
   * @dev Makes an artwork with `tokenID` public
   *
   * Requirements:
   * - `tokenID` must be owned by msg.sender
   * - `tokenID` must be already minted
   *
   * @param tokenID id of token that will be made public
   */
  function setArtworkPublic(uint256 tokenID) public {
    setArtworkPrivateState(tokenID, false);
  }

  /**
   * @dev Makes an artwork with `tokenID` private
   *
   * Requirements:
   * - `tokenID` must be owned by msg.sender
   * - `tokenID` must be already minted
   *
   * @param tokenID id of token that will be made private
   */
  function setArtworkPrivate(uint256 tokenID) public {
    setArtworkPrivateState(tokenID, true);
  }

  /**
   * @dev Checks if an artwork with `tokenID` is public or
   * private.
   *
   * Requirements:
   * - `tokenID` must be already minted
   *
   * @param tokenID id of artwork to check
   * @return true if private, false if public
   */
  function isArtworkPrivate(uint256 tokenID) public view returns (bool) {
    if (!_exists(tokenID)) revert ArtworkDoesNotExist();
    return tokenIdToIsArtworkPrivate[tokenID];
  }

  /**
   * @dev Helper function to modify the private/public state
   * of `tokenID`.
   *
   * Requirements:
   * - `tokenID` must be owned by msg.sender
   * - `tokenID` must be already minted
   *
   * @param tokenID id of token to be made private/public
   * @param flag true to make private, false to make public
   */
  function setArtworkPrivateState(uint256 tokenID, bool flag) private {
    if (!_exists(tokenID)) revert ArtworkDoesNotExist();
    if (ownerOf(tokenID) != msg.sender) revert SenderIsDifferentFromOwner();

    tokenIdToIsArtworkPrivate[tokenID] = flag;
  }

  /**
   * @dev Checks the number of artworks available for minting.
   *
   * @return the number of artworks available for minting
   */
  function artworksLeft() public view returns (uint256) {
    return maxNumberOfArtworks - numberOfMintedArtworks;
  }

  /**
   * @dev Changes the maximum number of artworks available for
   * minting.
   *
   * Requirements:
   * - new maximum must be < than total number of artworks
   * - new maximum must be > than  artworks minted so far
   *
   * @param newMaxNumberOfArtworks new maximum
   */
  function setMaxNumberOfArtworks(uint256 newMaxNumberOfArtworks)
    public
    onlyOwner
  {
    if (newMaxNumberOfArtworks < numberOfMintedArtworks)
      revert NewMaxNumberLowerThanCurrentNumberOfMintedArtworks();
    if (newMaxNumberOfArtworks >= totalSupply)
      revert NewMaxNumberGreaterThanTotalSupply();

    maxNumberOfArtworks = newMaxNumberOfArtworks;
  }

  /**
   * @dev Modifier ensuring that the minting tx is valid.
   *
   * Requirements:
   * - sale is not locked
   * - there are artworks left to be minted
   * - artwork is not already minted
   * - artwork `tokenId` is valid and <= total supply
   * - transaction value is exactly same as the base price
   *
   * @param newTokenId new token ID that will be minted
   */
  modifier isSaleValid(uint256 newTokenId) {
    if (msg.sender != owner() && saleLocked == true) revert SaleIsLocked();

    if (artworksLeft() == 0) revert AllArtworksSold();

    if (_exists(newTokenId)) revert ArtworkAlreadyMinted();

    if (newTokenId > totalSupply)
      revert ArtworkWithProvidedTokenIdDoesNotExist();

    if (msg.value < pricePerArtwork && msg.sender != owner())
      revert AmountLessThanSetPrice();

    if (msg.value > pricePerArtwork && msg.sender != owner())
      revert AmountMoreThanSetPrice();

    _;
  }

  /**
   * @dev Mints `artistProofs` number of artworks with
   * `tokenID` starting at 1 (inclusive) and increasing
   * to `artistProofs` (inclusive).
   *
   * Requirements:
   * - No artworks have been sold/minted already
   */
  function mintArtistProofs() public onlyOwner {
    if (numberOfMintedArtworks > 0) revert MoreThanOneArtworkExistsAlready();
    for (uint256 i = 1; i <= artistProofs; i++) {
      mintArtistProof(i);
    }
  }

  /**
   * @dev Helper function to mint artist proofs
   */
  function mintArtistProof(uint256 newTokenId) private isSaleValid(newTokenId) {
    emit Purchase(msg.sender, msg.value, newTokenId);
    _mintPiece(newTokenId, msg.sender);
  }

  /**
   * @dev Computes EIP 712 hash of typed structure of
   * `account` and `tokenID`
   *
   * @param account msg.sender of minting transaction
   * @param newTokenId artwork to be minted
   *
   * @return the hash of the fully encoded EIP712 message
   * for this domain.
   */
  function _hash(address account, uint256 newTokenId)
    internal
    view
    returns (bytes32)
  {
    return
      _hashTypedDataV4(
        keccak256(
          abi.encode(
            keccak256('NFT(uint256 Artwork,address Buyer)'),
            newTokenId,
            account
          )
        )
      );
  }

  /**
   * @dev Compares an EIP 712 signed message of
   * `account`and `newTokenId` with one provided by the `buy`
   * function (incoming from client/wallet app).
   *
   * @param account msg.sender of minting transaction
   * @param newTokenId artwork to be minted
   * @param signature provided by the client/wallet
   *
   * @return true if contract-computed signature matches the
   * client/wallet signature, otherwise false, meaning that
   * the msg.sender is different from the wallet account that
   * signed the message.
   */
  function matchAddressSigner(
    address account,
    uint256 newTokenId,
    bytes memory signature
  ) private view returns (bool) {
    return account == ECDSA.recover(_hash(account, newTokenId), signature);
    // return _hash(account, newTokenId) == signature;
  }

  /**
   * @dev Mints an artwork with `newTokenId`
   *
   * Requirements:
   * - see isSaleValid(uint256 newTokenId)
   * - see matchAddressSigner(...)
   *
   * @param signature client-provided wallet-signed keccak256
   * hash of msg.sender and `newTokenId`
   * @param newTokenId artwork to be minted
   *
   * Emits a {Transfer} and {Purchase} event.
   */
  function buy(bytes memory signature, uint256 newTokenId)
    public
    payable
    isSaleValid(newTokenId)
  {
    if (matchAddressSigner(msg.sender, newTokenId, signature) == false)
      revert DirectMintDisallowedSignatureMismatch();

    emit Purchase(msg.sender, msg.value, newTokenId);
    _mintPiece(newTokenId, msg.sender);
  }

  /**
   * @dev Gifting/airdrop functionality for owner
   *
   * Requirements:
   * - see isSaleValid(uint256 newTokenId)
   * - see onlyOwner(...)
   *
   * @param receiver address that will receive the artwork
   * @param newTokenId artwork to be minted
   *
   * Emits a {Transfer} and {Gift} event.
   */
  function gift(uint256 newTokenId, address receiver)
    public
    payable
    onlyOwner
    isSaleValid(newTokenId)
  {
    emit Gift(receiver, newTokenId);
    _mintPiece(newTokenId, receiver);
  }

  /**
   * @dev Helper function to mint artworks. Used in gift, buy,
   *and mintArtistProof.
   *
   * By defaults sets:
   * - the artwork to be public
   * - lent to the address of the owner (msg.sender)
   * - increments `numberOfMintedArtworks`, the number of
   *   artworks minted so far
   *
   * @param toAddress address that will own the artwork
   * @param newTokenId artwork to be minted
   *
   * Emits a {Transfer} event.
   */
  function _mintPiece(uint256 newTokenId, address toAddress) private {
    tokenIdToIsArtworkPrivate[newTokenId] = false;
    tokenIdToAddressArtworkIsLentTo[newTokenId] = msg.sender;
    numberOfMintedArtworks += 1;

    _safeMint(toAddress, newTokenId);
  }

  /**
   * @dev Locks public sale
   *
   * Requirements:
   * - see onlyOwner(...)
   */
  function lockSale() public onlyOwner {
    saleLocked = true;
  }

  /**
   * @dev Unlocks public sale
   *
   * Requirements:
   * - see onlyOwner(...)
   */
  function unlockSale() public onlyOwner {
    saleLocked = false;
  }

  /**
   * @dev Getter function to determine if public sale is
   * locked or not.
   *
   * @return true if sale is locked, false otherwise
   */
  function getSaleLocked() public view returns (bool) {
    return saleLocked;
  }
}
function buy(bytes memory signature, uint256 newTokenId)
  public
  payable
  isSaleValid(newTokenId)
{
  if (matchAddressSigner(msg.sender, newTokenId, signature) == false)
    revert DirectMintDisallowedSignatureMismatch();

  emit Purchase(msg.sender, msg.value, newTokenId);
  _mintPiece(newTokenId, msg.sender);
}

/**
 * @dev Mints an artwork with `newTokenId`
 *
 * Requirements:
 * - see isSaleValid(uint256 newTokenId)
 * - see matchAddressSigner(...)
 *
 * @param signature client-provided wallet-signed keccak256
 * hash of msg.sender and `newTokenId`
 * @param newTokenId artwork to be minted
 *
 * Emits a {Transfer} and {Purchase} event.
 */