// Contract: BuddyBuddyTags
// Target function: function testStr(string memory str) internal pure returns (bool)
 When the token to delete is the last token, the swap operation is unnecessary
        if (tokenIndex != lastTokenIndex) {
            uint256 lastTokenId = _ownedTokens[from][lastTokenIndex];

            _ownedTokens[from][tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token
            _ownedTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index
        }

        // This also deletes the contents at the last position of the array
        delete _ownedTokensIndex[tokenId];
        delete _ownedTokens[from][lastTokenIndex];
    }

    /**
     * @dev Private function to remove a token from this extension's token tracking data structures.
     * This has O(1) time complexity, but alters the order of the _allTokens array.
     * @param tokenId uint256 ID of the token to be removed from the tokens list
     */
    function _removeTokenFromAllTokensEnumeration(uint256 tokenId) private {
        // To prevent a gap in the tokens array, we store the last token in the index of the token to delete, and
        // then delete the last slot (swap and pop).

        uint256 lastTokenIndex = _allTokens.length - 1;
        uint256 tokenIndex = _allTokensIndex[tokenId];

        // When the token to delete is the last token, the swap operation is unnecessary. However, since this occurs so
        // rarely (when the last minted token is burnt) that we still do the swap here to avoid the gas cost of adding
        // an 'if' statement (like in _removeTokenFromOwnerEnumeration)
        uint256 lastTokenId = _allTokens[lastTokenIndex];

        _allTokens[tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token
        _allTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index

        // This also deletes the contents at the last position of the array
        delete _allTokensIndex[tokenId];
        _allTokens.pop();
    }
}


abstract contract FoPunks{
    function balanceOf(address owner) external view virtual returns (uint256 balance);
}

abstract contract AcclimatedMoonCats{
    function balanceOf(address owner) external view virtual returns (uint256 balance);
}


contract BuddyBuddyTags is ERC721Enumerable, ReentrancyGuard, Ownable {

    bool public launched = false;
    bytes private emoji_list;

    mapping(address => bool) private allow_list; // minters
    mapping(uint256 => address) public buddy_tagged; // token to ... from is owner
    mapping(address => string) private aliases;
    mapping(string => bool) private alias_unique;
    mapping(address => mapping(address => bool)) private confirmations; // find connections
    
    address public DONATION_WALLET = 0x92EE2370b56DC32794A6CD72585dC01d4288D314; // https://www.stjude.org/donate/crypto.html
    address public MOONCATS = 0xc3f733ca98E0daD0386979Eb96fb1722A1A05E69;
    address public FOPUNKS = 0xDb1cbD0B58fb4f7C9f6857025DA29f42371fB659;
    uint256 public TAG_PRICE = 10000000000000000; // 0.01 ETH
    uint256 public RETAG_PRICE = 5000000000000000; // 0.005 ETH
    uint256 public ALIAS_PRICE = 20000000000000000; // 0.02 ETH
    uint256 public BURN_PRICE = 50000000000000000; // 0.05 ETH
    uint256 public LAUNCH_BLOCK;

    function random(string memory input) internal view returns (uint256) {
        return uint256(keccak256(abi.encodePacked(input,LAUNCH_BLOCK)));
    }


    function getTokenColor(uint256 tokenId) internal view returns (uint256[3] memory){
        require(totalSupply() > tokenId,"Not minted yet.");
        if (!check_if_confirmed(tokenId)){
            return [uint256(212),uint256(15),uint256(22)]; // the default color
        }


        uint256 h = (random(string(abi.encodePacked(toString(tokenId),'h'))) % 360);
        uint256 s = (random(string(abi.encodePacked(toString(tokenId),'s'))) % 80) + 20;
        uint256 l = (random(string(abi.encodePacked(toString(tokenId),'l'))) % 60) + 20;
        return [h,s,l];
    }

    function getHarmony(uint256 tokenId) internal view returns (uint256) {
        return random(string(abi.encodePacked(tokenId, "HARMONY"))) % 5;
    }
    
    function getColor(uint256 tokenId, uint256 harmony, uint256 color_num) internal view returns (uint256[3] memory) {
        require(totalSupply() > tokenId,"Not minted yet.");
        if (!check_if_confirmed(tokenId)){
            return [uint256(212),uint256(15),uint256(22)]; // the default color
        }

        uint256[3] memory tokenColor = getTokenColor(tokenId);

        uint256[3] memory returnColor;
        returnColor[0] = tokenColor[0];
        returnColor[1] = tokenColor[1];
        returnColor[2] = tokenColor[2];
        if (harmony == 0){ // complementary
            returnColor[0] = (tokenColor[0] + 180*color_num) % 360;
        }
        else if(harmony == 1){ // monochrome
            returnColor[1] = ((tokenColor[1] + 20*color_num) % 80) + 20;
            returnColor[2] = ((tokenColor[2] + 20*color_num) % 60) + 20;
        }
        else if(harmony == 2){ // analagous
            returnColor[0] = (tokenColor[0] + 30*color_num) % 360;
        }
        else if(harmony == 3){ // triadic
            returnColor[0] = (tokenColor[0] + 120*color_num) % 360;
        }
        else if(harmony == 4){ // tetradic
            returnColor[0] = (tokenColor[0] + 90*color_num) % 360;
        }

        return returnColor;
    }

    function pullEmoji(uint256 tokenId, string memory word, uint256 r) internal view returns (string memory){
        require(totalSupply() > tokenId,"Not minted yet.");
        uint256 emoji_idx = (random(string(abi.encodePacked(toString(tokenId),word,toString(r)))) % (emoji_list.length / 3));
        bytes memory e = abi.encodePacked(emoji_list[emoji_idx*3],emoji_list[emoji_idx*3+1],emoji_list[emoji_idx*3+2]);
        return string(e);
    }

    function getEmojis(uint256 tokenId) internal view returns (string memory){
        require(totalSupply() > tokenId,"Not minted yet.");
        uint256 r = (random(string(abi.encodePacked(toString(tokenId),'emoji'))));
        string memory emoji = string(abi.encodePacked('&#x1F',pullEmoji(tokenId,'first',r),';'));
        if (r%200 == 199){
            return emoji;
        }
        emoji = string(abi.encodePacked(emoji,'&#x1F',pullEmoji(tokenId,'second',r),';'));
        if(r%200 > 194){
            return emoji;
        }
        emoji = string(abi.encodePacked(emoji,'&#x1F',pullEmoji(tokenId,'third',r),';'));
        return emoji;
    }

    function set_alias(string memory sender_alias) public payable{
        require(launched,"Not started yet");
        require(msg.value >= ALIAS_PRICE,"Not enough ETH");
        require(testStr(sender_alias),"Alias must be alphanumeric, no longer than 20 characters"); // less than 20, prevent fake looking addresses
        require(!alias_unique[sender_alias],"Alias taken");
        
        if (bytes(aliases[msg.sender]).length>0){
            alias_unique[aliases[msg.sender]] = false;
        }
        aliases[msg.sender] = sender_alias;
        alias_unique[sender_alias] = true;
    }

    function getAlias(address a) public view returns (string memory){
        if (keccak256(abi.encodePacked((aliases[a]))) != keccak256(abi.encodePacked(("")))){
            return aliases[a];
        }
        else{
            return string(abi.encodePacked('0x',toAsciiString(a)));
        }
    }

    function tokenURI(uint256 tokenId) override public view returns (string memory) {
        require(totalSupply() > tokenId,"Not minted yet.");

        string[17] memory parts;

        uint256 harmony = getHarmony(tokenId);

        uint256[3] memory color_0 = getTokenColor(tokenId);
        uint256[3] memory color_1 = getColor(tokenId,harmony,1);
        uint256[3] memory color_2 = getColor(tokenId,harmony,2);
        uint256[3] memory color_3 = getColor(tokenId,harmony,3);

        parts[0] = '<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 350"><style>.z { fill: #d8dee9; font-family: monospace; font-size: 13px;}</style><defs><pattern id="bP" width="5" height="5" viewBox="0,0,5,5" patternTransform="rotate(45 0 0)" patternUnits="userSpaceOnUse"><line x1="4"  y1="0" x2="4" y2="5" style="stroke:';

        parts[1] = string(abi.encodePacked('hsl(',toString(color_0[0]),',',toString(color_0[1]),'%,',toString(color_0[2]),'%); stroke-width:1"/>'));

        parts[2] = '<line x1="0"  y1="2" x2="5" y2="2" style="stroke:';

        parts[3] = string(abi.encodePacked('hsl(',toString(color_1[0]),',',toString(color_1[1]),'%,',toString(color_1[2]),'%); stroke-width:1"/>'));

        parts[4] = '<line x1="2"  y1="0" x2="2" y2="5" style="stroke:';

        parts[5] = string(abi.encodePacked('hsl(',toString(color_2[0]),',',toString(color_2[1]),'%,',toString(color_2[2]),'%); stroke-width:1"/>'));

        parts[6] = '<line x1="0"  y1="4" x2="5" y2="4" style="stroke:';

        parts[7] = string(abi.encodePacked('hsl(',toString(color_3[0]),',',toString(color_3[1]),'%,',toString(color_3[2]),'%); stroke-width:1"/>'));

        parts[8] = '</pattern></defs><rect width="100%" height="100%" fill="';

        parts[9] = '#303841';

        if (check_if_confirmed(tokenId)){
                parts[9] = 'url(#bP)';
        }
        
        parts[10] = '" /><rect width="90%" height="90%" x="5%" y="5%" fill="#303841" /><text x="50%" y="20%" class="z" dominant-baseline="middle" text-anchor="middle">';

        parts[11] = getAlias(ownerOf(tokenId));

        parts[12] = '</text><text x="50%" y="50%" font-size="62" stroke-width=".2" dominant-baseline="middle" text-anchor="middle">';

        parts[13] = getEmojis(tokenId);

        parts[14] = '</text><text x="50%" y="80%" class="z" dominant-baseline="middle" text-anchor="middle">';

        parts[15] = getAlias(buddy_tagged[tokenId]);

        parts[16] = '</text></svg>';

        string memory output = string(abi.encodePacked(parts[0], parts[1], parts[2], parts[3], parts[4], parts[5], parts[6], parts[7], parts[8]));
        output = string(abi.encodePacked(output, parts[9], parts[10], parts[11], parts[12], parts[13], parts[14], parts[15], parts[16]));
        
        string memory json = Base64.encode(bytes(string(abi.encodePacked('{"name": "Buddy Buddy Tag #', toString(tokenId), '","external_url":"https://buddybuddytag.com","description": "A buddy has been tagged! Will they accept? What will the network of on-chain relationships look like?", "image": "data:image/svg+xml;base64,', Base64.encode(bytes(output)), '"}'))));
        output = string(abi.encodePacked('data:application/json;base64,', json));

        return output;
    }

    function sort_addresses(address a1, address a2) internal pure returns (address[2] memory){
        if (a1 > a2){
            return [a2,a1];
        }
        return [a1,a2];
    }

    function can_addr_mint(address a) public view returns (bool){
        if (allow_list[a]){
            return true;
        }
        AcclimatedMoonCats contract_mc = AcclimatedMoonCats(MOONCATS);
        if (contract_mc.balanceOf(a) > 0){
            return true;
        }
        FoPunks contract_fp = FoPunks(FOPUNKS);
        if (contract_fp.balanceOf(a) > 0){
            return true;
        }
        return false;
    }

    function check_if_confirmed(uint256 tokenId) internal view returns (bool){
        address[2] memory sorted_addrs = sort_addresses(ownerOf(tokenId),buddy_tagged[tokenId]);
        return confirmations[sorted_addrs[0]][sorted_addrs[1]];    
    } 

    function tag_buddy(address requested_buddy) public payable{
        require(launched,"Not started yet");
        require(_msgSender()!=requested_buddy,"You cannot tag yourself");
        require(msg.value >= TAG_PRICE,"Not enough ETH");
        require(totalSupply()<11962,"No more tags available");
        require(can_addr_mint(_msgSender()),"Your address cannot mint yet");

        uint256 nextId = totalSupply();
        _safeMint(_msgSender(), nextId, "");
        buddy_tagged[nextId] = requested_buddy;
    }

    function tag_buddy(uint256 tokenId, address requested_buddy) public payable{
        require(launched,"Not started yet");
        require(_msgSender()==ownerOf(tokenId),"You must own this token");
        require(msg.value >= RETAG_PRICE,"Not enough ETH");

        buddy_tagged[tokenId] = requested_buddy;
    }

    function confirm_buddy(uint256 tokenId) public{
        require(launched,"Not started yet");
        require(totalSupply() > tokenId,"Not minted yet.");
        require(buddy_tagged[tokenId]==_msgSender(),"Your address was not tagged for this token");

        address[2] memory sorted_addrs = sort_addresses(ownerOf(tokenId),_msgSender());
        require(!confirmations[sorted_addrs[0]][sorted_addrs[1]],"Already confirmed");

        confirmations[sorted_addrs[0]][sorted_addrs[1]] = true;
        allow_list[_msgSender()] = true; // once youre in, youre in
    }

    function burn_bridge(uint256 tokenId) public payable{
        require(launched,"Not started yet");
        require(totalSupply() > tokenId,"Not minted yet.");
        require(buddy_tagged[tokenId]==_msgSender(),"Only the tagged buddy can burn bridges. Owner can tag_buddy with tokenID and new buddy");
        require(msg.value >= BURN_PRICE,"Burning bridges will cost you more...");

        address[2] memory sorted_addrs = sort_addresses(ownerOf(tokenId),_msgSender());
        require(confirmations[sorted_addrs[0]][sorted_addrs[1]],"These buddies never confirmed");

        confirmations[sorted_addrs[0]][sorted_addrs[1]] = false;
    }

    function add_e(string memory additions) public onlyOwner(){
        require(!launched,"Too late to change things now");
        emoji_list = abi.encodePacked(emoji_list, bytes(additions));
    }

    function reset_e() public onlyOwner(){
        // you're a real dummy if you need this
        require(!launched,"Already launched");
        delete emoji_list;
    }

    function launch() public onlyOwner(){
        // GOOD LUCK
        require(!launched,"Already launched");
        launched = true;
        LAUNCH_BLOCK = block.number;
    }

    function withdraw() public onlyOwner(){
        // 25% goes one way
        uint256 donation_amount = address(this).balance / 4;
        payable(DONATION_WALLET).transfer(donation_amount);

        // rest goes the other way
        payable(msg.sender).transfer(address(this).balance);
    }