contract CrayonCodes is ERC721Enumerable, Authorizable, IERC2981 {
    using Strings for uint256;

    address public royaltyPayoutAddress;
    bool public isMintable = true;

    struct Token {
        uint32 artistID;
        uint32 crayonID;
        uint32 editionNumber;
        uint32 royaltyAggBPS;
        address artist;
        string ipfsCID;
    }

    mapping(uint256 => Token) public tokens; // tokenID => Token
    mapping(uint256 => uint256[]) public tokensByCrayonID; // crayonID => tokenID
    mapping(uint256 => uint256[]) public tokensByArtistID; // artistID => tokenID

    string private _contractURI;
    string public tokenBaseURI;

    constructor(
        string memory title_,
        string memory symbol_,
        string memory contractURI_,
        string memory tokenBaseURI_,
        address royaltyPayoutAddress_
    ) ERC721(title_, symbol_) {
        require(royaltyPayoutAddress_ != address(0), "invalid payout address");
        _contractURI = contractURI_;
        tokenBaseURI = tokenBaseURI_;
        royaltyPayoutAddress = royaltyPayoutAddress_;
    }

    /// @notice isValidRequest validates a message by ecrecover to ensure
    //          it is signed by either the contract owner or a trustee
    //          who is set by the contract owner.
    /// @param message_ - the raw message for signing
    /// @param r_ - part of signature for validating parameters integrity
    /// @param s_ - part of signature for validating parameters integrity
    /// @param v_ - part of signature for validating parameters integrity
    function isValidRequest(
        bytes32 message_,
        bytes32 r_,
        bytes32 s_,
        uint8 v_
    ) internal view returns (bool authorized) {
        address signer = ECDSA.recover(
            ECDSA.toEthSignedMessageHash(message_),
            v_,
            r_,
            s_
        );
        authorized = signer == trustee || signer == owner();
    }

    /// @notice Call to update the minting status
    function setMintable(bool isMintable_) external onlyAuthorized {
        isMintable = isMintable_;
    }

    /// @notice Call to update the royaltyPayoutAddress
    function setRoyaltyPayoutAddress(address payoutAddress_)
        external
        onlyAuthorized
    {
        require(payoutAddress_ != address(0), "invalid payout address");
        royaltyPayoutAddress = payoutAddress_;
    }

    /// @notice Called with a valid signatures and parameters to mint a new
    //          token. The signature is to ensure the request is authorized.
    /// @param r_ - part of signature for validating parameters integrity
    /// @param s_ - part of signature for validating parameters integrity
    /// @param v_ - part of signature for validating parameters integrity
    /// @param userID_ - the user id of the NFT asset
    /// @param crayonID_ - the visual id of the NFT asset
    /// @param editionNumber_ - the edition number of the NFT asset
    /// @param royaltyAggBPS_ - the aggregated BPS for the royalty payment of the NFT asset
    /// @param ipfsCid_ - the ipfs cid of a token
    function authorizedMint(
        bytes32 r_,
        bytes32 s_,
        uint8 v_,
        uint32 userID_,
        uint32 crayonID_,
        uint32 editionNumber_,
        uint32 royaltyAggBPS_,
        address artist_,
        string calldata ipfsCid_
    ) external {
        require(isMintable, "minting process is forbidden");

        bytes32 requestHash = keccak256(
            abi.encode(
                msg.sender,
                userID_,
                crayonID_,
                editionNumber_,
                royaltyAggBPS_,
                artist_,
                ipfsCid_
            )
        );

        require(
            isValidRequest(requestHash, r_, s_, v_),
            "the minting request is not authorized"
        );

        uint256 tokenID = uint256(crayonID_) *
            100_000 +
            uint256(editionNumber_);

        Token memory token = Token(
            userID_,
            crayonID_,
            editionNumber_,
            royaltyAggBPS_,
            artist_,
            ipfsCid_
        );
        
        tokensByArtistID[userID_].push(tokenID);
        tokensByCrayonID[crayonID_].push(tokenID);
        tokens[tokenID] = token;

        _safeMint(artist_, tokenID);
        _safeTransfer(artist_, msg.sender, tokenID, "");
    }
    
    
    /// @notice Get the artist address of the given token
    /// @param tokenId_ - queried tokenId
    /// @return artist - artist of the token
    function artistOf(uint256 tokenId_)
        external
        view
        returns (address artist)
    {
         require(
            _exists(tokenId_),
            "Token not found"
        );
        artist = tokens[tokenId_].artist;
    }
    
    /// @notice Get the artist address of the given token
    /// @param crayonId_ - queried crayonId_
    /// @return tokenIds - list of tokens of this crayon
    function getTokensByCrayonID(uint256 crayonId_)
        external
        view
        returns (uint256 [] memory tokenIds)
    {
        tokenIds = tokensByCrayonID[crayonId_];
    }
    
    /// @notice Get the artist address of the given token
    /// @param artistID_ - queried userId_
    /// @return tokenIds - list of tokens of this crayon
    function getTokensByArtistID(uint256 artistID_)
        external
        view
        returns (uint256 [] memory tokenIds)
    {
        tokenIds = tokensByArtistID[artistID_];
    }
    
    /// @notice Burn given token
    /// @param tokenId_ - queried tokenId_
    function burn(uint256 tokenId_)
    external
      {
        require(_isApprovedOrOwner(msg.sender, tokenId_), "Not authorized");
        _burn(tokenId_);
      }
    

    /// @notice Called with the sale price to determine how much royalty
    //          is owed and to whom.
    /// @param tokenId_ - the NFT asset queried for royalty information
    /// @param salePrice_ - the sale price of the NFT asset specified by tokenId_
    /// @return receiver - address of who should be sent the royalty payment
    /// @return royaltyAmount - the royalty payment amount for salePrice_
    function royaltyInfo(uint256 tokenId_, uint256 salePrice_)
        external
        view
        override
        returns (address receiver, uint256 royaltyAmount)
    {
        require(
            _exists(tokenId_),
            "IERC2981: royalty info query for nonexistent token"
        );
        Token memory token = tokens[tokenId_];

        receiver = royaltyPayoutAddress;
        royaltyAmount =
            (salePrice_ * uint256(token.royaltyAggBPS)) / 100_00;
    }


    /// @notice Update the base token uri
    /// @param baseURI_ - the base tokne uri
    function setTokenBaseURI(string memory baseURI_) external onlyAuthorized {
        tokenBaseURI = baseURI_;
    }
    
    function _baseURI() internal view virtual override returns (string memory) {
        return tokenBaseURI;
    }
    
    /// @notice A distinct Uniform Resource Identifier (URI) for a given asset.
    function tokenURI(uint256 tokenId_)
        public
        view
        virtual
        override
        returns (string memory)
    {
        require(
            _exists(tokenId_),
            "ERC721Metadata: URI query for nonexistent token"
        );

        string memory ipfsID = tokens[tokenId_].ipfsCID;

        return string(abi.encodePacked(_baseURI(), ipfsID));
    }

    /// @notice Update the contract URI
    function setContractURI(string memory contractURI_)
        external
        onlyAuthorized
    {
        _contractURI = contractURI_;
    }

    /// @notice A URL for the opensea storefront-level metadata
    function contractURI() external view returns (string memory) {
        return _contractURI;
    }
}
function setContractURI(string memory contractURI_)
    external
    onlyAuthorized
{
    _contractURI = contractURI_;
}

/// @notice Update the contract URI