contract NFT_Genesis is ERC721, ERC2981ContractWideRoyalties, Ownable
{
    using Strings for uint256;
    using SafeMath for uint256;

    uint256 public nextTokenId = 1;

    uint256 public constant MAX_SUPPLY = 3999;

    // Public sale
    uint256 public PRICE = 0.15 ether;
    uint256 public publicsaleStartDate;
    mapping (address => uint256) public minted;


    // Pre-sale
    uint256 public presaleStartDate;
    uint256 public presaleEndDate;
    uint256 public PRE_PRICE = 0.12 ether;
    mapping (address => uint256) public preMinted;
    mapping (address => bool) public isWhitelisted;

    uint256 public revealDate;

    string public baseTokenURI;
    string public unrevealedURI;

    constructor(string memory baseURI, uint256 _presaleStartDate, uint256 _presaleEndDate, uint256 _publicsaleStartDate, string memory _unrevealedURI, uint256 _revealDate) ERC721("NFT Stadium Genesis Collection", "NSGC") {
        setBaseURI(baseURI);
        setUnrevealedURI(_unrevealedURI);
        
        publicsaleStartDate = _publicsaleStartDate;
        presaleStartDate = _presaleStartDate;
        presaleEndDate = _presaleEndDate;
        revealDate = _revealDate;
    }

    function _baseURI() internal view virtual override returns (string memory) {
        if (block.timestamp < revealDate) {
            return unrevealedURI;
        }
        
        return baseTokenURI;
    }

    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
        
        if (block.timestamp < revealDate) {
            return unrevealedURI;
        }
        
        else {
            string memory baseURI = _baseURI();
            return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
        }    
    }

    function supportsInterface(bytes4 interfaceId)
        public
        view
        virtual
        override(ERC721, ERC2981Base)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }

    function mintNFTs(uint256 _count) public payable {
        uint256 totalMinted = nextTokenId;

        require(block.timestamp > presaleStartDate, "Pre/public sale not started yet!");
        if (block.timestamp > presaleEndDate) {
            require(block.timestamp > publicsaleStartDate, "Public sale not started yet!");
        }

        require(totalMinted.add(_count) <= MAX_SUPPLY, "Not enough NFTs left!");
        require(_count > 0, "Cannot mint 0 NFTs.");
        require(msg.value >= calculatePrice(_count),"Not enough ether to purchase NFTs.");
        
        if (block.timestamp < presaleEndDate) {
            require(isWhitelisted[msg.sender], "Address is not whitelisted!");
            preMinted[msg.sender] += _count;
            require(preMinted[msg.sender] <= 4, "Address can't mint more NFTs in pre-sale.");
        } else {
            minted[msg.sender] += _count;
            require(minted[msg.sender] <= 4, "Address can't mint more NFTs in public-sale.");
        }

        for (uint256 i = 0; i < _count; i++) {
            _mintSingleNFT();
        }
    }

    function calculatePrice(uint256 _count) public view returns(uint256) {
        if (block.timestamp < presaleEndDate) {
            return _count * PRE_PRICE;
        }
        
        return _count * PRICE;
    }

    function _mintSingleNFT() private {
        uint256 tokenId = nextTokenId;
        _safeMint(msg.sender, tokenId, '');
        nextTokenId = tokenId + 1;
    }

    // Owner functions

    function withdraw() public payable onlyOwner {
        uint256 balance = address(this).balance;
        require(balance > 0, "No ether left to withdraw");

        (bool success, ) = (msg.sender).call{value: balance}("");
        require(success, "Transfer failed.");
    }

    function setRoyalties(address recipient, uint256 value) external onlyOwner() {
        _setRoyalties(recipient, value);
    }

    function setPresaleEndDate(uint256 _endDate) external onlyOwner {
        presaleEndDate = _endDate;
    }

    function setPresaleStartDate(uint256 _startDate) external onlyOwner {
        presaleStartDate = _startDate;
    }

    function setPublicSaleStartDate(uint256 _publicsaleStartDate) external onlyOwner {
        publicsaleStartDate = _publicsaleStartDate;
    }

    function addToWhitelist(address[] memory _addresses) external onlyOwner {
        for (uint256 i = 0;i < _addresses.length;i++) {
            isWhitelisted[_addresses[i]] = true;
        }
    }

    function removeFromWhitelist (address[] memory _addresses) external onlyOwner {
        for (uint256 i = 0;i < _addresses.length;i++) {
            isWhitelisted[_addresses[i]] = false;
        }
    }

    function mintFreeNFTs(uint256 _count) external onlyOwner {
        for (uint256 i = 0; i < _count; i++) {
            _mintSingleNFT();
        }
    }

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

    function setUnrevealedURI(string memory _unrevealedURI) public onlyOwner {
        unrevealedURI = _unrevealedURI;
    }

    function setRevealDate(uint256 _revealDate) public onlyOwner {
        revealDate = _revealDate;
    }
}
function withdraw() public payable onlyOwner {
    uint256 balance = address(this).balance;
    require(balance > 0, "No ether left to withdraw");

    (bool success, ) = (msg.sender).call{value: balance}("");
    require(success, "Transfer failed.");
}

// Owner functions