// File: contracts/Renderer.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;

interface IERC721 {
    function balanceOf(address owner) external view returns (uint256 balance);
}

contract BUGNRenderer {
    uint256 private constant _DECIMALS = 10_000;
    uint256 private constant _RACE_DECIMALS = 100_000_000;
    address private _updaterAddress;
    address private _owner;

    address private constant _AZUKI_CONTRACT_ADDRESS = 0xED5AF388653567Af2F388E6224dC7C4b3241C544;
    address private constant _BAYC_CONTRACT_ADDRESS = 0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D;
    address private constant _LOOT_CONTRACT_ADDRESS = 0xFF9C1b15B16263C61d017ee9F65C50e4AE0113D7;
    address private constant _DEGODS_CONTRACT_ADDRESS = 0x8821BeE2ba0dF28761AffF119D66390D594CD280;

    struct Token {
        uint8 spec;
        string race;
    }

    mapping(uint256 => Token) private _tokens;

    struct Properties {
        string status;
        uint256 probability;
    }

    struct otherProperties {
        string[] status;
        uint256 probability;
    }

    Properties[] private _race;
    Properties[] private _state;
    Properties[] private _nutrition;
    Properties[] private _iQ;
    Properties[] private _causeOfDeathCyborg;
    Properties[] private _causeOfDeathOthers;
    Properties[] private _matrix;
    Properties[] private _eyes;
    Properties[] private _headGeneral;
    Properties[] private _ear;
    Properties[] private _mouth;
    Properties[] private _backgroundGeneral;
    Properties[] private _azuki;
    Properties[] private _getup;

    constructor() {
        _owner = msg.sender;

        _race.push(Properties("Cyborg", 100_000));
        _race.push(Properties("Degen", 900_000));
        _race.push(Properties("Erectus", 9_000_000));
        _race.push(Properties("Neanderthals", 9_000_000));
        _race.push(Properties("Sapiens", 81_000_000));

        _state.push(Properties("Alive", 9000));
        _state.push(Properties("Dead", 1000));

        _nutrition.push(Properties("Vegan", 3000));
        _nutrition.push(Properties("Carnivore", 3000));
        _nutrition.push(Properties("Boring Omnivore", 800));
        _nutrition.push(Properties("Keto", 3000));
        _nutrition.push(Properties("Cannibal", 200));

        _iQ.push(Properties("Left", 200));
        _iQ.push(Properties("Right", 200));
        _iQ.push(Properties("Leftish", 1400));
        _iQ.push(Properties("Rightish", 1400));
        _iQ.push(Properties("Room Temperature", 6800));

        _causeOfDeathCyborg.push(Properties("Murder", 4000));
        _causeOfDeathCyborg.push(Properties("Natural Disaster", 4000));
        _causeOfDeathCyborg.push(Properties("Overprocessing", 2000));

        _causeOfDeathOthers.push(Properties("Murder", 3000));
        _causeOfDeathOthers.push(Properties("Disease", 3000));
        _causeOfDeathOthers.push(Properties("Natural Disaster", 1000));
        _causeOfDeathOthers.push(Properties("Overprocessing", 1000));
        _causeOfDeathOthers.push(Properties("Drugs", 1000));
        _causeOfDeathOthers.push(Properties("Dementia", 1800));

        _matrix.push(Properties("Red Pill", 1000));
        _matrix.push(Properties("Blue Pill", 9000));

        _eyes.push(Properties("_eyes1", 4000));
        _eyes.push(Properties("_eyes2", 4000));
        _eyes.push(Properties("_eyes3", 2000));

        _headGeneral.push(Properties("_head1", 1000));
        _headGeneral.push(Properties("_head2", 2000));
        _headGeneral.push(Properties("_head3", 2000));
        _headGeneral.push(Properties("_head4", 2000));
        _headGeneral.push(Properties("_head5", 3000));

        _ear.push(Properties("_ear1", 3333));
        _ear.push(Properties("_ear2", 3333));
        _ear.push(Properties("_ear3", 3334));

        _mouth.push(Properties("_mouth1", 2500));
        _mouth.push(Properties("_mouth2", 2500));
        _mouth.push(Properties("_mouth3", 2500));
        _mouth.push(Properties("_mouth4", 2500));

        _backgroundGeneral.push(Properties("_background1", 6700));
        _backgroundGeneral.push(Properties("_background2", 3300));

        _azuki.push(Properties("Kimono", 5000));
        _azuki.push(Properties("Hoodie", 5000));

        _getup.push(Properties("Naked Gryffindor", 2500));
        _getup.push(Properties("Naked Slytherin", 2500));
        _getup.push(Properties("Naked Ravenclaw", 2500));
        _getup.push(Properties("Naked Hufflepuff", 2500));
    }

    function _getItem(uint256 tokenId, Properties[] memory prop) internal pure returns (string memory) {
        return _get(tokenId, prop).status;
    }

    function _getHoldersReward(uint256 tokenId, uint8 spec) internal view returns (string memory) {
        if (spec == 2) {
            return _get(tokenId, _azuki).status;
        } else if (spec == 3) {
            return "Kings Robe";
        } else if (spec == 4) {
            return "Divine Robe";
        } else if (spec == 5) {
            return "Mythic War Armor";
        } else {
            return _get(tokenId, _getup).status;
        }
    }

    string[] private _eyes1 = ["Laser", "Blood", "Black", "Brown", "3D Glasses", "Vision Pro", "Visor", "Hololens"];
    string[] private _eyes2 = ["Pirate Patch", "Sleeping Mask", "Conjunctivitis", "Dark shades"];
    string[] private _eyes3 = ["Wight", "Ice", "Fire", "Blind", "Blue Ivy", "Cat"];

    function _getEyes(uint256 tokenId) internal view returns (string memory) {
        string memory _uEyes = _get(tokenId, _eyes).status;
        if (Base64.compareStrings(_uEyes, "_eyes1")) {
            return
                _eyes1[_getRandomNumber(_eyes1.length, string(abi.encodePacked("uEyes1", Base64.toString(tokenId))))];
        } else if (Base64.compareStrings(_uEyes, "_eyes2")) {
            return
                _eyes2[_getRandomNumber(_eyes2.length, string(abi.encodePacked("uEyes2", Base64.toString(tokenId))))];
        } else {
            return
                _eyes3[_getRandomNumber(_eyes3.length, string(abi.encodePacked("uEyes3", Base64.toString(tokenId))))];
        }
    }

    string[] private _head1 = ["Bald", "Beanie", "Scarf"];
    string[] private _head2 = ["Feather Bonnet", "Bonnet", "Crown", "Dauphin"];
    string[] private _head3 = ["Front Cap", "Back Cap"];
    string[] private _head4 = ["Blonde Ponytail", "French Braids", "Dreadlocks", "Bicorne", "Grey Spiky", "Pink Spiky"];
    string[] private _head5 = ["Messy hair", "Hair surgery", "Targaryen Hat", "Tyrell cut", "Targaryen blonde"];

    function _getHeadGeneral(uint256 tokenId) internal view returns (string memory) {
        string memory _uHead = _get(tokenId, _headGeneral).status;
        if (Base64.compareStrings(_uHead, "_head1")) {
            return
                _head1[_getRandomNumber(_head1.length, string(abi.encodePacked("uHead1", Base64.toString(tokenId))))];
        } else if (Base64.compareStrings(_uHead, "_head2")) {
            return
                _head2[_getRandomNumber(_head2.length, string(abi.encodePacked("uHead2", Base64.toString(tokenId))))];
        } else if (Base64.compareStrings(_uHead, "_head3")) {
            return
                _head3[_getRandomNumber(_head3.length, string(abi.encodePacked("uHead3", Base64.toString(tokenId))))];
        } else if (Base64.compareStrings(_uHead, "_head4")) {
            return
                _head4[_getRandomNumber(_head4.length, string(abi.encodePacked("uHead4", Base64.toString(tokenId))))];
        } else {
            return
                _head5[_getRandomNumber(_head5.length, string(abi.encodePacked("uHead5", Base64.toString(tokenId))))];
        }
    }

    function _getHeadCyborg() internal pure returns (string memory) {
        return "Bald";
    }

    string[] private _ear1 = ["Cortana", "Siri", "Jarvis", "Friday", "Bard"];
    string[] private _ear2 = ["Alexa", "Assistant"];
    string[] private _ear3 = ["Sydney", "Clippy", "Sherlock", "Mycroft", "Luddite"];

    function _getEar(uint256 tokenId) internal view returns (string memory) {
        string memory _uEar = _get(tokenId, _ear).status;
        if (Base64.compareStrings(_uEar, "_ear1")) {
            return _ear1[_getRandomNumber(_ear1.length, string(abi.encodePacked("uEar1", Base64.toString(tokenId))))];
        } else if (Base64.compareStrings(_uEar, "_ear2")) {
            return _ear2[_getRandomNumber(_ear2.length, string(abi.encodePacked("uEar2", Base64.toString(tokenId))))];
        } else {
            return _ear3[_getRandomNumber(_ear3.length, string(abi.encodePacked("uEar3", Base64.toString(tokenId))))];
        }
    }

    string[] private _mouth1 = ["Cigarrete", "Vape", "Sisha"];
    string[] private _mouth2 = ["Cheeky Tongue", "White teeth", "Braces", "No teeth", "Baby teeth"];
    string[] private _mouth3 = ["Closed", "Open", "Smoke", "Vapor"];
    string[] private _mouth4 = ["Red lips smoke", "Red lips cheeky tongue", "Red lips closed"];

    function _getMouth(uint256 tokenId) internal view returns (string memory) {
        string memory _uMouth = _get(tokenId, _mouth).status;
        if (Base64.compareStrings(_uMouth, "_mouth1")) {
            return _mouth1[_getRandomNumber(
                _mouth1.length, string(abi.encodePacked("uMouth1", Base64.toString(tokenId)))
            )];
        } else if (Base64.compareStrings(_uMouth, "_mouth2")) {
            return _mouth2[_getRandomNumber(
                _mouth2.length, string(abi.encodePacked("uMouth2", Base64.toString(tokenId)))
            )];
        } else if (Base64.compareStrings(_uMouth, "_mouth3")) {
            return _mouth3[_getRandomNumber(
                _mouth3.length, string(abi.encodePacked("uMouth3", Base64.toString(tokenId)))
            )];
        } else {
            return _mouth4[_getRandomNumber(
                _mouth4.length, string(abi.encodePacked("uMouth4", Base64.toString(tokenId)))
            )];
        }
    }

    string[] private _background1 = ["White", "Grey", "Black"];

    string[] private _background2 = ["Binary", "Stripe", "Matrix"];

    function _getBackgroundGeneral(uint256 tokenId) internal view returns (string memory) {
        string memory _uBackground = _get(tokenId, _backgroundGeneral).status;
        if (Base64.compareStrings(_uBackground, "_background1")) {
            return _background1[_getRandomNumber(
                _background1.length, string(abi.encodePacked("uBackground1", Base64.toString(tokenId)))
            )];
        } else {
            return _background2[_getRandomNumber(
                _background2.length, string(abi.encodePacked("uBackground2", Base64.toString(tokenId)))
            )];
        }
    }

    function _getBackgroundCyborg() internal pure returns (string memory) {
        return "White";
    }

    function _getRandomNumber(uint256 range, string memory input) internal pure returns (uint256) {
        return uint256(keccak256(abi.encodePacked(input, range + 1_222_989))) % range;
    }

    function _getRace(uint256 tokenId, Properties[] memory sourceArray) internal pure returns (string memory) {
        uint256 randomNumber = _getRandomNumber(_RACE_DECIMALS, string(abi.encodePacked(Base64.toString(tokenId))));
        uint256 cumulativeProbability = 0;

        for (uint256 j = 0; j < sourceArray.length; j++) {
            cumulativeProbability += sourceArray[j].probability;
            if (randomNumber < cumulativeProbability) {
                return sourceArray[j].status;
            }
        }
        return sourceArray[0].status;
    }

    function _get(uint256 tokenId, Properties[] memory sourceArray) internal pure returns (Properties memory) {
        uint256 randomNumber = _getRandomNumber(_DECIMALS, string(abi.encodePacked(Base64.toString(tokenId))));
        uint256 cumulativeProbability = 0;

        for (uint256 j = 0; j < sourceArray.length; j++) {
            cumulativeProbability += sourceArray[j].probability;
            if (randomNumber < cumulativeProbability) {
                return sourceArray[j];
            }
        }
        return sourceArray[0];
    }

    function save(uint256 tokenId, address _contractAddress) external onlyUpdater {
        string memory race = _getRace(tokenId, _race);
        uint8 spec = _checkIfHolder(_contractAddress);
        _tokens[tokenId] = Token(spec, race);
    }

    modifier onlyUpdater() {
        require(msg.sender == _updaterAddress);
        _;
    }

    function _checkIfHolder(address _contractAddress) internal view returns (uint8) {
        if (_contractAddress == address(0)) {
            return 1;
        }
        IERC721 specialContract = IERC721(_contractAddress);
        uint256 held = specialContract.balanceOf(msg.sender);

        if (held > 0) {
            if (_contractAddress == _AZUKI_CONTRACT_ADDRESS) {
                return 2;
            } else if (_contractAddress == _BAYC_CONTRACT_ADDRESS) {
                return 3;
            } else if (_contractAddress == _LOOT_CONTRACT_ADDRESS) {
                return 4;
            } else if (_contractAddress == _DEGODS_CONTRACT_ADDRESS) {
                return 5;
            }
        }

        return 1;
    }
    
    function render(uint256 tokenId) public view returns (string memory) {
        require(_tokens[tokenId].spec != 0, "Unassigned Token Id");

        uint8 spec = _tokens[tokenId].spec;
        string memory race = _tokens[tokenId].race;
        string memory causeOfDeath = _getItem(tokenId, _causeOfDeathOthers);
        string memory head = _getHeadGeneral(tokenId);
        string memory background = _getBackgroundGeneral(tokenId);
        if (Base64.compareStrings(race, "Cyborg")) {
            causeOfDeath = _getItem(tokenId, _causeOfDeathCyborg);
            head = _getHeadCyborg();
            background = _getBackgroundCyborg();
        }

        string[25] memory parts;
        {
            parts[0] =
                '<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 350"><style>.base { fill: white; font-family: serif; font-size: 14px; }</style><rect width="100%" height="100%" fill="black" /><text x="10" y="20" class="base">';

            parts[1] = race;

            parts[2] = '</text><text x="10" y="40" class="base">';

            parts[3] = _getItem(tokenId, _state);

            parts[4] = '</text><text x="10" y="60" class="base">';

            parts[5] = _getItem(tokenId, _nutrition);

            parts[6] = '</text><text x="10" y="80" class="base">';

            parts[7] = _getItem(tokenId, _iQ);

            parts[8] = '</text><text x="10" y="100" class="base">';

            parts[9] = causeOfDeath;

            parts[10] = '</text><text x="10" y="120" class="base">';

            parts[11] = _getItem(tokenId, _matrix);

            parts[12] = '</text><text x="10" y="140" class="base">';

            parts[13] = _getEyes(tokenId);

            parts[14] = '</text><text x="10" y="160" class="base">';

            parts[15] = head;

            parts[16] = '</text><text x="10" y="180" class="base">';

            parts[17] = _getEar(tokenId);

            parts[18] = '</text><text x="10" y="200" class="base">';

            parts[19] = _getMouth(tokenId);

            parts[20] = '</text><text x="10" y="220" class="base">';

            parts[21] = _getHoldersReward(tokenId, spec);

            parts[22] = '</text><text x="10" y="240" class="base">';

            parts[23] = background;

            parts[24] = "</text></svg>";
        }
        string memory output;
        {
            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]
                )
            );
            output = string(
                abi.encodePacked(
                    output, parts[17], parts[18], parts[19], parts[20], parts[21], parts[22], parts[23], parts[24]
                )
            );
        }

        string[12] memory attributes;
        string memory aOutput;
        {
            attributes[0] = string(abi.encodePacked('[{"trait_type": "Race", "value": "', race, '" },'));
            attributes[1] =
                string(abi.encodePacked('{"trait_type": "State", "value": "', _getItem(tokenId, _state), '" },'));
            attributes[2] = string(
                abi.encodePacked('{"trait_type": "Nutrition", "value": "', _getItem(tokenId, _nutrition), '" },')
            );
            attributes[3] = string(abi.encodePacked('{"trait_type": "IQ", "value": "', _getItem(tokenId, _iQ), '" },'));
            attributes[4] =
                string(abi.encodePacked('{"trait_type": "Cause Of Death", "value": "', causeOfDeath, '" },'));
            attributes[5] = string(abi.encodePacked('{"trait_type": "Matrix", "value": "', _getItem(tokenId,_matrix), '" },'));
            attributes[6] = string(abi.encodePacked('{"trait_type": "Head", "value": "', head, '" },'));
            attributes[7] = string(abi.encodePacked('{"trait_type": "Eyes", "value": "', _getEyes(tokenId), '" },'));
            attributes[8] = string(abi.encodePacked('{"trait_type": "Ears", "value": "', _getEar(tokenId), '" },'));
            attributes[9] = string(abi.encodePacked('{"trait_type": "Mouth", "value": "', _getMouth(tokenId), '" },'));
            attributes[10] =
                string(abi.encodePacked('{"trait_type": "Cloak", "value": "', _getHoldersReward(tokenId, spec), '" },'));
            attributes[11] = string(abi.encodePacked('{"trait_type": "Background", "value": "', background, '" }]'));
        }
        {
            aOutput = string(
                abi.encodePacked(
                    attributes[0],
                    attributes[1],
                    attributes[2],
                    attributes[3],
                    attributes[4],
                    attributes[5],
                    attributes[6],
                    attributes[7],
                    attributes[8],
                    attributes[9],
                    attributes[10],
                    attributes[11]
                )
            );
        }

        string memory json = Base64.encode(
            bytes(
                string(
                    abi.encodePacked(
                        '{"name": "Bug #',
                        Base64.toString(tokenId),
                        '", "description": "BUGN NFT", "image": "data:image/svg+xml;base64,',
                        Base64.encode(bytes(output)),
                        '", "attributes": ',
                        aOutput,
                        "}"
                    )
                )
            )
        );
        return string(abi.encodePacked("data:application/json;base64,", json));
    }

    function updateRaceProbability(uint256 c, uint256 d, uint256 e, uint256 n, uint256 s) external onlyUpdater {
        _race[0] = Properties("Cyborg", c);    
        _race[1] = Properties("Degen", d);
        _race[2] = Properties("Erectus", e);
        _race[3] = Properties("Neanderthals", n);
        _race[4] = Properties("Sapiens", s);
    }

    function setUpdaterAddress(address updaterAddress) public {
        require(msg.sender == _owner, "No Access");
        _updaterAddress = updaterAddress;
    }
}

/// [MIT License]
/// @title Base64
/// @notice Provides a function for encoding some bytes in base64
/// @author Brecht Devos <brecht@loopring.org>
library Base64 {
    function compareStrings(string memory str1, string memory str2) internal pure returns (bool) {
        return keccak256(bytes(str1)) == keccak256(bytes(str2));
    }

    function toString(uint256 value) internal pure returns (string memory) {
        // Inspired by OraclizeAPI's implementation - MIT license
        // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol

        if (value == 0) {
            return "0";
        }
        uint256 temp = value;
        uint256 digits;
        while (temp != 0) {
            digits++;
            temp /= 10;
        }
        bytes memory buffer = new bytes(digits);
        while (value != 0) {
            digits -= 1;
            buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
            value /= 10;
        }
        return string(buffer);
    }

    bytes internal constant _TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

    /// @notice Encodes some bytes to the base64 representation
    function encode(bytes memory data) internal pure returns (string memory) {
        uint256 len = data.length;
        if (len == 0) return "";

        // multiply by 4/3 rounded up
        uint256 encodedLen = 4 * ((len + 2) / 3);

        // Add some extra buffer at the end
        bytes memory result = new bytes(encodedLen + 32);

        bytes memory _table = _TABLE;

        assembly {
            let _tablePtr := add(_table, 1)
            let resultPtr := add(result, 32)

            for { let i := 0 } lt(i, len) {} {
                i := add(i, 3)
                let input := and(mload(add(data, i)), 0xffffff)

                let out := mload(add(_tablePtr, and(shr(18, input), 0x3F)))
                out := shl(8, out)
                out := add(out, and(mload(add(_tablePtr, and(shr(12, input), 0x3F))), 0xFF))
                out := shl(8, out)
                out := add(out, and(mload(add(_tablePtr, and(shr(6, input), 0x3F))), 0xFF))
                out := shl(8, out)
                out := add(out, and(mload(add(_tablePtr, and(input, 0x3F))), 0xFF))
                out := shl(224, out)

                mstore(resultPtr, out)

                resultPtr := add(resultPtr, 4)
            }

            switch mod(len, 3)
            case 1 { mstore(sub(resultPtr, 2), shl(240, 0x3d3d)) }
            case 2 { mstore(sub(resultPtr, 1), shl(248, 0x3d)) }

            mstore(result, encodedLen)
        }

        return string(result);
    }
}