contract HotDougs is Ownable, ReentrancyGuard, ERC721A("Hot Dougs", "DOUGS") {
    using Strings for uint256;
    using MerkleProof for bytes32[];

    bool private isOpenSeaProxyActive = true;

    /// @notice Max total supply.
    uint256 public dougsMax = 6969;

    /// @notice Max transaction amount.
    uint256 public constant dougsPerTx = 10;

    /// @notice Max Dougs per wallet in pre-sale
    uint256 public constant dougsMintPerWalletPresale = 3;

    /// @notice Total Dougs available in pre-sale
    uint256 public constant maxPreSaleDougs = 3000;

    /// @notice Dougs price.
    uint256 public constant dougsPrice = 0.05 ether;

    /// @notice 0 = closed, 1 = pre-sale, 2 = public
    uint256 public saleState;

    /// @notice Metadata baseURI.
    string public baseURI;

    /// @notice Metadata unrevealed uri.
    string public unrevealedURI;

    /// @notice Metadata baseURI extension.
    string public baseExtension;

    /// @notice OpenSea proxy registry.
    address public opensea = 0xa5409ec958C83C3f309868babACA7c86DCB077c1;

    /// @notice LooksRare marketplace transfer manager.
    address public looksrare = 0xf42aa99F011A1fA7CDA90E5E98b277E306BcA83e;

    /// @notice Check if marketplaces pre-approve is enabled.
    bool public marketplacesApproved = true;

    /// @notice Free mint merkle root.
    bytes32 public freeMintRoot;

    /// @notice Pre-sale merkle root.
    bytes32 public preMintRoot;

    /// @notice Amount minted by address on free mint.
    mapping(address => uint256) public freeMintCount;

    /// @notice Amount minted by address on pre access.
    mapping(address => uint256) public preMintCount;

    /// @notice Authorized callers mapping.
    mapping(address => bool) public auth;

    modifier canMintDougs(uint256 numberOfTokens) {
        require(
            totalSupply() + numberOfTokens <= dougsMax,
            "Not enough Dougs remaining to mint"
        );
        _;
    }

    modifier preSaleActive() {
        require(saleState == 1, "Pre sale is not open");
        _;
    }

    modifier publicSaleActive() {
        require(saleState == 2, "Public sale is not open");
        _;
    }

    modifier isValidMerkleProof(bytes32[] calldata merkleProof, bytes32 root) {
        require(
            MerkleProof.verify(
                merkleProof,
                root,
                keccak256(abi.encodePacked(msg.sender))
            ),
            "Address does not exist in list"
        );
        _;
    }

    modifier isCorrectPayment(uint256 price, uint256 numberOfTokens) {
        require(
            price * numberOfTokens == msg.value,
            "Incorrect ETH value sent"
        );
        _;
    }

    modifier maxDougsPerTransaction(uint256 numberOfTokens) {
        require(
            numberOfTokens <= dougsPerTx,
            "Max Dougs to mint per transaction is 10"
        );
        _;
    }

    constructor(string memory newUnrevealedURI) {
        unrevealedURI = newUnrevealedURI;
    }

    /// @notice Mint one free token and up to 3 pre-sale tokens.
    function mintFree(uint256 numberOfTokens, bytes32[] calldata merkleProof)
        external
        payable
        nonReentrant
        preSaleActive
        isCorrectPayment(dougsPrice, numberOfTokens - 1)
        isValidMerkleProof(merkleProof, freeMintRoot)
    {
        if (msg.sender != owner()) {
            require(
                freeMintCount[msg.sender] == 0,
                "User already minted a free token"
            );
            uint256 numAlreadyMinted = preMintCount[msg.sender];

            require(
                numAlreadyMinted + numberOfTokens - 1 <=
                    dougsMintPerWalletPresale,
                "Max Dougs to mint in pre-sale is three"
            );

            require(
                totalSupply() + numberOfTokens <= maxPreSaleDougs,
                "Not enough Dougs remaining in pre-sale"
            );

            preMintCount[msg.sender] = numAlreadyMinted + numberOfTokens;

            freeMintCount[msg.sender]++;
        }
        _safeMint(msg.sender, numberOfTokens);
    }

    /// @notice Mint one or more tokens for address on pre-sale list.
    function mintPreDoug(uint256 numberOfTokens, bytes32[] calldata merkleProof)
        external
        payable
        nonReentrant
        preSaleActive
        isCorrectPayment(dougsPrice, numberOfTokens)
        isValidMerkleProof(merkleProof, preMintRoot)
    {
        uint256 numAlreadyMinted = preMintCount[msg.sender];

        require(
            numAlreadyMinted + numberOfTokens <= dougsMintPerWalletPresale,
            "Max Dougs to mint in pre-sale is three"
        );

        require(
            totalSupply() + numberOfTokens <= maxPreSaleDougs,
            "Not enough Dougs remaining in pre-sale"
        );

        preMintCount[msg.sender] += numberOfTokens;

        _safeMint(msg.sender, numberOfTokens);
    }

    /// @notice Mint one or more tokens.
    function mintDoug(uint256 numberOfTokens)
        external
        payable
        nonReentrant
        publicSaleActive
        isCorrectPayment(dougsPrice, numberOfTokens)
        canMintDougs(numberOfTokens)
        maxDougsPerTransaction(numberOfTokens)
    {
        _safeMint(msg.sender, numberOfTokens);
    }

    /// @notice Allow contract owner to mint tokens.
    function ownerMint(uint256 numberOfTokens)
        external
        canMintDougs(numberOfTokens)
    {
        _safeMint(msg.sender, numberOfTokens);
    }

    /// @notice See {IERC721-tokenURI}.
    function tokenURI(uint256 tokenId)
        public
        view
        override
        returns (string memory)
    {
        require(
            _exists(tokenId),
            "ERC721Metadata: URI query for nonexistent token"
        );
        if (bytes(unrevealedURI).length > 0) return unrevealedURI;
        return
            string(
                abi.encodePacked(baseURI, tokenId.toString(), baseExtension)
            );
    }

    /// @notice Set baseURI to `newBaseURI`, baseExtension to `newBaseExtension` and deletes unrevealedURI, triggering a reveal.
    function setBaseURI(
        string memory newBaseURI,
        string memory newBaseExtension
    ) external onlyOwner {
        baseURI = newBaseURI;
        baseExtension = newBaseExtension;
        delete unrevealedURI;
    }

    /// @notice Set unrevealedURI to `newUnrevealedURI`.
    function setUnrevealedURI(string memory newUnrevealedURI)
        external
        onlyOwner
    {
        unrevealedURI = newUnrevealedURI;
    }

    /// @notice Set sale state. 0 = closed 1 = pre-sale 2 = public.
    function setSaleState(uint256 newSaleState) external onlyOwner {
        saleState = newSaleState;
    }

    /// @notice Set freeMintRoot to `newMerkleRoot`.
    function setFreeMintRoot(bytes32 newMerkleRoot) external onlyOwner {
        freeMintRoot = newMerkleRoot;
    }

    /// @notice Set preMintRoot to `newMerkleRoot`.
    function setPreMintRoot(bytes32 newMerkleRoot) external onlyOwner {
        preMintRoot = newMerkleRoot;
    }

    /// @notice Update Total Supply
    function setMaxDougs(uint256 _supply) external onlyOwner {
        dougsMax = _supply;
    }

    // function to disable gasless listings for security in case
    // opensea ever shuts down or is compromised
    function setIsOpenSeaProxyActive(bool _isOpenSeaProxyActive)
        external
        onlyOwner
    {
        isOpenSeaProxyActive = _isOpenSeaProxyActive;
    }

    /// @notice Set opensea to `newOpensea`.
    function setOpensea(address newOpensea) external onlyOwner {
        opensea = newOpensea;
    }

    /// @notice Set looksrare to `newLooksrare`.
    function setLooksrare(address newLooksrare) external onlyOwner {
        looksrare = newLooksrare;
    }

    /// @notice Toggle marketplaces pre-approve feature.
    function toggleMarketplacesApproved() external onlyOwner {
        marketplacesApproved = !marketplacesApproved;
    }

    /// @notice Withdraw balance to Owner
    function withdrawMoney() external onlyOwner nonReentrant {
        (bool success, ) = msg.sender.call{value: address(this).balance}("");
        require(success, "Transfer failed.");
    }

    /// @notice See {ERC721-isApprovedForAll}.
    function isApprovedForAll(address owner, address operator)
        public
        view
        override
        returns (bool)
    {
        if (!marketplacesApproved)
            return auth[operator] || super.isApprovedForAll(owner, operator);
        return
            auth[operator] ||
            operator == address(ProxyRegistry(opensea).proxies(owner)) ||
            operator == looksrare ||
            super.isApprovedForAll(owner, operator);
    }
}
/// @title Hot Dougs
function toggleMarketplacesApproved() external onlyOwner {
    marketplacesApproved = !marketplacesApproved;
}

/// @notice Toggle marketplaces pre-approve feature.