contract flipperthebird is ERC721, Ownable, ReentrancyGuard {
    using ECDSA for bytes32;
	using Strings for uint256;
    using SafeERC20 for IERC20;

    uint256 private _currentIndex = 0;
    uint256 private _totalBurned = 0;

	string private _baseTokenURI = "https://metadata.api.tokenfy.com/21/metadata/";
	string private _contractURI = "https://tokenfy-production-public.s3.us-east-2.amazonaws.com/21/contract-uri";

    address public tokenfyAddress = 0xa6dD98031551C23bb4A2fBE2C4d524e8f737c6f7;

    // sale currency settings
    bool public ethEnabled = true;
    bool public tokenfyEnabled = false;
    
 	// mint settings
	uint256 public maxSupply = 500;
	uint256 public maxPerMint = 10;
	uint256 public pricePerToken = 70000000000000000;
    uint256 public tokenfyPrice = 0;

    // presale settings
    address private signerAddress;
    mapping(address => uint256) public presaleMinted;
	uint256 public presaleMaxSupply = 500;
	uint256 public presaleMaxPerMint = 3;
	uint256 public presalePricePerToken = 69000000000000000;
    uint256 public tokenfyPresalePrice = 0;

 	// activation flags
	bool public instantRevealActive = false;
    bool public presaleLive = false;
	bool public saleLive = false;
	bool public burnLive = false;

    modifier presaleValid(bytes32 hash, bytes memory sig, uint256 qty, uint256 max, uint256 expiresAt) {
        require(presaleLive, "Presale not live");
        require(matchAddresSigner(hash, sig), "Invalid signer");
		require(hashTransaction(msg.sender, qty, max, expiresAt) == hash, "Hash check failed");
        require(expiresAt >= block.timestamp, "Signature is expired");
		require(qty <= presaleMaxPerMint, "Max per mint exceeded");
        require(presaleMinted[msg.sender] + qty <= max, "Max per wallet exceeded");
		require(_currentIndex + qty <= presaleMaxSupply, "Exceeds max supply");
        _;
    }

    modifier saleValid(uint256 qty) {
        require(saleLive, "Sale not live");
		require(qty <= maxPerMint, "Max per mint exceeded");
		require(_currentIndex + qty <= maxSupply, "Exceeds max supply");
        _;
    }    

	constructor(address _signerAddress) ERC721("Flipper the Bird", "FLPR") {
		signerAddress = _signerAddress;
	}

    function presaleMint(
        bytes32 hash,
        bytes memory sig,
        uint256 qty,
        uint256 max,
        uint256 expiresAt
    ) external payable nonReentrant presaleValid(hash, sig, qty, max, expiresAt) {
        require(ethEnabled, "ETH not enabled");
		require(presalePricePerToken * qty == msg.value, "Incorrect price");
        
        presaleMinted[msg.sender] += qty;

		for (uint256 i = 0; i < qty; i++) {
			mintToken(msg.sender);
		}
    }

    function presaleMintTokenfy(
        bytes32 hash, 
        bytes memory sig, 
        uint256 qty, 
        uint256 max,
        uint256 expiresAt,
        uint256 value
    ) external nonReentrant presaleValid(hash, sig, qty, max, expiresAt) {
        require(tokenfyEnabled, "TKNFY not enabled");
		require(tokenfyPresalePrice * qty == value, "Incorrect price");

        IERC20(tokenfyAddress).safeTransferFrom(msg.sender, address(this), value);

        presaleMinted[msg.sender] += qty;

		for (uint256 i = 0; i < qty; i++) {
			mintToken(msg.sender);
		}
    }

	function mint(uint256 qty) external payable nonReentrant saleValid(qty) {
        require(ethEnabled, "ETH not enabled");
		require(pricePerToken * qty == msg.value, "Incorrect price");

		for (uint256 i = 0; i < qty; i++) {
            mintToken(msg.sender);
		}
	}

    function mintTokenfy(uint256 qty, uint256 value) external nonReentrant saleValid(qty) {
        require(tokenfyEnabled, "TKNFY not enabled");
		require(tokenfyPrice * qty == value, "Incorrect price");

        IERC20(tokenfyAddress).safeTransferFrom(msg.sender, address(this), value);

		for (uint256 i = 0; i < qty; i++) {
            mintToken(msg.sender);
		}
	}

	function adminMint(uint256 qty, address to) public onlyOwner {
		require(qty > 0, "Must mint at least 1 token");
		require(_currentIndex + qty <= maxSupply, "Exceeds max supply");
		for (uint256 i = 0; i < qty; i++) {
			mintToken(to);
		}
	}

    function mintToken(address receiver) private {
        _currentIndex += 1;
        _safeMint(receiver, _currentIndex);
    }

    function hashTransaction(address sender, uint256 qty, uint256 max, uint256 expiresAt) private pure returns (bytes32) {
		bytes32 hash = keccak256(
			abi.encodePacked("\x19Ethereum Signed Message:\n32", keccak256(abi.encodePacked(sender, qty, max, expiresAt)))
		);
		return hash;
	}

	function matchAddresSigner(bytes32 hash, bytes memory signature) private view returns (bool) {
		return signerAddress == hash.recover(signature);
	}

	function burn(uint256 tokenId) public nonReentrant {
	    require(burnLive, "Burn not live");
		require(_isApprovedOrOwner(_msgSender(), tokenId), "Caller is not owner nor approved");
        _totalBurned += 1;
		_burn(tokenId);
	}

    function totalSupply() public view virtual returns (uint256) {
        return _currentIndex - _totalBurned;
    }

	function exists(uint256 _tokenId) external view returns (bool) {
		return _exists(_tokenId);
	}

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

	function setBaseURI(string memory newBaseURI) public onlyOwner {
		_baseTokenURI = newBaseURI;
	}

	function setContractURI(string memory newuri) public onlyOwner {
		_contractURI = newuri;
	}

	function contractURI() public view returns (string memory) {
		return _contractURI;
	}

	// withdrawing earnings
	function withdrawEarnings() public onlyOwner {
        uint256 balance = address(this).balance;
		payable(msg.sender).transfer(balance);
	}

    // reclaiming tokens
	function reclaimERC20(IERC20 erc20Token, address to) public onlyOwner {
		erc20Token.safeTransfer(to, erc20Token.balanceOf(address(this)));
	}

	function reclaimERC1155(IERC1155 erc1155Token, uint256 id, address to) public onlyOwner {
        erc1155Token.safeTransferFrom(address(this), to, id, 1, "");
    }

    function reclaimERC721(IERC721 erc721Token, uint256 id, address to) public onlyOwner {
        erc721Token.safeTransferFrom(address(this), to, id);
    }

    // activating contact events
    function setPresaleLive(bool live) external onlyOwner {
		presaleLive = live;
	}

	function setSaleLive(bool live) external onlyOwner {
		saleLive = live;
	}

	function setBurnLive(bool live) external onlyOwner {
		burnLive = live;
	}

	function setInstantReveal(bool reveal) external onlyOwner {
		instantRevealActive = reveal;
	}

    // mint-related settings
	function changePrice(uint256 newPrice) external onlyOwner {
		pricePerToken = newPrice;
	}

    function changeTokenfyPrice(uint256 newPrice) external onlyOwner {
		tokenfyPrice = newPrice;
	}

	function changeMaxSupply(uint256 _maxSupply) external onlyOwner {
		require(_maxSupply >= _currentIndex, "Must be larger than minted count");
		maxSupply = _maxSupply;
	}

	function setMaxPerMint(uint256 _maxPerMint) external onlyOwner {
	    require(_maxPerMint > 0, "Invalid max per mint");
		maxPerMint = _maxPerMint;
	}

    // presale-related settings
    function changePresalePrice(uint256 newPrice) external onlyOwner {
		presalePricePerToken = newPrice;
	}

    function changeTokenfyPresalePrice(uint256 newPrice) external onlyOwner {
		tokenfyPresalePrice = newPrice;
	}

	function changePresaleMaxSupply(uint256 _maxSupply) external onlyOwner {
		require(_maxSupply >= _currentIndex, "Must be larger than minted count");
        require(_maxSupply <= maxSupply, "Must be less than max supply");
		presaleMaxSupply = _maxSupply;
	}

	function setPresaleMaxPerMint(uint256 _maxPerMint) external onlyOwner {
	    require(_maxPerMint > 0, "Must be > 0");
		presaleMaxPerMint = _maxPerMint;
	}

    function setSignerAddress(address _signer) external onlyOwner {
        signerAddress = _signer;
    }

    function setETHEnabled(bool enabled) external onlyOwner {
        ethEnabled = enabled;
    }

    function setTokenfyEnabled(bool enabled) external onlyOwner {
        tokenfyEnabled = enabled;
    }
}
function withdrawEarnings() public onlyOwner {
       uint256 balance = address(this).balance;
	payable(msg.sender).transfer(balance);
}

// withdrawing earnings