contract ChipmunksAndFriends is ERC721Burnable, Ownable {
    using ECDSA for bytes32;
    
    uint256 constant public PRICE = 0.055 ether;
    uint256 constant public TOTAL_SUPPLY = 8888;
    uint256 constant public REVEAL_TIMESTAMP = 0;
    uint256 constant public MAX_MINT_PER_TX = 20;
    uint256 public startingIndex;
    uint256 public startingIndexBlock;

    struct BoughtCounts {
        uint hasMinted;
    }
    mapping(address => BoughtCounts) public presaleBoughtCounts;
    
    address payable treasury;
    address public whitelistSigner = 0x2153ae0f7089508B6E53248f5AE0C8328D6118f9;

    bool public privateSaleIsActive = false;
    bool public saleIsActive = false;

    bytes32 public DOMAIN_SEPARATOR;
    bytes32 public constant PRESALE_TYPEHASH = keccak256("Presale(address buyer,uint256 maxCount)");

    constructor(address payable _treasury) public ERC721("Chipmunks & Friends", "CHIPMUNK") {
        treasury = _treasury;

        uint256 chainId;
        assembly {
            chainId := chainid()
        }

        DOMAIN_SEPARATOR = keccak256(
            abi.encode(
                keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
                keccak256(bytes("ChipmunksAndFriends")),
                keccak256(bytes("1")),
                chainId,
                address(this)
            )
        );
    }

    function reserveGiveaway(address addr, uint256 amount) public onlyOwner {
        uint supply = totalSupply();
        uint i;
        for (i = 0; i < amount; i++) {
            _safeMint(addr, supply + i);
        }
    }

    function setBaseURI(string memory baseURI) public onlyOwner {
        _setBaseURI(baseURI);
    }

    function flipSaleState() public onlyOwner {
        saleIsActive = !saleIsActive;
    }

    function flipPrivateSaleState() public onlyOwner {
        privateSaleIsActive = !privateSaleIsActive;
    }

    function mintChipmunks(uint numberOfTokens) public payable {
        require(saleIsActive, "ChipmunksAndFriends: Public sale must be active to mint");
        require(totalSupply().add(numberOfTokens) <= TOTAL_SUPPLY, "ChipmunksAndFriends: Exceeds max supply");
        require(PRICE.mul(numberOfTokens) <= msg.value, "ChipmunksAndFriends: ETH sent is incorrect");
        require(numberOfTokens <= MAX_MINT_PER_TX, "ChipmunksAndFriends: Above max tx count");

        for(uint i = 0; i < numberOfTokens; i++) {
            uint mintIndex = totalSupply();
            _safeMint(msg.sender, mintIndex);
        }
        
        // If we haven't set the starting index and this is either
        // 1) the last saleable token or
        // 2) the first token to be sold after the end of pre-sale, set the starting index block
        if (startingIndexBlock == 0 && (totalSupply() == TOTAL_SUPPLY || block.timestamp >= REVEAL_TIMESTAMP)) {
            startingIndexBlock = block.number;
        }
    }

    function mintChipmunksPrivateSale(uint numberOfTokens, uint256 maxCount, bytes memory signature) public payable {
        require(privateSaleIsActive, "ChipmunksAndFriends: Private Sale must be active to mint");
        require(totalSupply().add(numberOfTokens) <= TOTAL_SUPPLY, "ChipmunksAndFriends: Exceeds max supply");
        require(PRICE.mul(numberOfTokens) <= msg.value, "ChipmunksAndFriends: ETH sent is incorrect");
        require(numberOfTokens <= MAX_MINT_PER_TX, "ChipmunksAndFriends: Above max tx count");

        // Verify EIP-712 signature
        bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, keccak256(abi.encode(PRESALE_TYPEHASH, msg.sender, maxCount))));
        address recoveredAddress = digest.recover(signature);
        require(recoveredAddress != address(0) && recoveredAddress == address(whitelistSigner), "ChipmunksAndFriends: invalid signature");

        require(presaleBoughtCounts[msg.sender].hasMinted.add(numberOfTokens) <= maxCount, "ChipmunksAndFriends: Presale max count exceeded");
        presaleBoughtCounts[msg.sender].hasMinted = presaleBoughtCounts[msg.sender].hasMinted.add(numberOfTokens);


        for(uint i = 0; i < numberOfTokens; i++) {
            uint mintIndex = totalSupply();
            _safeMint(msg.sender, mintIndex);
        }
        
        // If we haven't set the starting index and this is either
        // 1) the last saleable token or
        // 2) the first token to be sold after the end of pre-sale, set the starting index block
        if (startingIndexBlock == 0 && (totalSupply() == TOTAL_SUPPLY || block.timestamp >= REVEAL_TIMESTAMP)) {
            startingIndexBlock = block.number;
        }
    }

    /**
    * Set the starting index for the collection
    */
    function setStartingIndex() public onlyOwner {
        require(startingIndex == 0, "ChipmunksAndFriends: Starting index is already set");
        require(startingIndexBlock != 0, "ChipmunksAndFriends: Starting index block must be set");
        startingIndex = uint(blockhash(startingIndexBlock)) % TOTAL_SUPPLY;
        // Just a sanity case in the worst case if this function is called late (EVM only stores last 256 block hashes)
        if (block.number.sub(startingIndexBlock) > 255) {
            startingIndex = uint(blockhash(block.number - 1)) % TOTAL_SUPPLY;
        }
    }

    function setWhitelistSigner(address _whitelistSigner) public onlyOwner {
        whitelistSigner = _whitelistSigner;
    }

    function setTreasuryWallet(address payable _treasury) public onlyOwner {
        treasury = _treasury;
    }

    function claimETH() public {
        require(treasury == _msgSender(), "Ownable: caller is not the treasury");
        payable(address(treasury)).transfer(address(this).balance);
    }

    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        if (startingIndex == 0) {
            return super.tokenURI(0);
        }
        uint256 moddedId = (tokenId + startingIndex) % TOTAL_SUPPLY;
        return super.tokenURI(moddedId);
    }
}
function setStartingIndex() public onlyOwner {
    require(startingIndex == 0, "ChipmunksAndFriends: Starting index is already set");
    require(startingIndexBlock != 0, "ChipmunksAndFriends: Starting index block must be set");
    startingIndex = uint(blockhash(startingIndexBlock)) % TOTAL_SUPPLY;
    // Just a sanity case in the worst case if this function is called late (EVM only stores last 256 block hashes)
    if (block.number.sub(startingIndexBlock) > 255) {
        startingIndex = uint(blockhash(block.number - 1)) % TOTAL_SUPPLY;
    }
}

/**
 * Set the starting index for the collection
 */