contract Role is ERC721Enumerable, ReentrancyGuard, Ownable {
    uint256 public seed;
    bool public saleIsActive = false;
    uint public next_token;

    mapping(uint => uint) public xp;
    mapping(uint => uint) public level;
    event leveled(address indexed owner, uint level, uint role);

    // --- Auth ---
    mapping (address => uint) public wards;
    function rely(address guy) external auth { wards[guy] = 1; }
    function deny(address guy) external auth { wards[guy] = 0; }
    modifier auth {
        require(wards[msg.sender] == 1, "RPK/not-authorized");
        _;
    }

    string[] private genus = [
        "Fox",
        "Piggy",
        "Doge",
        "Wolf",
        "Steed",
        "Alpaca",
        "Cat",
        "Chook",
        "Octopus",
        "Whale",
        "Owl",
        "Dragon"
    ];

    string[] private gender = ["Male", "Female", "Gynander"];

    // The initial age, No more than 30 (range: 0 - 30)
    uint256 private age = 31;

    string[] private career = [
        "Banker",
        "Game Player",
        "Strategist",
        "Negotiator",
        "Fortune Builder",
        "Theologian",
        "Appeaser",
        "Tactician",
        "Architect",
        "Warrior",
        "Physician",
        "Soldier",
        "Gastronome",
        "Cleric",
        "Outdoorsman",
        "Scholar",
        "Reader",
        "Writer",
        "Artist",
        "Pyromaniac",
        "Scientist",
        "Actor",
        "Nurse",
        "Security",
        "Trader",
        "Doctor",
        "Merchant",
        "Musician",
        "Engineer",
        "Hacker",
        "Student",
        "Broker",
        "Bartender",
        "Athlete",
        "Police",
        "Thief",
        "Farmer",
        "Teacher",
        "Professor",
        "Sailor",
        "Investor",
        "Degen",
        "Diplomat",
        "Politician"
    ];

    string[] private regional = [
        "Wind Land",
        "Cloud Land",
        "Rain Land",
        "Light Land",
        "Dark Land"
    ];

    string[] private marriage = ["Married", "Unmarried"];

    string[] private skin = [
        "#BACBFB",
        "#AA0000",
        "#4668FF",
        "#008C0E",
        "#CAB8FB",
        "#BF0049",
        "#9950FD",
        "#ED401D",
        "#58D1B3",
        "#00A763",
        "#FFDEC5",
        "#F29710",
        "#EDC3DB",
        "#D9D9D9",
        "#32EDA6"
    ];

    string[] private character = [
        "Arbitrary",
        "Ascetic",
        "Babbling Buffoon",
        "Bloodlust",
        "Body Modder",
        "Giant",
        "Gregarious",
        "Hedonist",
        "Iron Gut",
        "Lefthanded",
        "Light Eater",
        "Out Of Shape",
        "Playful",
        "Robust",
        "Secretive",
        "Shy",
        "Sturdy",
        "Tortured",
        "Short Sighted",
        "Severely Injured",
        "Smoker",
        "Weak Stomach",
        "Bastard",
        "Dwarf",
        "Feeble",
        "Fever",
        "Harelip",
        "Measles",
        "Tuberculosis",
        "Indolent",
        "Indulgent",
        "Infection",
        "Infirm",
        "Lisp",
        "Obese",
        "One Eyed",
        "One Handed",
        "One Legged",
        "Stout",
        "Syphilitic",
        "Aggressive",
        "Alert",
        "Ambitious",
        "Attractive",
        "Candid",
        "Careful",
        "Devoted",
        "Dutiful",
        "Easy-Going",
        "Forceful",
        "Forgetful",
        "Frank",
        "Genteel",
        "Frugal",
        "Gullible",
        "Happy",
        "Hard-Working",
        "Initiative",
        "Inventive",
        "Lazy",
        "Liberal",
        "Modest",
        "Obedient",
        "Selfless",
        "Sensible",
        "Sensitive",
        "Sincere",
        "Skeptical",
        "Smart",
        "Sociable",
        "Sporting",
        "Steady",
        "Straightforward",
        "Strict",
        "Strong-Willed",
        "Sympathetic",
        "Talented",
        "Trustful",
        "Understanding",
        "Unselfish",
        "Active",
        "Adroit",
        "Analytical",
        "Apprehensive",
        "Argumentative",
        "Bad-Tempered",
        "Bossy",
        "Brave",
        "Brilliant",
        "Caring",
        "Charitable",
        "Cheerful",
        "Childish",
        "Comical",
        "Conceited",
        "Confident",
        "Conscientious",
        "Dashing",
        "Dedicated",
        "Demanding",
        "Dependable",
        "Depressing",
        "Determined",
        "Diplomatic",
        "Disciplined",
        "Disorganized",
        "Energetic",
        "Enthusiastic",
        "Faithful",
        "Friendly",
        "Funny",
        "Generous",
        "Hearty",
        "Helpful",
        "Helpless",
        "Bloodthirsty",
        "Charismatic",
        "Mastermind",
        "Naive",
        "Skilled",
        "Visionary",
        "Misguided",
        "Tough",
        "Martial",
        "Weird",
        "Amiable",
        "Rude",
        "Crazy",
        "Mediocre",
        "Naughty",
        "Benevolent",
        "Graceful",
        "Nudist"
    ];

    // No more than 100 (range: 0 - 100)
    uint256 private lucky = 101;

    function random(string memory input) internal pure returns (uint256) {
        return uint256(keccak256(abi.encodePacked(input)));
    }

    function getGenus(uint256 tokenId) public view returns (string memory) {
        return pluck(tokenId, "GENUS", genus);
    }

    function getGender(uint256 tokenId) public view returns (string memory) {
        return pluck(tokenId, "GENDER", gender);
    }

    function getAge(uint256 tokenId) public view returns (string memory) {
        uint256 growthAge = (block.timestamp - seed) / 31536000;
        uint256 newAge = pluck(tokenId, "AGE", age) + growthAge;
        return toString(newAge);
    }

    function getCareer(uint256 tokenId) public view returns (string memory) {
        return pluck(tokenId, "CAREER", career);
    }

    function getRegional(uint256 tokenId) public view returns (string memory) {
        return pluck(tokenId, "REGIONAL", regional);
    }

    function getMarriage(uint256 tokenId) public view returns (string memory) {
        return pluck(tokenId, "MARRIAGE", marriage);
    }

    function getSkin(uint256 tokenId) public view returns (string memory) {
        return pluck(tokenId, "SKIN", skin);
    }

    function getCharacter(uint256 tokenId) public view returns (string memory) {
        return pluck(tokenId, "CHARACTER", character);
    }

    function getXp(uint256 tokenId) public view returns (string memory) {
        return toString(xp[tokenId]);
    }

    function getLevel(uint256 tokenId) public view returns (string memory) {
        return toString(level[tokenId]);
    }

    function getLucky(uint256 tokenId) public view returns (string memory) {
        return toString(pluck(tokenId, "LUCKY", lucky));
    }

    function pluck(
        uint256 tokenId,
        string memory keyPrefix,
        string[] memory sourceArray
    ) internal view returns (string memory) {
        uint256 rand = random(
            string(abi.encodePacked(seed, keyPrefix, toString(tokenId)))
        );
        return sourceArray[rand % sourceArray.length];
    }

    function pluck(
        uint256 tokenId,
        string memory keyPrefix,
        uint256 randomRange
    ) internal view returns (uint256) {
        uint256 rand = random(
            string(abi.encodePacked(seed, keyPrefix, toString(tokenId)))
        );
        return rand % randomRange;
    }

    function tokenURI(uint256 tokenId)
        public
        view
        override
        returns (string memory)
    {
        string[26] memory parts;
        parts[0] = '<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 350">';

        parts[1] = "<style>.base{font-family:serif;font-size:14px;fill:";

        parts[2] = getSkin(tokenId);

        parts[3] = ';}</style><rect width="100%" height="100%" fill="#281A33" />';

        parts[4] = '<path class="base" d="M50.7,59c0.7,0,1.3-0.1,2-0.2s1.3-0.3,1.8-0.6c0.5-0.3,0.7-0.7,0.7-1.3c0-0.4-0.2-0.6-0.7-0.6c-1.1,0-2.2-0.1-3.1-0.3c-1-0.2-1.9-0.7-2.8-1.5c-1.3-1.2-2.5-2.5-3.6-3.7s-2-2.5-2.9-3.8c-0.9-1.3-1.9-2.7-2.8-4.2c-0.1-0.1-0.1-0.3-0.2-0.4c0-0.1,0-0.2,0.2-0.2c0.9-0.4,1.8-0.9,2.7-1.6c0.9-0.7,1.6-1.5,2.2-2.5c0.6-1,0.9-2.3,0.9-3.7c0-2.6-1-4.6-2.9-5.9c-1.9-1.3-4.7-1.9-8.2-1.9c-1,0-1.8,0-2.5,0.1c-0.6,0-1.2,0.1-1.7,0.2c-0.5,0.1-1.1,0.1-1.7,0.1c-0.7,0-1.3,0-1.7,0s-0.9-0.1-1.5-0.1s-1.2,0-2.1,0c-0.4,0-0.7,0-0.9,0.1c-0.2,0.1-0.3,0.2-0.3,0.5c0,0.2,0.1,0.4,0.3,0.6c0.2,0.2,0.6,0.3,1.1,0.4c0.9,0.1,1.5,0.3,1.9,0.5c0.4,0.3,0.7,0.7,0.8,1.2s0.2,1.4,0.2,2.4l0,0v20.8c0,1.1-0.3,1.9-0.8,2.4s-1.4,0.8-2.6,1.1c-0.9,0.2-1.4,0.5-1.4,0.9c0,0.2,0.1,0.3,0.4,0.4c0.2,0.1,0.5,0.1,0.9,0.1c0.8,0,1.6,0,2.3-0.1c0.7-0.1,1.4-0.1,2.1-0.2c0.7,0,1.4-0.1,2.2-0.1c0.6,0,1.2,0,1.8,0.1c0.6,0,1.1,0.1,1.8,0.1c0.6,0.1,1.3,0.1,2.2,0.1c0.4,0,0.7,0,0.9-0.1c0.2-0.1,0.3-0.2,0.3-0.4c0-0.3-0.1-0.4-0.3-0.6c-0.2-0.1-0.6-0.2-1-0.3c-1.2-0.2-2.1-0.7-2.6-1.3c-0.5-0.6-0.8-1.5-0.8-2.5l0,0v-8.7c0-0.3,0.1-0.5,0.2-0.7c0.1-0.2,0.3-0.3,0.5-0.2c0.4,0,0.8,0.1,1.4,0.2c0.5,0.1,1.1,0.3,1.7,0.6c0.8,0.4,1.4,1,1.8,1.7c1,1.7,1.8,3,2.6,4.2c0.7,1.1,1.4,2.1,2.1,3c0.7,0.9,1.4,1.8,2.1,2.8c0.6,0.7,1.2,1.3,2,1.8c0.7,0.5,1.5,0.8,2.4,1.1C48.5,58.9,49.6,59,50.7,59z M33.2,42c-0.7,0-1.3-0.1-1.7-0.3c-0.4-0.2-0.6-0.7-0.6-1.4c0-1.8,0-3.3,0-4.6c0-1.3,0-2.3,0.1-3.1c0-1.7,0.2-2.9,0.5-3.5c0.3-0.6,0.9-0.9,1.8-0.9c1.7,0,3.1,0.3,4,0.9s1.6,1.4,1.9,2.5s0.5,2.3,0.5,3.6c0,1.2-0.3,2.3-0.8,3.4c-0.5,1-1.3,1.9-2.3,2.5C35.8,41.7,34.6,42,33.2,42z M68.6,36.1c0.3,0,0.4-0.2,0.4-0.5c0-0.1,0-0.2,0-0.4c0-0.2-0.1-0.3-0.1-0.5c-0.2-0.6-0.6-1.4-1-2.3c-0.5-0.9-0.9-1.8-1.4-2.7c-0.5-0.9-0.8-1.6-1.1-2c-0.2-0.3-0.4-0.6-0.7-0.7s-0.6-0.2-0.9-0.2c-0.7,0-1.3,0.1-1.8,0.2c-0.5,0.2-0.8,0.4-0.8,0.8c0,0.2,0,0.4,0.1,0.5c0.1,0.1,0.2,0.3,0.4,0.6c0.5,0.6,1,1.3,1.7,2.1c0.7,0.8,1.3,1.6,2,2.4c0.7,0.8,1.3,1.4,1.8,1.9C67.8,35.8,68.3,36.1,68.6,36.1z M66.3,58.7c2.5,0,4.5-0.5,6.2-1.6c1.6-1,2.9-2.4,3.7-4.1s1.2-3.5,1.2-5.4c0-1.8-0.4-3.4-1.3-4.8c-0.9-1.5-2.1-2.7-3.6-3.6c-1.5-0.9-3.2-1.4-5.1-1.4c-1.5,0-2.9,0.3-4.2,0.9s-2.5,1.5-3.5,2.5c-1,1.1-1.8,2.2-2.3,3.6c-0.5,1.3-0.8,2.7-0.8,4.1c0,1.9,0.4,3.6,1.3,5c0.9,1.5,2,2.6,3.5,3.5C62.8,58.2,64.5,58.7,66.3,58.7z M67.3,57c-1.2,0-2.2-0.4-3-1.3c-0.9-0.9-1.6-2-2.1-3.5c-0.5-1.4-0.8-2.9-0.8-4.4c0-1.4,0.1-2.6,0.3-3.8c0.2-1.2,0.6-2.1,1.2-2.9c0.5-0.6,1-1,1.6-1.2c0.6-0.2,1.3-0.4,2-0.4c1.2,0,2.3,0.4,3.2,1.3c0.9,0.9,1.6,2,2.1,3.4c0.5,1.4,0.8,2.9,0.8,4.5c0,1.1-0.1,2.2-0.2,3.3c-0.1,1.1-0.4,2-0.8,2.7c-0.4,0.8-1,1.3-1.7,1.7S68.2,57,67.3,57z M80.7,58.1c0.6,0,1.2,0,1.6-0.1s0.8-0.1,1.3-0.1c0.4,0,0.9,0,1.4,0c0.6,0,1.1,0,1.5,0c0.4,0,0.8,0.1,1.3,0.1c0.4,0,1,0.1,1.6,0.1c0.2,0,0.4,0,0.6-0.1c0.2-0.1,0.3-0.2,0.3-0.4c0-0.3-0.1-0.4-0.3-0.6c-0.2-0.1-0.5-0.2-0.8-0.3c-0.5-0.2-0.9-0.3-1.4-0.5s-0.6-0.6-0.6-1.3l0,0V30c0-1.7,0.1-2.9,0.2-3.7s0.1-1.4,0.1-1.6c0-0.1,0-0.2-0.1-0.3s-0.1-0.1-0.2-0.1c-0.1,0-0.2,0-0.4,0.1c-0.5,0.2-1.1,0.4-1.7,0.7c-0.6,0.3-1.3,0.5-2.1,0.8c-0.7,0.2-1.5,0.5-2.3,0.6c-0.3,0.1-0.4,0.3-0.4,0.6c0,0.4,0.1,0.6,0.4,0.7c0.8,0.2,1.3,0.6,1.6,1.2s0.4,1.4,0.4,2.3l0,0v23.8c0,0.7-0.2,1.1-0.6,1.3c-0.4,0.2-0.9,0.4-1.3,0.5c-0.3,0.1-0.5,0.2-0.8,0.3s-0.3,0.3-0.3,0.6c0,0.2,0.1,0.3,0.3,0.4C80.3,58.1,80.5,58.1,80.7,58.1z M101.4,58.7c1.1,0,2-0.2,3-0.5c0.9-0.3,1.7-0.8,2.4-1.3c0.7-0.6,1.3-1.2,1.7-1.9c0.5-0.7,0.7-1.3,0.7-1.8c0-0.3-0.1-0.5-0.4-0.5c-0.1,0-0.2,0-0.3,0.1c-0.1,0.1-0.2,0.2-0.3,0.3c-0.7,0.7-1.4,1.2-2.2,1.6c-0.8,0.4-1.7,0.5-2.6,0.5c-1.3,0-2.5-0.3-3.5-1c-1.1-0.7-1.9-1.6-2.5-2.8c-0.6-1.2-0.9-2.5-0.9-3.9c0-0.7,0.1-1.1,0.3-1.4c0.2-0.2,0.5-0.4,0.9-0.4l0,0h9.9c1,0,1.5-0.5,1.5-1.6c0-1.2-0.3-2.3-0.9-3.2c-0.6-0.9-1.4-1.7-2.4-2.2c-1-0.5-2.2-0.8-3.5-0.8c-1.8,0-3.4,0.5-4.8,1.4c-1.5,0.9-2.6,2.2-3.5,3.8c-0.9,1.6-1.3,3.3-1.3,5.2c0,2.1,0.4,3.9,1.1,5.5s1.8,2.8,3.1,3.7C98.1,58.3,99.6,58.7,101.4,58.7z M101.5,44h-3.8c-0.4,0-0.6-0.2-0.6-0.5c0-0.6,0.2-1.3,0.7-1.9c0.4-0.6,1-1.1,1.7-1.6c0.7-0.4,1.4-0.6,2.1-0.6c1,0,1.9,0.3,2.7,0.9s1.1,1.4,1.1,2.3c0,0.3,0,0.6-0.1,0.8c-0.1,0.2-0.2,0.3-0.3,0.4c-0.4,0.1-0.9,0.2-1.4,0.2C103,44,102.4,44,101.5,44L101.5,44z"/>';
        
        parts[5] = '<text x="20" y="100" class="base">';

        parts[6] = getGenus(tokenId);

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

        parts[8] = getGender(tokenId);

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

        parts[10] = getAge(tokenId);

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

        parts[12] = getMarriage(tokenId);

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

        parts[14] = getRegional(tokenId);

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

        parts[16] = getCareer(tokenId);

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

        parts[18] = getCharacter(tokenId);
        
        parts[19] = '</text><text x="20" y="240" class="base">Lv ';

        parts[20] = getLevel(tokenId);

        parts[21] = '</text><text x="20" y="260" class="base">XP ';

        parts[22] = getXp(tokenId);

        parts[23] = '</text><text x="20" y="280" class="base">LUK+';

        parts[24] = getLucky(tokenId);

        parts[25] = "</text></svg>";

        string memory output = string(
            abi.encodePacked(
                parts[0],
                parts[1],
                parts[2],
                parts[3],
                parts[4],
                parts[5],
                parts[6]
            )
        );
        output = string(
            abi.encodePacked(
                output,
                parts[7],
                parts[8],
                parts[9],
                parts[10],
                parts[11],
                parts[12],
                parts[13]
            )
        );
        output = string(
            abi.encodePacked(
                output,
                parts[14],
                parts[15],
                parts[16],
                parts[17],
                parts[18],
                parts[19],
                parts[20],
                parts[21]
            )
        );
        output = string(
            abi.encodePacked(
                output,
                parts[22],
                parts[23],
                parts[24],
                parts[25]
            )
        );

        string memory json = Base64.encode(
            bytes(
                string(
                    abi.encodePacked(
                        '{"name": "Role(for 0xAdventure) #',
                        toString(tokenId),
                        '", "description": "0xAdventure is a guild of Dimensional Breakers. We are willing to give precious life to become a beacon for all Dimensional Breakers.", "image": "data:image/svg+xml;base64,',
                        Base64.encode(bytes(output)),
                        '"}'
                    )
                )
            )
        );
        output = string(
            abi.encodePacked("data:application/json;base64,", json)
        );

        return output;
    }

    /*
     * Pause sale if active, make active if paused
     */
    function flipSaleState() public onlyOwner {
        saleIsActive = !saleIsActive;
    }

    function claim() public nonReentrant {
        require(saleIsActive, "Sale must be active to mint Role");
        uint _next_role = next_token;
        level[_next_role] = 1;
        _safeMint(_msgSender(), _next_role);
        next_token++;
    }

    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);
    }

    // level
    function addXp(uint _tokenId, uint _xp) external auth {
        require(_tokenId < totalSupply(), "operator query for nonexistent token");
        xp[_tokenId] += _xp;
    }

    function addListXp(uint[] memory _tokenIds, uint[] memory _xps) external auth {
        require(_tokenIds.length == _xps.length, "tokenIds are inconsistent with xps length");
        for (uint i = 0; i < _tokenIds.length; i++) {
            require(_tokenIds[i] < totalSupply(), "operator query for nonexistent token");
            uint _tokenId = _tokenIds[i];
            uint _xp = _xps[i];
            xp[_tokenId] += _xp;
        }
    }
    
    function levelUp(uint _tokenId) external {
        require(_isApprovedOrOwner(msg.sender, _tokenId));
        uint _level = level[_tokenId];
        uint _xp_required = xp_required(_level);
        require(xp[_tokenId] >= _xp_required, "xp is not enough");
        xp[_tokenId] -= _xp_required;
        level[_tokenId] = _level + 1;
        emit leveled(msg.sender, _level, _tokenId);
    }
    
    function xp_required(uint curent_level) public pure returns (uint xp_to_next_level) {
        xp_to_next_level = curent_level * 1000;
        for (uint i = 1; i < curent_level; i++) {
            xp_to_next_level += i * 1000;
        }
    }

    constructor()
        ERC721("Role(for 0xAdventure)", "ROLE")
        Ownable()
    {
        seed = block.timestamp;
        wards[msg.sender] = 1;
    }
}
function addXp(uint _tokenId, uint _xp) external auth {
    require(_tokenId < totalSupply(), "operator query for nonexistent token");
    xp[_tokenId] += _xp;
}

// level