contract Avatars is Ownable, ERC721 {
    using SafeMath for uint256;

    // Original Crypteriors Contract Address
    address public _nftAddress;

    uint256 public publicSaleMaxMint = 2;
    mapping(address => uint256) private _publicSaleClaimed;

    // This will hold the provenance hash for all Crypteriors Locals Artwork in existence
    string public  CRYPTERIORS_LOCALS_PROVENANCE = "";    

    // The price for a Crypterior Local
        
    uint256 public NFT_PRICE = 0; // yay!

    // The maximum number of Local Crypterior will ever exists. 

    uint256 public totalGiftSupply;
    uint256 public totalPublicSupply;

    uint256 public constant LOCALS_RESERVE = 150;
    uint256 public constant LOCALS_MAX = 4294;

    uint256 public constant MAX_NFT_SUPPLY = LOCALS_RESERVE + LOCALS_MAX;
    
    // Maximum mints per original Crypterior
    uint256 public MAX_MINTS_PER_ORIGINAL_CRYPTERIOR = 2;

    // Which Original Crypterior has been used for mint and for how many times in a particular wallet
    mapping (address => mapping(uint256 => uint256)) v1TokenIdUsedForMint;

    uint256 public startingIndexBlock;
    uint256 public startingIndex;

    // When the sale will start. We can also start the sale manually.
    uint256 public constant SALE_START_TIMESTAMP = 1631905200; 
    
    // Automatically reveal after 7 days, or when all minted, but we can manually reveal at will!
    uint256 public REVEAL_TIMESTAMP =
        SALE_START_TIMESTAMP + (86400 * 7);

    // How long the top or bottom meme text can be on chain.
    uint256 public constant MAXIMUM_MEME_STRING_LENGHT = 50;

    // This holds the top meme text associated to a token.
    mapping(uint256 => string) tokenMemeTextTop;

    // This holds the bottom meme text associated to a token.
    mapping(uint256 => string) tokenMemeTextBottom;

    // Public Sale status.
    bool public hasSaleStarted = false;
    
    // Private Sale status
    bool public hasPrivateSaleStarted = false;

    // Token name
    string private _name;

    // Token symbol
    string private _symbol;
 
    // Event when a meme change has been done.
    event MemeChange(
        uint256 indexed tokenId,
        string tokenMemeTextTop,
        string tokenMemeTextBottom
    );

    // The event emitted (useable by web3) when a token is purchased
    // event BoughtToken(address indexed buyer, uint256 tokenId);

    constructor(
        string memory name,
        string memory symbol,
        string memory baseURI,
        string memory provenanceHash
    ) ERC721(name, symbol) {

        _name = name;
        _symbol = symbol;

        setBaseURI(baseURI);
        setProvenanceHash(provenanceHash);
    }

    /**
     * @dev Mints Crypteriors Locals
     */
    function mintNFT(uint256 numberOfNfts) external {
        require(hasSaleStarted == true, "Sale has not started");
        require(numberOfNfts > 0, "numberOfNfts cannot be 0");
        require(numberOfNfts <= 2, "You may not buy more than 2 NFTs at once");
        require(totalPublicSupply + numberOfNfts <= LOCALS_MAX, 'Purchase would exceed LOCALS_MAX');

        require(_publicSaleClaimed[msg.sender] + numberOfNfts <= publicSaleMaxMint, 'Purchase exceeds max allowed');

        for (uint256 i = 0; i < numberOfNfts; i++) {
            if (totalPublicSupply < LOCALS_MAX) {
                uint256 tokenId = LOCALS_RESERVE + totalPublicSupply + 1;

                totalPublicSupply += 1;

                _publicSaleClaimed[msg.sender] += 1;

                _safeMint(msg.sender, tokenId);     
            }
        }

        if (
            startingIndexBlock == 0 &&
            (totalSupply() == MAX_NFT_SUPPLY ||
                block.timestamp >= REVEAL_TIMESTAMP)
        ) {
            startingIndexBlock = block.number;            
        }
    }
     /**
     * @dev Returns how many times an Original Crypterior used for minting.
     */
    function checkIfOriginalCrypteriorUsedForMint(uint256 _tokenId) public view returns (uint256)
    {
        return v1TokenIdUsedForMint[msg.sender][_tokenId];
    }

    /**
     * @dev Mint for an Original Crypterior
     */
    function mintForOriginalCrypteriors(uint256[] calldata tokenIds) external {
        require( hasPrivateSaleStarted == true, "Sale has not started");

        require( tokenIds.length < 10, "Cannot mint more than 10 in one transaction!");
        require( tokenIds.length > 0,  "Number of tokens can not be less than or equal to 0");        
        require(totalPublicSupply + tokenIds.length <= LOCALS_MAX, 'Purchase would exceed LOCALS_MAX');

        for (uint256 i = 0; i < tokenIds.length; i++) {
            require(INFOriginalCrypteriors(_nftAddress).ownerOf(tokenIds[i]) == msg.sender, "Original Crypterior NFT have a different owner");
            require(v1TokenIdUsedForMint[msg.sender][tokenIds[i]] < MAX_MINTS_PER_ORIGINAL_CRYPTERIOR, "This Crypterior already been used for a free mint in your wallet");
            
            v1TokenIdUsedForMint[msg.sender][tokenIds[i]] += 1;

            uint256 tokenId = LOCALS_RESERVE + totalPublicSupply + 1;
            totalPublicSupply += 1;

            _safeMint(msg.sender,  tokenId);
        }

        if (
            startingIndexBlock == 0 &&
            (totalSupply() == MAX_NFT_SUPPLY ||
                block.timestamp >= REVEAL_TIMESTAMP)
        ) {
            startingIndexBlock = block.number;
        }        
    }

    function mintReserve(uint256 _count, address _to) public onlyOwner {
        require(totalSupply() < MAX_NFT_SUPPLY, 'All tokens have been minted');
        require(totalGiftSupply + _count <= LOCALS_RESERVE, 'Not enough tokens left to reserve');

        for(uint256 i = 0; i < _count; i++) {
            uint256 tokenId = totalGiftSupply + 1;

            totalGiftSupply += 1;
            _safeMint(_to, tokenId);
        }
    }

    /**
     * @dev It is set right after deployment. This contains the original Crypterior NFT Contract address.
     */
    function setNFTAddress(address nftAddress) public {
        require(_nftAddress == address(0), "Already set");

        _nftAddress = nftAddress;
    }

    /**
     * @dev Will be called again for reveal. 
     */
    function setBaseURI(string memory baseURI) public onlyOwner {
        _setBaseURI(baseURI);
    }

    function startSale() public onlyOwner {
        hasSaleStarted = true;
    }

    function pauseSale() public onlyOwner {
        hasSaleStarted = false;
    }


    function startPrivateSale() public onlyOwner {
        hasPrivateSaleStarted = true;
    }

    function pausePrivateSale() public onlyOwner {
        hasPrivateSaleStarted = false;
    }


    function setProvenanceHash(string memory _hash) public onlyOwner {
        CRYPTERIORS_LOCALS_PROVENANCE = _hash;
    }

    /**
     * @dev Finalize starting index
     */
    function finalizeStartingIndex() public {
        require(startingIndex == 0, "Starting index is already set");
        require(startingIndexBlock != 0, "Starting index block must be set");

        startingIndex = uint256(blockhash(startingIndexBlock)) % MAX_NFT_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_NFT_SUPPLY;
        }
        // Prevent default sequence
        if (startingIndex == 0) {
            startingIndex = startingIndex.add(1);
        }
    }


    /**
     * @dev Set reveal timestamp when finished the sale.
     */

    function setRevealTimestamp(uint256 _revealTimeStamp) external onlyOwner {
        REVEAL_TIMESTAMP = _revealTimeStamp;
    } 

    /**
     * @dev If we want to reveal at will we will call this. 
     */

    function mintStartingIndexBlock() external onlyOwner {
        require(startingIndexBlock == 0, "Starting indexblock already mintend");
        
        startingIndexBlock = block.number;        
    }



    /**
     * @dev Changes the Meme texts for a tokenId
     */
    function changeMeme(
        uint256 tokenId,
        string memory _tokenMemeTextTop,
        string memory _tokenMemeTextBottom
    ) public {
        address owner = ownerOf(tokenId);

        require(_msgSender() == owner, "ERC721: caller is not the owner");
        require(validateString(_tokenMemeTextTop) == true, "Not valid string for top");
        require(validateString(_tokenMemeTextBottom) == true, "Not valid string for bottom");

        tokenMemeTextTop[tokenId] = _tokenMemeTextTop;
        tokenMemeTextBottom[tokenId] = _tokenMemeTextBottom;

        emit MemeChange(tokenId, _tokenMemeTextTop, _tokenMemeTextBottom);
    }

    /** 
    * @dev  Returns a list of all IDs assigned to an address.
    */
    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);
            uint256 resultIndex = 0;
            uint256 tokenId;

            for (tokenId = 0; tokenId < (LOCALS_RESERVE + totalPublicSupply + 1); tokenId++) {
                if ( _exists(tokenId) ) {
                    if (ownerOf(tokenId) == _owner) {
                        result[resultIndex] = tokenId;
                        resultIndex++;
                    }
                }                
            }

            return result;
        }
    }

    /// @dev Returns all the relevant information about a specific token
    /// @param _tokenId The ID of the token of interest

    function getToken(uint256 _tokenId)
        external
        view
        returns (
            string memory memeTextTop_,
            string memory memeTextBottom_
        )
    {
        memeTextTop_ = tokenMemeTextTop[_tokenId];
        memeTextBottom_ = tokenMemeTextBottom[_tokenId];
    }

    /// @dev Returns the current price for each token
    function getCurrentPrice() public view returns (uint256) {
        require(hasSaleStarted == true, "Sale has not started");
        require(totalSupply() < MAX_NFT_SUPPLY, "Sale has already ended");
        
        return NFT_PRICE;   // The price!
    }

    /**
     * @dev Withdraw ether from this contract (Callable by owner)
     */

    function withdraw() public onlyOwner {
        uint256 balance = address(this).balance;

        payable(msg.sender).transfer(balance);
    }   


    /**
     * @dev Check if the name string is valid (Alphanumeric and spaces without leading or trailing space)
     */
    function validateString(string memory str) public pure returns (bool) {
        bytes memory b = bytes(str);
        if (b.length < 1) return false;
        if (b.length > MAXIMUM_MEME_STRING_LENGHT) 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 (uint256 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;
    }

}
/**
 * @title Crypteriors Locals Contract
 * @dev Extends ERC721 Non-Fungible Token Standard basic implementation
 */
function getCurrentPrice() public view returns (uint256) {
    require(hasSaleStarted == true, "Sale has not started");
    require(totalSupply() < MAX_NFT_SUPPLY, "Sale has already ended");
        
    return NFT_PRICE;   // The price!
}

/// @dev Returns the current price for each token