// Contract: TravelerLoot
// Target function: function metadata(uint256 tokenId, Traveler memory details) internal view returns (string memory)
Start() {
      require(blockActivation > 0 && block.number >= blockActivation,ERROR_COME_BACK_LATER);
      _;
    }

    /// @notice                     Everyone can claim an available tokenId for free (+ gas)
    function claim() external nonReentrant checkStart {
        require(owner() != _msgSender(),ERROR_OWNER_NOT_ALLOWED);
        uint16 adjusted = 1 + counterStandard + MAX_FOR_GUILDS + MAX_FOR_OWNER;
        require(adjusted <= MAX_ID, ERROR_TOKEN_ID_INVALID);
        processing(adjusted,PH_USERS,1,false);
        _safeMint(_msgSender(), adjusted);
        counterStandard++;
    }

    /// @notice                     Guilds can use this function to mint a forged Traveler Loot.
    ///                             Forged TLs are recognizable by a colored line on the NFT.
    ///                             When all the forgeable TLs are minted, the guild who forged
    ///                             the most becomes Conqueror. Its members gain access to
    ///                             claimByConquerors() function
    /// @param                      tokenId         derivative's tokenId owned by the user
    /// @param                      contractAddress derivative's address pretending to be guild
    function claimByGuilds(uint256 tokenId, address contractAddress) external nonReentrant checkStart{
        require(!conqueror.elected, ERROR_COMPETITION_ENDED);
        Traveler storage details = detailsByAddress[contractAddress];
        require(details.verified, ERROR_ADDRESS_NOT_VERIFIED);
        IERC721 looter = IERC721(contractAddress);
        require(tokenId > 0 && looter.ownerOf(tokenId) == _msgSender(), ERROR_NOT_THE_OWNER);
        if (details.counter == 0 && contractAddress != whiteListedGuilds[0]){
            details.color = pickAColor();
        }
        uint16 discretId = uint16(tokenId % MAX_FOR_GUILDS);

        if (counterGuild == MAX_FOR_GUILDS - 1 ){
            (conqueror.addr, conqueror.count) = whoIsWinning();
            conqueror.elected = true;
            detailsByAddress[PH_CONQUERORS].color = details.color;
            detailsByAddress[PH_CONQUERORS].familyName = details.familyName;
        }
        // after this mint, the price for patrons will be increased by 1%
        uint16 finalId = discretId == 0 ? MAX_FOR_GUILDS : discretId;
        processing(finalId, contractAddress, 1, true);
        _safeMint(_msgSender(), finalId);
        counterGuild++;
    }

    /// @notice                     Becoming a Patron. Requires payment
    function claimByPatrons() external payable nonReentrant checkStart{
      require(msg.value >= priceForPatrons, ERROR_LOW_VALUE);
      if (priceForPatrons < INITIAL_PRICE_FOR_PATRONS){
        priceForPatrons == INITIAL_PRICE_FOR_PATRONS;
      }
      // After this mint, the price for next patrons will be increased by 5%
      reservedMinting(PH_PATRONS, 5, true);
      counterPatrons++;
    }

    /// @notice                     - Loot (for Adventurers) holders can become Patrons for free if:
    ///                             . the Conqueror Guild is not yet elected,
    ///                             . or Dominik Hoffman (@ dhof on twitter) is still younger than 40 y/o.
    function claimByOGLooters() external nonReentrant checkStart{
      require(IERC721(whiteListedGuilds[0]).balanceOf(_msgSender()) > 0, ERROR_NOT_THE_OWNER);
      require(!conqueror.elected, ERROR_COMPETITION_ENDED);
      require(block.timestamp <= DISCOUNT_EXPIRATION, ERROR_DOM_40TH_BIRTHDAY);

      // After this mint, the price for patrons will be decreased by 5%
      reservedMinting(PH_OG_LOOT, 5, false);
      counterPatrons++;
    }

    /// @notice                     Conquerors can mint a CONQUEROR Traveler Loot for free + gas
    ///                             only valid if they have not already minted a PATRON type before
    function claimByConquerors() external nonReentrant checkStart{
        require(conqueror.elected, ERROR_COMPETITION_ONGOING);
        require(IERC721(conqueror.addr).balanceOf(_msgSender()) > 0, ERROR_NOT_THE_OWNER);
        // After this mint, the price for patrons is increased by 2%
        reservedMinting(PH_CONQUERORS, 2, true);
        counterPatrons++;
    }

    /// @notice                     Owner can claim its reserved tokenIds.
    function claimByOwner() external nonReentrant onlyOwner checkStart{
        uint16 adjusted = 1 + counterOwner + MAX_FOR_GUILDS;
        require(adjusted <= MAX_FOR_GUILDS + MAX_FOR_OWNER, ERROR_TOKEN_ID_INVALID);
        //after this mint, the price for patrons will remain the same
        processing(adjusted,PH_OWNER,0,true);
        _safeMint(_msgSender(), adjusted);
        counterOwner++;
    }

    /// @notice                     Owner can call this function to activate the mintings.
    ///                             Note that the activation is delayed of LOCK_TIME blocks
    function activateClaims() external onlyOwner{
      require (blockActivation == 0, ERROR_ALREADY_ACTIVATED);
      blockActivation = block.number + LOCK_TIME;
    }

    /// @notice                     Owner can order the withdraw all the funds coming from Patron mints
    ///                             to the Treasurer address previously set.
    function withdraw() external onlyOwner {
      require(block.number >= treasurer.blockNumber, ERROR_COME_BACK_LATER);
      canChangeTreasurer = true; //release treasurer
      payable(treasurer.current).transfer(address(this).balance);
    }


    /// @notice                     . Owner calls this function with lockTreasurer = true if the intent is
    ///                             to do a donation to a public-good project. Lock guarantees that nobody
    ///                             (not even the owner) can change treasurer till the donation is made.
    ///                             . Owner calls this function with lockTreasurer = false if the intent is
    ///                             to keep open the chance to change the treasurer even if no any
    ///                             withdrawal has been performed in the meanwhile.
    /// @param      newAddress      the new treasurer address
    /// @param      lockTreasurer   true if the the treasurer has to be locked till one withdraw is performed
    function setTreasurer(address newAddress, bool lockTreasurer) external onlyOwner{
      require(canChangeTreasurer, ERROR_WITHDRAW_NEEDED); //this is a safety measure for when donating to public goods
      if (lockTreasurer){
        canChangeTreasurer = false;
      }
      uint256 blockNumber = block.number + LOCK_TIME;
      treasurer.old = treasurer.current;
      treasurer.current = newAddress;
      treasurer.blockNumber = blockNumber;
    }

    /// @notice                     You can register new Guilds to the whitelist, if:
    ///                             - You own at least 50 Traveler Loots in your address
    ///                             - Or if you have mint a Patron version by using claimByPatrons() or
    ///                             claimByOGLooters() functions (no matter if you dont own them anymore)
    ///                             - Or if you are the owner of this Contract
    ///                             NOTE: no Guilds can be registered once the competition has ended
    /// @param      addresses       List of contract addresses to be whitelisted for the game.
    ///                             They'll be allowed to mint GUILD type Traveler Loot
    function addNewGuildToWhiteList(address[] calldata addresses) nonReentrant external {
        require(!conqueror.elected, ERROR_COMPETITION_ENDED);
        for (uint8 j = 0; j< addresses.length; j++){
            address addr = addresses[j];
            require(address(this) != addr, ERROR_CANNOT_ADD_THIS_CONTRACT);
            require(IERC721(address(this)).balanceOf(_msgSender()) > 50
                        || _exists(uint160(_msgSender()))
                        || owner() == _msgSender()
                    ,ERROR_NOT_ENTITLED);
            for (uint16 i = 0; i < whiteListedGuilds.length; i++){
                require(whiteListedGuilds[i] != addr, ERROR_GUILD_ALREADY_WHITELISTED);
            }
            whiteListedGuilds.push(addr);
            detailsByAddress[addr] = Traveler({color:BLACK,familyType:GUILD,familyName:familyName(addr),counter:0,verified:true});
        }
    }

    /// @notice                     Given a Guild address, returns the count for that addr
    /// @param      addr            Address of the Guild to be checked (the contract address)
    /// @return                     Number of the NFT minted from the members of the given Guild address
    function getCounterForAddress(address addr) external view returns (uint256){
        Traveler memory details = detailsByAddress[addr];
        require(details.verified, ERROR_ADDRESS_NOT_VERIFIED);
        return details.counter;
    }

    /// @notice                     Call this function if you want to know the list of the current Guilds
    /// return                      Array containing the addresses of all the whitelisted guilds
    function getWhiteListedGuilds() external view returns (address[] memory){
        return whiteListedGuilds;
    }

    /// @notice                     Call this function to see the element for a given tokenId and categoryId
    /// @param      tokenId         The id of the Traveler Loot you want to check
    /// @param      categoryId      The category of the Traveler Loot you want to check
    /// @return                     The element of the given Token Id for the given categoryId
    function getElement(uint256 tokenId, uint8 categoryId) public view returns (string memory){
      return pluck(tokenId, categories[categoryId], elements[categories[categoryId]]);
    }

    /// @notice                     Provides metadata and image of the PATRONS and CONQUEROR
    /// @param      addr            Address of the user who minted the PATRON or CONQUEROR you are checking
    /// @return                     The json containing the NFT info
    function addressURI(address addr) external view returns (string memory){
        return tokenURI(uint160(addr));
    }

    /// @notice                     Provides metadata and image of the Traveler Loot
    /// @param      tokenId         Token if of the Traveler Loot to process
    /// @return                     The json containing the NFT info
    function tokenURI(uint256 tokenId) override public view returns (string memory) {
        Traveler memory details = detailsByAddress[addressList[tokenId]];
        string[4] memory parts;
        parts[0] = string(abi.encodePacked('<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 350"><style>.b { fill:white; font-family: serif; font-size: 14px; }</style> <rect width="100%" height="100%" fill="black" /><text x="10" y="20" class="b">'));
        parts[1] = string(abi.encodePacked(getElement(tokenId,0),'</text><text x="10" y="40" class="b">',getElement(tokenId,1),'</text><text x="10" y="60" class="b">',getElement(tokenId,2),'</text><text x="10" y="80" class="b">',getElement(tokenId,3),'</text><text x="10" y="100" class="b">',getElement(tokenId,4),'</text><text x="10" y="120" class="b">',getElement(tokenId,5)));
        parts[2] = string(abi.encodePacked('</text><text x="10" y="140" class="b">',getElement(tokenId,6),'</text><text x="10" y="160" class="b">',getElement(tokenId,7),'</text><text x="10" y="180" class="b">',getElement(tokenId,8),'</text><text x="10" y="200" class="b">',getElement(tokenId,9),'</text>'));
        parts[3] = string(abi.encodePacked('<line x1="0" x2="350" y1="300" y2="300" stroke="',details.color,'" stroke-width="4"/>','<text x="340" y="294" text-anchor="end" class="b">',details.familyType,'</text></svg>'));

        string memory compact = string(abi.encodePacked(parts[0], parts[1], parts[2],parts[3]));
        string memory json = Base64.encode(bytes(string(abi.encodePacked('{"name": "Traveler #', toString(tokenId), '", "description": "Traveler Loot is a Loot derivative for the Travel Industry, generated and stored on chain. Feel free to use the Traveler Loot in any way you want", "image": "data:image/svg+xml;base64,', Base64.encode(bytes(compact)), '","attributes":[',metadata(tokenId,details),']}'))));

        return string(abi.encodePacked('data:application/json;base64,', json));
    }

    /// @notice                     Call this method to check the Guild which is owning the most Forged Traveler Loot
    ///
    /// @return                     The address of the guild who has the most Forged Traveler Loot so far
    /// @return                     The amount of Forged Traveler Loot minted by the Winning Guild so far
    function whoIsWinning() public view returns (address, uint16){
      if (conqueror.elected){
        return (conqueror.addr,conqueror.count);
      }
      address winningLoot = whiteListedGuilds[0];
      uint16 winningCount = uint16(detailsByAddress[winningLoot].counter);

      for (uint8 i = 1; i < whiteListedGuilds.length; i++){
        address addr = whiteListedGuilds[i];
        (winningLoot, winningCount) = checkWinning(winningLoot,winningCount, addr, uint16(detailsByAddress[addr].counter));
      }
      return (winningLoot, winningCount);
    }

    /// @notice                     Call this function to get a random color using block difficulty, timestamp as seed
    /// @return                     A string containing the HEX number of a random color
    function pickAColor() internal view returns (string memory){
        string[16] memory list = ["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"];
        string memory color = "#";
        for (uint8 i=0; i<6;i++){
          uint rand = uint(keccak256(abi.encodePacked(block.difficulty, block.timestamp, i )));
          color = string(abi.encodePacked(color,list[rand % list.length]));
        }
        return color;
    }

    /// @notice                     This function calls an external NFT contract and gets the symbol to be used into the metadata
    /// @param      addr            The address of the contract address to whitelist
    /// @return                     The symbol of the given external contract address
    function familyName(address addr) internal view returns (string memory){
      return IERC721Metadata(addr).symbol();
    }

    function random(string memory input) internal pure returns (uint256) {
        return uint256(keccak256(abi.encodePacked(input)));
    }

    /// @notice                     Update the PATRON price and the address counter
    /// @param      id              TokenId minted
    /// @param      addr            address Guild of the token to mint
    /// @param      percentage      percentage number of the PATRON'price variability
    /// @param      isPositive      true if the variability is positive, false if it's negative
    function processing(uint256 id, address addr, uint8 percentage, bool isPositive) internal{
      if (percentage != 0){
        uint160 x = priceForPatrons / (100 / percentage);
        priceForPatrons = isPositive ? priceForPatrons + x : priceForPatrons - x;
      }
      detailsByAddress[addr].counter++;
      addressList[id] = addr;
    }

    /// @notice                     It mints the Traveler Loot using the address as tokenId.
    ///                             Only particular cases can access to this:
    ///                             - Users who decide to become Patrons by paying the patronPrice
    ///                             - Conqueror Guild members, as the prize for have won the competition
    ///                             - OG Loot owners, as privilegiate users till @ dhof 40 bday or till Conqueror Guild election
    /// @param      addr            address Guild of the token to mint
    /// @param      percentage      percentage number of the PATRON'price variability
    /// @param      isPositive      true if the variability is positive, false if it's negative
    function reservedMinting(address addr,uint8 percentage, bool isPositive) internal{
      uint160 castedAddress = uint160(_msgSender());
      require(castedAddress > MAX_ID, ERROR_ADDRESS_NOT_VERIFIED);
      processing(castedAddress, addr, percentage, isPositive);
      _safeMint(_msgSender(), castedAddress);
    }

    /// @notice                     Call this function to get pick the element for a given Traveler Loot id and category
    /// @param      tokenId         TokenId
    /// @param      keyPrefix       Seed/nounce of the randomity
    /// @param      sourceArray     The list of all the elements
    /// @return                     The element extracted
    function pluck(uint256 tokenId, string memory keyPrefix, string[] memory sourceArray) internal view returns (string memory) {
        uint256 rand = random(string(abi.encodePacked(keyPrefix, toString(tokenId))));
        uint8 toHoundred = uint8(rand % 100);
        uint8 delta = toHoundred > 95 ? 1 : toHoundred > 80 ? 2 : 3;
        uint8 len = uint8(sourceArray.length);
        uint8 x = len / 3;
        uint8 min = len - (delta * x);
        uint8 max = (len - 1) - ((delta - 1) * x);
        uint8 rand2 = uint8((random(string(abi.encodePacked(msg.sender, toHoundred, keyPrefix ))) % (max - min + 1)) + min);
        return sourceArray[rand2];
    }