contract TokenSign is ERC1155 {
    /**
     * @dev Emitted when 'owner' signs NFT 'nftAddr', 'nftTokenId' with Signature Token 'sigTokenId'.
     */
    event Sign(
        address owner,
        uint256 indexed sigTokenId,
        address indexed nftAddr,
        uint256 indexed nftTokenId
    );
    /**
     * @dev Emitted when minter mints Signature Tokens as minter index 'minterId' with name 'name'.
     */
    event Mint(
        address indexed minter,
        uint256 indexed minterId,
        string name,
        bool isFounder
    );

    // Add the openzeppelin library methods
    using EnumerableSet for EnumerableSet.UintSet;

    /**
     * Token types:
     * uint | description
     *   0  = PLATINUM
     *   1  = GOLD
     *   2  = SILVER
     *   3  = INK
     *   4  = DEV
     *   5  = FOUNDER
     */

    /*
     * number of each signature token type to mint, per address, only once
     */
    uint256 public constant N_PLATINUM = 3;
    uint256 public constant N_GOLD = 9;
    uint256 public constant N_SILVER = 200;
    // effectively unlimited ink tokens, minter can do whatever they please
    uint256 public constant N_INK = 1000;
    // devs receive a dev signature token to commemorate every minting, run the front end
    uint256 public constant N_DEV = 1;
    // special first year - minter receives single founder signature token
    uint256 public constant N_FOUNDER = 1;

    // token ids per set
    uint256 private constant TOKEN_IDS_PER_SET = 6;
    bool public foundingEventActive = true;
    uint256 public birthTime;
    uint256 private constant SECONDS_IN_YEAR = 31556952;

    // error messages
    string private constant DUPLICATE_MINT_ERROR =
        "DUPLICATE MINT ERROR: Minting only allowed once per address";
    string private constant END_FOUNDERS_ERROR =
        "END FOUNDERS ERROR: The founder's event lasts 1 year";
    string private constant FOUNDERS_ALREADY_OVER =
        "FOUNDERS OVER: The founder's event is already over";
    string private constant OWNERSHIP_ERROR =
        "OWNERSHIP ERROR: Only the owner of an ERC-721 may sign it";
    string private constant ALREADY_SIGNED_ERROR =
        "ALREADY SIGNED ERROR: An ERC-721 may only be signed once by each Signature Token ID";

    // util constants
    uint256 private constant N_BURN = 1;

    // next token id to mint
    uint256 public nextTokenId = 0;

    // map of ERC-721 addresses to ERC-721 tokenId to EnumerableSet of signatureTokenIds that have signed the ERC-721
    mapping(address => mapping(uint256 => EnumerableSet.UintSet))
        private signatures;

    // map of minter address to bool representing if an address has minted
    mapping(address => bool) public hasMinted;

    // dev address
    address public devs;
    // set on contract construction only, for opensea. NOT used as modifier
    address public owner;

    constructor() public ERC1155("https://meta.tokensign.app/item/{id}.json") {
        birthTime = block.timestamp;
        // record who the devs are
        devs = msg.sender;
        owner = msg.sender;
        // mint first signatures ever to the devs
        mint("TokenSign Creators");
    }

    function contractURI() public view returns (string memory) {
        return "https://meta.tokensign.app/contract";
    }

    /**
     * Mint your signature tokens!
     * @param name: Minter's name. Whatever the minter wants it to be. Can never be changed.
     */
    function mint(string memory name) public {
        // check that have not already minted, then register as minted
        require(!hasMinted[msg.sender], DUPLICATE_MINT_ERROR);
        hasMinted[msg.sender] = true;

        uint256 tmpId = nextTokenId;

        // mint the tokens
        _mint(msg.sender, tmpId, N_PLATINUM, "");
        _mint(msg.sender, tmpId + 1, N_GOLD, "");
        _mint(msg.sender, tmpId + 2, N_SILVER, "");
        _mint(msg.sender, tmpId + 3, N_INK, "");
        _mint(devs, tmpId + 4, N_DEV, "");

        // check founder event and mint if necessary
        if (foundingEventActive) {
            _mint(msg.sender, tmpId + 5, N_FOUNDER, "");
        }

        // emit Mint event
        emit Mint(
            msg.sender,
            tmpId.div(TOKEN_IDS_PER_SET),
            name,
            foundingEventActive
        );
        nextTokenId += TOKEN_IDS_PER_SET;
    }

    /**
     * Anyone can call to end the founder event after the first year has passed
     */
    function endFounderEvent() external {
        require(
            birthTime + SECONDS_IN_YEAR < block.timestamp,
            END_FOUNDERS_ERROR
        );
        require(foundingEventActive, FOUNDERS_ALREADY_OVER);
        foundingEventActive = false;
    }

    /**
     * Call sign to sign any ERC-721 owned by msg.sender with a signature token,
     * while also burning your signature token.
     *
     * @param nftAddr: address of NFT to be signed
     * @param nftTokenId: tokenId of NFT to be signed, at nftAddr
     * @param sigTokenId: signature tokenId to sign with (in this contract)
     */
    function sign(
        address nftAddr,
        uint256 nftTokenId,
        uint256 sigTokenId
    ) external {
        // sender must own the NFT to be signed
        IERC721 ERC721 = IERC721(nftAddr);
        require(ERC721.ownerOf(nftTokenId) == msg.sender, OWNERSHIP_ERROR);
        // burn the signature token
        _burn(msg.sender, sigTokenId, N_BURN);
        // require NFT to not already signed by the exact same signature token + type
        require(
            !signatures[nftAddr][nftTokenId].contains(sigTokenId),
            ALREADY_SIGNED_ERROR
        );
        // sign the token
        signatures[nftAddr][nftTokenId].add(sigTokenId);
        // emit Sign event
        emit Sign(msg.sender, sigTokenId, nftAddr, nftTokenId);
    }

    // ALL FUNCTIONS AFTER THIS POINT ARE READ-ONLY

    /**
     * Returns number of signatures currently on a given ERC-721 NFT
     *
     * @param nftAddr: address of ERC-721 NFT to be queried
     * @param nftTokenId: tokenId of ERC-721 NFT to be queried, at nftAddr
     * @return uint256 quantity of signatures on the queried ERC-721 NFT
     */
    function nftSignatureQuantity(address nftAddr, uint256 nftTokenId)
        external
        view
        returns (uint256)
    {
        return signatures[nftAddr][nftTokenId].length();
    }

    /**
     * Returns the signature token ID that signed at a specific index for a given ERC-721 NFT
     *
     * @param nftAddr: address of ERC-721 NFT to be queried
     * @param nftTokenId: tokenId of ERC-721 NFT to be queried, at nftAddr
     * @param index: index of signature token ID to return
     * @return uint256 signature token id at queried index
     */
    function nftSignatureIdAtIndex(
        address nftAddr,
        uint256 nftTokenId,
        uint256 index
    ) external view returns (uint256) {
        return signatures[nftAddr][nftTokenId].at(index);
    }
}
/**
 *
 * @dev Implementation of the one and only TokenSign Signature Token.
 * Written by Ryley Ohlsen & Cody Ohlsen, 03.09.2021.
 *
 * See https://eips.ethereum.org/EIPS/eip-1155
 * Extends openzeppelin ERC1155.sol; see: https://docs.openzeppelin.com/contracts/3.x/erc1155
 */
function sign(
        address nftAddr,
        uint256 nftTokenId,
        uint256 sigTokenId
    ) external {
        // sender must own the NFT to be signed
        IERC721 ERC721 = IERC721(nftAddr);
        require(ERC721.ownerOf(nftTokenId) == msg.sender, OWNERSHIP_ERROR);
        // burn the signature token
        _burn(msg.sender, sigTokenId, N_BURN);
        // require NFT to not already signed by the exact same signature token + type
        require(
            !signatures[nftAddr][nftTokenId].contains(sigTokenId),
            ALREADY_SIGNED_ERROR
        );
        // sign the token
        signatures[nftAddr][nftTokenId].add(sigTokenId);
        // emit Sign event
        emit Sign(msg.sender, sigTokenId, nftAddr, nftTokenId);
    }
/**
 * Call sign to sign any ERC-721 owned by msg.sender with a signature token,
 * while also burning your signature token.
 *
 * @param nftAddr: address of NFT to be signed
 * @param nftTokenId: tokenId of NFT to be signed, at nftAddr
 * @param sigTokenId: signature tokenId to sign with (in this contract)
 */