contract TerraInfinite is ERC721Enumerable, Ownable {
    using Strings for uint256;

    uint256 private constant TOTAL_BATCHES = 116;
    uint256 public constant MAX_TOKEN_PURCHASE = 10;

    uint private constant EMPATH_VIEW = 1;
    uint private constant ANIMAL_SIGN_VIEW = 2;
    uint private constant WITH_ANIMADROID_VIEW = 3;

    // Contract that helps to manage the android appearance in the next levels..
    address public tokenURIResolver;

    uint256 public tokenPrice = 0.02 ether;
    uint256 public maxAndroids = 500;
    uint256 public maxAnimaDroids = 0;

    uint256 public androidTotalSupply = 0;
    uint256 public animaDroidTotalSupply = 0;
    
    bool public allowMintAndroids = false;
    bool public allowMintForWhiteListed = false;

    address[] public whiteListedAddresses;
    
    uint256 private seed;

    mapping (uint256 => uint256) private empathShifts; // stores the shifts in ipfs data.
    mapping (address => uint8) public animaDroidPerWallet;

    mapping (uint256 => bool) public markedToProxy;
    bool private proxyEverything;
    
    string private _baseURIextended;

    constructor() ERC721("Terra Infinite", "TI") {
    }

    function setBaseURI(string memory baseURI_) external onlyOwner {
        _baseURIextended = baseURI_;
    }

    function withdraw() external onlyOwner {
        uint256 balance = address(this).balance;
        payable(msg.sender).transfer(balance);
    }
    
    function setNextContractAddress(address nextContractAddress_) external onlyOwner {
        tokenURIResolver = nextContractAddress_;
    }

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

    /////////////////////////////////////
    // For giveaways
    /////////////////////////////////////
    function reserveTokens(uint numberOfTokens) external onlyOwner {
        require(androidTotalSupply + numberOfTokens <= maxAndroids, "Reserve would exceed max supply of tokens");
        for (uint i = androidTotalSupply + 1; i <= androidTotalSupply + numberOfTokens; i++) {
            _safeMint(msg.sender, i);
        }
        androidTotalSupply += numberOfTokens;
    }

    function giveToken(address winner) external onlyOwner {
        require(androidTotalSupply + 1 <= maxAndroids, "Gift would exceed max supply of tokens");
        androidTotalSupply += 1;
        _safeMint(winner, androidTotalSupply);
    }
    
    /////////////////////////////////////
    // Sale state
    /////////////////////////////////////
    function flipAndroidsSaleState() external onlyOwner {
        allowMintAndroids = !allowMintAndroids;
    }

    function flipWhiteListSaleState() external onlyOwner {
        allowMintForWhiteListed = !allowMintForWhiteListed;
    }
    
    function setRevealTimestamp(uint256 timestamp) external onlyOwner {
        seed = timestamp;   
    }

    function setMaxAndroidsForMint(uint256 num) external onlyOwner {
        maxAndroids = num;
    }

    function setMaxAnimadroidsForMint(uint256 num) external onlyOwner {
        maxAnimaDroids = num;
    }

    function setPrice(uint256 newPrice) external onlyOwner {
        tokenPrice = newPrice;
    }

    function setWhiteList(address[] memory addrs) external onlyOwner {
        whiteListedAddresses = addrs;
    }

    function addToWhiteList(address[] memory addrs) external onlyOwner {
        for (uint i = 0; i < addrs.length; i++) {
            whiteListedAddresses.push(addrs[i]);
        }
    }

    function isWhiteListed(address addr) public view returns (bool) {
        for(uint i = 0; i < whiteListedAddresses.length; i++) {
            if (whiteListedAddresses[i] == addr) {
                return true;
            }
        }
        return false;
    }
    
    /////////////////////////////////////
    // TERRA INFINITE MECHANICS
    /////////////////////////////////////
    function mintAndroids(uint numberOfTokens) external payable {
        require(
            allowMintAndroids || 
            (allowMintForWhiteListed && isWhiteListed(msg.sender)), "The Terra Infinite sale is not active at that time");
        require(numberOfTokens <= MAX_TOKEN_PURCHASE, "Exceeded max token purchase");
        require((androidTotalSupply + numberOfTokens) <= maxAndroids, "Purchase would exceed max supply of tokens");
        require(tokenPrice * numberOfTokens <= msg.value, "Ether value sent is not correct");
        
        for (uint i = androidTotalSupply + 1; i <= androidTotalSupply + numberOfTokens; i++) {
            _safeMint(msg.sender, i);
        }
        androidTotalSupply += numberOfTokens;
    }

    function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal override {
        if (isAnimaDroid(tokenId)) {
            if (animaDroidPerWallet[from] > 0) {
               animaDroidPerWallet[from] -= 1;
            } 
            animaDroidPerWallet[to] += 1;
        }
        super._beforeTokenTransfer(from, to, tokenId);
    }

    function isAnimaDroid(uint256 tokenID) public view returns (bool) {
        return tokenID > maxAndroids;
    }

    function mintAnimaDroid(uint256 androidTokenID) external {
        require(animaDroidTotalSupply + 1 <= maxAnimaDroids, "Purchase would exceed max supply of tokens");
        require(animaDroidPerWallet[msg.sender] < 1, "You can't mint an AnimaDroid while you have another in your wallet");
        require(ownerOf(androidTokenID) == msg.sender, "You don't own this android");
        require(isEmpath(androidTokenID), "Your android is not Empath");
        
        animaDroidTotalSupply += 1;
        _safeMint(msg.sender, maxAndroids + animaDroidTotalSupply);
    }

    function becomeEmpath(uint256 androidID, uint256 animaDroidID, uint256 viewType) external {
        require(ownerOf(androidID) == msg.sender, "You don't own this android");
        require(ownerOf(animaDroidID) == msg.sender, "You don't own this animaDroid");
        require(!isAnimaDroid(androidID) && isAnimaDroid(animaDroidID), "Android should me Android and Animadroid should be AnimaDroid");
        require(!isEmpath(androidID), "Your android is Empath already");
        require(
            (
                (viewType == EMPATH_VIEW) || 
                (viewType == ANIMAL_SIGN_VIEW) || 
                (viewType == WITH_ANIMADROID_VIEW)
            ), "Unknown view type");

        if (viewType == EMPATH_VIEW) {
            empathShifts[androidID] = maxAndroids;
            return;
        }

        if (viewType == ANIMAL_SIGN_VIEW) {
            if (animaDroidID % 3 == 0) { // dog
                empathShifts[androidID] = maxAndroids * 2;
                return;
            }
            if (animaDroidID % 3 == 1) { // cat
                empathShifts[androidID] = maxAndroids * 3;
                return;
            }
            if (animaDroidID % 3 == 2) { // bird
                empathShifts[androidID] = maxAndroids * 4;
                return;
            }
        }

        empathShifts[androidID] = maxAndroids * (animaDroidID - maxAndroids + 4); // 4 to skip empath and animal signes. 
    }

    function isEmpath(uint256 tokenID) public view returns (bool) {
        require(_exists(tokenID), "Query for nonexistent token");

        // Check special maps.
        if (empathShifts[tokenID] != 0) {
            return true;
        }

        // Check reveal empaths;
        return isEmpathAfterReveal(tokenID);
    }

    function isLinkedWithAnimadroid(uint256 tokenID) public view returns (bool) {
        require(_exists(tokenID));
        if (empathShifts[tokenID] != 0) {
            return true;
        }
        return false;
    }

    function isEmpathAfterReveal(uint256 tokenID) public view returns (bool) {
        require(_exists(tokenID));
        uint256 empathID = (tokenID + seed) % maxAndroids + 1;
        return seed != 0 && empathID % 10 == 0;
    }

    /////////////////////////////////////
    // TOKEN URI RESOLVING
    /////////////////////////////////////
    function flipProxyEverythingState() external onlyOwner {
        proxyEverything = !proxyEverything;
    }

    function setProxyTokenState(uint256 tokenID, bool state) external {
        require(_exists(tokenID), "ERC721Metadata: URI query for nonexistent token");
        require((tokenURIResolver != address(0) && msg.sender == tokenURIResolver), "Can't mark to proxy token URI");

        markedToProxy[tokenID] = state;
    }

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

        if ((tokenURIResolver != address(0)) && 
            (markedToProxy[tokenID] || proxyEverything)) {
            return ProxyTokenURI(tokenURIResolver).resolveOldTokenURI(tokenID);
        }

        uint256 tokenShift = tokenID;

        if (isAnimaDroid(tokenID)) {
            tokenShift = tokenID - maxAndroids + (maxAndroids * TOTAL_BATCHES);
        } else if (isEmpathAfterReveal(tokenID)) {
            tokenShift = tokenID + maxAndroids;
        } else if (isLinkedWithAnimadroid(tokenID)) {
            tokenShift = tokenID + empathShifts[tokenID];
        }

        string memory baseURI = _baseURI();
        return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenShift.toString())) : "";
    }
}
function flipAndroidsSaleState() external onlyOwner {
    allowMintAndroids = !allowMintAndroids;
}

/////////////////////////////////////