LoRUtility

--

```
-- Inherit from https://github.com/SwitchbladeBot/runeterra

local lib               = require('Module:Feature')
local MAX_KNOWN_VERSION = 2
local p                 = {}
local VarInt            = {}
local BitOperator       = {}
local Utility           = {}

local REGIONS = {
    [1]  = {code = "DE", name = "Demacia"},
    [2]  = {code = "FR", name = "Freljord"},
    [3]  = {code = "IO", name = "Ionia"},
    [4]  = {code = "NX", name = "Noxus"},
    [5]  = {code = "PZ", name = "Piltover and Zaun"},
    [6]  = {code = "SI", name = "Shadow Isles"},
    [7]  = {code = "BW", name = "Bilgewater"},
    [10] = {code = "MT", name = "Targon"}
}

function base32_decode(str)
    local base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"

    local binary =
        str:gsub(
        ".",
        function(char)
            if char == "=" then
                return ""
            end
            local pos = string.find(base32Alphabet, char)
            pos = pos - 1
            return string.format("%05u", Utility.dec2bin(pos))
        end
    )

    local bytes = Utility.str_split(binary, 8)

    local result = {}
    for _, byte in pairs(bytes) do
        table.insert(result, tonumber(byte, 2))
    end

    return result
end

function p.deckDataFromCode(frame)
    local args; if frame.args == nil then args = lib.arguments(frame) else args = lib.arguments(frame.args) end

    local deckCode  = args['code'] or nil
    local getDetail = args['detail'] or nil

    local lorData = mw.loadData("Module:LoRData/data")
    local bytes = base32_decode(deckCode)
    local firstByte = bytes[1]
    local deckList = {}

    table.remove(bytes, 1)
    local version = BitOperator.AND(tonumber(firstByte), 0xF)

    if (version > MAX_KNOWN_VERSION) then
        return "The provided code requires a higher version of this library; please update."
    end

    local totalShards = 0
    local regions = {}
    local followerCount = 0
    local spellCount = 0
    local rarities = {}
    local champions = {}
    local shardValues = {
        ["None"] = 0,
        ["Common"] = 100,
        ["Rare"] = 300,
        ["Epic"] = 1200,
        ["Champion"] = 3000,
    }
    local topChampion = nil
    local topFollower = nil

    function handleCardData(cardData, count, cardCode, regionName)
        local cardType = cardData.type
        if cardType == "Unit" then
            if cardData.supertype ~= nil and cardData.supertype == "Champion" then
                cardType = "Champion"
            else
                cardType = "Follower"
            end
        end

        if (getDetail) then
            totalShards = totalShards + shardValues[cardData.rarity] * count
            if cardData.rarity ~= 'None' then
                if cardData.rarity == 'Common' then
                    rarities[1] = (rarities[1] or 0) + count
                elseif cardData.rarity == 'Rare' then
                    rarities[2] = (rarities[2] or 0) + count
                elseif cardData.rarity == 'Epic' then
                    rarities[3] = (rarities[3] or 0) + count
                else
                    rarities[4] = (rarities[4] or 0) + count
                end
            end

            if cardType == "Champion" or cardType =='Follower' then
                if cardType == "Champion" then
                    table.insert(champions, {
                        code = cardCode,
                        count = count
                    })
                    if topChampion == nil or cardData.cost > topChampion.cost then
                        topChampion = {
                            cost = cardData.cost,
                            code = cardCode
                        }
                    end
                else
                    if topFollower == nil or cardData.cost > topFollower.cost then
                        topFollower = {
                            cost = cardData.cost,
                            code = cardCode
                        }
                    end
                    followerCount = followerCount + count
                end
            else
                spellCount = spellCount + count
            end

            regions[regionName] = (regions[regionName] or 0) + count
        else
            table.insert(
                deckList,
                {
                    code = cardCode,
                    name = cardData.name,
                    count = count,
                    region = regionName,
                    type = cardType
                }
            )
        end
    end

    for i = 3, 0, -1 do
        local numGroupOfs = tonumber(VarInt.Pop(bytes)) or 0
        for j = 1, numGroupOfs, 1 do
            local numOfsInThisGroup = tonumber(VarInt.Pop(bytes)) or 0
            local set = VarInt.Pop(bytes)
            local region = VarInt.Pop(bytes)

            for k = 1, numOfsInThisGroup, 1 do
                local card = VarInt.Pop(bytes)
                if card ~= nil then
                    local setString = Utility.padLeft(tostring(set), 2, "0")
                    local regionData = REGIONS[region + 1]
                    local cardString = Utility.padLeft(tostring(card), 3, "0")

                    local cardCode = setString .. regionData.code .. cardString

                    if (not lorData[cardCode]) then
                        return 'Card Code "' .. cardCode .. '" is not exist'
                    end
                    handleCardData(lorData[cardCode], i, cardCode, regionData.name)
                end
            end
        end
    end

    while #bytes > 0 do
        local fourPlusCount = VarInt.Pop(bytes)
        local fourPlusSet = VarInt.Pop(bytes)
        local fourPlusFaction = VarInt.Pop(bytes)
        local fourPlusNumber = VarInt.Pop(bytes)

        if fourPlusFaction == nil then
            return 'Invalid Deck Code'
        end

        local fourPlusSetString = Utility.padLeft(tostring(fourPlusSet), 2, "0")
        local fourPlusregionData = REGIONS[fourPlusFaction + 1]
        local fourPlusNumberString = Utility.padLeft(tostring(fourPlusNumber), 3, "0")

        local cardCode = fourPlusSetString .. fourPlusregionData.code .. fourPlusNumberString

        if (not lorData[cardCode]) then
            return 'Card Code "' .. cardCode .. '" is not exist'
        end

        handleCardData(lorData[cardCode], fourPlusNumber, cardCode, regionData.name)
    end

    if (getDetail) then
        return {
            shards = totalShards,
            topUnitCode = (topChampion or topFollower).code,
            regions = regions,
            champions = champions,
            followerCount = followerCount,
            spellCount = spellCount,
            rarities = rarities
        }
    else
        return deckList
    end
end

function p.headerDeck(frame)
    local args; if frame.args == nil then args = lib.arguments(frame) else args = lib.arguments(frame.args) end

    local deckCode        = args['code'] or nil
    local deckName        = args['name'] or 'Untitled Deck'
    local deckDescription = args['description'] or nil

    if deckCode == nil then
        return 'Empty Code'
    end

    local result = {}

    local deckDetail = p.deckDataFromCode{code=deckCode,detail=true}
    if type(deckDetail) ~= 'table' then
        return deckDetail
    end

    local totalShards   = deckDetail['shards'] or nil
    local topUnitCode   = deckDetail['topUnitCode'] or nil
    local regions       = deckDetail['regions'] or nil
    local champions     = deckDetail['champions'] or nil
    local followerCount = deckDetail['followerCount'] or nil
    local spellCount    = deckDetail['spellCount'] or nil
    local rarities      = deckDetail['rarities'] or nil

    local template = mw.html.create('div')
        :css('position', 'relative')
        :css('font-family', 'BeaufortLoL')
        :tag('div')
            :css('opacity', '0.5')
            :wikitext('[[File:' .. topUnitCode .. '-full.png|700px]]')
            :done()

    local deckRegions = mw.html.create('div')
            :addClass('deck-regions')
            :css('position', 'absolute')
            :css('top', '10px')
            :css('left', '10px')
    for regionName, regionCount in pairs(regions) do
        deckRegions
            :tag('span')
                :addClass('inline-image')
                :wikitext('[[File:' .. regionName .. ' LoR Region.png|60px]]')
                :done()
    end
    deckRegions
            :tag('span')
                :css('display', 'inline-block')
                :css('position', 'relative')
                :css('color', '#FFE3B0')
                :css('font-size', '16px')
                :wikitext('[[File:Shard icon.png|40px|link=Shard (Legends of Runeterra)]]' .. Utility.comma_value(totalShards))
            :done()
    deckRegions:done()

    local deckName = mw.html.create('div')
        :addClass('deck-name')
        :css('position', 'absolute')
        :css('top', '80px')
        :css('left', '20px')
        :css('font-weight', 'bold')
        :css('color', '#FFE3B0')
            :tag('span')
                :css('font-size', '30px')
                :wikitext(deckName)
            :done()
            :tag('br')
            :done()
    if deckDescription then
        deckName
            :tag('span')
                :css('display', 'inline-block')
                :css('max-width', '660px')
                :css('font-size', '16px')
                :wikitext(deckDescription)
            :done()
    end
    deckName:done()

    template
        :node(deckRegions)
        :node(deckName)
        :tag('div')
            :addClass('deck-info')
            :css('position', 'absolute')
            :css('top', '170px')
            :css('left', '20px')
            :css('font-size', '20px')
            :css('color', '#FFE3B0')
            :tag('span')
                :addClass('inline-image')
                :wikitext('[[File:Follower card.svg|25px]]' .. followerCount .. ' ')
            :done()
            :tag('span')
                :addClass('inline-image')
                :wikitext('[[File:Spell card.svg|25px]]' .. spellCount .. ' ')
            :done()
            :tag('br')
            :done()
            :tag('span')
                :addClass('inline-image')
                :wikitext('[[File:Common rarity.svg|25px]]' .. (rarities[1] or 0) .. ' ')
            :done()
            :tag('span')
                :addClass('inline-image')
                :wikitext('[[File:Rare rarity.svg|25px]]' .. (rarities[2] or 0) .. ' ')
            :done()
            :tag('span')
                :addClass('inline-image')
                :wikitext('[[File:Epic rarity.svg|25px]]' .. (rarities[3] or 0) .. ' ')
            :done()
            :tag('span')
                :addClass('inline-image')
                :wikitext('[[File:Champion rarity.svg|25px]]' .. (rarities[4] or 0) .. ' ')
            :done()
        :done()

    if #champions then
        local deckChampions = mw.html.create('div')
            :addClass('deck-champions')
            :css('position', 'absolute')
            :css('bottom', '20px')
            :css('left', '20px')
            :css('font-size', '20px')
            :css('color', '#FFE3B0')
                :tag('span')
                    :addClass('inline-image')
                    :wikitext('[[File:Champion card.svg|25px]]')
                :done()
        for _, deckData in pairs(champions) do
            deckChampions
                :tag('span')
                    :addClass('inline-image')
                    :wikitext('[[File:' .. deckData.code .. '.png|60px]]')
                :done()
                :tag('span')
                    :addClass('inline-image')
                    :wikitext('x' .. deckData.count .. ' ')
                :done()
        end
        deckChampions:
            done()
        template
            :node(deckChampions)
    end

    template:allDone()

    return tostring(template)
end

function p.deckDecoder(frame)
    local args; if frame.args == nil then args = lib.arguments(frame) else args = lib.arguments(frame.args) end

    local deckCode        = args['code'] or nil
    local deckName        = args['name'] or nil
    local deckDescription = args['description'] or nil

    if deckCode == nil then
        return 'Empty Code'
    end

    local groupBy = args['groupBy'] or 'none'
    local groupByValues = {'region', 'type', 'none'}
    if Utility.table_contains(groupByValues, groupBy) == false then
        return 'Only support Group By "region", "type" or none'
    end

    local result = {}

    local deckList = p.deckDataFromCode{code=deckCode}
    if type(deckList) ~= 'table' then
        return deckList
    end

    for _, deckData in pairs(deckList) do
        local groupByKey = ''
        if groupBy == 'region' then
            groupByKey = deckData.region
        elseif groupBy == 'type' then
            groupByKey = cardType .. 's'
        else
            groupByKey = 'List'
        end

        if result[groupByKey] == nil then
            result[groupByKey] = {}
        end

        table.insert(
            result[groupByKey],
            {
                code = deckData.code,
                count = deckData.count,
                name = deckData.name,
                type = deckData.type
            }
        )
    end

    local sortedKeys = {}
    for k in pairs(result) do
        table.insert(sortedKeys, k)
    end
    table.sort(sortedKeys)

    local template = ""
    template = template .. p.headerDeck{code=deckCode,name=deckName,description=deckDescription}

    local newline = "\n"
    template = template .. "<tabber>" .. newline
    template = template .. "Hide=" .. newline .. '|-|' .. newline
    for i, groupByKey in ipairs(sortedKeys) do
        if i ~= 1 then
            template = template .. "|-|" .. newline
        end
        template = template .. groupByKey .. "=" .. newline

        local regionData = result[groupByKey]
        table.sort(
            regionData,
            function(a, b)
                if (a.type == b.type) then
                    return a.name < b.name
                end
                return a.type < b.type
            end
        )

        local type = ""
        for _, cardData in ipairs(regionData) do
            if cardData.type ~= type then
                if (type ~= "") then
                    template = template .. "}}" .. newline
                end
                if groupBy == 'region' or groupBy == 'none' then
                    template = template .. ";" .. cardData.type .. "s" .. newline
                end
                template = template .. "{{column|3|" .. newline

                type = cardData.type
            end

            local cardCodeTemplate = "* {{LoR|" .. cardData.name
            if cardData.type ~= "Champion" then
                cardCodeTemplate = cardCodeTemplate .. "|code=" .. cardData.code
            end
            cardCodeTemplate = cardCodeTemplate .. "}}"
            if cardData.count > 1 then
                cardCodeTemplate = cardCodeTemplate .. " (x" .. cardData.count .. ")"
            end

            template = template .. cardCodeTemplate .. newline
        end

        template = template .. "}}" .. newline
    end
    template = template .. "</tabber>"

    return frame:preprocess(template)
end

-- VarInit
VarInt.AllButMSB = 0x7f
VarInt.JustMSB = 0x80

function VarInt.Pop(bytes)
    local result = 0
    local currentShift = 0
    local bytesPopped = 0

    for i = 1, #bytes do
        bytesPopped = bytesPopped + 1
        local current = BitOperator.AND(bytes[i], VarInt.AllButMSB)
        result = BitOperator.OR(result, BitOperator.RIGHT_SHIFT(current, currentShift))

        if BitOperator.AND(bytes[i], VarInt.JustMSB) ~= VarInt.JustMSB then
            Utility.table_splice(bytes, 1, bytesPopped)
            return result
        end

        currentShift = currentShift + 7
    end

    -- error('Invalid Deck Code: Byte array did not contain valid varints.')
    return nil
end

-- BitOperator
function BitOperator.OR(m, n)
    local tbl_m = Utility.to_bits(m)
    local tbl_n = Utility.to_bits(n)
    Utility.expand(tbl_m, tbl_n)

    local tbl = {}
    local rslt = math.max(table.getn(tbl_m), table.getn(tbl_n))
    for i = 1, rslt do
        if (tbl_m[i] == 0 and tbl_n[i] == 0) then
            tbl[i] = 0
        else
            tbl[i] = 1
        end
    end

    return Utility.table2number(tbl)
end

function BitOperator.AND(m, n)
    local tbl_m = Utility.to_bits(m)
    local tbl_n = Utility.to_bits(n)
    Utility.expand(tbl_m, tbl_n)

    local tbl = {}
    local rslt = math.max(table.getn(tbl_m), table.getn(tbl_n))
    for i = 1, rslt do
        if (tbl_m[i] == 0 or tbl_n[i] == 0) then
            tbl[i] = 0
        else
            tbl[i] = 1
        end
    end

    return Utility.table2number(tbl)
end

function BitOperator.RIGHT_SHIFT(n, bits)
    Utility.check_int(n)

    local high_bit = 0
    if (n < 0) then
        -- negative
        n = BitOperator.NOT(math.abs(n)) + 1
        high_bit = 2147483648 -- 0x80000000
    end

    for i = 1, bits do
        n = n / 2
        n = BitOperator.OR(math.floor(n), high_bit)
    end
    return math.floor(n)
end

function BitOperator.NOT(n)
    local tbl = Utility.to_bits(n)
    local size = math.max(table.getn(tbl), 32)
    for i = 1, size do
        if (tbl[i] == 1) then
            tbl[i] = 0
        else
            tbl[i] = 1
        end
    end
    return Utility.table2number(tbl)
end

function BitOperator.LEFT_SHIFT(n, bits)
    Utility.check_int(n)

    if (n < 0) then
        -- negative
        n = BitOperator.NOT(math.abs(n)) + 1
    end

    for i = 1, bits do
        n = n * 2
    end
    return BitOperator.AND(n, 4294967295) -- 0xFFFFFFFF
end

-- Utility
function Utility.str_split(str, size)
    local result = {}
    for i = 1, #str, size do
        table.insert(result, str:sub(i, i + size - 1))
    end
    return result
end

function Utility.dec2bin(num)
    local result = ""
    repeat
        local halved = num / 2
        local int, frac = math.modf(halved)
        num = int
        result = math.ceil(frac) .. result
    until num == 0
    return result
end

function Utility.padRight(str, length, char)
    while #str % length ~= 0 do
        str = str .. char
    end
    return str
end

function Utility.padLeft(str, length, char)
    while #str % length ~= 0 do
        str = char .. str
    end
    return str
end

function Utility.table2number(tbl)
    local n = table.getn(tbl)

    local rslt = 0
    local power = 1
    for i = 1, n do
        rslt = rslt + tbl[i] * power
        power = power * 2
    end

    return rslt
end

function Utility.to_bits(n)
    Utility.check_int(n)
    if (n < 0) then
        -- negative
        return Utility.to_bits(bit.bnot(math.abs(n)) + 1)
    end
    -- to bits table
    local tbl = {}
    local cnt = 1
    while (n > 0) do
        local last = math.mod(n, 2)
        if (last == 1) then
            tbl[cnt] = 1
        else
            tbl[cnt] = 0
        end
        n = (n - last) / 2
        cnt = cnt + 1
    end

    return tbl
end

function Utility.check_int(n)
    -- checking not float
    if (n - math.floor(n) > 0) then
        error("trying to use bitwise operation on non-integer!")
    end
end

function Utility.expand(tbl_m, tbl_n)
    local big = {}
    local small = {}
    if (table.getn(tbl_m) > table.getn(tbl_n)) then
        big = tbl_m
        small = tbl_n
    else
        big = tbl_n
        small = tbl_m
    end
    -- expand small
    for i = table.getn(small) + 1, table.getn(big) do
        small[i] = 0
    end
end

function Utility.table_splice(tab, idx, n, ...) --> removed
    local values = {...}
    local init_tab_size = #tab

    local removed = {}
    if n > 0 then
        for i = idx, (idx + n - 1) do
            table.insert(removed, tab[i])
            tab[i] = nil
        end
    end

    local tail = {}
    for i = (idx + n), init_tab_size do
        table.insert(tail, tab[i])
        tab[i] = nil
    end

    local i = idx
    for _, v in ipairs(values) do
        tab[i] = v
        i = i + 1
    end

    i = idx + #values
    for _, v in ipairs(tail) do
        tab[i] = v
        i = i + 1
    end

    return removed
end

function Utility.table_contains(table, val)
   for i=1,#table do
      if table[i] == val then
         return true
      end
   end
   return false
end

function Utility.comma_value(n) -- credit http://richard.warburton.it
	local left,num,right = string.match(n,'^([^%d]*%d)(%d*)(.-)$')
	return left..(num:reverse():gsub('(%d%d%d)','%1,'):reverse())..right
end

return p
--
```

--