local n_bits = {0x1, 0x3, 0x7, 0xf, 0x1f, 0x3f, 0x7f, 0xff, 0x1ff, 0x3ff, 0x7ff, 0xfff, 0x1fff, 0x3fff, 0x7fff, 0xffff,
                0x1ffff, 0x3ffff, 0x7ffff, 0xfffff, 0x1fffff, 0x3fffff, 0x7fffff, 0xffffff, 0x1ffffff, 0x3ffffff,
                0x7ffffff, 0xfffffff, 0x1fffffff, 0x3fffffff, 0x7fffffff, 0xffffffff}

-- Custom decoders by Kotlin type
-- These are used automatically when a field has a matching kotlin_type
-- and no explicit custom_decode is defined
local type_decoders = {
    ["com.txse.avail.Price"] = function(buffer, pinfo, tree)
        local numeric = buffer(0, 8):le_uint64()
        tree:append_text(string.format(" $%0.4f", numeric:tonumber() / 100000000))
    end,
    ["com.txse.avail.TimeStamp"] = function(buffer, pinfo, tree)
        local nanos = buffer(0, 8):le_uint64():tonumber()
        local secs = math.floor(nanos / 1000000000)
        local nsec = nanos % 1000000000
        tree:append_text(string.format(" %s.%09d", os.date("!%H:%M:%S", secs), nsec))
    end,
    ["com.txse.avail.Session"] = function(buffer, pinfo, tree)
        local ts = buffer(0, 4)
        local env = buffer(4, 4)
        tree:add(env, string.format("environment: %s", string.reverse(env:string())))
        tree:add(ts, string.format("timestamp: %s", os.date("!%Y-%m-%d %H:%M:%S", ts:le_uint())))
    end,
    ["com.txse.exchange.MPID"] = function(buffer, pinfo, tree)
        tree:append_text(" '" .. buffer(0, 4):string() .. "'")
    end,
    ["com.txse.exchange.MemberGroup"] = function(buffer, pinfo, tree)
        tree:append_text(" '" .. buffer(0, 2):string() .. "'")
    end,
    ["com.txse.foundation.protocols.rake.morel.define.SenderComp"] = function(buffer, pinfo, tree)
        tree:append_text(" '" .. buffer(0, 8):string() .. "'")
    end,
    ["com.txse.foundation.protocols.rake.morel.define.RakeToken"] = function(buffer, pinfo, tree)
        tree:append_text(" '" .. buffer(0, 8):string() .. "'")
    end,
    ["com.txse.exchange.AppName"] = function(buffer, pinfo, tree)
        tree:append_text(" '" .. buffer(0, 8):string() .. "'")
    end,
    ["com.txse.exchange.SymbolRoot"] = function(buffer, pinfo, tree)
        tree:append_text(" '" .. buffer(0, 8):string() .. "'")
    end,
    ["com.txse.exchange.SymbolSuffix"] = function(buffer, pinfo, tree)
        tree:append_text(" '" .. buffer(0, 8):string() .. "'")
    end
}

local extract_bits = function(number, offset, length_arg)
    local length = length_arg or 1
    if offset < 0 then
        error("offset cannot be negative")
    end
    if length <= 0 then
        error("length cannot be negative or zero")
    end
    if offset >= 31 then
        error("offset cannot exceed 31")
    end
    if length > 32 then
        error("length cannot exceed 32")
    end
    if offset + length > 32 then
        error("32-bit limitation on offset + length")
    end
    return bit.band(bit.rshift(number, offset), n_bits[length])
end

local ProtoFieldCache = {
    new = function()
        return {
            entries = {},
            get = function(self, field_spec)
                local key = ""
                for k, v in pairs(field_spec) do
                    key = key .. "|" .. tostring(k) .. "@" .. tostring(v) .. "^"
                end
                local entry = self.entries[key]
                if not entry then
                    entry = ProtoField.new(field_spec.name, field_spec.abbr, field_spec.ftype, field_spec.value_map)
                    self.entries[key] = entry
                end
                return entry
            end
        }
    end
}

-- Built-in payload field definitions for trailing payloads
local builtin_fields = {
    binaryPayload = {
        abbr = "payload",
        name = "payload",
        ftype = ftypes.BYTES,
        encoding = ENC_NA
        -- custom_decode can be set by dissector to call nested protocol
    },
    asciiPayload = {
        abbr = "payload",
        name = "payload",
        ftype = ftypes.STRING,
        encoding = ENC_ASCII
    }
}

local Field = {
    new = function(spec, proto_field_cache, field_name, abbr_prefix)
        -- Check spec.fields first (allows override), then built-in fields
        local field_spec = (spec.fields and spec.fields[field_name]) or builtin_fields[field_name] or
                               error("Did not find field: " .. field_name)

        -- For built-in fields, prefix the abbreviation with the protocol name
        local actual_spec = field_spec
        if builtin_fields[field_name] and not (spec.fields and spec.fields[field_name]) and abbr_prefix then
            actual_spec = {}
            for k, v in pairs(field_spec) do
                actual_spec[k] = v
            end
            actual_spec.abbr = abbr_prefix .. "." .. field_spec.abbr
        end

        local proto_field = proto_field_cache:get(actual_spec)
        -- Use explicit custom_decode if present, otherwise look up by kotlin_type
        local decoder = actual_spec.custom_decode
        if not decoder and actual_spec.kotlin_type then
            decoder = type_decoders[actual_spec.kotlin_type]
        end
        local is_trailing_payload = (field_name == "binaryPayload" or field_name == "asciiPayload")
        return {
            name = field_name,
            is_presence_field = actual_spec.is_presence_field or false,
            is_trailing_payload = is_trailing_payload,
            parse = function(self, message_buffer, pinfo, subtree, offset, msg_context)
                -- For trailing payloads, use remaining buffer length
                local field_length = actual_spec.length
                if is_trailing_payload and not field_length then
                    field_length = message_buffer:len() - offset
                    if field_length < 0 then
                        field_length = 0
                    end
                end
                if not field_length or field_length <= 0 then
                    -- Skip fields with no length (shouldn't happen, but be defensive)
                    return
                end
                if offset >= message_buffer:len() then
                    -- Field entirely missing - just skip silently
                    return
                elseif (offset + field_length) > message_buffer:len() then
                    -- Partial field - show what we have
                    local remaining = message_buffer:len() - offset
                    if remaining > 0 then
                        pcall(function()
                            subtree:add(proto_field, message_buffer(offset, remaining)):append_text(string.format(
                                " (truncated: %d of %d bytes)", remaining, field_length))
                        end)
                    end
                else
                    local field_buffer = message_buffer(offset, field_length)
                    local tree_item = subtree:add_packet_field(proto_field, field_buffer, actual_spec.encoding)
                    if decoder then
                        decoder(field_buffer, pinfo, tree_item, msg_context)
                    end
                end
            end,
            length = actual_spec.length
        }
    end
}

local Message = {
    new = function(spec, proto_field_cache, msg_def)
        local fields = {}
        local presence_fields = {}
        local list_fields = {}
        local min_size = 0

        -- Build list of field names, adding payload field if trailing_payload is set
        local field_names = {}
        for _, field_name in ipairs(msg_def.fields) do
            table.insert(field_names, field_name)
        end
        local payload_field_name = nil
        if msg_def.trailing_payload == "binary" then
            payload_field_name = "binaryPayload"
            table.insert(field_names, payload_field_name)
        elseif msg_def.trailing_payload == "ascii" then
            payload_field_name = "asciiPayload"
            table.insert(field_names, payload_field_name)
        end

        for _, field_name in ipairs(field_names) do
            -- For binaryPayload, allow spec.payload_decoder to set custom_decode
            local field
            if field_name == "binaryPayload" and spec.payload_decoder then
                -- Create a modified field spec with the custom decoder
                local modified_spec = {
                    fields = {
                        binaryPayload = {
                            abbr = "payload",
                            name = "payload",
                            ftype = ftypes.BYTES,
                            encoding = ENC_NA,
                            custom_decode = spec.payload_decoder
                        }
                    },
                    name = spec.name
                }
                field = Field.new(modified_spec, proto_field_cache, field_name, spec.name)
            else
                field = Field.new(spec, proto_field_cache, field_name, spec.name)
            end
            table.insert(fields, field)
            min_size = min_size + (field.length or 0)
        end
        local bit = 0
        for _, field_name in ipairs(msg_def.presence_fields or {}) do
            local field = Field.new(spec, proto_field_cache, field_name, spec.name)
            table.insert(presence_fields, {
                field = field,
                bit = bit
            })
            bit = bit + 1
        end
        -- Build list field descriptors
        for _, list_def in ipairs(msg_def.list_fields or {}) do
            local item_fields = {}
            for _, item_field_name in ipairs(list_def.item_fields) do
                local item_field = Field.new(spec, proto_field_cache, item_field_name, spec.name)
                table.insert(item_fields, item_field)
            end
            table.insert(list_fields, {
                name = list_def.name,
                offset_field = list_def.offset_field,
                item_type = list_def.item_type,
                item_length = list_def.item_length,
                item_fields = item_fields
            })
        end
        local msg_context = {
            name = msg_def.name,
            type = msg_def.type,
        }
        return {
            name = msg_def.name,
            type = msg_def.type and string.byte(msg_def.type) or nil,
            parse = function(self, message_buffer, pinfo, subtree)
                local current_offset = 0
                local presence_bits = 0
                local list_offsets = {}
                for _, field in ipairs(fields) do
                    local length = field.length or 0
                    field:parse(message_buffer, pinfo, subtree, current_offset, msg_context)
                    if field.is_presence_field then
                        presence_bits = message_buffer(current_offset, length):le_uint()
                    end
                    -- Capture list offset values
                    for _, list_field in ipairs(list_fields) do
                        if field.name == list_field.offset_field then
                            list_offsets[list_field.name] = message_buffer(current_offset, length):le_uint()
                        end
                    end
                    current_offset = current_offset + length
                end
                for _, field_descriptor in ipairs(presence_fields) do
                    if extract_bits(presence_bits, field_descriptor.bit) == 1 then
                        local field = field_descriptor.field
                        local length = field.length or 0
                        field:parse(message_buffer, pinfo, subtree, current_offset, msg_context)
                        current_offset = current_offset + length
                    end
                end
                -- Parse list fields
                for _, list_field in ipairs(list_fields) do
                    local list_offset = list_offsets[list_field.name]
                    -- Check bounds: offset must be valid and have room for at least the 1-byte count
                    if list_offset and list_offset > 0 and (list_offset + 1) <= message_buffer:len() then
                        -- Read list length (1-byte count at the offset)
                        local list_count = message_buffer(list_offset, 1):uint()
                        local list_data_size = 1 + (list_count * list_field.item_length)
                        -- Check that the full list data fits in the buffer
                        if list_count > 0 and (list_offset + list_data_size) <= message_buffer:len() then
                            local list_tree = subtree:add(message_buffer(list_offset, list_data_size),
                                string.format("%s[%d]", list_field.name, list_count))
                            local item_offset = list_offset + 1  -- Skip the 1-byte count
                            for i = 0, list_count - 1 do
                                local item_buffer = message_buffer(item_offset, list_field.item_length)
                                local item_tree = list_tree:add(item_buffer, string.format("%s[%d]", list_field.item_type, i))
                                local field_offset = 0
                                for _, item_field in ipairs(list_field.item_fields) do
                                    local field_length = item_field.length or 0
                                    item_field:parse(item_buffer, pinfo, item_tree, field_offset, msg_context)
                                    field_offset = field_offset + field_length
                                end
                                item_offset = item_offset + list_field.item_length
                            end
                        end
                    end
                end
            end,
            min_size = min_size
        }
    end
}

local Messages = {
    new = function(spec)
        local proto_field_cache = ProtoFieldCache.new()
        local messages = {}
        local mapping = {}
        for _, msg_def in ipairs(spec.messages) do
            local message = Message.new(spec, proto_field_cache, msg_def)
            table.insert(messages, message)
            if message.type then
                mapping[message.type] = message
            end
        end
        local proto_fields = {}
        for _, field in pairs(proto_field_cache.entries) do
            table.insert(proto_fields, field)
        end
        return {
            messages = messages,
            mapping = mapping,
            proto_fields = proto_fields
        }
    end
}

local TXSE_lib = nil -- Forward declaration for createSpec

-- Create a spec from a unified protocol definition
-- The protocol_def has: name, description, enums, createFields, messages
-- Options can include: payload_decoder
local function createSpec(protocol_def, options)
    options = options or {}
    local fields = protocol_def.createFields(TXSE_lib)
    return {
        name = protocol_def.name,
        description = protocol_def.description,
        fields = fields,
        messages = protocol_def.messages,
        payload_decoder = options.payload_decoder
    }
end

local TxseProto = {
    new = function(spec)
        local proto = Proto.new(spec.name, spec.description)
        local messages = Messages.new(spec)
        proto.fields = messages.proto_fields
        return proto, messages
    end
}

-- Cached dissector lookup
local cached_dissectors = {}
local function dissector(name)
    if cached_dissectors[name] == nil then
        cached_dissectors[name] = Dissector.get(name) or error("Unable to load " .. name .. " dissector")
    end
    return cached_dissectors[name]
end

-- ASCII character range constants
local ASCII_PRINTABLE_MIN = 0x20  -- Space character ' '
local ASCII_PRINTABLE_MAX = 0x7E  -- Tilde character '~'

--- Check if a byte value is a printable ASCII character.
-- Printable ASCII characters are in the range 0x20 (space) to 0x7E (tilde).
-- This excludes control characters (0x00-0x1F) and DEL (0x7F).
-- @param byte number The byte value to check (0-255)
-- @return boolean True if the byte is a printable ASCII character
local function is_printable_ascii(byte)
    return byte >= ASCII_PRINTABLE_MIN and byte <= ASCII_PRINTABLE_MAX
end

TXSE_lib = {
    Proto = TxseProto,
    extract_bits = extract_bits,
    createSpec = createSpec,
    dissector = dissector,
    is_printable_ascii = is_printable_ascii
}

return TXSE_lib
