contract DarkZodiacCollectible is ERC721, Ownable {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    using SafeMath for uint256;
    string private _tokenBaseURI = "https://meta.darkzodiac.com/";
    string public DARKZ_PROVENANCE = "";
    
    uint256 public constant zodiacPrice = 0.09 ether;

    bool public saleLive = false;
    bool public presaleLive = false;
    bool public locked;
    bool private reserved = false;

    uint256 public publicAmountMinted;
    uint256 public privateAmountMinted;

    uint256 public RESERVED_ZODIAC = 50;
    uint256 public MAX_ZODIACS = 3876;
    uint256 public presalePurchaseLimit = 2;
    uint256 public purchaseLimit = 10;

    mapping(address => bool) public presalerList;
    mapping(address => uint256) public presalerListPurchases;
    

    modifier notLocked {
        require(!locked, "Contract metadata methods are locked");
        _;
    }
    
    constructor () public ERC721 ("DarkZodiac", "DARKZ"){
    }
    
    function reserveZodiac() public onlyOwner {
        require(!reserved, "Already reserved");

        reserved = true;

        uint i;
        for (i = 0; i < RESERVED_ZODIAC; i++) {    
            _tokenIds.increment();
            uint256 newItemId = _tokenIds.current();

            _safeMint(msg.sender, newItemId);
        }
    }

    function withdraw() public onlyOwner {
        uint balance = address(this).balance;
        msg.sender.transfer(balance);
    }
    
    /*     
    * Set provenance once it's calculated
    */
    function setProvenanceHash(string memory provenanceHash) public onlyOwner {
        DARKZ_PROVENANCE = provenanceHash;
    }
    
    function setBaseURI(string calldata URI) external onlyOwner notLocked {
        _tokenBaseURI = URI;
    }
    
    function flipSaleState() public onlyOwner {
        saleLive = !saleLive;
    }
    
    function flipPresaleStatus() public onlyOwner {
        presaleLive = !presaleLive;
    }
    
    // This function is only used for exceptional scenario
    function flipReserved() public onlyOwner {
        reserved = !reserved;
    }

    function mintZodiac(uint256 tokenQuantity) external payable {
        require(saleLive, "Sale must be active to mint Zodiac");
        require(!presaleLive, "ONLY_PRESALE");
        require(zodiacPrice * tokenQuantity <= msg.value, "INSUFFICIENT_ETH");
        require(_tokenIds.current() + tokenQuantity <= MAX_ZODIACS, "Purchase would exceed max supply of Zodiac");
        require(tokenQuantity <= purchaseLimit, "EXCEED_ALLOC");
        require(_tokenIds.current() < MAX_ZODIACS, "OUT_OF_STOCK");
        
        for (uint256 i = 0; i < tokenQuantity; i++) {
            publicAmountMinted++;

            _tokenIds.increment();
            uint256 newItemId = _tokenIds.current();

            if (newItemId <= MAX_ZODIACS) {
                _safeMint(msg.sender, newItemId);
            }
        }
    }
    
    function addToPresaleList(address[] calldata entries) external onlyOwner {
        for(uint256 i = 0; i < entries.length; i++) {
            address entry = entries[i];
            require(entry != address(0), "NULL_ADDRESS");

            presalerList[entry] = true;
        }   
    }

    function removeFromPresaleList(address[] calldata entries) external onlyOwner {
        for(uint256 i = 0; i < entries.length; i++) {
            address entry = entries[i];
            require(entry != address(0), "NULL_ADDRESS");
            
            presalerList[entry] = false;
        }
    }
    
    function presaleBuy(uint256 tokenQuantity) external payable {
        require(!saleLive && presaleLive, "PRESALE_CLOSED");
        require(presalerList[msg.sender], "NOT_QUALIFIED");
        require(_tokenIds.current() + tokenQuantity <= MAX_ZODIACS, "OUT_OF_STOCK");

        require(presalerListPurchases[msg.sender] + tokenQuantity <= presalePurchaseLimit, "EXCEED_ALLOC");
        require(zodiacPrice * tokenQuantity <= msg.value, "INSUFFICIENT_ETH");
        
        for (uint256 i = 0; i < tokenQuantity; i++) {
            presalerListPurchases[msg.sender]++;
            privateAmountMinted++;

            _tokenIds.increment();
            uint256 newItemId = _tokenIds.current();

            if (newItemId < MAX_ZODIACS) {
                _safeMint(msg.sender, newItemId);
            }
        }
    }
    
    // Owner functions for enabling presale, sale, revealing and setting the provenance hash
    function lockMetadata() external onlyOwner {
        locked = true;
    }

    // View functions
    function currentId() external view returns (uint256) {
        return _tokenIds.current();
    }
    
    function isPresaler(address addr) external view returns (bool) {
        return presalerList[addr];
    }
    
    function presalePurchasedCount(address addr) external view returns (uint256) {
        return presalerListPurchases[addr];
    }
    
    function getPresaleStatus() external view returns (bool) {
        return presaleLive;
    }
    
    function getSaleStatus() external view returns (bool) {
        return saleLive;
    }
    
    function tokenURI(uint256 tokenId) public view override(ERC721) returns (string memory) {
        require(_exists(tokenId), "Cannot query non-existent token");
        
        return string(abi.encodePacked(_tokenBaseURI, tokenId.toString()));
    }

}
function lockMetadata() external onlyOwner {
    locked = true;
}

// Owner functions for enabling presale, sale, revealing and setting the provenance hash