contract HypeHippos is ERC721Enumerable, Ownable{
    using SafeMath for uint256;
    using Address for address;
    using Strings for uint256;

    // This is SHA256 hash of the provenance record of all artworks
    // It is derived by hashing every individual NFT's picture, and then concatenating all those hash, deriving yet another SHA256 from that.
    string public PROVENANCE_HASH = "";
    uint256 public REVEAL_TIMESTAMP;

    uint256 public NFT_PRICE = 60000000000000000; // 0.06 ETH
    uint public constant MAX_NFT_PURCHASE = 20;
    uint256 public MAX_SUPPLY = 10000;
    bool public saleIsActive = false;
    uint256 public startingIndexBlock;
    uint256 public startingIndex;

    string private _baseURIExtended;
    mapping (uint256 => string) _tokenURIs;

    constructor() ERC721("Hype Hippos","HypeHippos"){
        REVEAL_TIMESTAMP = block.timestamp + (86400 * 7);
    }

    function flipSaleState() public onlyOwner {
        saleIsActive = !saleIsActive;
    }

    /*     
    * Set provenance once it's calculated
    */
    function setProvenanceHash(string memory _provenanceHash) external onlyOwner {
        PROVENANCE_HASH = _provenanceHash;
    }

    function withdraw() public onlyOwner {
        uint balance = address(this).balance;
        payable(_msgSender()).transfer(balance);
    }

    function reserveTokens(uint256 num) public onlyOwner {
        uint supply = totalSupply();
        uint i;
        for (i = 0; i < num; i++) {
            _safeMint(msg.sender, supply + i);
        }
    }

    function mint(uint numberOfTokens) public payable {
        require(saleIsActive, "Sale is not active at the moment");
        require(numberOfTokens > 0, "Number of tokens can not be less than or equal to 0");
        require(totalSupply().add(numberOfTokens) <= MAX_SUPPLY, "Purchase would exceed max supply");
        if(msg.sender != owner()) {
            require(numberOfTokens <= MAX_NFT_PURCHASE,"Mint account exceed max");
            require(NFT_PRICE.mul(numberOfTokens) <= msg.value, "Sent ether value is incorrect");
        }

        for (uint i = 0; i < numberOfTokens; i++) {
            _safeMint(msg.sender, totalSupply());
        }

        // If we haven't set the starting index and this is either
        // 1) the last saleable token or 2) the first token to be sold after
        // the end of pre-sale, set the starting index block
        if (startingIndexBlock == 0 && (totalSupply() == MAX_SUPPLY || block.timestamp >= REVEAL_TIMESTAMP)) {
            startingIndexBlock = block.number;
        } 
    }

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

    // Sets base URI for all tokens, only able to be called by contract owner
    function setBaseURI(string memory baseURI_) external onlyOwner {
        _baseURIExtended = baseURI_;
    }

    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");

        string memory _tokenURI = _tokenURIs[tokenId];
        string memory base = _baseURI();

        // If there is no base URI, return the token URI.
        if (bytes(base).length == 0) {
            return _tokenURI;
        }
        // If both are set, concatenate the baseURI and tokenURI (via abi.encodePacked).
        if (bytes(_tokenURI).length > 0) {
            return string(abi.encodePacked(base, _tokenURI));
        }
        // If there is a baseURI but no tokenURI, concatenate the tokenID to the baseURI.
        return string(abi.encodePacked(base, tokenId.toString()));
    }

    function setMintPrice(uint256 _price) external onlyOwner {
        NFT_PRICE = _price;
    }

    /**
     * Set reveal timestamp when finished the sale.
     */
    function setRevealTimestamp(uint256 _revealTimeStamp) external onlyOwner {
        REVEAL_TIMESTAMP = _revealTimeStamp;
    } 

    /**
     * Set the starting index for the collection
     */
    function setStartingIndex() external {
        require(startingIndex == 0, "Starting index is already set");
        require(startingIndexBlock != 0, "Starting index block must be set");
        
        startingIndex = uint256(blockhash(startingIndexBlock)) % MAX_SUPPLY;
        // Just a sanity case in the worst case if this function is called late (EVM only stores last 256 block hashes)
        if (block.number.sub(startingIndexBlock) > 255) {
            startingIndex = uint256(blockhash(block.number - 1)) % MAX_SUPPLY;
        }
        // Prevent default sequence
        if (startingIndex == 0) {
            startingIndex = startingIndex.add(1);
        }
    }

    /**
     * Set the starting index block for the collection, essentially unblocking
     * setting starting index
     */
    function emergencySetStartingIndexBlock() external onlyOwner {
        require(startingIndex == 0, "Starting index is already set");
        
        startingIndexBlock = block.number;
    }

    /**
     * Get the array of token for owner.
     */
    function tokensOfOwner(address _owner) external view returns(uint256[] memory) {
        uint256 tokenCount = balanceOf(_owner);
        if (tokenCount == 0) {
            return new uint256[](0);
        } else {
            uint256[] memory result = new uint256[](tokenCount);
            for (uint256 index; index < tokenCount; index++) {
                result[index] = tokenOfOwnerByIndex(_owner, index);
            }
            return result;
        }
    }
}
function setBaseURI(string memory baseURI_) external onlyOwner {
    _baseURIExtended = baseURI_;
}

// Sets base URI for all tokens, only able to be called by contract owner