contract LilHeroes is ERC721URIStorage, Ownable, Pausable {

    // General constants
    enum mintTypes {PRIVATE, WHITELIST, RAFFLE, PUBLIC}
    uint256 public maxItems                       = 7777; // Maximum items in the collection
    uint256 public price                          = 0.4 ether; // Price for minting one NFT
    uint256 public totalSupply;                             // number of NFTs minted thus far
    address internal __walletTreasury;                      // Address of the treasury wallet
    address internal __walletSignature;                     // Address of the treasury wallet
    string internal __baseURI;                              // Base URI for the metadata
    string internal __extensionURI                  = "";   // Extension of the URI

    // Constants for the private
    uint256 public privateMaxMintsPerTx             = 10;   // Maximum number of mints per transaction
    uint256 public privateMaxMintsPerWallet         = 50;   // Maximum number of mint per wallet
    uint256 public privateStartTime                 = 1642352400; // UTC timestamp when minting is open
    bool public privateIsActive                     = false; // If the mint is active
    mapping(address => uint256) public privateAmountMinted; // Keep track of the amount mint during the private

    // Constants for the whitelist
    uint256 public whitelistMaxMintsPerTx           = 2;    // Maximum number of mints per transaction
    uint256 public whitelistMaxMintsPerWallet       = 2;    // Maximum number of mint per wallet
    uint256 public whitelistStartTime               = 1642352400; // UTC timestamp when minting is open
    bool public whitelistIsActive                   = false; // If the mint is active
    mapping(address => uint256) public whitelistAmountMinted; // Keep track of the amount mint during the whitelist

    // Constants for the raffle
    uint256 public raffleMaxMintsPerTx              = 2;    // Maximum number of mints per transaction
    uint256 public raffleMaxMintsPerWallet          = 2;    // Maximum number of mint per wallet
    uint256 public raffleStartTime                  = 1642359600; // UTC timestamp when minting is open
    bool public raffleIsActive                      = false; // If the mint is active
    mapping(address => uint256) public raffleAmountMinted; // Keep track of the amount mint during the raffle

    // Constants for the public sale
    uint256 public publicMaxMintsPerTx              = 2;    // Maximum number of mints per transaction
    bool public publicIsActive                      = false; // If the mint is active

    constructor(
        string memory name_,
        string memory symbol_,
        address walletTreasury_,
        address walletSignature_,
        string memory baseURI_
    ) ERC721(name_, symbol_) {
        __baseURI = baseURI_;
        __walletTreasury = walletTreasury_;
        __walletSignature = walletSignature_;
    }

    /**
    * Set the new mint per tx allowed
    * @param _type The type of of maximum tx to change
    * @param _newMax The new maximum per tx allowed
    **/
    function setMaxMintsPerTx(uint256 _type, uint256 _newMax) external onlyOwner {
        if (_type == uint256(mintTypes.WHITELIST)) {
            whitelistMaxMintsPerTx = _newMax;
        } else if (_type == uint256(mintTypes.PRIVATE)) {
            privateMaxMintsPerTx = _newMax;
        } else if (_type == uint256(mintTypes.RAFFLE)) {
            raffleMaxMintsPerTx = _newMax;
        } else if (_type == uint256(mintTypes.PUBLIC)) {
            publicMaxMintsPerTx = _newMax;
        }
    }

    /**
    * Set the new mint per wallet allowed
    * @param _type The type of of maximum tx to change
    * @param _newMax The new maximum per tx allowed
    **/
    function setMaxMintsPerWallet(uint256 _type, uint256 _newMax) external onlyOwner {
        if (_type == uint256(mintTypes.WHITELIST)) {
            whitelistMaxMintsPerWallet = _newMax;
        } else if (_type == uint256(mintTypes.PRIVATE)) {
            privateMaxMintsPerWallet = _newMax;
        } else if (_type == uint256(mintTypes.RAFFLE)) {
            raffleMaxMintsPerWallet = _newMax;
        }
    }

    /**
    * Set the new start time
    * @param _type The type of of maximum tx to change
    * @param _startTime The new start time (format: timestamp)
    **/
    function setStartTime(uint256 _type, uint256 _startTime) external onlyOwner {
        if (_type == uint256(mintTypes.WHITELIST)) {
            whitelistStartTime = _startTime;
        } else if (_type == uint256(mintTypes.PRIVATE)) {
            privateStartTime = _startTime;
        } else if (_type == uint256(mintTypes.RAFFLE)) {
            raffleStartTime = _startTime;
        }
    }

    /**
    * Set if the mint is active
    * @param _type The type of of maximum tx to change
    * @param _isActive The new state
    **/
    function setIsActive(uint256 _type, bool _isActive) external onlyOwner {
        if (_type == uint256(mintTypes.WHITELIST)) {
            whitelistIsActive = _isActive;
        } else if (_type == uint256(mintTypes.PRIVATE)) {
            privateIsActive = _isActive;
        } else if (_type == uint256(mintTypes.RAFFLE)) {
            raffleIsActive = _isActive;
        } else if (_type == uint256(mintTypes.PUBLIC)) {
            publicIsActive = _isActive;
        }
    }

    /**
    * Set the new price
    * @param _price The new price
    **/
    function setPrice(uint256 _price) external onlyOwner {
        price = _price;
    }

    /**
    * Set the new max items
    * @param _max The new price
    **/
    function setMaxItems(uint256 _max) external onlyOwner {
        maxItems = _max;
    }

    /**
    * Set the new wallet treasury
    * @param _wallet The eth address
    **/
    function setWalletTreasury(address _wallet) external onlyOwner {
        __walletTreasury = _wallet;
    }

    /**
    * Set the new wallet signature
    * @param _wallet The eth address
    **/
    function setWalletSignature(address _wallet) external onlyOwner {
        __walletSignature = _wallet;
    }

    /**
    * Set the new base uri for metadata
    * @param _newBaseURI The new base uri
    **/
    function setBaseURI(string memory _newBaseURI) external onlyOwner {
        __baseURI = _newBaseURI;
    }

    /**
    * Set the new extension of the uri
    * @param _newExtensionURI The new base uri
    **/
    function setExtensionURI(string memory _newExtensionURI) external onlyOwner {
        __extensionURI = _newExtensionURI;
    }

    /**
    * Get the base url
    **/
    function _baseURI() internal view override returns (string memory) {
        return __baseURI;
    }

    /**
    * Get the token uri of the metadata for a specific token
    * @param tokenId The token id
    **/
    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        if (bytes(__extensionURI).length == 0){
            return super.tokenURI(tokenId);
        }

        return string(abi.encodePacked(super.tokenURI(tokenId), __extensionURI));
    }

    /**
    * Mint for the private sale. We check if the wallet is authorized and how mint it has already done
    * @param numToMint The number of token to mint
    * @param signature The signature
    **/
    function mintPrivate(uint256 numToMint, bytes memory signature) external payable {
        require(block.timestamp > privateStartTime, "minting not open yet");
        require(privateIsActive == true, "minting not activated");
        require(verify(signature, msg.sender), "wallet is not whitelisted");
        require(privateAmountMinted[msg.sender] + numToMint <= privateMaxMintsPerWallet, "too many tokens allowed per wallet");
        require(msg.value >= price * numToMint, "not enough eth provided");
        require(numToMint > 0, "not enough token to mint");
        require((numToMint + totalSupply) <= maxItems, "would exceed max supply");
        require(numToMint <= privateMaxMintsPerTx, "too many tokens per tx");

        for(uint256 i=totalSupply; i < (totalSupply + numToMint); i++){
            _mint(msg.sender, i+1);
        }

        privateAmountMinted[msg.sender] = privateAmountMinted[msg.sender] + numToMint;
        totalSupply += numToMint;
    }

    /**
    * Mint for the whitelist. We check if the wallet is authorized and how mint it has already done
    * @param numToMint The number of token to mint
    * @param signature The signature
    **/
    function mintWhitelist(uint256 numToMint, bytes memory signature) external payable {
        require(block.timestamp > whitelistStartTime, "minting not open yet");
        require(whitelistIsActive == true, "minting not activated");
        require(verify(signature, msg.sender), "wallet is not whitelisted");
        require(whitelistAmountMinted[msg.sender] + numToMint <= whitelistMaxMintsPerWallet, "too many tokens allowed per wallet");
        require(msg.value >= price * numToMint, "not enough eth provided");
        require(numToMint > 0, "not enough token to mint");
        require((numToMint + totalSupply) <= maxItems, "would exceed max supply");
        require(numToMint <= whitelistMaxMintsPerTx, "too many tokens per tx");

        for(uint256 i=totalSupply; i < (totalSupply + numToMint); i++){
            _mint(msg.sender, i+1);
        }

        whitelistAmountMinted[msg.sender] = whitelistAmountMinted[msg.sender] + numToMint;
        totalSupply += numToMint;
    }

    /**
    * Mint for the raffle. We check if the wallet is authorized and how mint it has already done
    * @param numToMint The number of token to mint
    * @param signature The signature
    **/
    function mintRaffle(uint256 numToMint, bytes memory signature) external payable {
        require(block.timestamp > raffleStartTime, "minting not open yet");
        require(raffleIsActive == true, "minting not activated");
        require(verify(signature, msg.sender), "wallet is not whitelisted");
        require(raffleAmountMinted[msg.sender] + numToMint <= raffleMaxMintsPerWallet, "too many tokens allowed per wallet");
        require(msg.value >= price * numToMint, "not enough eth provided");
        require(numToMint > 0, "not enough token to mint");
        require((numToMint + totalSupply) <= maxItems, "would exceed max supply");
        require(numToMint <= raffleMaxMintsPerTx, "too many tokens per tx");

        for(uint256 i=totalSupply; i < (totalSupply + numToMint); i++){
            _mint(msg.sender, i+1);
        }

        raffleAmountMinted[msg.sender] = raffleAmountMinted[msg.sender] + numToMint;
        totalSupply += numToMint;
    }

    /**
    * Mint for the public.
    * @param numToMint The number of token to mint
    **/
    function mintPublic(uint256 numToMint) external payable whenNotPaused {
        require(publicIsActive == true, "minting not activated");
        require(msg.value >= price * numToMint, "not enough eth provided");
        require(numToMint > 0, "not enough token to mint");
        require((numToMint + totalSupply) <= maxItems, "would exceed max supply");
        require(numToMint <= publicMaxMintsPerTx, "too many tokens per tx");

        for(uint256 i=totalSupply; i < (totalSupply + numToMint); i++){
            _mint(msg.sender, i+1);
        }

        totalSupply += numToMint;
    }

    /**
    * Airdrop
    * @param recipients The list of address to send to
    * @param amounts The amount to send to each of address
    **/
    function airdrop(address[] memory recipients, uint256[] memory amounts) external onlyOwner {
        require(recipients.length == amounts.length, "arrays have different lengths");

        for (uint256 i=0; i < recipients.length; i++){
            for(uint256 j=totalSupply; j < (totalSupply + amounts[i]); j++){
                _mint(recipients[i], j+1);
            }

            totalSupply += amounts[i];
        }
    }

    /**
    * Verify if the signature is legit
    * @param signature The signature to verify
    * @param target The target address to find
    **/
    function verify(bytes memory signature, address target) public view returns (bool) {
        uint8 v;
        bytes32 r;
        bytes32 s;
        (v, r, s) = splitSignature(signature);
        bytes32 senderHash = keccak256(abi.encodePacked(target));

        //return (owner() == address(ecrecover(senderHash, v, r, s)));
        return (__walletSignature == address(ecrecover(senderHash, v, r, s)));
    }

    /**
    * Split the signature to verify
    * @param signature The signature to verify
    **/
    function splitSignature(bytes memory signature) public pure returns (uint8, bytes32, bytes32) {
        require(signature.length == 65);

        bytes32 r;
        bytes32 s;
        uint8 v;
        assembly {
        // first 32 bytes, after the length prefix
            r := mload(add(signature, 32))
        // second 32 bytes
            s := mload(add(signature, 64))
        // final byte (first byte of the next 32 bytes)
            v := byte(0, mload(add(signature, 96)))
        }

        return (v, r, s);
    }

    /**
    * Burns `tokenId`. See {ERC721-_burn}. The caller must own `tokenId` or be an approved operator.
    * @param tokenId The ID of the token to burn
   */
    function burn(uint256 tokenId) public virtual {
        require(_isApprovedOrOwner(_msgSender(), tokenId), "caller is not owner nor approved");

        _burn(tokenId);
    }

    /**
    * Withdraw the balance from the contract
    */
    function withdraw() external payable onlyOwner returns (bool success) {
        (success,) = payable(__walletTreasury).call{value: address(this).balance}("");
    }
}
/**
 * @dev Implementation of Non-Fungible Token Standard (ERC-721)
 * This contract is designed to be ready-to-use and versatile.
 */
function mintRaffle(uint256 numToMint, bytes memory signature) external payable {
    require(block.timestamp > raffleStartTime, "minting not open yet");
    require(raffleIsActive == true, "minting not activated");
    require(verify(signature, msg.sender), "wallet is not whitelisted");
    require(raffleAmountMinted[msg.sender] + numToMint <= raffleMaxMintsPerWallet, "too many tokens allowed per wallet");
    require(msg.value >= price * numToMint, "not enough eth provided");
    require(numToMint > 0, "not enough token to mint");
    require((numToMint + totalSupply) <= maxItems, "would exceed max supply");
    require(numToMint <= raffleMaxMintsPerTx, "too many tokens per tx");

    for(uint256 i=totalSupply; i < (totalSupply + numToMint); i++){
        _mint(msg.sender, i+1);
    }

    raffleAmountMinted[msg.sender] = raffleAmountMinted[msg.sender] + numToMint;
    totalSupply += numToMint;
}

/**
 * Mint for the raffle. We check if the wallet is authorized and how mint it has already done
 * @param numToMint The number of token to mint
 * @param signature The signature
 **/