Jump to content

Module:MasonryLayout: Difference between revisions

// via Wikitext Extension for VSCode
// via Wikitext Extension for VSCode
Line 107: Line 107:


-- Estimate the rendered height of a card based on its content
-- Estimate the rendered height of a card based on its content
-- @param cardData table Card information: {title, rowCount, contentType, hasLongNames, isEmpty}
-- @param cardData table Card information: {title, rowCount, contentType, hasLongNames, isEmpty, rawDataCount}
-- @param options table Options: {mobileMode, customFactors}
-- @param options table Options: {mobileMode, customFactors}
-- @return number Estimated height in pixels
-- @return number Estimated height in pixels
Line 117: Line 117:
         return 0
         return 0
     end
     end
   
    -- Use raw data count if available for most accurate estimation
    local effectiveRowCount = cardData.rawDataCount or cardData.rowCount or 0
      
      
     -- Generate cache key
     -- Generate cache key
Line 122: Line 125:
         'masonryCardSize',
         'masonryCardSize',
         cardData.title or '',
         cardData.title or '',
         cardData.rowCount or 0,
         effectiveRowCount,
         cardData.contentType or 'default',
         cardData.contentType or 'default',
         cardData.hasLongNames and 'longNames' or 'shortNames',
         cardData.hasLongNames and 'longNames' or 'shortNames',
Line 131: Line 134:
         -- Special handling for intro cards (text content, not tables)
         -- Special handling for intro cards (text content, not tables)
         if cardData.contentType == 'intro' then
         if cardData.contentType == 'intro' then
             local textLength = cardData.contentLength or 0
             -- Intro is typically 2-3 lines of welcome text
            local estimatedHeight = math.max(80, math.min(200, textLength / 4))
             return 120 -- Fixed height for consistency
             return estimatedHeight
         end
         end
          
          
         -- Special handling for infoBox to ensure conservative estimation
         -- Special handling for infoBox
         if cardData.contentType == 'infoBox' then
         if cardData.contentType == 'infoBox' then
             local baseHeight = SIZE_ESTIMATES.BASE_CARD_HEIGHT * 0.8 -- More conservative base
             -- InfoBox has fixed structure: header + 4-5 rows
            local rowCount = cardData.rowCount or 5 -- Default to 5 rows for infoBox
             return 200 -- Fixed height based on actual structure
            local rowHeight = rowCount * SIZE_ESTIMATES.ROW_HEIGHT
             return baseHeight + rowHeight
         end
         end
          
          
         -- Base height calculation for regular cards
         -- Precise calculation for data tables
         local baseHeight = SIZE_ESTIMATES.BASE_CARD_HEIGHT
        -- Based on actual MediaWiki table rendering measurements:
        local TABLE_HEADER = 45      -- Table header with title
        local TABLE_PADDING = 20    -- Top/bottom padding
        local ROW_HEIGHT = 28        -- Actual height per row in pixels
         local BORDER_SPACING = 2    -- Border between rows
          
          
         -- Add height for table rows
         -- Calculate exact height based on row count
         local rowCount = cardData.rowCount or 0
         local contentHeight = TABLE_HEADER + TABLE_PADDING
        local rowHeight = rowCount * SIZE_ESTIMATES.ROW_HEIGHT
          
          
         -- Add bonus for long titles
         if effectiveRowCount > 0 then
        local titleBonus = 0
            -- Each row takes ROW_HEIGHT plus border spacing
        if cardData.title and #cardData.title > 20 then
            contentHeight = contentHeight + (effectiveRowCount * (ROW_HEIGHT + BORDER_SPACING))
            titleBonus = SIZE_ESTIMATES.TITLE_CHAR_FACTOR * (#cardData.title - 20)
           
            -- Add extra height for long content names (wrapping)
            if cardData.hasLongNames then
                -- Long names typically wrap to 2 lines
                local wrapBonus = math.floor(effectiveRowCount * 0.3) * ROW_HEIGHT
                contentHeight = contentHeight + wrapBonus
            end
         end
         end
          
          
         -- Add bonus for long content names
         -- Add small buffer for margin/padding variations
         local contentBonus = 0
         contentHeight = contentHeight + 15
        if cardData.hasLongNames then
            contentBonus = rowCount * 5 -- Extra height per row for long names
        end
          
          
         -- Mobile adjustments
         -- Mobile adjustments (slightly more spacing)
        local mobileBonus = 0
         if options.mobileMode then
         if options.mobileMode then
             mobileBonus = rowCount * 3 -- Slightly more height on mobile
             contentHeight = contentHeight * 1.1
         end
         end
          
          
        -- Calculate total height
         -- Apply reasonable bounds
        local totalHeight = baseHeight + rowHeight + titleBonus + contentBonus + mobileBonus
         contentHeight = math.max(80, contentHeight) -- Minimum height
       
         contentHeight = math.min(2000, contentHeight) -- Maximum height (very large tables)
         -- Apply bounds
         totalHeight = math.max(SIZE_ESTIMATES.MIN_CARD_HEIGHT, totalHeight)
         totalHeight = math.min(SIZE_ESTIMATES.MAX_CARD_HEIGHT, totalHeight)
          
          
         return totalHeight
         return math.floor(contentHeight)
     end)
     end)
end
end

Revision as of 01:22, 8 June 2025

Documentation for this module may be created at Module:MasonryLayout/doc

--[[
 * MasonryLayout.lua
 * Intelligent masonry layout system for content distribution
 * 
 * This module provides smart card distribution across columns for optimal visual balance.
 * Integrates with the Blueprint architecture and follows ICANNWiki performance patterns.
 *
 * Key Features:
 * - Content-aware size estimation
 * - Intelligent card distribution algorithm
 * - Responsive column management
 * - Aggressive caching for performance
 * - Blueprint integration
 * - Error handling integration
 *
 * Integration with other modules:
 * - ErrorHandling: All operations are protected with centralized error handling
 * - TemplateHelpers: Uses caching mechanisms and utility functions
 * - TemplateStructure: Integrates with block-based rendering
]]

local p = {}

-- ========== Constants as Upvalues ==========
local EMPTY_STRING = ''
local DEFAULT_COLUMNS = 3
local MOBILE_BREAKPOINT = 480
local TABLET_BREAKPOINT = 768

-- Size estimation constants (based on typical MediaWiki table rendering)
local SIZE_ESTIMATES = {
    BASE_CARD_HEIGHT = 120,      -- Base table overhead (header + borders + padding)
    HEADER_HEIGHT = 40,          -- Table header height
    ROW_HEIGHT = 25,             -- Average height per table row
    TITLE_CHAR_FACTOR = 1.2,     -- Additional height for long titles
    CONTENT_OVERFLOW_FACTOR = 15, -- Extra height for content overflow
    MIN_CARD_HEIGHT = 80,        -- Minimum card height
    MAX_CARD_HEIGHT = 800        -- Maximum card height (safety limit)
}

-- Column distribution weights for different screen sizes
local RESPONSIVE_CONFIG = {
    desktop = { columns = 3, minWidth = TABLET_BREAKPOINT + 1 },
    tablet = { columns = 2, minWidth = MOBILE_BREAKPOINT + 1, maxWidth = TABLET_BREAKPOINT },
    mobile = { columns = 1, maxWidth = MOBILE_BREAKPOINT }
}

-- ========== Required modules ==========
local ErrorHandling = require('Module:ErrorHandling')
local TemplateHelpers = require('Module:TemplateHelpers')
local NormalizationText = require('Module:NormalizationText')

-- ========== Module-level Caches ==========
local sizeCache = {}
local distributionCache = {}

-- ========== Utility Functions ==========

-- Generate a signature for a set of cards to use in cache keys
-- @param cards table Array of card objects
-- @return string A signature representing the card set
local function generateCardSignature(cards)
    if not cards or #cards == 0 then
        return 'empty'
    end
    
    local parts = {}
    for i, card in ipairs(cards) do
        parts[i] = string.format('%s:%d:%s', 
            card.id or 'unknown',
            card.estimatedSize or 0,
            card.contentType or 'default'
        )
    end
    
    return table.concat(parts, '|')
end

-- Calculate balance score for column heights (lower is better)
-- @param columnHeights table Array of column heights
-- @return number Balance score (0 = perfect balance)
local function calculateBalance(columnHeights)
    if not columnHeights or #columnHeights < 2 then
        return 0
    end
    
    local total = 0
    local count = #columnHeights
    
    -- Calculate average height
    for _, height in ipairs(columnHeights) do
        total = total + height
    end
    local average = total / count
    
    -- Calculate variance from average
    local variance = 0
    for _, height in ipairs(columnHeights) do
        local diff = height - average
        variance = variance + (diff * diff)
    end
    
    return variance / count
end

-- ========== Core Size Estimation ==========

-- Estimate the rendered height of a card based on its content
-- @param cardData table Card information: {title, rowCount, contentType, hasLongNames, isEmpty, rawDataCount}
-- @param options table Options: {mobileMode, customFactors}
-- @return number Estimated height in pixels
function p.estimateCardSize(cardData, options)
    options = options or {}
    
    -- Handle empty cards
    if cardData.isEmpty then
        return 0
    end
    
    -- Use raw data count if available for most accurate estimation
    local effectiveRowCount = cardData.rawDataCount or cardData.rowCount or 0
    
    -- Generate cache key
    local cacheKey = TemplateHelpers.generateCacheKey(
        'masonryCardSize',
        cardData.title or '',
        effectiveRowCount,
        cardData.contentType or 'default',
        cardData.hasLongNames and 'longNames' or 'shortNames',
        options.mobileMode and 'mobile' or 'desktop'
    )
    
    return TemplateHelpers.withCache(cacheKey, function()
        -- Special handling for intro cards (text content, not tables)
        if cardData.contentType == 'intro' then
            -- Intro is typically 2-3 lines of welcome text
            return 120 -- Fixed height for consistency
        end
        
        -- Special handling for infoBox
        if cardData.contentType == 'infoBox' then
            -- InfoBox has fixed structure: header + 4-5 rows
            return 200 -- Fixed height based on actual structure
        end
        
        -- Precise calculation for data tables
        -- Based on actual MediaWiki table rendering measurements:
        local TABLE_HEADER = 45      -- Table header with title
        local TABLE_PADDING = 20     -- Top/bottom padding
        local ROW_HEIGHT = 28        -- Actual height per row in pixels
        local BORDER_SPACING = 2     -- Border between rows
        
        -- Calculate exact height based on row count
        local contentHeight = TABLE_HEADER + TABLE_PADDING
        
        if effectiveRowCount > 0 then
            -- Each row takes ROW_HEIGHT plus border spacing
            contentHeight = contentHeight + (effectiveRowCount * (ROW_HEIGHT + BORDER_SPACING))
            
            -- Add extra height for long content names (wrapping)
            if cardData.hasLongNames then
                -- Long names typically wrap to 2 lines
                local wrapBonus = math.floor(effectiveRowCount * 0.3) * ROW_HEIGHT
                contentHeight = contentHeight + wrapBonus
            end
        end
        
        -- Add small buffer for margin/padding variations
        contentHeight = contentHeight + 15
        
        -- Mobile adjustments (slightly more spacing)
        if options.mobileMode then
            contentHeight = contentHeight * 1.1
        end
        
        -- Apply reasonable bounds
        contentHeight = math.max(80, contentHeight)  -- Minimum height
        contentHeight = math.min(2000, contentHeight) -- Maximum height (very large tables)
        
        return math.floor(contentHeight)
    end)
end

-- Analyze SMW query results to estimate card content
-- @param queryResults table Results from SMW query
-- @param cardType string Type of card ('organizations', 'people', etc.)
-- @return table Card data for size estimation
function p.analyzeQueryResults(queryResults, cardType)
    if not queryResults or #queryResults == 0 then
        return {
            isEmpty = true,
            rowCount = 0,
            contentType = cardType
        }
    end
    
    local rowCount = #queryResults
    local hasLongNames = false
    local totalNameLength = 0
    
    -- Analyze content for long names
    for _, result in ipairs(queryResults) do
        local name = result.result or result[1] or ''
        totalNameLength = totalNameLength + #name
        if #name > 30 then
            hasLongNames = true
        end
    end
    
    local averageNameLength = totalNameLength / rowCount
    
    return {
        isEmpty = false,
        rowCount = rowCount,
        contentType = cardType,
        hasLongNames = hasLongNames or averageNameLength > 25,
        averageNameLength = averageNameLength
    }
end

-- ========== Smart Distribution Algorithm ==========

-- Distribute cards across columns for optimal balance
-- @param cards table Array of card objects with size estimates
-- @param columnCount number Target number of columns (default: 3)
-- @return table Distribution result: {columns, heights, balance}
function p.distributeCards(cards, columnCount)
    columnCount = columnCount or DEFAULT_COLUMNS
    
    -- Handle edge cases
    if not cards or #cards == 0 then
        local emptyColumns = {}
        local emptyHeights = {}
        for i = 1, columnCount do
            emptyColumns[i] = {}
            emptyHeights[i] = 0
        end
        return {
            columns = emptyColumns,
            heights = emptyHeights,
            balance = 0
        }
    end
    
    -- Generate cache key
    local cardSignature = generateCardSignature(cards)
    local cacheKey = TemplateHelpers.generateCacheKey('masonryDistribution', cardSignature, columnCount)
    
    return TemplateHelpers.withCache(cacheKey, function()
        return p.distributeCardsIntelligent(cards, columnCount)
    end)
end

-- Intelligent card distribution with balance optimization
-- @param cards table Array of card objects with size estimates
-- @param columnCount number Target number of columns
-- @return table Distribution result: {columns, heights, balance}
function p.distributeCardsIntelligent(cards, columnCount)
    -- Pre-allocate column arrays for performance
    local columns = {}
    local columnHeights = {}
    
    for i = 1, columnCount do
        columns[i] = {}
        columnHeights[i] = 0
    end
    
    -- Separate special positioning cards from regular cards
    local regularCards = {}
    local specialCards = {}
    
    for _, card in ipairs(cards) do
        if card.id == 'infoBox' then
            -- InfoBox always goes to rightmost column
            table.insert(specialCards, {card = card, targetColumn = columnCount})
        elseif card.id == 'intro' then
            -- Intro always goes to leftmost column
            table.insert(specialCards, {card = card, targetColumn = 1})
        else
            table.insert(regularCards, card)
        end
    end
    
    -- Place special cards first but use deferred height calculation for alignment
    local specialCardHeights = {}
    for i = 1, columnCount do
        specialCardHeights[i] = 0
    end
    
    for _, special in ipairs(specialCards) do
        table.insert(columns[special.targetColumn], special.card)
        specialCardHeights[special.targetColumn] = special.card.estimatedSize or 0
        -- Don't add to columnHeights yet - this ensures regular cards distribute from equal starting points
    end
    
    -- Sort regular cards by size (largest first) for better distribution
    table.sort(regularCards, function(a, b) 
        return (a.estimatedSize or 0) > (b.estimatedSize or 0) 
    end)
    
    -- Calculate total content size and average per column
    local totalSize = 0
    for _, card in ipairs(regularCards) do
        totalSize = totalSize + (card.estimatedSize or 0)
    end
    local averageColumnHeight = totalSize / columnCount
    
    -- Smart distribution: check if we should use fewer columns for better balance
    local optimalColumnCount = columnCount
    
    -- If we have very few cards, use fewer columns
    if #regularCards <= 2 then
        optimalColumnCount = math.min(#regularCards, columnCount)
    else
        -- Check for extremely large cards that would dominate the layout
        local largestCard = regularCards[1]
        local secondLargestCard = regularCards[2]
        
        -- If the largest card is more than 50% of total content, we need special handling
        if largestCard and largestCard.estimatedSize > (totalSize * 0.5) then
            -- If we have multiple very large cards, distribute them across columns
            if secondLargestCard and secondLargestCard.estimatedSize > (totalSize * 0.3) then
                -- Multiple large cards - use all columns but with special placement
                optimalColumnCount = columnCount
            else
                -- One dominant card - use fewer columns for better balance
                optimalColumnCount = math.max(2, math.min(columnCount, #regularCards))
            end
        end
    end
    
    -- Redistribute columns if we're using fewer
    if optimalColumnCount < columnCount then
        -- Reset columns for optimal count but preserve special cards
        local newColumns = {}
        local newColumnHeights = {}
        local newSpecialCardHeights = {}
        
        for i = 1, optimalColumnCount do
            newColumns[i] = {}
            newColumnHeights[i] = 0
            newSpecialCardHeights[i] = 0
        end
        
        -- Re-place special cards in the new column structure
        for _, special in ipairs(specialCards) do
            local targetCol = math.min(special.targetColumn, optimalColumnCount)
            table.insert(newColumns[targetCol], special.card)
            newSpecialCardHeights[targetCol] = special.card.estimatedSize or 0
        end
        
        -- Add empty columns at the end
        for i = optimalColumnCount + 1, columnCount do
            newColumns[i] = {}
            newColumnHeights[i] = 0
            newSpecialCardHeights[i] = 0
        end
        
        columns = newColumns
        columnHeights = newColumnHeights
        specialCardHeights = newSpecialCardHeights
    end
    
    -- Enhanced distribution strategy for handling very large cards
    if #regularCards > 0 then
        local largestCard = regularCards[1]
        local totalRegularSize = 0
        for _, card in ipairs(regularCards) do
            totalRegularSize = totalRegularSize + (card.estimatedSize or 0)
        end
        
        -- If we have one extremely large card (>60% of content), use special placement
        if largestCard and largestCard.estimatedSize > (totalRegularSize * 0.6) then
            -- Place the largest card in the shortest column
            local shortestColumn = 1
            local shortestHeight = columnHeights[1]
            for i = 2, optimalColumnCount do
                if columnHeights[i] < shortestHeight then
                    shortestColumn = i
                    shortestHeight = columnHeights[i]
                end
            end
            
            table.insert(columns[shortestColumn], largestCard)
            columnHeights[shortestColumn] = columnHeights[shortestColumn] + (largestCard.estimatedSize or 0)
            
            -- Remove the largest card from the list
            table.remove(regularCards, 1)
            
            -- Distribute remaining cards using round-robin to avoid clustering
            local currentColumn = 1
            for _, card in ipairs(regularCards) do
                -- Skip the column with the large card for the first few placements
                if currentColumn == shortestColumn and #regularCards > optimalColumnCount then
                    currentColumn = currentColumn + 1
                    if currentColumn > optimalColumnCount then
                        currentColumn = 1
                    end
                end
                
                table.insert(columns[currentColumn], card)
                columnHeights[currentColumn] = columnHeights[currentColumn] + (card.estimatedSize or 0)
                
                currentColumn = currentColumn + 1
                if currentColumn > optimalColumnCount then
                    currentColumn = 1
                end
            end
        else
            -- Standard greedy placement for balanced content
            for _, card in ipairs(regularCards) do
                local bestColumn = 1
                local bestHeight = columnHeights[1]
                
                -- Find the best column (shortest among active columns)
                for i = 2, optimalColumnCount do
                    if columnHeights[i] < bestHeight then
                        bestColumn = i
                        bestHeight = columnHeights[i]
                    end
                end
                
                -- Place card and update height
                table.insert(columns[bestColumn], card)
                columnHeights[bestColumn] = columnHeights[bestColumn] + (card.estimatedSize or 0)
            end
        end
    end
    
    -- Now add special card heights for final balance calculation
    -- This ensures special cards are aligned at the top but still count toward total column height
    for i = 1, columnCount do
        columnHeights[i] = columnHeights[i] + specialCardHeights[i]
    end
    
    return {
        columns = columns,
        heights = columnHeights,
        balance = calculateBalance(columnHeights),
        optimalColumnCount = optimalColumnCount,
        totalSize = totalSize
    }
end

-- ========== Blueprint Integration ==========

-- Create Blueprint-compatible block functions for masonry layout
-- @param cardDefinitions table Array of card definitions
-- @param options table Layout options
-- @return table Blueprint block configuration
function p.createMasonryBlocks(cardDefinitions, options)
    options = options or {}
    
    return {
        masonryWrapperOpen = {
            feature = 'fullPage',
            render = function(template, args)
                return p.renderMasonryOpen(options)
            end
        },
        
        masonryContent = {
            feature = 'fullPage',
            render = function(template, args)
                return p.renderMasonryContent(template, args, cardDefinitions, options)
            end
        },
        
        masonryWrapperClose = {
            feature = 'fullPage',
            render = function(template, args)
                return p.renderMasonryClose()
            end
        }
    }
end

-- Render the opening masonry container
-- @param options table Layout options
-- @return string HTML for masonry container opening
function p.renderMasonryOpen(options)
    options = options or {}
    local cssClass = options.containerClass or 'country-hub-masonry-container'
    
    return string.format('<div class="%s">', cssClass)
end

-- Render the closing masonry container
-- @return string HTML for masonry container closing
function p.renderMasonryClose()
    return '</div>'
end

-- Render the main masonry content with intelligent distribution
-- @param template table Template object
-- @param args table Template arguments
-- @param cardDefinitions table Array of card definitions
-- @param options table Layout options
-- @return string Complete masonry layout HTML
function p.renderMasonryContent(template, args, cardDefinitions, options)
    if not template._errorContext then
        template._errorContext = ErrorHandling.createContext("MasonryLayout")
    end
    
    return ErrorHandling.protect(
        template._errorContext,
        "renderMasonryContent",
        function()
            return p.renderMasonryContentInternal(template, args, cardDefinitions, options)
        end,
        EMPTY_STRING,
        template, args, cardDefinitions, options
    )
end

-- Internal masonry content rendering (protected by error handling)
-- @param template table Template object
-- @param args table Template arguments
-- @param cardDefinitions table Array of card definitions
-- @param options table Layout options
-- @return string Complete masonry layout HTML
function p.renderMasonryContentInternal(template, args, cardDefinitions, options)
    options = options or {}
    local columnCount = options.columns or DEFAULT_COLUMNS
    
    -- Build cards with content analysis
    local cards = {}
    local cardIndex = 1
    
    for _, cardDef in ipairs(cardDefinitions) do
        -- Check if this card's feature is enabled
        if template.features[cardDef.feature] then
            -- Get the block renderer for this card
            local renderer = template._blocks and template._blocks[cardDef.blockId]
            if renderer and renderer.render then
                -- Render the card content
                local cardContent = renderer.render(template, args)
                
                if cardContent and cardContent ~= EMPTY_STRING then
                    -- Get raw data count if available
                    local rawDataCount = template._rawDataCounts and template._rawDataCounts[cardDef.blockId] or nil
                    
                    -- Analyze the content for size estimation
                    local cardData = p.analyzeCardContent(cardContent, cardDef, rawDataCount)
                    cardData.id = cardDef.blockId
                    cardData.title = cardDef.title or cardDef.blockId
                    cardData.content = cardContent
                    cardData.estimatedSize = p.estimateCardSize(cardData, options)
                    
                    cards[cardIndex] = cardData
                    cardIndex = cardIndex + 1
                end
            end
        end
    end
    
    -- Distribute cards across columns
    local distribution = p.distributeCards(cards, columnCount)
    
    -- Render the distributed layout
    return p.renderDistributedLayout(distribution, options)
end

-- Analyze rendered card content to extract size information
-- @param cardContent string Rendered HTML content
-- @param cardDef table Card definition
-- @param rawDataCount number Optional: actual count of data entries from SMW query
-- @return table Card data for size estimation
function p.analyzeCardContent(cardContent, cardDef, rawDataCount)
    if not cardContent or cardContent == EMPTY_STRING then
        return {
            isEmpty = true,
            rowCount = 0,
            contentType = cardDef.blockId,
            rawDataCount = 0
        }
    end
    
    -- Use raw data count if provided (most accurate)
    local rowCount = rawDataCount or 0
    
    -- Fallback: Count table rows in the content (look for <tr> tags or table row patterns)
    if rowCount == 0 then
        local _, trCount = cardContent:gsub('<tr[^>]*>', '')
        local _, wikiRowCount = cardContent:gsub('|-', '')
        rowCount = math.max(trCount, wikiRowCount)
    end
    
    -- Final fallback: estimate based on content length
    if rowCount == 0 and #cardContent > 100 then
        -- Rough estimation: every ~200 characters might be a row
        rowCount = math.ceil(#cardContent / 200)
    end
    
    -- Check for long content (rough heuristic)
    local hasLongNames = #cardContent > 1000 or cardContent:find('[%w%s%-%.]+[%w%s%-%.]+[%w%s%-%.]+[%w%s%-%.]+[%w%s%-%.]+[%w%s%-%.]+')
    
    return {
        isEmpty = false,
        rowCount = math.max(1, rowCount), -- At least 1 row if content exists
        contentType = cardDef.blockId,
        hasLongNames = hasLongNames,
        contentLength = #cardContent,
        rawDataCount = rawDataCount or rowCount -- Store the actual count for debugging
    }
end

-- Render the final distributed layout
-- @param distribution table Distribution result from distributeCards
-- @param options table Layout options
-- @return string Complete HTML for the distributed layout
function p.renderDistributedLayout(distribution, options)
    options = options or {}
    local columnClass = options.columnClass or 'country-hub-masonry-column'
    local cardClass = options.cardClass or 'country-hub-masonry-card'
    
    local columns = distribution.columns
    local columnCount = #columns
    
    if columnCount == 0 then
        return EMPTY_STRING
    end
    
    -- Build HTML for each column
    local columnHtml = {}
    
    for i = 1, columnCount do
        local columnCards = columns[i]
        local cardHtml = {}
        
        -- Render cards in this column
        for j, card in ipairs(columnCards) do
            cardHtml[j] = string.format(
                '<div class="%s" data-card-id="%s">%s</div>',
                cardClass,
                card.id or 'unknown',
                card.content or EMPTY_STRING
            )
        end
        
        -- Wrap column
        columnHtml[i] = string.format(
            '<div class="%s" data-column="%d">%s</div>',
            columnClass,
            i,
            table.concat(cardHtml, '\n')
        )
    end
    
    return table.concat(columnHtml, '\n')
end

-- ========== Responsive Utilities ==========

-- Get responsive column count based on screen size
-- @param screenWidth number Screen width in pixels
-- @return number Appropriate column count
function p.getResponsiveColumns(screenWidth)
    if screenWidth <= MOBILE_BREAKPOINT then
        return 1
    elseif screenWidth <= TABLET_BREAKPOINT then
        return 2
    else
        return 3
    end
end

-- Generate responsive CSS classes
-- @param options table Layout options
-- @return string CSS classes for responsive behavior
function p.generateResponsiveClasses(options)
    options = options or {}
    local baseClass = options.baseClass or 'country-hub-masonry'
    
    local classes = {baseClass}
    
    if options.mobileColumns then
        table.insert(classes, baseClass .. '-mobile-' .. options.mobileColumns)
    end
    
    if options.tabletColumns then
        table.insert(classes, baseClass .. '-tablet-' .. options.tabletColumns)
    end
    
    if options.desktopColumns then
        table.insert(classes, baseClass .. '-desktop-' .. options.desktopColumns)
    end
    
    return table.concat(classes, ' ')
end

-- ========== Intelligent Layout Rendering ==========

-- Main render function for intelligent masonry layout
-- This is the core function that coordinates content rendering, analysis, and distribution
-- @param template table Template object with features and configuration
-- @param args table Template arguments
-- @param config table Configuration with cardDefinitions, options, and blockRenderers
-- @return string Complete masonry layout HTML
function p.renderIntelligentLayout(template, args, config)
    -- Create render-time error context (Blueprint pattern)
    local errorContext = ErrorHandling.createContext("MasonryLayout")
    
    return ErrorHandling.protect(
        errorContext,
        "renderIntelligentLayout",
        function()
            return p.renderIntelligentLayoutInternal(template, args, config, errorContext)
        end,
        EMPTY_STRING,
        template, args, config
    )
end

-- Internal implementation of intelligent layout rendering
-- @param template table Template object
-- @param args table Template arguments  
-- @param config table Configuration object
-- @param errorContext table Error context for protected operations
-- @return string Complete masonry layout HTML
function p.renderIntelligentLayoutInternal(template, args, config, errorContext)
    local cardDefinitions = config.cardDefinitions or {}
    local options = config.options or {}
    local blockRenderers = config.blockRenderers or {}
    
    local columnCount = options.columns or DEFAULT_COLUMNS
    
    -- Build cards with render-time content generation
    local cards = {}
    local cardIndex = 1
    
    for _, cardDef in ipairs(cardDefinitions) do
        -- Check if this card's feature is enabled
        if template.features[cardDef.feature] then
            -- Get the block renderer for this card
            local renderer = blockRenderers[cardDef.blockId]
            if renderer and renderer.render then
                -- Render the card content at render-time (not configuration-time)
                local cardContent = ErrorHandling.protect(
                    errorContext,
                    "renderCard_" .. cardDef.blockId,
                    function()
                        return renderer.render(template, args)
                    end,
                    EMPTY_STRING,
                    template, args
                )
                
                if cardContent and cardContent ~= EMPTY_STRING then
                    -- Analyze the content for size estimation
                    local cardData = p.analyzeCardContent(cardContent, cardDef)
                    cardData.id = cardDef.blockId
                    cardData.title = cardDef.title or cardDef.blockId
                    cardData.content = cardContent
                    cardData.estimatedSize = p.estimateCardSize(cardData, options)
                    
                    cards[cardIndex] = cardData
                    cardIndex = cardIndex + 1
                end
            end
        end
    end
    
    -- Distribute cards across columns
    local distribution = p.distributeCards(cards, columnCount)
    
    -- Render the complete masonry layout
    local containerClass = options.containerClass or 'country-hub-masonry-container'
    local masonryHtml = string.format('<div class="%s">', containerClass)
    masonryHtml = masonryHtml .. p.renderDistributedLayout(distribution, options)
    masonryHtml = masonryHtml .. '</div>'
    
    return masonryHtml
end

-- ========== Debug and Utilities ==========

-- Get distribution statistics for debugging
-- @param distribution table Distribution result
-- @return table Statistics about the distribution
function p.getDistributionStats(distribution)
    if not distribution or not distribution.columns then
        return {
            columnCount = 0,
            totalCards = 0,
            balance = 0,
            heights = {}
        }
    end
    
    local totalCards = 0
    for _, column in ipairs(distribution.columns) do
        totalCards = totalCards + #column
    end
    
    return {
        columnCount = #distribution.columns,
        totalCards = totalCards,
        balance = distribution.balance or 0,
        heights = distribution.heights or {},
        averageHeight = distribution.heights and (#distribution.heights > 0) and 
                       (table.concat(distribution.heights, '+') / #distribution.heights) or 0
    }
end

-- Clear caches (for debugging/testing)
function p.clearCaches()
    sizeCache = {}
    distributionCache = {}
    -- Also clear TemplateHelpers cache if needed
end

return p