contract CrazyJoker is ERC721Enumerable, Ownable {
    using ECDSA for bytes32;
    using SafeCast for uint256;
    
    event BaseURIChanged(string newBaseURI);
    event TokenPriceChanged(uint256 newTokenPrice);
    event PresaleInfoChanged(address whitelistSigner, uint32 startTime, uint32 endTime);
    event SaleInfoChanged(uint32 startTime, uint32 maxCountPerTx);
    event PresaleMint(address minter, uint256 count);
    event SaleMint(address minter, uint256 count);

    uint256 public maxTokenSupply;
    uint256 public mintPrice = 70000000000000000; // 0.070 ETH
    uint256 public nextTokenId = 0;

    struct PresaleInfo {
        address whitelistSigner;
        uint32 startTime;
        uint32 endTime;
    }
    struct SaleInfo {
        uint32 startTime;
        uint32 maxCountPerTx;
    }
    PresaleInfo public presaleInfo;
    SaleInfo public saleInfo;

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

    mapping(address => uint256) public presaleBoughtCounts;
    string public baseURI;

    constructor(string memory name, string memory symbol, uint256 maxSupply) ERC721(name, symbol) {
        maxTokenSupply = maxSupply;

        uint256 chainId;
        assembly {
            chainId := chainid()
        }
        DOMAIN_SEPARATOR = keccak256(
            abi.encode(
                keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
                keccak256(bytes("Crazy Joker")),
                keccak256(bytes("1")),
                chainId,
                address(this)
            )
        );
    }

    function configPresale(
        address whitelistSigner,
        uint256 startTime,
        uint256 endTime
    ) external onlyOwner {
        uint32 _startTime = startTime.toUint32();
        uint32 _endTime = endTime.toUint32();

        require(whitelistSigner != address(0), "Crazy Joker: Zero address");
        require(_startTime > 0 && _endTime > _startTime, "Crazy Joker: Invalid time range");

        presaleInfo = PresaleInfo({whitelistSigner: whitelistSigner, startTime: _startTime, endTime: _endTime});

        emit PresaleInfoChanged(whitelistSigner, _startTime, _endTime);
    }

    function configSale(
        uint256 startTime,
        uint256 maxCountPerTx
    ) external onlyOwner {
        uint32 _startTime = startTime.toUint32();
        uint32 _maxCountPerTx = maxCountPerTx.toUint32();

        require(_maxCountPerTx > 0, "Crazy Joker: Zero amount");
        require(_startTime > 0, "Crazy Joker: Invalid time range");

        saleInfo = SaleInfo({startTime: _startTime, maxCountPerTx: _maxCountPerTx});

        emit SaleInfoChanged(_startTime, _maxCountPerTx);
    }

    function setMintPrice(uint256 newPrice) public onlyOwner {
        mintPrice = newPrice;
        emit TokenPriceChanged(mintPrice);
    }

    function withdraw() public onlyOwner {
        uint balance = address(this).balance;
        Address.sendValue(payable(owner()), balance);
    }

    /*
    * Mint reserved NFTs for giveaways, devs, etc.
    */
    function reserveTokens(address recipient, uint256 count) external onlyOwner {
        require(recipient != address(0), "Crazy Joker: Zero address");
        require(nextTokenId + count <= maxTokenSupply, "Crazy Joker: Max supply exceeded");

        uint256 _nextTokenId = nextTokenId;
        for (uint256 ind = 0; ind < count; ind++) {
            _safeMint(recipient, _nextTokenId + ind);
        }
        nextTokenId += count;
    }

    function mintPresaleJokers(
        uint256 count,
        uint256 maxCount,
        bytes calldata signature
    ) external payable {
        uint256 _nextTokenId = nextTokenId;

        PresaleInfo memory _presaleInfo = presaleInfo;
        require(_presaleInfo.whitelistSigner != address(0), "Crazy Joker: Presale not configured");

        require(mintPrice > 0, "Crazy Joker: Token price not set");
        require(count > 0, "Crazy Joker: Invalid count");
        require(block.timestamp >= _presaleInfo.startTime, "Crazy Joker: Presale not started");
        require(block.timestamp < _presaleInfo.endTime, "Crazy Joker: Presale has ended");

        require(_nextTokenId + count <= maxTokenSupply, "Crazy Joker: Max supply exceeded");
        require(mintPrice * count <= msg.value, "Crazy Joker: Incorrect Ether value");

        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 == _presaleInfo.whitelistSigner,
            "Crazy Joker: Invalid signature"
        );

        require(presaleBoughtCounts[msg.sender] + count <= maxCount, "Crazy Joker: Presale max count exceeded");
        presaleBoughtCounts[msg.sender] += count;

        for (uint256 ind = 0; ind < count; ind++) {
            _safeMint(msg.sender, _nextTokenId + ind);
        }
        nextTokenId += count;

        emit PresaleMint(msg.sender, count);
    }

    function mintJokers(uint256 count) external payable {
        uint256 _nextTokenId = nextTokenId;

        SaleInfo memory _saleInfo = saleInfo;
        require(_saleInfo.startTime > 0, "Crazy Joker: Sale not configured");
        require(mintPrice > 0, "Crazy Joker: Token price not set");
        require(count > 0, "Crazy Joker: Invalid count");
        require(block.timestamp >= _saleInfo.startTime, "Crazy Joker: Sale not started");
        require(count <= _saleInfo.maxCountPerTx, "Crazy Joker: Max count per tx exceeded");
        require(_nextTokenId + count <= maxTokenSupply, "Crazy Joker: Max supply exceeded");
        require(mintPrice * count <= msg.value, "Crazy Joker: Incorrect Ether value");

        for (uint256 ind = 0; ind < count; ind++) {
            _safeMint(msg.sender, _nextTokenId + ind);
        }
        nextTokenId += count;

        emit SaleMint(msg.sender, count);
    }

    function _baseURI() internal view virtual override returns (string memory) {
        return baseURI;
    }

    function setBaseURI(string memory newBaseURI) public onlyOwner {
        baseURI = newBaseURI;
        emit BaseURIChanged(baseURI);
    }
}
//
//  _________                                  ____.       __                 
//  \_   ___ \____________  ___________.__.   |    | ____ |  | __ ___________ 
//  /    \  \/\_  __ \__  \ \___   <   |  |   |    |/  _ \|  |/ // __ \_  __ \
//  \     \____|  | \// __ \_/    / \___  /\__|    (  <_> )    <\  ___/|  | \/
//   \______  /|__|  (____  /_____ \/ ____\________|\____/|__|_ \\___  >__|   
//          \/            \/      \/\/                         \/    \/       
//
function reserveTokens(address recipient, uint256 count) external onlyOwner {
    require(recipient != address(0), "Crazy Joker: Zero address");
    require(nextTokenId + count <= maxTokenSupply, "Crazy Joker: Max supply exceeded");

    uint256 _nextTokenId = nextTokenId;
    for (uint256 ind = 0; ind < count; ind++) {
        _safeMint(recipient, _nextTokenId + ind);
    }
    nextTokenId += count;
}

/*
 * Mint reserved NFTs for giveaways, devs, etc.
 */