contract MetaKnights is ERC721Enumerable, Ownable, ReentrancyGuard {

    uint256 public MAX_NFT_SUPPLY = 9999;
    uint256 public PRESALE_NFT = 1500;
    // uint256 public RESERVE_NFT = 3;

    uint256 public PRESALE_PRICE = 0.045 ether;

    uint256 public NFT_PRICE = 0.06 ether;
    uint256 public MAX_NFT_WALLET_PRESALE = 3;
    
    uint256 public MAX_NFT_WALLET = 10;

    uint256 public presaleClaimed;
    // uint256 public reserveClaimed;

    // uint256 public publicSaleStartTimestamp;

    uint256 public NAME_CHANGE_PRICE = 150 * (10 ** 18);

    string public baseTokenURI;

    bool public publicSaleActive ;
    bool public preSaleActive ;

    // Name change token address
    address public _utilityContract;
    
    mapping(address => bool) private whitelisted;
    mapping (uint256 => string) private _tokenName;
    mapping (string => bool) private _nameReserved;

    event NameChange (uint256 indexed NFTIndex, string newName);

    event BaseURIChanged(string baseURI);
    event PreSaleMint(address minter, uint256 amountOfNFTs);
    event FinalSaleMint(address minter, uint256 amountOfNFTs);

    constructor(address utilityContract) ERC721("MetaKnights", "Meta Token") {
        // baseTokenURI = baseURI;
        // publicSaleStartTimestamp = _publicSaleStartTimestamp;
        _utilityContract = utilityContract;
    }

    /**
     * @dev Returns name of the NFT at index.
     */
    function tokenNameByIndex(uint256 index) public view returns (string memory) {
        return _tokenName[index];
    }

    /**
     * @dev Returns if the name has been reserved.
     */
    function isNameReserved(string memory nameString) public view returns (bool) {
        return _nameReserved[toLower(nameString)];
    }

    function addToWhitelist(address[] calldata addresses) external onlyOwner {
        for (uint256 i = 0; i < addresses.length; i++) {
            require(addresses[i] != address(0), "Cannot add null address");

            whitelisted[addresses[i]] = true;
        }
    }

    function removeFromWhitelist(address[] calldata addresses) external onlyOwner {
        for (uint256 i = 0; i < addresses.length; i++) {
            require(addresses[i] != address(0), "Cannot add null address");

            whitelisted[addresses[i]] = false;
        }
    }

    function checkIfWhitelisted(address addr) external view returns (bool) {
        return whitelisted[addr];
    }

    function reserveNFT(uint256 amountOfNFTs) public onlyOwner {        
        // require(reserveClaimed + amountOfNFTs <= RESERVE_NFT, "Minting would exceed max reserved NFTs");
        require(totalSupply() + amountOfNFTs <= MAX_NFT_SUPPLY, "Minting would exceed max supply");
        for (uint i = 0; i < amountOfNFTs; i++) {
            uint supply = totalSupply();
            // if (totalSupply() < MAX_NFT_SUPPLY) 
            // {
                // reserveClaimed += 1;
                _safeMint(msg.sender, supply);
            // }
        }
    }
    
    function presaleMINT(uint256 amountOfNFTs) external payable {
        require(preSaleActive, "Presale has not started");
        require(whitelisted[msg.sender], "You are not whitelisted");
        require(presaleClaimed + amountOfNFTs <= PRESALE_NFT, "Minting would exceed max Presale NFTs");
        require(totalSupply() + amountOfNFTs <= MAX_NFT_SUPPLY, "Minting would exceed max supply");
        require(balanceOf(msg.sender) + amountOfNFTs <= MAX_NFT_WALLET_PRESALE, "Purchase exceeds max allowed per wallet");
        require(PRESALE_PRICE * amountOfNFTs == msg.value, "ETH amount is incorrect");

        for (uint256 i = 0; i < amountOfNFTs; i++) {
            uint256 tokenId = totalSupply();

            presaleClaimed += 1;
            _safeMint(msg.sender, tokenId);
        }
        
        emit PreSaleMint(msg.sender, amountOfNFTs);
    }

    function mintNFT(uint256 amountOfNFTs) external payable {
        require(publicSaleActive, "Final sale has not started");
        require(totalSupply() + amountOfNFTs <= MAX_NFT_SUPPLY, "Minting would exceed max supply");
        require(balanceOf(msg.sender) + amountOfNFTs <= MAX_NFT_WALLET, "Purchase exceeds max allowed per wallet");
        require(NFT_PRICE * amountOfNFTs == msg.value, "ETH amount is incorrect");

        for (uint256 i = 0; i < amountOfNFTs; i++) {
            uint256 tokenId = totalSupply();

            _safeMint(msg.sender, tokenId);
        }

        emit FinalSaleMint(msg.sender, amountOfNFTs);
    }

    /**
     * @dev Changes the name for MetaKnights tokenId
     */
    function changeName(uint256 tokenId, string memory newName) external {
        address owner = ownerOf(tokenId);

        require(_msgSender() == owner, "ERC721: caller is not the owner");
        require(validateName(newName) == true, "Not a valid new name");
        require(sha256(bytes(newName)) != sha256(bytes(_tokenName[tokenId])), "New name is same as the current one");
        require(isNameReserved(newName) == false, "Name already reserved");

        IERC20(_utilityContract).transferFrom(msg.sender, address(this), NAME_CHANGE_PRICE);
        // If already named, dereserve old name
        if (bytes(_tokenName[tokenId]).length > 0) {
            toggleReserveName(_tokenName[tokenId], false);
        }
        toggleReserveName(newName, true);
        _tokenName[tokenId] = newName;
        IERC20(_utilityContract).burn(NAME_CHANGE_PRICE);
        emit NameChange(tokenId, newName);
    }

    // function hasFinalSaleStarted() public view returns(bool){
    //     return (block.timestamp >= publicSaleStartTimestamp || reserveClaimed >= PRESALE_NFT) ? true : false;
    // }

    function setBaseURI(string memory baseURI) public onlyOwner {
        baseTokenURI = baseURI;
        emit BaseURIChanged(baseURI);
    }

    function setMAX_NFTS_WALLET_PRESALE(uint256 _amount) external onlyOwner {
        MAX_NFT_WALLET_PRESALE = _amount;
    }

    function setMAX_NFTS_WALLET(uint256 _amount) external onlyOwner {
        MAX_NFT_WALLET = _amount;
    }

    function setNFT_PRICE(uint256 _amount) external onlyOwner {
        NFT_PRICE = _amount;
    }

    function setPRESALE_PRICE(uint256 _amount) external onlyOwner {
        PRESALE_PRICE = _amount;
    }

    function setPRESALE_MAX_NFT(uint256 _amount) external onlyOwner {
        PRESALE_NFT = _amount;
    }

    function setNAME_CHANGE_PRICE(uint256 _amount) external onlyOwner {
        NAME_CHANGE_PRICE = _amount;
    }

    function flipPublicSaleActive() external onlyOwner {
        publicSaleActive = !publicSaleActive;
    }

    function flipPreSaleActive() external onlyOwner {
        preSaleActive = !preSaleActive;
    }

    function setUtility_Contract(address utilityContract) external onlyOwner {
        _utilityContract = utilityContract;
    }

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

    function withdrawAll() public onlyOwner nonReentrant {
        uint256 balance = address(this).balance;
        require(balance > 0, "Insufficent balance");
        uint256 Mike = (balance * 3250) /10000 ;
        uint256 Mathew = (balance * 3250) /10000 ;
        uint256 Aug = (balance * 700) /10000 ;
        uint256 Sean = (balance * 500) /10000 ;
        uint256 Ahmed = (balance * 1800) /10000 ;
        uint256 Dev = (balance * 500) / 10000;

        (bool success, ) = payable(0xb020365CAb82927383db859A10129b6773254602).call{value: Mike}("");
        require(success, "Transfer failed to Mike");
        (bool success2, ) = payable(0x8FA2f269B0Ef3b8b9435AB5aAAcE4f92dA9E8e15).call{value: Mathew}("");
        require(success2, "Transfer failed to Mathew");
        (bool success3, ) = payable(0x981bB577A8CFF6A18eB70E06ADc958C60c651FF4).call{value: Aug}("");
        require(success3, "Transfer failed to Aug");
        (bool success4, ) = payable(0x87413B2dEb63bB81cB712b75dB02dB594b8d19af).call{value: Sean}("");
        require(success4, "Transfer failed to Sean");
        (bool success5, ) = payable(0x7fd290D331a4245E70bb98924143703dB9FB0A06).call{value: Ahmed}("");
        require(success5, "Transfer failed to Ahmed"); 
        (bool success6, ) = payable(0x2A140D715fE3Db1a083dA7C796a315e07BEF52EA).call{value: Dev}("");
        require(success6, "Transfer failed to Dev");

    }

    /**
     * @dev Reserves the name if isReserve is set to true, de-reserves if set to false
     */
    function toggleReserveName(string memory str, bool isReserve) internal {
        _nameReserved[toLower(str)] = isReserve;
    }

    /**
     * @dev Check if the name string is valid (Alphanumeric and spaces without leading or trailing space)
     */
    function validateName(string memory str) public pure returns (bool){
        bytes memory b = bytes(str);
        if(b.length < 1) return false;
        if(b.length > 25) return false; // Cannot be longer than 25 characters
        if(b[0] == 0x20) return false; // Leading space
        if (b[b.length - 1] == 0x20) return false; // Trailing space

        bytes1 lastChar = b[0];

        for(uint i; i<b.length; i++){
            bytes1 char = b[i];

            if (char == 0x20 && lastChar == 0x20) return false; // Cannot contain continous spaces

            if(
                !(char >= 0x30 && char <= 0x39) && //9-0
                !(char >= 0x41 && char <= 0x5A) && //A-Z
                !(char >= 0x61 && char <= 0x7A) && //a-z
                !(char == 0x20) //space
            )
                return false;

            lastChar = char;
        }

        return true;
    }

    /**
     * @dev Converts the string to lowercase
     */
    function toLower(string memory str) public pure returns (string memory){
        bytes memory bStr = bytes(str);
        bytes memory bLower = new bytes(bStr.length);
        for (uint i = 0; i < bStr.length; i++) {
            // Uppercase character
            if ((uint8(bStr[i]) >= 65) && (uint8(bStr[i]) <= 90)) {
                bLower[i] = bytes1(uint8(bStr[i]) + 32);
            } else {
                bLower[i] = bStr[i];
            }
        }
        return string(bLower);
    }

}
/**
 * @title MetaKnights contract
 * @dev Extends ERC721 Non-Fungible Token Standard basic implementation
 */
function toLower(string memory str) public pure returns (string memory){
    bytes memory bStr = bytes(str);
    bytes memory bLower = new bytes(bStr.length);
    for (uint i = 0; i < bStr.length; i++) {
        // Uppercase character
        if ((uint8(bStr[i]) >= 65) && (uint8(bStr[i]) <= 90)) {
            bLower[i] = bytes1(uint8(bStr[i]) + 32);
        } else {
            bLower[i] = bStr[i];
        }
    }
    return string(bLower);
}

/**
 * @dev Converts the string to lowercase
 */