#!/usr/bin/env lua5.1

--[[
Copyright © 2023  Nils Dagsson Moskopp (erle)

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

Dieses Programm hat das Ziel, die Medienkompetenz der Leser zu
steigern. Gelegentlich packe ich sogar einen handfesten Buffer
Overflow oder eine Format String Vulnerability zwischen die anderen
Codezeilen und schreibe das auch nicht dran.
]]--

local utf8 = {}

-- convert a table with codepoints into an UTF-8 string
-- inspired by <http://news.dieweltistgarnichtso.net/bin/unicode>
utf8.codepoints_to_text = function(codepoints)
   assert(
      "table" == type(codepoints)
   )
   for _, codepoint in ipairs(codepoints) do
      if (
         0 >= codepoint or
         1114111 < codepoint or
         math.floor(codepoint) ~= codepoint
      ) then
         error(
            string.format(
               "invalid codepoint: %s",
               codepoint
            )
         )
      end
   end
   local codepoints_encoded = {}
   codepoints_encoded.append = function(...)
      codepoints_encoded[#codepoints_encoded+1] = string.char(...)
   end
   for _, codepoint in ipairs(codepoints) do
      if codepoint <= 127 then
         -- one byte encoding
         codepoints_encoded.append(codepoint)
      elseif codepoint <= 2048 then
         -- two bytes encoding
         codepoints_encoded.append(
            math.floor(codepoint / 64) + 192,
            codepoint % 64 + 128
         )
      elseif codepoint <= 65535 then
         -- three bytes encoding
         codepoints_encoded.append(
            math.floor(codepoint / 4096) + 224,
            math.floor(codepoint / 64) % 64 + 128,
            codepoint % 64 + 128
         )
      elseif codepoint <= 1114111 then
         -- four bytes encoding
         codepoints_encoded.append(
            math.floor(codepoint / 262144) + 240,
            math.floor(codepoint / 4096) % 64 + 128,
            math.floor(codepoint / 64) % 64 + 128,
            codepoint % 64 + 128
         )
      end
   end
   return table.concat(codepoints_encoded)
end

-- Test one codepoint for each byte length:
local text = utf8.codepoints_to_text(
   {119, 240, 9829, 66376}  -- U+0077 U+00F0 U+2665 U+10348
)
assert(
   "wð♥𐍈" == text
)

-- convert an UTF-8 string into a table with codepoints
-- inspired by <https://lua-users.org/wiki/LuaUnicode>
utf8.text_to_codepoints = function(text)
   assert(
      "string" == type(text)
   )
   local result = {}
   local sequence_length = 0
   local i = 1
   while i <= #text do
      local value = nil
      local byte_1, byte_2, byte_3, byte_4
      byte_1 = string.byte(text, i)
      local sequence_length =
         byte_1 <= 127 and 1 or  -- 0xxxxxxx
         byte_1 <= 223 and 2 or  -- 110xxxxx 10xxxxxx
         byte_1 <= 239 and 3 or  -- 1110xxxx 10xxxxxx 10xxxxxx
         byte_1 <= 247 and 4 or  -- 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
         error("invalid UTF-8 sequence")
      if sequence_length > 1 then
         byte_2 = string.byte(text, i+1)
      end
      if sequence_length > 2 then
         byte_3 = string.byte(text, i+2)
      end
      if sequence_length > 3 then
         byte_4 = string.byte(text, i+3)
      end
      if 1 == sequence_length then
         -- 0xxxxxxx
         value = byte_1
      elseif 2 == sequence_length then
         -- 110xxxxx 10xxxxxx
         value =
            (byte_1 % 64) * 64 +
            (byte_2 % 64)
      elseif 3 == sequence_length then
         -- 1110xxxx 10xxxxxx 10xxxxxx
         value =
            (byte_1 % 32) * 4096 +
            (byte_2 % 64) * 64 +
            (byte_3 % 64)
      elseif 4 == sequence_length then
         -- 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
         value =
            (byte_1 % 16) * 262144 +
            (byte_2 % 64) * 4096 +
            (byte_3 % 64) * 64 +
            (byte_4 % 64)
      end

      table.insert(result, value)
      i = i + sequence_length
   end
   return result
end

-- From [ucsigns] mod <https://codeberg.org/cora/uc_signs.git>
-- Crops a string `txt` to the given length.
-- Returns the result string.
-- This function makes sure the resulting string is still valid UTF-8,
-- so multi-byte codepoints are not chopped in half.
function utf8.crop_text(txt, length)
        local bytes = 1
        local str = ""
        local chars = 0
        local final_string = ""
        for i = 1, txt:len() do
                local byte = txt:sub(i, i)
                if bytes ~= 1 then
                        bytes = bytes - 1
                        str = str .. byte
                        if bytes == 1 then
                                if chars < length then
                                        final_string = final_string .. str
                                        chars = chars + 1
                                else
                                        break
                                end
                                str = ""
                        end
                else
                        local octal = string.format('%o', string.byte(byte))
                        -- Four bytes
                        if octal:find("^36") ~= nil or octal:find("^37") ~= nil then
                                bytes = 4
                                str = str .. byte
                        -- Three bytes
                        elseif octal:find("^34") ~= nil or octal:find("^35") ~= nil then
                                bytes = 3
                                str = str .. byte
                        -- Two bytes
                        elseif octal:find("^33") ~= nil or octal:find("^32") ~= nil or octal:find("^31") ~= nil or octal:find("^30") ~= nil then
                                bytes = 2
                                str = str .. byte
                        -- Assume everything else is one byte
                        else
                                bytes = 1
                                str = ""
                                if chars < length then
                                        final_string = final_string .. byte
                                        chars = chars + 1
                                else
                                        break
                                end
                        end
                end
        end
        return final_string
end


-- Test one codepoint for each byte length:
local codepoints = utf8.text_to_codepoints(
   "wð♥𐍈"  -- U+0077 U+00F0 U+2665 U+10348
)
assert(
   table.concat(codepoints, " ") == "119 240 9829 66376"
)

unicode_text.utf8 = utf8
