contract Astrada is ERC721, Ownable {

    uint256 public MAX_SUPPLY = 12000; // 120000 tokens

    uint256 public MINT_INTERVAL = 1000; // 1000 tokens

    uint256 public MAX_PURCHASABLE = 10;

    uint256 public tokenPrice = 0; // first launch FREE, succession launches for a price
    uint256 public currentInterval = 1; // current interval 1-12
    string public notRevealedURI;

    bool public whitelistActive = false; // whitelist active to start with first mint
 
    bool public saleIsActive = false; // sale is not active on launch

    bool public mintOnePerRelease = false;

    mapping(uint256 => bool) public revealMapping;
    address[] public whitelistedAddresses;

    constructor(string memory name, string memory symbol, string memory _baseURI, string memory _notRevealedURI) public
    ERC721(name, symbol){
        setBaseURI(_baseURI);
        setNotRevealedURI(_notRevealedURI);
        reserveTokens(12);
    }

    // -------------------------GETTERS-------------------------

    function tokenURI(uint256 tokenId) public view virtual override returns (string memory){
        require(_exists(tokenId), "TokenId does not exist");
        if(!revealMapping[checkIntervalForTokenId(tokenId)]){
            return string(abi.encodePacked(notRevealedURI, tokenId.toString()));
        } else {
            string memory currentBaseURI = baseURI();
            return bytes(currentBaseURI).length > 0 ? string(abi.encodePacked(currentBaseURI, tokenId.toString())) : "";
        }
    }

    // -------------------------SETTERS-------------------------

    function setBaseURI(string memory _baseURI) public onlyOwner {
        _setBaseURI(_baseURI);
    }

    function setTokenPrice(uint256 _newPrice) public onlyOwner {
        tokenPrice = _newPrice;
    }

    function openSale() public onlyOwner {
        saleIsActive = true;
    }

    function closeSale() public onlyOwner {
        saleIsActive = false;
    }

    function openWhitelist() public onlyOwner {
        whitelistActive = true;
    }

    function closeWhitelist() public onlyOwner {
        whitelistActive = false;
    }

    function setMaxPurchasable(uint256 _maxPurchasable) public onlyOwner {
        MAX_PURCHASABLE = _maxPurchasable;
    }

    function incrementCurrentInterval() public onlyOwner{
        currentInterval++;
    }

    function setRevealForRelease(uint256 _releaseInterval) public onlyOwner {
        revealMapping[_releaseInterval] = true;
    }

    function setNotRevealedURI(string memory _notRevealedURI) public onlyOwner {
        notRevealedURI = _notRevealedURI;
    }

    function setMintOnePerRelease(bool _value) public onlyOwner {
        mintOnePerRelease = _value;
    }

    // -------------------------TOKEN PROCESSING FUNCTIONS-------------------------

    /**
     * check interval for tokenId
     * floor divide tokenId and then add 1
     * 12 total intervals
     * interval 1: 0-999
     * interval 2: 1000-1999 etc
     */
    function checkIntervalForTokenId(uint256 tokenId) internal view returns (uint256){
        require(_exists(tokenId), "TokenId does not exist");
        return (tokenId / MINT_INTERVAL) + 1;
    }

    function mintedInCurrentInterval(address _owner) public view returns (bool){
        uint256[] memory tokenIds = this.tokensOfOwner(_owner);
        for(uint256 i; i < tokenIds.length; i++){
            if(checkIntervalForTokenId(tokenIds[i]) == currentInterval){
                return true;
            }
        }
        return false;
    }

    function tokensOfOwner(address _owner) external view returns (uint256[] memory) {
        uint256 tokenCount = balanceOf(_owner);
        if (tokenCount == 0) {
            // Return an empty array
            return new uint256[](0);
        } else {
            uint256[] memory result = new uint256[](tokenCount);
            uint256 index;
            for (index = 0; index < tokenCount; index++) {
                result[index] = tokenOfOwnerByIndex(_owner, index);
            }
            return result;
        }
    }

    // -------------------------MINTING FUNCTIONS-------------------------

    function mintAstrada(uint256 numberOfTokens) public payable {
        require(saleIsActive, "Sale must be active in order to mint");
        require(totalSupply().add(numberOfTokens) <= MAX_SUPPLY, "Token purchase would exceed the number of Astrada");
        require(totalSupply().add(numberOfTokens) <= MINT_INTERVAL * currentInterval, "Token purchase would exceed the mint interval");
        require(tokenPrice.mul(numberOfTokens) <= msg.value, "Not enough ETH sent");
        require(!Address.isContract(msg.sender), "Contracts are not allowed to mint");
        require(numberOfTokens > 0, "Number of tokens to mint must be at least 1");
        require(numberOfTokens <= MAX_PURCHASABLE, "Number of tokens requested is higher than allowed");
        if(mintOnePerRelease || whitelistActive){
            require(!mintedInCurrentInterval(msg.sender), "You already minted this round");
        }

        if(whitelistActive){
            require(isWhitelisted(msg.sender), "Whitelist is active and user is not on whitelist");
        }

        for(uint i = 0; i < numberOfTokens; i++) {
            uint mintIndex = totalSupply();
            if (totalSupply() <= MAX_SUPPLY) {
                _safeMint(msg.sender, mintIndex);
            }
            if(totalSupply() % MINT_INTERVAL == 0){
                closeCurrentMintInterval();
            }
        }
    }

    /**
        On mint interval close
        close sale state
        increment interval to next
     */
    function closeCurrentMintInterval() internal  {
        saleIsActive = false;
        currentInterval++;
    }

    /**
     * Set some Astrada aside
     */
    function reserveTokens(uint256 _reserveAmount) public onlyOwner {        
        uint supply = totalSupply();

        require(
            _reserveAmount > 0 && _reserveAmount + supply <= MAX_SUPPLY,
            "Not enough reserve left for team"
        );

        for (uint256 i = 0; i < _reserveAmount; i++) {
            _safeMint(msg.sender, supply + i);
        }
    }

    function withdraw() public payable onlyOwner {
        require(payable(msg.sender).send(address(this).balance));
    }

    function addToWhitelist(address[] calldata _users) public onlyOwner {
        delete whitelistedAddresses;
        whitelistedAddresses = _users;
    }

    function isWhitelisted(address _user) public view returns (bool){
        for (uint i = 0; i < whitelistedAddresses.length; i++) {
            if(whitelistedAddresses[i] == _user){
                return true;
            }
        }
        return false;
    }

    function clearWhitelist() public onlyOwner {
        delete whitelistedAddresses;
    }

    function getWhitelistSize() public view returns (uint256){
        return whitelistedAddresses.length;
    }
}
function closeCurrentMintInterval() internal  {
    saleIsActive = false;
    currentInterval++;
}

/**
 On mint interval close
 close sale state
 increment interval to next
 */