-- Code128 barcode generator module
-- Copyright (C) 2019-2022 Roberto Giacomelli
--
-- All dimension must be in scaled point (sp)
-- every fields that starts with an undercore sign are intended as private

local Code128 = {
    _VERSION     = "code128 v0.0.6",
    _NAME        = "Code128",
    _DESCRIPTION = "Code128 barcode encoder",
}

Code128._int_def_bar = {-- code bar definitions
    [0] = 212222,   222122, 222221, 121223, 121322, 131222, 122213, 122312,
    132212, 221213, 221312, 231212, 112232, 122132, 122231, 113222, 123122,
    123221, 223211, 221132, 221231, 213212, 223112, 312131, 311222, 321122,
    321221, 312212, 322112, 322211, 212123, 212321, 232121, 111323, 131123,
    131321, 112313, 132113, 132311, 211313, 231113, 231311, 112133, 112331,
    132131, 113123, 113321, 133121, 313121, 211331, 231131, 213113, 213311,
    213131, 311123, 311321, 331121, 312113, 312311, 332111, 314111, 221411,
    431111, 111224, 111422, 121124, 121421, 141122, 141221, 112214, 112412,
    122114, 122411, 142112, 142211, 241211, 221114, 413111, 241112, 134111,
    111242, 121142, 121241, 114212, 124112, 124211, 411212, 421112, 421211,
    212141, 214121, 412121, 111143, 111341, 131141, 114113, 114311, 411113,
    411311, 113141, 114131, 311141, 411131, 211412, 211214, 211232,
    2331112, -- the last number is the stop char at index 106
}

Code128._codeset = {
    A        = 103, -- Start char for Codeset A
    B        = 104, -- Start char for Codeset B
    C        = 105, -- Start char for Codeset C
    stopChar = 106, -- Stop char
    shift    =  98, -- A to B or B to A
}

Code128._switch = { -- codes for switching from a codeset to another one
    [103] = {[104] = 100, [105] =  99}, -- from A to B or C
    [104] = {[103] = 101, [105] =  99}, -- from B to A or C
    [105] = {[103] = 101, [104] = 100}, -- from C to A or B
}

-- parameters definition
Code128._par_order = {
    "xdim",
    "ydim",
    "quietzone_factor",
}
Code128._par_def = {}
local pardef = Code128._par_def

-- module main parameter
pardef.xdim = {
    default    = 0.21 * 186467, -- X dimension
    unit       = "sp", -- scaled point
    isReserved = true,
    fncheck    = function (self, x, _) --> boolean, err
        if x >= self.default then
            return true, nil
        else
            return false, "[OutOfRange] too small value for xdim"
        end
    end,
}

pardef.ydim = {
    default    = 10 * 186467, -- Y dimension
    unit       = "sp",
    isReserved = false,
    fncheck    = function (self, y, tpar) --> boolean, err
        local xdim = tpar.xdim
        if y >= 10*xdim then
            return true, nil
        else
            return false, "[OutOfRange] too small value for ydim"
        end
    end,
}

pardef.quietzone_factor = {
    default    = 10,
    unit       = "absolute-number",
    isReserved = false,
    fncheck    = function (self, z, _) --> boolean, err
        if z >= 10 then
            return true, nil
        else
            return false, "[OutOfRange] too small value for quietzone_factor"
        end
    end,
}

-- create vbar objects
function Code128:_config() --> ok, err
    -- build Vbar object for the start/stop symbol
    local mod = self.xdim
    local sc = self._codeset.stopChar -- build the stop char
    local n = self._int_def_bar[sc]
    local repo = self._libgeo.Archive:new()
    self._vbar_archive = repo
    local Vbar = self._libgeo.Vbar
    repo:insert(Vbar:from_int(n, mod, true), 106)
    return true, nil
end

-- utility functions

-- evaluate the check digit of encoded data
local function check_digit(code)
    local sum = code[1] -- this is the start character
    for i = 2, #code do
        sum = sum + code[i]*(i-1)
    end
    code[#code + 1] = sum % 103
end

local function isdigit(char) --> true/false
    assert(char)
    return (char > 47) and (char < 58)
end

local function iscontrol(char) --> true/false
    assert(char)
    -- [0,31] control chars interval
    return char < 32
end

local function islower(char)
    assert(char)
    -- [96, 127] lower case chars
    return (char > 95) and (char < 128)
end

-- count digits
local function digits_group(data, len) --> counting digits
    local res = {}
    local last = false
    for i = len, 1, -1 do
        local digit = isdigit(data[i])
        if last then
            if digit then
                res[i] = res[i+1] + 1
            else
                res[i] = 0
                last = false
            end
        else
            if digit then
                res[i] = 1
                last = true
            else
                res[i] = 0
            end
        end
    end
    return res
end

-- find the first char in the codeset that adhere
-- with the function argument 'filter'
local function indexof_char_by(filter, arr, counter) --> index or nil
    counter = counter or 1
    local char = arr[counter]
    while char do
        if filter(char) then
            return counter
        end
        counter = counter + 1
        char = arr[counter]
    end
end

-- determine the Start character
local function start_codeset_char(codeset, arr, len)
    assert(len>0)
    local ctrl = indexof_char_by(iscontrol, arr)
    local lowc = indexof_char_by(islower, arr)
    local t_digits = digits_group(arr, len)
    local first_digits = t_digits[1]
    -- case 1
    if (len == 2) and (first_digits == 2) then
        return lowc, ctrl, codeset.C, t_digits
    end
    -- case 2
    if first_digits >= 4 then
        return lowc, ctrl, codeset.C, t_digits
    end
    -- case 3
    local cs = codeset.B
    if (ctrl and lowc) and (ctrl < lowc) then
        cs = codeset.A
    end
    -- case 4
    return lowc, ctrl, cs, t_digits
end

-- codeset A char
local function encode_char_A(res, char)
    local code
    if char < 32 then
        code = char + 64
    elseif char > 31 and char < 96 then
        code = char - 32
    else
        error("[InternalErr] Not implemented or wrong code" )
    end
    res[#res + 1] = code
end
-- codeset B char
local function encode_char_B(res, char)
    local code
    if char > 31 and char < 128 then
        code = char - 32
    else
        error("[InternalErr] Not implemented or wrong code")
    end
    res[#res + 1] = code
end

-- every function encodes a group of chars
-- A = 103, -- Start char for Codeset A
-- B = 104, -- Start char for Codeset B
-- C = 105, -- Start char for Codeset C
local encode_codeset = {
    -- A
    [103] = function (codeset, res, data, index, t_digits, i_low, _ctrl)
        assert(t_digits[index] < 4, "[InternalErr] in codeset A digits must be less than 4")
        while data[index] do
            local char = data[index]
            if i_low and islower(char) then -- ops a lower case char
                local next = data[index + 1]
                local next_next = data[index + 2]
                if next and next_next then
                    -- case 5a
                    if iscontrol(char) and islower(next_next) then
                        res[#res+1] = codeset.shift
                        encode_char_B(res, char)
                        index = index + 1
                    end
                else
                    -- case 5b
                    return codeset.B, index
                end
            else
                local digits = t_digits[index]
                if digits > 3 then -- go to codeset C
                    if (digits % 2) == 1 then -- odd number of a group of digits
                        encode_char_A(res, char)
                        digits = digits - 1
                        index = index + 1
                    end
                    return codeset.C, index
                end
                encode_char_A(res, char)
                index = index + 1
            end
        end
        return nil, index
    end,
    -- B
    [104] = function (codeset, res, data, index, t_digits, _low, i_ctrl)
        assert(t_digits[index] < 4, "[InternalErr] in codeset B digits must be less than 4")
        while data[index] do
            local char = data[index]
            if i_ctrl and iscontrol(char) then -- ops a control char
                local next = data[index + 1]
                local next_next = data[index + 2]
                if next and next_next then
                    -- case 4a
                    if islower(next) and iscontrol(next_next) then
                        res[#res+1] = codeset.shift
                        encode_char_A(res, char)
                        index = index + 1
                    end
                else
                    -- case 4b
                    return codeset.A, index
                end
            else
                local digits = t_digits[index]
                if digits > 3 then -- go to codeset C
                    if (digits % 2) == 1 then -- odd number of a group of digits
                        encode_char_B(res, char)
                        digits = digits - 1
                        index = index + 1
                    end
                    return codeset.C, index
                end
                encode_char_B(res, char)
                index = index + 1
            end
        end
        return nil, index
    end,
    -- C
    [105] = function (codeset, res, data, index, t_digits, i_low, i_ctrl)
        local digits = t_digits[index]
        assert(digits > 1, "[InternalErr] at least a pair of digit is required")
        while digits > 1 do
            local d1, d2 = data[index], data[index + 1]
            res[#res + 1] = (d1 - 48)*10 + d2 - 48
            digits = digits - 2
            index = index + 2
        end
        local res_codeset
        if i_ctrl and i_low then
            local ctrl = indexof_char_by(iscontrol, data, index)
            local low = indexof_char_by(islower, data, index)
            if low and (ctrl < low) then
                res_codeset = codeset.A
            end
        else
            res_codeset = codeset.B
        end
        return res_codeset, index
    end,
}

-- encode the message in a sequence of Code128 symbol minimizing the symbol width
local function encode128(arr, codeset, switch) --> data, err
    local len = #arr
    local i_low, i_ctrl, cur_codeset, t_digits = start_codeset_char(codeset, arr, len)
    local res = {cur_codeset} -- the result array (the check character will be appended later)
    local switch_codeset
    local cur_index = 1
    while cur_index <= len do
        if switch_codeset then
            res[#res+1] = switch[cur_codeset][switch_codeset]
            cur_codeset = switch_codeset
        end
        local fn = assert(encode_codeset[cur_codeset], "[InternalErr] cur_codeset is "..(cur_codeset or "nil"))
        switch_codeset, cur_index = fn(codeset, res, arr, cur_index, t_digits, i_low, i_ctrl)
    end
    check_digit(res)
    return res, nil
end

-- Code 128 internal functions used by Barcode costructors

function Code128:_process_char(c) --> char_code, char_text, err
    local b = string.byte(c)
    if b > 127 then
        local fmt = "[unimplemented] the '%d' is an ASCII extented char"
        return nil, string.format(fmt, c)
    end
    return b, c, nil
end

function Code128:_process_digit(n) --> digit_code, char_text, err
    local res = n + 48
    return res, string.char(res), nil
end

function Code128:_finalize() --> ok, err
    local chr = assert(self._code_data, "[InternalErr] '_code_data' field is nil")
    local data, err = encode128(chr, self._codeset, self._switch)
    if err then return false, err end
    self._enc_data = data
    -- dynamically load the required Vbar objects
    local Repo = self._vbar_archive
    local Vbar = self._libgeo.Vbar
    for _, c in ipairs(data) do
        if not Repo:contains_key(c) then
            local n = self._int_def_bar[c]
            local mod = self.xdim
            assert(Repo:insert(Vbar:from_int(n, mod, true), c))
        end
    end
    return true, nil
end

-- Drawing into the provided channel the geometrical barcode data
-- tx, ty is the optional translator vector
-- the function return the canvas reference to allow call chaining
function Code128:_append_ga(canvas, tx, ty) --> bbox
    local data = self._enc_data
    local Repo = self._vbar_archive
    local queue = self._libgeo.Vbar_queue:new()
    for _, c in ipairs(data) do
        queue = queue + Repo:get(c)
    end
    local stop = self._codeset.stopChar
    queue = queue + Repo:get(stop)
    local xdim, h = self.xdim, self.ydim
    local ns = #data + 1
    local w = (11*ns + 2) * xdim -- total symbol width
    local ax, ay = self.ax, self.ay
    local x0 = tx - ax * w
    local y0 = ty - ay * h
    local x1 = x0 + w
    local y1 = y0 + h
    -- drawing the symbol
    assert(canvas:encode_disable_bbox())
    assert(canvas:encode_vbar_queue(queue, x0, y0, y1))
    -- bounding box setting
    local qz = self.quietzone_factor * xdim
    -- { xmin, ymin, xmax, ymax }
    assert(canvas:encode_set_bbox(x0 - qz, y0, x1 + qz, y1))
    assert(canvas:encode_enable_bbox())
    return {x0, y0, x1, y1, qz, nil, qz, nil,}
end

return Code128