contract YellowDuckCollection is ERC721Enumerable, Ownable {
    using Strings for uint256;

    // for whitelist and yellowlist server signatures
    using ECDSA for bytes32;

    // metadata
    string public baseURI;
    
    string public placeholderURI;

    string private baseExtension = ".json";

    // cost of token
    // change using setCost
    uint256 public cost = 0.11 ether;

    // max total ducks
    uint256 public maxSupply = 999;

    // running supply
    uint256 public runningSupply = 0;

    // max mints per whitelisted address
    uint256 public maxWhitelistMintAmount = 3;

    // max public sale mints per address
    uint256 public maxPublicSaleMintAmount = 3;

    // number of mints per address for whitelist
    mapping(address => uint8) whitelistAddressMintedAmount;
    // number of mints per address for public sale
    mapping(address => uint8) publicSaleAddressMintedAmount;

    // 0: yellowlist
    // 1: whitelist
    // 2: public
    // 3: ended
    uint8 public saleState = 3;

    // yellowlist token IDs that have already been minted
    mapping(uint256 => bool) private mintedTokenIds;

    // yellowlist server signatures already used to mint a token (whitelist or yellow list)
    mapping(bytes => bool) private usedYellowlistBackendSignatures;

    // adddress for server signing for yellowlist or whitelist
    address private adminWalletAddress =
        0x4b9F8c0c724d231401Fc5709a956a4159f48754e;

    // keeps track of next sequential token ID
    uint256 private _nextTokenId = 122;

    bool private isRevealed = false;

    constructor(
        string memory _name,
        string memory _symbol
    ) ERC721(_name, _symbol) {
    }

    // UTILITY FUNCTIONS

    // use this function to get the hash of any string
    function _getHash(string memory str) internal pure returns (bytes32) {
        string memory prefix = "\x19Ethereum Signed Message:\n";
        string memory stringLength = Strings.toString(bytes(str).length);

        bytes memory abiPackedConcattedBytes = bytes.concat(
            bytes(prefix),
            bytes(stringLength),
            bytes(str)
        );

        bytes32 ret = keccak256(abiPackedConcattedBytes);
        return ret;
    }

    // used to verify signature from backend for yellowlist and whitelist
    function _verifyBackendSignature(
        string memory message,
        bytes memory signature
    ) public view returns (bool) {
        require(
            !usedYellowlistBackendSignatures[signature],
            "Signature has already been used to mint"
        );

        //we hash it on our side to verify the message is the one signed for
        bytes32 messageHash = _getHash(message);

        //check the signing address of the message did in fact sign that message message
        address signingAddress = ECDSA.recover(messageHash, signature);

        //verify that the message hash was signed by the admin wallet addreess
        bool valid = signingAddress == adminWalletAddress;

        require(valid, "Message not signed");

        return true;
    }

    // gets the next token id to mint
    function _getNextTokenId() private returns (uint256) {
        // we start at the current nextTokenId
        for (uint256 i = _nextTokenId; i <= maxSupply; i++) {
            // this begins token IDs at 1
            _nextTokenId++;
            // inject solidity

            // if the token has already been minted (via yellowlist), we keep going
            if (!mintedTokenIds[_nextTokenId]) {
                // otherwise, that's the ID we are gonna mint!
                return _nextTokenId;
            }
        }

        // there are no more tokens!
        require(false, "All tokens have been minted");
        return 0;
    }

    // ADMIN FUNCTIONS

    // requires yellowlist state
    modifier requireYellowlistState() {
        require(saleState == 0, "Not in yellowlist sale");
        _;
    }

    // requires yellowlist state
    modifier requireWhitelistState() {
        require(saleState == 1, "Not in whitelist sale");
        _;
    }

    // requires yellowlist state
    modifier _requirePublicSaleState() {
        require(saleState == 2, "Not in public sale");
        _;
    }

    // transition to yellowlist state
    function reveal() public onlyOwner {
        isRevealed = true;
    }

    // end sale
    function endSale() public onlyOwner {
        saleState = 3;
    }

    // transition to yellowlist state
    function enableYellowlistState() public onlyOwner {
        saleState = 0;
        cost = 0.11 ether;
    }

    // transition to whitelist state
    function enableWhitelistState() public onlyOwner {
        saleState = 1;
        cost = 0.11 ether;
    }

    // transition to public sale state
    function enablePublicSaleState() public onlyOwner {
        saleState = 2;
        cost = 0.12 ether;
    }

    // change the cost of the token
    function setCost(uint256 _newCost) public onlyOwner {
        cost = _newCost;
    }

    // set new base URI for metadata
    function setBaseURI(string memory _uri) public onlyOwner {
        baseURI = _uri;
    }

    // set new base URI for metadata
    function setPlaceholderURI(string memory _uri) public onlyOwner {
        placeholderURI = _uri;
    }

    // BUYER FUNCTIONS

    // used by buyer to mint a specific token if they are on yellowlist
    // must provide a signature from the backend
    function yellowlistMint(uint256 tokenId, bytes memory signature)
        public
        payable
        requireYellowlistState
    {
        // fails if signature is invalid
        _verifyBackendSignature(Strings.toString(tokenId), signature);

        require(msg.value == cost, "You must pay for your duck");

        // mint the specified tokenId
        _safeMint(msg.sender, tokenId);

        runningSupply++;

        mintedTokenIds[tokenId] = true;

        // use up the signature so that it can no longer be used to mint
        usedYellowlistBackendSignatures[signature] = true;
    }

    function toAsciiString(address x) internal pure returns (string memory) {
        bytes memory s = new bytes(40);
        for (uint i = 0; i < 20; i++) {
            bytes1 b = bytes1(uint8(uint(uint160(x)) / (2**(8*(19 - i)))));
            bytes1 hi = bytes1(uint8(b) / 16);
            bytes1 lo = bytes1(uint8(b) - 16 * uint8(hi));
            s[2*i] = char(hi);
            s[2*i+1] = char(lo);            
        }
        return string(bytes.concat(bytes("0x"), s));
    }

    function char(bytes1 b) internal pure returns (bytes1 c) {
        if (uint8(b) < 10) return bytes1(uint8(b) + 0x30);
        else return bytes1(uint8(b) + 0x57);
    }

    // used by buyer to mint if they're on the whitelist
    function whitelistMint(bytes memory signature, uint8 numberOfTokens)
        public
        payable
        requireWhitelistState

    {
        require(numberOfTokens > 0, "You must mint at least one duck.");
        // 111 is max yellowlist supply, so we'll remove these from the supply
        require(runningSupply + numberOfTokens <= (maxSupply - 122), "Purchase would exceed max supply.");
        require(
            whitelistAddressMintedAmount[msg.sender] + numberOfTokens <= maxWhitelistMintAmount,
            "You may mint up to 3 ducks."
        );

        require(msg.value == cost * numberOfTokens, "You must pay for your duck");

        string memory sender = toAsciiString(msg.sender);

        // fails if signature is invalid (message is the sender wallet address)
        _verifyBackendSignature(sender, signature);

        for (uint i = 0; i < numberOfTokens; i++) {
            // mint the next avaialble token id
            _safeMint(msg.sender, _getNextTokenId());

            runningSupply++;
            // track that the wallet minted another token
            whitelistAddressMintedAmount[msg.sender]++;
        }
    }

    // public
    function publicMint(uint8 numberOfTokens) public payable _requirePublicSaleState {
        require(numberOfTokens > 0, "You must mint at least one duck for public sale.");
        // 114 is max yellowlist supply, so we'll remove these from the supply
        require(runningSupply + numberOfTokens <= (maxSupply - 122), "Purchase would exceed max supply.");
        require(
            publicSaleAddressMintedAmount[msg.sender] + numberOfTokens <= maxPublicSaleMintAmount,
            "You may mint up to 3 ducks on the public list sale."
        );

        require(msg.value == cost * numberOfTokens, "You must pay for your duck");

        for (uint i = 0; i < numberOfTokens; i++) {
            // mint the next avaialble token id
            _safeMint(msg.sender, _getNextTokenId());

            runningSupply++;

            // track that the wallet minted another token!
            publicSaleAddressMintedAmount[msg.sender]++;
        }
    }

    // for OpenSea
    function tokenURI(uint256 tokenId)
        public
        view
        virtual
        override
        returns (string memory)
    {
        require(
            _exists(tokenId),
            "ERC721Metadata: URI query for nonexistent token"
        );

        return string(
            abi.encodePacked(
                isRevealed ? baseURI : placeholderURI,
                "/",
                tokenId.toString(),
                baseExtension
            )
        );
    }

    function withdraw() onlyOwner public {
        payable(msg.sender).transfer(address(this).balance);
    }

    function whitelistMintedByAddress() public view returns (uint8) {
        return whitelistAddressMintedAmount[msg.sender];
    }

    function publicMintedByAddress() public view returns (uint8) {
        return publicSaleAddressMintedAmount[msg.sender];
    }
}
// for whitelist and yellowlist server signatures
function setPlaceholderURI(string memory _uri) public onlyOwner {
    placeholderURI = _uri;
}

// set new base URI for metadata