summary history files

vendor/github.com/olekukonko/tablewriter/tablewriter.go
package tablewriter

import (
	"bytes"
	"io"
	"math"
	"os"
	"reflect"
	"runtime"
	"strings"

	"github.com/olekukonko/errors"
	"github.com/olekukonko/ll"
	"github.com/olekukonko/ll/lh"
	"github.com/olekukonko/tablewriter/pkg/twcache"
	"github.com/olekukonko/tablewriter/pkg/twwarp"
	"github.com/olekukonko/tablewriter/pkg/twwidth"
	"github.com/olekukonko/tablewriter/renderer"
	"github.com/olekukonko/tablewriter/tw"
)

// Table represents a table instance with content and rendering capabilities.
type Table struct {
	writer       io.Writer           // Destination for table output
	counters     []tw.Counter        // Counters for indices
	rows         [][]string          // Row data, one slice of strings per logical row
	headers      [][]string          // Header content
	footers      [][]string          // Footer content
	headerWidths tw.Mapper[int, int] // Computed widths for header columns
	rowWidths    tw.Mapper[int, int] // Computed widths for row columns
	footerWidths tw.Mapper[int, int] // Computed widths for footer columns
	renderer     tw.Renderer         // Engine for rendering the table
	config       Config              // Table configuration settings
	stringer     any                 // Function to convert rows to strings
	newLine      string              // Newline character (e.g., "\n")
	hasPrinted   bool                // Indicates if the table has been rendered
	logger       *ll.Logger          // Debug trace log
	trace        *bytes.Buffer       // Debug trace log

	// Caption fields
	caption tw.Caption

	// streaming
	streamWidths            tw.Mapper[int, int]           // Fixed column widths for streaming mode, calculated once
	streamFooterLines       [][]string                    // Processed footer lines for streaming, stored until Close().
	headerRendered          bool                          // Tracks if header has been rendered in streaming mode
	firstRowRendered        bool                          // Tracks if the first data row has been rendered in streaming mode
	lastRenderedLineContent []string                      // Content of the very last line rendered (for Previous context in streaming)
	lastRenderedMergeState  tw.Mapper[int, tw.MergeState] // Merge state of the very last line rendered (for Previous context in streaming)
	lastRenderedPosition    tw.Position                   // Position (Header/Row/Footer/Separator) of the last line rendered (for Previous context in streaming)
	streamNumCols           int                           // The derived number of columns in streaming mode
	streamRowCounter        int                           // Counter for rows rendered in streaming mode (0-indexed logical rows)

	// cache
	stringerCache twcache.Cache[reflect.Type, reflect.Value] // Cache for stringer reflection

	batchRenderNumCols      int
	isBatchRenderNumColsSet bool
}

// renderContext holds the core state for rendering the table.
type renderContext struct {
	table           *Table                                      // Reference to the table instance
	renderer        tw.Renderer                                 // Renderer instance
	cfg             tw.Rendition                                // Renderer configuration
	numCols         int                                         // Total number of columns
	headerLines     [][]string                                  // Processed header lines
	rowLines        [][][]string                                // Processed row lines
	footerLines     [][]string                                  // Processed footer lines
	widths          tw.Mapper[tw.Position, tw.Mapper[int, int]] // Widths per section
	footerPrepared  bool                                        // Tracks if footer is prepared
	emptyColumns    []bool                                      // Tracks which original columns are empty (true if empty)
	visibleColCount int                                         // Count of columns that are NOT empty
	logger          *ll.Logger                                  // Debug trace log
}

// mergeContext holds state related to cell merging.
type mergeContext struct {
	headerMerges map[int]tw.MergeState        // Merge states for header columns
	rowMerges    []map[int]tw.MergeState      // Merge states for each row
	footerMerges map[int]tw.MergeState        // Merge states for footer columns
	horzMerges   map[tw.Position]map[int]bool // Tracks horizontal merges (unused)
}

// helperContext holds additional data for rendering helpers.
type helperContext struct {
	position tw.Position // Section being processed (Header, Row, Footer)
	rowIdx   int         // Row index within section
	lineIdx  int         // Line index within row
	location tw.Location // Boundary location (First, Middle, End)
	line     []string    // Current line content
}

// renderMergeResponse holds cell context data from rendering operations.
type renderMergeResponse struct {
	cells        map[int]tw.CellContext // Current line cells
	prevCells    map[int]tw.CellContext // Previous line cells
	nextCells    map[int]tw.CellContext // Next line cells
	location     tw.Location            // Determined Location for this line
	cellsContent []string
}

// NewTable creates a new table instance with specified writer and options.
// Parameters include writer for output and optional configuration options.
// Returns a pointer to the initialized Table instance.
func NewTable(w io.Writer, opts ...Option) *Table {
	t := &Table{
		writer:       w,
		headerWidths: tw.NewMapper[int, int](),
		rowWidths:    tw.NewMapper[int, int](),
		footerWidths: tw.NewMapper[int, int](),
		renderer:     renderer.NewBlueprint(),
		config:       defaultConfig(),
		newLine:      tw.NewLine,
		trace:        &bytes.Buffer{},

		// Streaming
		streamWidths:           tw.NewMapper[int, int](), // Initialize empty mapper for streaming widths
		lastRenderedMergeState: tw.NewMapper[int, tw.MergeState](),
		headerRendered:         false,
		firstRowRendered:       false,
		lastRenderedPosition:   "",
		streamNumCols:          0,
		streamRowCounter:       0,

		//  Cache
		stringerCache: twcache.NewLRU[reflect.Type, reflect.Value](tw.DefaultCacheStringCapacity),
	}

	// set Options
	t.Options(opts...)

	t.logger.Infof("Table initialized with %d options", len(opts))
	return t
}

// NewWriter creates a new table with default settings for backward compatibility.
// It logs the creation if debugging is enabled.
func NewWriter(w io.Writer) *Table {
	t := NewTable(w)
	if t.logger != nil {
		t.logger.Debug("NewWriter created buffered Table")
	}
	return t
}

// Caption sets the table caption (legacy method).
// Defaults to BottomCenter alignment, wrapping to table width.
// Use SetCaptionOptions for more control.
func (t *Table) Caption(caption tw.Caption) *Table { // This is the one we modified
	originalSpot := caption.Spot
	originalAlign := caption.Align

	if caption.Spot == tw.SpotNone {
		caption.Spot = tw.SpotBottomCenter
		t.logger.Debugf("[Table.Caption] Input Spot was SpotNone, defaulting Spot to SpotBottomCenter (%d)", caption.Spot)
	}

	if caption.Align == "" || caption.Align == tw.AlignDefault || caption.Align == tw.AlignNone {
		switch caption.Spot {
		case tw.SpotTopLeft, tw.SpotBottomLeft:
			caption.Align = tw.AlignLeft
		case tw.SpotTopRight, tw.SpotBottomRight:
			caption.Align = tw.AlignRight
		default:
			caption.Align = tw.AlignCenter
		}
		t.logger.Debugf("[Table.Caption] Input Align was empty/default, defaulting Align to %s for Spot %d", caption.Align, caption.Spot)
	}

	t.caption = caption // t.caption on the struct is now updated.
	t.logger.Debugf("Caption method called: Input(Spot:%v, Align:%q), Final(Spot:%v, Align:%q), Text:'%.20s', MaxWidth:%d",
		originalSpot, originalAlign, t.caption.Spot, t.caption.Align, t.caption.Text, t.caption.Width)
	return t
}

// Append adds data to the current row being built for the table.
// This method always contributes to a single logical row in the table.
// To add multiple distinct rows, call Append multiple times (once for each row's data)
// or use the Bulk() method if providing a slice where each element is a row.
func (t *Table) Append(rows ...interface{}) error {
	t.ensureInitialized()

	if t.config.Stream.Enable && t.hasPrinted {
		// Streaming logic remains unchanged, as AutoHeader is a batch-mode concept.
		t.logger.Debugf("Append() called in streaming mode with %d items for a single row", len(rows))
		var rowItemForStream interface{}
		if len(rows) == 1 {
			rowItemForStream = rows[0]
		} else {
			rowItemForStream = rows
		}
		if err := t.streamAppendRow(rowItemForStream); err != nil {
			t.logger.Errorf("Error rendering streaming row: %v", err)
			return errors.Newf("failed to stream append row").Wrap(err)
		}
		return nil
	}

	// Batch Mode Logic
	t.logger.Debugf("Append (Batch) received %d arguments: %v", len(rows), rows)

	var cellsSource interface{}
	if len(rows) == 1 {
		cellsSource = rows[0]
	} else {
		cellsSource = rows
	}
	// Check if we should attempt to auto-generate headers from this append operation.
	// Conditions: AutoHeader is on, no headers are set yet, and this is the first data row.
	isFirstRow := len(t.rows) == 0
	if t.config.Behavior.Structs.AutoHeader.Enabled() && len(t.headers) == 0 && isFirstRow {
		t.logger.Debug("Append: Triggering AutoHeader for the first row.")
		headers := t.extractHeadersFromStruct(cellsSource)
		if len(headers) > 0 {
			// Set the extracted headers. The Header() method handles the rest.
			t.Header(headers)
		}
	}

	cells, err := t.convertCellsToStrings(cellsSource, t.config.Row)
	if err != nil {
		t.logger.Errorf("Append (Batch) failed for cellsSource %v: %v", cellsSource, err)
		return err
	}
	t.rows = append(t.rows, cells)

	t.logger.Debugf("Append (Batch) completed for one row, total rows in table: %d", len(t.rows))
	return nil
}

// Bulk adds multiple rows from a slice to the table.
// If Behavior.AutoHeader is enabled, no headers set, and rows is a slice of structs,
// automatically extracts/sets headers from the first struct.
func (t *Table) Bulk(rows interface{}) error {
	rv := reflect.ValueOf(rows)
	if rv.Kind() != reflect.Slice {
		return errors.Newf("Bulk expects a slice, got %T", rows)
	}
	if rv.Len() == 0 {
		return nil
	}

	// AutoHeader logic remains here, as it's a "Bulk" operation concept.
	if t.config.Behavior.Structs.AutoHeader.Enabled() && len(t.headers) == 0 {
		first := rv.Index(0).Interface()
		// We can now correctly get headers from pointers or embedded structs
		headers := t.extractHeadersFromStruct(first)
		if len(headers) > 0 {
			t.Header(headers)
		}
	}

	// The rest of the logic is now just a loop over Append.
	for i := 0; i < rv.Len(); i++ {
		row := rv.Index(i).Interface()
		if err := t.Append(row); err != nil { // Use Append
			return err
		}
	}
	return nil
}

// Config returns the current table configuration.
// No parameters are required.
// Returns the Config struct with current settings.
func (t *Table) Config() Config {
	return t.config
}

// Configure updates the table's configuration using a provided function.
// Parameter fn is a function that modifies the Config struct.
// Returns the Table instance for method chaining.
func (t *Table) Configure(fn func(cfg *Config)) *Table {
	fn(&t.config) // Let the user modify the config directly
	// Handle any immediate side-effects of config changes, e.g., logger state
	if t.config.Debug {
		t.logger.Enable()
		t.logger.Resume() // in case it was suspended
	} else {
		t.logger.Disable()
		t.logger.Suspend() // suspend totally, especially because of tight loops
	}
	t.logger.Debugf("Configure complete. New t.config: %+v", t.config)
	return t
}

// Debug retrieves the accumulated debug trace logs.
// No parameters are required.
// Returns a slice of debug messages including renderer logs.
func (t *Table) Debug() *bytes.Buffer {
	return t.trace
}

// Header sets the table's header content, padding to match column count.
// Parameter elements is a slice of strings for header content.
// No return value.
// In streaming mode, this processes and renders the header immediately.
func (t *Table) Header(elements ...any) {
	t.ensureInitialized()
	t.logger.Debugf("Header() method called with raw variadic elements: %v (len %d). Streaming: %v, Started: %v", elements, len(elements), t.config.Stream.Enable, t.hasPrinted)

	// just forget
	if t.config.Behavior.Header.Hide.Enabled() {
		return
	}

	// add come common default
	if t.config.Header.Formatting.AutoFormat == tw.Unknown {
		t.config.Header.Formatting.AutoFormat = tw.On
	}

	if t.config.Stream.Enable && t.hasPrinted {
		//  Streaming Path
		actualCellsToProcess := t.processVariadic(elements)
		headersAsStrings, err := t.convertCellsToStrings(actualCellsToProcess, t.config.Header)
		if err != nil {
			t.logger.Errorf("Header(): Failed to convert header elements to strings for streaming: %v", err)
			headersAsStrings = []string{} // Use empty on error
		}
		errStream := t.streamRenderHeader(headersAsStrings) // streamRenderHeader handles padding to streamNumCols internally
		if errStream != nil {
			t.logger.Errorf("Error rendering streaming header: %v", errStream)
		}
		return
	}

	//  Batch Path
	processedElements := t.processVariadic(elements)
	t.logger.Debugf("Header() (Batch): Effective cells to process: %v", processedElements)

	headersAsStrings, err := t.convertCellsToStrings(processedElements, t.config.Header)
	if err != nil {
		t.logger.Errorf("Header() (Batch): Failed to convert to strings: %v", err)
		t.headers = [][]string{} // Set to empty on error
		return
	}

	// prepareContent uses t.config.Header for AutoFormat and MaxWidth constraints.
	// It processes based on the number of columns in headersAsStrings.
	preparedHeaderLines := t.prepareContent(headersAsStrings, t.config.Header)
	t.headers = preparedHeaderLines // Store directly. Padding to t.maxColumns() will happen in prepareContexts.

	t.logger.Debugf("Header set (batch mode), lines stored: %d. First line if exists: %v", len(t.headers), func() []string {
		if len(t.headers) > 0 {
			return t.headers[0]
		} else {
			return nil
		}
	}())
}

// Footer sets the table's footer content, padding to match column count.
// Parameter footers is a slice of strings for footer content.
// No return value.
// Footer sets the table's footer content.
// Parameter footers is a slice of strings for footer content.
// In streaming mode, this processes and stores the footer for rendering by Close().
func (t *Table) Footer(elements ...any) {
	t.ensureInitialized()
	t.logger.Debugf("Footer() method called with raw variadic elements: %v (len %d). Streaming: %v, Started: %v", elements, len(elements), t.config.Stream.Enable, t.hasPrinted)

	// just forget
	if t.config.Behavior.Footer.Hide.Enabled() {
		return
	}

	if t.config.Stream.Enable && t.hasPrinted {
		//  Streaming Path
		actualCellsToProcess := t.processVariadic(elements)
		footersAsStrings, err := t.convertCellsToStrings(actualCellsToProcess, t.config.Footer)
		if err != nil {
			t.logger.Errorf("Footer(): Failed to convert footer elements to strings for streaming: %v", err)
			footersAsStrings = []string{} // Use empty on error
		}
		errStream := t.streamStoreFooter(footersAsStrings) // streamStoreFooter handles padding to streamNumCols internally
		if errStream != nil {
			t.logger.Errorf("Error processing streaming footer: %v", errStream)
		}
		return
	}

	//  Batch Path
	processedElements := t.processVariadic(elements)
	t.logger.Debugf("Footer() (Batch): Effective cells to process: %v", processedElements)

	footersAsStrings, err := t.convertCellsToStrings(processedElements, t.config.Footer)
	if err != nil {
		t.logger.Errorf("Footer() (Batch): Failed to convert to strings: %v", err)
		t.footers = [][]string{} // Set to empty on error
		return
	}

	preparedFooterLines := t.prepareContent(footersAsStrings, t.config.Footer)
	t.footers = preparedFooterLines // Store directly. Padding to t.maxColumns() will happen in prepareContexts.

	t.logger.Debugf("Footer set (batch mode), lines stored: %d. First line if exists: %v",
		len(t.footers), func() []string {
			if len(t.footers) > 0 {
				return t.footers[0]
			} else {
				return nil
			}
		}())
}

// Options updates the table's Options using a provided function.
// Parameter opts is a function that modifies the Table struct.
// Returns the Table instance for method chaining.
func (t *Table) Options(opts ...Option) *Table {
	// add logger
	if t.logger == nil {
		t.logger = ll.New("table").Handler(lh.NewTextHandler(t.trace))
	}

	// loop through options
	for _, opt := range opts {
		opt(t)
	}

	// force debugging mode if set
	// This should  be move away form WithDebug
	if t.config.Debug {
		t.logger.Enable()
		t.logger.Resume()
	} else {
		t.logger.Disable()
		t.logger.Suspend()
	}

	// Get additional system information for debugging
	goVersion := runtime.Version()
	goOS := runtime.GOOS
	goArch := runtime.GOARCH
	numCPU := runtime.NumCPU()

	t.logger.Infof("Environment: LC_CTYPE=%s, LANG=%s, TERM=%s", os.Getenv("LC_CTYPE"), os.Getenv("LANG"), os.Getenv("TERM"))
	t.logger.Infof("Go Runtime: Version=%s, OS=%s, Arch=%s, CPUs=%d", goVersion, goOS, goArch, numCPU)

	// send logger to renderer
	// this will overwrite the default logger
	t.renderer.Logger(t.logger)
	return t
}

// Reset clears all data (headers, rows, footers, caption) and rendering state
// from the table, allowing the Table instance to be reused for a new table
// with the same configuration and writer.
// It does NOT reset the configuration itself (set by NewTable options or Configure)
// or the underlying io.Writer.
func (t *Table) Reset() {
	t.logger.Debug("Reset() called. Clearing table data and render state.")

	// Clear data slices
	t.rows = nil    // Or t.rows = make([][]string, 0)
	t.headers = nil // Or t.headers = make([][]string, 0)
	t.footers = nil // Or t.footers = make([][]string, 0)

	// Reset width mappers (important for recalculating widths for the new table)
	t.headerWidths = tw.NewMapper[int, int]()
	t.rowWidths = tw.NewMapper[int, int]()
	t.footerWidths = tw.NewMapper[int, int]()

	// Reset caption
	t.caption = tw.Caption{} // Reset to zero value

	// Reset rendering state flags
	t.hasPrinted = false // Critical for allowing Render() or stream Start() again

	// Reset streaming-specific state
	// (Important if the table was used in streaming mode and might be reused in batch or another stream)
	t.streamWidths = tw.NewMapper[int, int]()
	t.streamFooterLines = nil
	t.headerRendered = false
	t.firstRowRendered = false
	t.lastRenderedLineContent = nil
	t.lastRenderedMergeState = tw.NewMapper[int, tw.MergeState]() // Re-initialize
	t.lastRenderedPosition = ""
	t.streamNumCols = 0
	t.streamRowCounter = 0

	// The stringer and its cache are part of the table's configuration,
	if t.stringerCache == nil {
		t.stringerCache = twcache.NewLRU[reflect.Type, reflect.Value](tw.DefaultCacheStringCapacity)
		t.logger.Debug("Reset(): Stringer cache reset to default capacity.")
	} else {
		t.stringerCache.Purge()
		t.logger.Debug("Reset(): Stringer cache cleared.")
	}

	// If the renderer has its own state that needs resetting after a table is done,
	// this would be the place to call a renderer.Reset() method if it existed.
	// Most current renderers are stateless per render call or reset in their Start/Close.
	// For instance, HTML and SVG renderers have their own Reset method.
	// It might be good practice to call it if available.
	if r, ok := t.renderer.(interface{ Reset() }); ok {
		t.logger.Debug("Reset(): Calling Reset() on the current renderer.")
		r.Reset()
	}

	t.logger.Info("Table instance has been reset.")
}

// Render triggers the table rendering process to the configured writer.
// No parameters are required.
// Returns an error if rendering fails.
func (t *Table) Render() error {
	return t.render()
}

// Lines returns the total number of lines rendered.
// This method is only effective if the WithLineCounter() option was used during
// table initialization and must be called *after* Render().
// It actively searches for the default tw.LineCounter among all active counters.
// It returns -1 if the line counter was not enabled.
func (t *Table) Lines() int {
	for _, counter := range t.counters {
		if lc, ok := counter.(*tw.LineCounter); ok {
			return lc.Total()
		}
	}
	// use -1 to indicate no line counter is attached
	return -1
}

// Counters returns the slice of all active counter instances.
// This is useful when multiple counters are enabled.
// It must be called *after* Render().
func (t *Table) Counters() []tw.Counter {
	return t.counters
}

// Trimmer trims whitespace from a string based on the Table’s configuration.
// It conditionally applies strings.TrimSpace to the input string if the TrimSpace behavior
// is enabled in t.config.Behavior, otherwise returning the string unchanged. This method
// is used in the logging library to format strings for tabular output, ensuring consistent
// display in log messages. Thread-safe as it only reads configuration and operates on the
// input string.
func (t *Table) Trimmer(str string) string {
	if t.config.Behavior.TrimSpace.Enabled() {
		return strings.TrimSpace(str)
	}
	return str
}

// appendSingle adds a single row to the table's row data.
// Parameter row is the data to append, converted via stringer if needed.
// Returns an error if conversion or appending fails.
func (t *Table) appendSingle(row interface{}) error {
	t.ensureInitialized() // Already here

	if t.config.Stream.Enable && t.hasPrinted { // If streaming is active
		t.logger.Debugf("appendSingle: Dispatching to streamAppendRow for row: %v", row)
		return t.streamAppendRow(row) // Call the streaming render function
	}

	t.logger.Debugf("appendSingle: Processing for batch mode, row: %v", row)
	cells, err := t.convertCellsToStrings(row, t.config.Row)
	if err != nil {
		t.logger.Debugf("Error in convertCellsToStrings (batch mode): %v", err)
		return err
	}
	t.rows = append(t.rows, cells) // Add to batch storage
	t.logger.Debugf("Row appended to batch t.rows, total batch rows: %d", len(t.rows))
	return nil
}

// buildAligns constructs a map of column alignments from configuration.
// Parameter config provides alignment settings for the section.
// Returns a map of column indices to alignment settings.
func (t *Table) buildAligns(config tw.CellConfig) map[int]tw.Align {
	// Start with global alignment, preferring deprecated Formatting.Alignment
	effectiveGlobalAlign := config.Formatting.Alignment
	if effectiveGlobalAlign == tw.Empty || effectiveGlobalAlign == tw.Skip {
		effectiveGlobalAlign = config.Alignment.Global
		if config.Formatting.Alignment != tw.Empty && config.Formatting.Alignment != tw.Skip {
			t.logger.Warnf("Using deprecated CellFormatting.Alignment (%s). Migrate to CellConfig.Alignment.Global.", config.Formatting.Alignment)
		}
	}

	// Use per-column alignments, preferring deprecated ColumnAligns
	effectivePerColumn := config.ColumnAligns
	if len(effectivePerColumn) == 0 && len(config.Alignment.PerColumn) > 0 {
		effectivePerColumn = make([]tw.Align, len(config.Alignment.PerColumn))
		copy(effectivePerColumn, config.Alignment.PerColumn)
		if len(config.ColumnAligns) > 0 {
			t.logger.Warnf("Using deprecated CellConfig.ColumnAligns (%v). Migrate to CellConfig.Alignment.PerColumn.", config.ColumnAligns)
		}
	}

	// Log input for debugging
	t.logger.Debugf("buildAligns INPUT: deprecated Formatting.Alignment=%s, deprecated ColumnAligns=%v, config.Alignment.Global=%s, config.Alignment.PerColumn=%v",
		config.Formatting.Alignment, config.ColumnAligns, config.Alignment.Global, config.Alignment.PerColumn)

	numColsToUse := t.getNumColsToUse()
	colAlignsResult := make(map[int]tw.Align)
	for i := 0; i < numColsToUse; i++ {
		currentAlign := effectiveGlobalAlign
		if i < len(effectivePerColumn) && effectivePerColumn[i] != tw.Empty && effectivePerColumn[i] != tw.Skip {
			currentAlign = effectivePerColumn[i]
		}
		// Skip validation here; rely on rendering to handle invalid alignments
		colAlignsResult[i] = currentAlign
	}

	t.logger.Debugf("Aligns built: %v (length %d)", colAlignsResult, len(colAlignsResult))
	return colAlignsResult
}

// buildPadding constructs a map of column padding settings from configuration.
// Parameter padding provides padding settings for the section.
// Returns a map of column indices to padding settings.
func (t *Table) buildPadding(padding tw.CellPadding) map[int]tw.Padding {
	numColsToUse := t.getNumColsToUse()
	colPadding := make(map[int]tw.Padding)
	for i := 0; i < numColsToUse; i++ {
		if i < len(padding.PerColumn) && padding.PerColumn[i].Paddable() {
			colPadding[i] = padding.PerColumn[i]
		} else {
			colPadding[i] = padding.Global
		}
	}
	t.logger.Debugf("Padding built: %v (length %d)", colPadding, len(colPadding))
	return colPadding
}

// ensureInitialized initializes required fields before use.
// No parameters are required.
// No return value.
func (t *Table) ensureInitialized() {
	if t.headerWidths == nil {
		t.headerWidths = tw.NewMapper[int, int]()
	}
	if t.rowWidths == nil {
		t.rowWidths = tw.NewMapper[int, int]()
	}
	if t.footerWidths == nil {
		t.footerWidths = tw.NewMapper[int, int]()
	}
	if t.renderer == nil {
		t.renderer = renderer.NewBlueprint()
	}
	t.logger.Debug("ensureInitialized called")
}

// finalizeHierarchicalMergeBlock sets Span and End for hierarchical merges.
// Parameters include ctx, mctx, col, startRow, and endRow.
// No return value.
func (t *Table) finalizeHierarchicalMergeBlock(ctx *renderContext, mctx *mergeContext, col, startRow, endRow int) {
	if endRow < startRow {
		ctx.logger.Debugf("Hierarchical merge FINALIZE WARNING: Invalid block col %d, start %d > end %d", col, startRow, endRow)
		return
	}
	if startRow < 0 || endRow < 0 {
		ctx.logger.Debugf("Hierarchical merge FINALIZE WARNING: Negative row indices col %d, start %d, end %d", col, startRow, endRow)
		return
	}
	requiredLen := endRow + 1
	if requiredLen > len(mctx.rowMerges) {
		ctx.logger.Debugf("Hierarchical merge FINALIZE WARNING: rowMerges slice too short (len %d) for endRow %d", len(mctx.rowMerges), endRow)
		return
	}
	if mctx.rowMerges[startRow] == nil {
		mctx.rowMerges[startRow] = make(map[int]tw.MergeState)
	}
	if mctx.rowMerges[endRow] == nil {
		mctx.rowMerges[endRow] = make(map[int]tw.MergeState)
	}

	finalSpan := (endRow - startRow) + 1
	ctx.logger.Debugf("Finalizing H-merge block: col=%d, startRow=%d, endRow=%d, span=%d", col, startRow, endRow, finalSpan)

	startState := mctx.rowMerges[startRow][col]
	if startState.Hierarchical.Present && startState.Hierarchical.Start {
		startState.Hierarchical.Span = finalSpan
		startState.Hierarchical.End = finalSpan == 1
		mctx.rowMerges[startRow][col] = startState
		ctx.logger.Debugf(" -> Updated start state: %+v", startState.Hierarchical)
	} else {
		ctx.logger.Debugf("Hierarchical merge FINALIZE WARNING: col %d, startRow %d was not marked as Present/Start? Current state: %+v. Attempting recovery.", col, startRow, startState.Hierarchical)
		startState.Hierarchical.Present = true
		startState.Hierarchical.Start = true
		startState.Hierarchical.Span = finalSpan
		startState.Hierarchical.End = finalSpan == 1
		mctx.rowMerges[startRow][col] = startState
	}

	if endRow > startRow {
		endState := mctx.rowMerges[endRow][col]
		if endState.Hierarchical.Present && !endState.Hierarchical.Start {
			endState.Hierarchical.End = true
			endState.Hierarchical.Span = finalSpan
			mctx.rowMerges[endRow][col] = endState
			ctx.logger.Debugf(" -> Updated end state: %+v", endState.Hierarchical)
		} else {
			ctx.logger.Debugf("Hierarchical merge FINALIZE WARNING: col %d, endRow %d was not marked as Present/Continuation? Current state: %+v. Attempting recovery.", col, endRow, endState.Hierarchical)
			endState.Hierarchical.Present = true
			endState.Hierarchical.Start = false
			endState.Hierarchical.End = true
			endState.Hierarchical.Span = finalSpan
			mctx.rowMerges[endRow][col] = endState
		}
	} else {
		ctx.logger.Debugf(" -> Span is 1, startRow is also endRow.")
	}
}

// getLevel maps a position to its rendering level.
// Parameter position specifies the section (Header, Row, Footer).
// Returns the corresponding tw.Level (Header, Body, Footer).
func (t *Table) getLevel(position tw.Position) tw.Level {
	switch position {
	case tw.Header:
		return tw.LevelHeader
	case tw.Row:
		return tw.LevelBody
	case tw.Footer:
		return tw.LevelFooter
	default:
		return tw.LevelBody
	}
}

// hasFooterElements checks if the footer has renderable elements.
// No parameters are required.
// Returns true if footer has content or padding, false otherwise.
func (t *Table) hasFooterElements() bool {
	hasContent := len(t.footers) > 0
	hasTopPadding := t.config.Footer.Padding.Global.Top != tw.Empty
	hasBottomPaddingConfig := t.config.Footer.Padding.Global.Bottom != tw.Empty || t.hasPerColumnBottomPadding()
	return hasContent || hasTopPadding || hasBottomPaddingConfig
}

// hasPerColumnBottomPadding checks for per-column bottom padding in footer.
// No parameters are required.
// Returns true if any per-column bottom padding is defined.
func (t *Table) hasPerColumnBottomPadding() bool {
	if t.config.Footer.Padding.PerColumn == nil {
		return false
	}
	for _, pad := range t.config.Footer.Padding.PerColumn {
		if pad.Bottom != tw.Empty {
			return true
		}
	}
	return false
}

// Logger retrieves the table's logger instance.
// No parameters are required.
// Returns the ll.Logger instance used for debug tracing.
func (t *Table) Logger() *ll.Logger {
	return t.logger
}

// Renderer retrieves the current renderer instance used by the table.
// No parameters are required.
// Returns the tw.Renderer interface instance.
func (t *Table) Renderer() tw.Renderer {
	t.logger.Debug("Renderer requested")
	return t.renderer
}

// maxColumns calculates the maximum column count across sections.
// No parameters are required.
// Returns the highest number of columns found.
func (t *Table) maxColumns() int {
	m := 0
	if len(t.headers) > 0 && len(t.headers[0]) > m {
		m = len(t.headers[0])
	}
	for _, row := range t.rows {
		if len(row) > m {
			m = len(row)
		}
	}
	if len(t.footers) > 0 && len(t.footers[0]) > m {
		m = len(t.footers[0])
	}
	t.logger.Debugf("Max columns: %d", m)
	return m
}

// printTopBottomCaption prints the table's caption at the specified top or bottom position.
// It wraps the caption text to fit the table width or a user-defined width, aligns it according
// to the specified alignment, and writes it to the provided writer. If the caption text is empty
// or the spot is invalid, it logs the issue and returns without printing. The function handles
// wrapping errors by falling back to splitting on newlines or using the original text.
func (t *Table) printTopBottomCaption(w io.Writer, actualTableWidth int) {
	t.logger.Debugf("[printCaption Entry] Text=%q, Spot=%v (type %T), Align=%q, UserWidth=%d, ActualTableWidth=%d",
		t.caption.Text, t.caption.Spot, t.caption.Spot, t.caption.Align, t.caption.Width, actualTableWidth)

	currentCaptionSpot := t.caption.Spot
	isValidSpot := currentCaptionSpot >= tw.SpotTopLeft && currentCaptionSpot <= tw.SpotBottomRight
	if t.caption.Text == "" || !isValidSpot {
		t.logger.Debugf("[printCaption] Aborting: Text empty OR Spot invalid...")
		return
	}

	var captionWrapWidth int
	if t.caption.Width > 0 {
		captionWrapWidth = t.caption.Width
		t.logger.Debugf("[printCaption] Using user-defined caption.Width %d for wrapping.", captionWrapWidth)
	} else if actualTableWidth <= 4 {
		captionWrapWidth = twwidth.Width(t.caption.Text)
		t.logger.Debugf("[printCaption] Empty table, no user caption.Width: Using natural caption width %d.", captionWrapWidth)
	} else {
		captionWrapWidth = actualTableWidth
		t.logger.Debugf("[printCaption] Non-empty table, no user caption.Width: Using actualTableWidth %d for wrapping.", captionWrapWidth)
	}

	if captionWrapWidth <= 0 {
		captionWrapWidth = 10
		t.logger.Warnf("[printCaption] captionWrapWidth was %d (<=0). Setting to minimum %d.", captionWrapWidth, 10)
	}
	t.logger.Debugf("[printCaption] Final captionWrapWidth to be used by twwarp: %d", captionWrapWidth)

	wrappedCaptionLines, count := twwarp.WrapString(t.caption.Text, captionWrapWidth)
	if count == 0 {
		t.logger.Errorf("[printCaption] Error from twwarp.WrapString (width %d): %v. Text: %q", captionWrapWidth, count, t.caption.Text)
		if strings.Contains(t.caption.Text, "\n") {
			wrappedCaptionLines = strings.Split(t.caption.Text, "\n")
		} else {
			wrappedCaptionLines = []string{t.caption.Text}
		}
		t.logger.Debugf("[printCaption] Fallback: using %d lines from original text.", len(wrappedCaptionLines))
	}

	if len(wrappedCaptionLines) == 0 && t.caption.Text != "" {
		t.logger.Warn("[printCaption] Wrapping resulted in zero lines for non-empty text. Using fallback.")
		if strings.Contains(t.caption.Text, "\n") {
			wrappedCaptionLines = strings.Split(t.caption.Text, "\n")
		} else {
			wrappedCaptionLines = []string{t.caption.Text}
		}
	} else if t.caption.Text != "" {
		t.logger.Debugf("[printCaption] Wrapped caption into %d lines: %v", len(wrappedCaptionLines), wrappedCaptionLines)
	}

	paddingTargetWidth := actualTableWidth
	if t.caption.Width > 0 {
		paddingTargetWidth = t.caption.Width
	} else if actualTableWidth <= 4 {
		paddingTargetWidth = captionWrapWidth
	}
	t.logger.Debugf("[printCaption] Final paddingTargetWidth for tw.Pad: %d", paddingTargetWidth)

	for i, line := range wrappedCaptionLines {
		align := t.caption.Align
		if align == "" || align == tw.AlignDefault || align == tw.AlignNone {
			switch t.caption.Spot {
			case tw.SpotTopLeft, tw.SpotBottomLeft:
				align = tw.AlignLeft
			case tw.SpotTopRight, tw.SpotBottomRight:
				align = tw.AlignRight
			default:
				align = tw.AlignCenter
			}
			t.logger.Debugf("[printCaption] Line %d: Alignment defaulted to %s based on Spot %v", i, align, t.caption.Spot)
		}
		paddedLine := tw.Pad(line, " ", paddingTargetWidth, align)
		t.logger.Debugf("[printCaption] Printing line %d: InputLine=%q, Align=%s, PaddingTargetWidth=%d, PaddedLine=%q",
			i, line, align, paddingTargetWidth, paddedLine)
		w.Write([]byte(paddedLine))
		w.Write([]byte(tw.NewLine))
	}

	t.logger.Debugf("[printCaption] Finished printing all caption lines.")
}

// prepareContent processes cell content with formatting and wrapping.
// Parameters include cells to process and config for formatting rules.
// Returns a slice of string slices representing processed lines.
func (t *Table) prepareContent(cells []string, config tw.CellConfig) [][]string {
	isStreaming := t.config.Stream.Enable && t.hasPrinted
	t.logger.Debugf("prepareContent: Processing cells=%v (streaming: %v)", cells, isStreaming)
	initialInputCellCount := len(cells)
	result := make([][]string, 0)

	effectiveNumCols := initialInputCellCount
	if isStreaming {
		if t.streamNumCols > 0 {
			effectiveNumCols = t.streamNumCols
			t.logger.Debugf("prepareContent: Streaming mode, using fixed streamNumCols: %d", effectiveNumCols)
			if len(cells) != effectiveNumCols {
				t.logger.Warnf("prepareContent: Streaming mode, input cell count (%d) does not match streamNumCols (%d). Input cells will be padded/truncated.", len(cells), effectiveNumCols)
				if len(cells) < effectiveNumCols {
					paddedCells := make([]string, effectiveNumCols)
					copy(paddedCells, cells)
					for i := len(cells); i < effectiveNumCols; i++ {
						paddedCells[i] = tw.Empty
					}
					cells = paddedCells
				} else if len(cells) > effectiveNumCols {
					cells = cells[:effectiveNumCols]
				}
			}
		} else {
			t.logger.Warnf("prepareContent: Streaming mode enabled but streamNumCols is 0. Using input cell count %d. Stream widths may not be available.", effectiveNumCols)
		}
	}

	if t.config.MaxWidth > 0 && !t.config.Widths.Constrained() {
		if effectiveNumCols > 0 {
			derivedSectionGlobalMaxWidth := int(math.Floor(float64(t.config.MaxWidth) / float64(effectiveNumCols)))
			config.ColMaxWidths.Global = derivedSectionGlobalMaxWidth
			t.logger.Debugf("prepareContent: Table MaxWidth %d active and t.config.Widths not constrained. "+
				"Derived section ColMaxWidths.Global: %d for %d columns. This will be used by calculateContentMaxWidth if no higher priority constraints exist.",
				t.config.MaxWidth, config.ColMaxWidths.Global, effectiveNumCols)
		}
	}

	for i := 0; i < effectiveNumCols; i++ {
		cellContent := ""
		if i < len(cells) {
			cellContent = cells[i]
		} else {
			cellContent = tw.Empty
		}

		cellContent = t.Trimmer(cellContent)

		colPad := config.Padding.Global
		if i < len(config.Padding.PerColumn) && config.Padding.PerColumn[i].Paddable() {
			colPad = config.Padding.PerColumn[i]
		}

		padLeftWidth := twwidth.Width(colPad.Left)
		padRightWidth := twwidth.Width(colPad.Right)

		effectiveContentMaxWidth := t.calculateContentMaxWidth(i, config, padLeftWidth, padRightWidth, isStreaming)

		if config.Formatting.AutoFormat.Enabled() {
			cellContent = tw.Title(strings.Join(tw.SplitCamelCase(cellContent), tw.Space))
		}

		lines := strings.Split(cellContent, "\n")
		finalLinesForCell := make([]string, 0)
		for _, line := range lines {
			if effectiveContentMaxWidth > 0 {
				switch config.Formatting.AutoWrap {
				case tw.WrapNormal:
					var wrapped []string
					if t.config.Behavior.TrimSpace.Enabled() {
						wrapped, _ = twwarp.WrapString(line, effectiveContentMaxWidth)
					} else {
						wrapped, _ = twwarp.WrapStringWithSpaces(line, effectiveContentMaxWidth)
					}
					finalLinesForCell = append(finalLinesForCell, wrapped...)
				case tw.WrapTruncate:
					if twwidth.Width(line) > effectiveContentMaxWidth {
						ellipsisWidth := twwidth.Width(tw.CharEllipsis)
						if effectiveContentMaxWidth >= ellipsisWidth {
							finalLinesForCell = append(finalLinesForCell, twwidth.Truncate(line, effectiveContentMaxWidth-ellipsisWidth, tw.CharEllipsis))
						} else {
							finalLinesForCell = append(finalLinesForCell, twwidth.Truncate(line, effectiveContentMaxWidth, ""))
						}
					} else {
						finalLinesForCell = append(finalLinesForCell, line)
					}
				case tw.WrapBreak:
					wrapped := make([]string, 0)
					currentLine := line
					breakCharWidth := twwidth.Width(tw.CharBreak)
					for twwidth.Width(currentLine) > effectiveContentMaxWidth {
						targetWidth := max(effectiveContentMaxWidth-breakCharWidth, 0)
						breakPoint := tw.BreakPoint(currentLine, targetWidth)
						runes := []rune(currentLine)
						if breakPoint <= 0 || breakPoint > len(runes) {
							t.logger.Warnf("prepareContent: WrapBreak - Invalid BreakPoint %d for line '%s' at width %d. Attempting manual break.", breakPoint, currentLine, targetWidth)
							actualBreakRuneCount := 0
							tempWidth := 0
							for charIdx, r := range runes {
								runeStr := string(r)
								rw := twwidth.Width(runeStr)
								if tempWidth+rw > targetWidth && charIdx > 0 {
									break
								}
								tempWidth += rw
								actualBreakRuneCount = charIdx + 1
								if tempWidth >= targetWidth && charIdx == 0 {
									break
								}
							}
							if actualBreakRuneCount == 0 && len(runes) > 0 {
								actualBreakRuneCount = 1
							}
							if actualBreakRuneCount > 0 && actualBreakRuneCount <= len(runes) {
								wrapped = append(wrapped, string(runes[:actualBreakRuneCount])+tw.CharBreak)
								currentLine = string(runes[actualBreakRuneCount:])
							} else {
								t.logger.Warnf("prepareContent: WrapBreak - Cannot break line '%s'. Adding as is.", currentLine)
								wrapped = append(wrapped, currentLine)
								currentLine = ""
								break
							}
						} else {
							wrapped = append(wrapped, string(runes[:breakPoint])+tw.CharBreak)
							currentLine = string(runes[breakPoint:])
						}
					}
					if twwidth.Width(currentLine) > 0 {
						wrapped = append(wrapped, currentLine)
					}
					if len(wrapped) == 0 && twwidth.Width(line) > 0 && len(finalLinesForCell) == 0 {
						finalLinesForCell = append(finalLinesForCell, line)
					} else {
						finalLinesForCell = append(finalLinesForCell, wrapped...)
					}
				default:
					finalLinesForCell = append(finalLinesForCell, line)
				}
			} else {
				finalLinesForCell = append(finalLinesForCell, line)
			}
		}

		for len(result) < len(finalLinesForCell) {
			newRow := make([]string, effectiveNumCols)
			for j := range newRow {
				newRow[j] = tw.Empty
			}
			result = append(result, newRow)
		}

		for j := 0; j < len(result); j++ {
			cellLineContent := tw.Empty
			if j < len(finalLinesForCell) {
				cellLineContent = finalLinesForCell[j]
			}
			if i < len(result[j]) {
				result[j][i] = cellLineContent
			} else {
				t.logger.Warnf("prepareContent: Column index %d out of bounds (%d) during result matrix population. EffectiveNumCols: %d. This indicates a logic error.",
					i, len(result[j]), effectiveNumCols)
			}
		}
	}

	t.logger.Debugf("prepareContent: Content prepared, result %d lines.", len(result))
	return result
}

// prepareContexts initializes rendering and merge contexts.
// No parameters are required.
// Returns renderContext, mergeContext, and an error if initialization fails.
func (t *Table) prepareContexts() (*renderContext, *mergeContext, error) {
	numOriginalCols := t.maxColumns()
	t.logger.Debugf("prepareContexts: Original number of columns: %d", numOriginalCols)

	ctx := &renderContext{
		table:    t,
		renderer: t.renderer,
		cfg:      t.renderer.Config(),
		numCols:  numOriginalCols,
		widths: map[tw.Position]tw.Mapper[int, int]{
			tw.Header: tw.NewMapper[int, int](),
			tw.Row:    tw.NewMapper[int, int](),
			tw.Footer: tw.NewMapper[int, int](),
		},
		logger: t.logger,
	}

	// Process raw rows into visual, multi-line rows
	processedRowLines := make([][][]string, len(t.rows))
	for i, rawRow := range t.rows {
		processedRowLines[i] = t.prepareContent(rawRow, t.config.Row)
	}
	ctx.rowLines = processedRowLines

	isEmpty, visibleCount := t.getEmptyColumnInfo(ctx.rowLines, numOriginalCols)
	ctx.emptyColumns = isEmpty
	ctx.visibleColCount = visibleCount

	mctx := &mergeContext{
		headerMerges: make(map[int]tw.MergeState),
		rowMerges:    make([]map[int]tw.MergeState, len(ctx.rowLines)),
		footerMerges: make(map[int]tw.MergeState),
		horzMerges:   make(map[tw.Position]map[int]bool),
	}
	for i := range mctx.rowMerges {
		mctx.rowMerges[i] = make(map[int]tw.MergeState)
	}

	ctx.headerLines = t.headers
	ctx.footerLines = t.footers

	if err := t.calculateAndNormalizeWidths(ctx); err != nil {
		t.logger.Debugf("Error during initial width calculation: %v", err)
		return nil, nil, err
	}
	t.logger.Debugf("Initial normalized widths (before hiding): H=%v, R=%v, F=%v",
		ctx.widths[tw.Header], ctx.widths[tw.Row], ctx.widths[tw.Footer])

	preparedHeaderLines, headerMerges, _ := t.prepareWithMerges(ctx.headerLines, t.config.Header, tw.Header)
	ctx.headerLines = preparedHeaderLines
	mctx.headerMerges = headerMerges

	// Re-process row lines for merges now that widths are known
	processedRowLinesWithMerges := make([][][]string, len(ctx.rowLines))
	for i, row := range ctx.rowLines {
		if mctx.rowMerges[i] == nil {
			mctx.rowMerges[i] = make(map[int]tw.MergeState)
		}
		processedRowLinesWithMerges[i], mctx.rowMerges[i], _ = t.prepareWithMerges(row, t.config.Row, tw.Row)
	}
	ctx.rowLines = processedRowLinesWithMerges

	t.applyHorizontalMerges(tw.Header, ctx, mctx.headerMerges)

	mergeMode := t.config.Row.Merging.Mode
	if mergeMode == 0 {
		mergeMode = t.config.Row.Formatting.MergeMode
	}

	// Now check against the effective mode
	if mergeMode&tw.MergeVertical != 0 {
		t.applyVerticalMerges(ctx, mctx)
	}
	if mergeMode&tw.MergeHierarchical != 0 {
		t.applyHierarchicalMerges(ctx, mctx)
	}

	t.prepareFooter(ctx, mctx)
	t.logger.Debugf("Footer prepared. Widths before hiding: H=%v, R=%v, F=%v",
		ctx.widths[tw.Header], ctx.widths[tw.Row], ctx.widths[tw.Footer])

	if t.config.Behavior.AutoHide.Enabled() {
		t.logger.Debugf("Applying AutoHide: Adjusting widths for empty columns.")
		if ctx.emptyColumns == nil {
			t.logger.Debugf("Warning: ctx.emptyColumns is nil during width adjustment.")
		} else if len(ctx.emptyColumns) != ctx.numCols {
			t.logger.Debugf("Warning: Length mismatch between emptyColumns (%d) and numCols (%d). Skipping adjustment.", len(ctx.emptyColumns), ctx.numCols)
		} else {
			for colIdx := 0; colIdx < ctx.numCols; colIdx++ {
				if ctx.emptyColumns[colIdx] {
					t.logger.Debugf("AutoHide: Hiding column %d by setting width to 0.", colIdx)
					ctx.widths[tw.Header].Set(colIdx, 0)
					ctx.widths[tw.Row].Set(colIdx, 0)
					ctx.widths[tw.Footer].Set(colIdx, 0)
				}
			}
			t.logger.Debugf("Widths after AutoHide adjustment: H=%v, R=%v, F=%v",
				ctx.widths[tw.Header], ctx.widths[tw.Row], ctx.widths[tw.Footer])
		}
	} else {
		t.logger.Debugf("AutoHide is disabled, skipping width adjustment.")
	}
	t.logger.Debugf("prepareContexts completed all stages.")
	return ctx, mctx, nil
}

// prepareFooter processes footer content and applies merges.
// Parameters ctx and mctx hold rendering and merge state.
// No return value.
func (t *Table) prepareFooter(ctx *renderContext, mctx *mergeContext) {
	if len(t.footers) == 0 {
		ctx.logger.Debugf("Skipping footer preparation - no footer data")
		if ctx.widths[tw.Footer] == nil {
			ctx.widths[tw.Footer] = tw.NewMapper[int, int]()
		}
		numCols := ctx.numCols
		for i := 0; i < numCols; i++ {
			ctx.widths[tw.Footer].Set(i, ctx.widths[tw.Row].Get(i))
		}
		t.logger.Debug("Initialized empty footer widths based on row widths: %v", ctx.widths[tw.Footer])
		ctx.footerPrepared = true
		return
	}

	t.logger.Debugf("Preparing footer with merge mode: %d", t.config.Footer.Formatting.MergeMode)
	preparedLines, mergeStates, _ := t.prepareWithMerges(t.footers, t.config.Footer, tw.Footer)
	t.footers = preparedLines
	mctx.footerMerges = mergeStates
	ctx.footerLines = t.footers
	t.logger.Debugf("Base footer widths (normalized from rows/header): %v", ctx.widths[tw.Footer])
	t.applyHorizontalMerges(tw.Footer, ctx, mctx.footerMerges)
	ctx.footerPrepared = true
	t.logger.Debugf("Footer preparation completed. Final footer widths: %v", ctx.widths[tw.Footer])
}

// prepareWithMerges processes content and detects horizontal merges.
// Parameters include content, config, and position (Header, Row, Footer).
// Returns processed lines, merge states, and horizontal merge map.
func (t *Table) prepareWithMerges(content [][]string, config tw.CellConfig, position tw.Position) ([][]string, map[int]tw.MergeState, map[int]bool) {
	t.logger.Debugf("PrepareWithMerges START: position=%s, mergeMode=%d", position, config.Formatting.MergeMode)
	if len(content) == 0 {
		t.logger.Debugf("PrepareWithMerges END: No content.")
		return content, nil, nil
	}

	numCols := 0
	if len(content) > 0 && len(content[0]) > 0 { // Assumes content[0] exists and has items
		numCols = len(content[0])
	} else { // Fallback if first line is empty or content is empty
		for _, line := range content { // Find max columns from any line
			if len(line) > numCols {
				numCols = len(line)
			}
		}
		if numCols == 0 { // If still 0, try table-wide max (batch mode context)
			numCols = t.maxColumns()
		}
	}

	if numCols == 0 {
		t.logger.Debugf("PrepareWithMerges END: numCols is zero.")
		return content, nil, nil
	}

	horzMergeMap := make(map[int]bool)      // Tracks if a column is part of any horizontal merge for this logical row
	mergeMap := make(map[int]tw.MergeState) // Final merge states for this logical row

	// Ensure all lines in 'content' are padded to numCols for consistent processing
	// This result is what will be modified and returned.
	result := make([][]string, len(content))
	for i := range content {
		result[i] = padLine(content[i], numCols)
	}

	if config.Formatting.MergeMode&tw.MergeHorizontal != 0 {
		t.logger.Debugf("Checking for horizontal merges (logical cell comparison) for %d visual lines, %d columns", len(content), numCols)

		// Special handling for footer lead merge (often for "TOTAL" spanning empty cells)
		// This logic only applies if it's a footer and typically to the first (often only) visual line.
		if position == tw.Footer && len(content) > 0 {
			lineIdx := 0                                       // Assume footer lead merge applies to the first visual line primarily
			originalLine := padLine(content[lineIdx], numCols) // Use original content for decision
			currentLineResult := result[lineIdx]               // Modify the result line

			firstContentIdx := -1
			var firstContent string
			for c := 0; c < numCols; c++ {
				if c >= len(originalLine) {
					break
				}
				trimmedVal := t.Trimmer(originalLine[c])

				if trimmedVal != "" && trimmedVal != "-" { // "-" is often a placeholder not to merge over
					firstContentIdx = c
					firstContent = originalLine[c] // Store the raw content for placement
					break
				} else if trimmedVal == "-" { // Stop if we hit a hard non-mergeable placeholder
					break
				}
			}

			if firstContentIdx > 0 { // If content starts after the first column
				span := firstContentIdx + 1 // Merge from col 0 up to and including firstContentIdx
				startCol := 0

				allEmptyBefore := true
				for c := 0; c < firstContentIdx; c++ {
					originalLine[c] = t.Trimmer(originalLine[c])
					if c >= len(originalLine) || originalLine[c] != "" {
						allEmptyBefore = false
						break
					}
				}

				if allEmptyBefore {
					t.logger.Debugf("Footer lead-merge applied line %d: content '%s' from col %d moved to col %d, span %d",
						lineIdx, firstContent, firstContentIdx, startCol, span)

					if startCol < len(currentLineResult) {
						currentLineResult[startCol] = firstContent // Place the original content
					}
					for k := startCol + 1; k < startCol+span; k++ { // Clear out other cells in the span
						if k < len(currentLineResult) {
							currentLineResult[k] = tw.Empty
						}
					}

					// Update mergeMap for all visual lines of this logical row
					for visualLine := 0; visualLine < len(result); visualLine++ {
						// Only apply the data move to the line where it was detected,
						// but the merge state should apply to the logical cell (all its visual lines).
						if visualLine != lineIdx { // For other visual lines, just clear the cells in the span
							if startCol < len(result[visualLine]) {
								result[visualLine][startCol] = tw.Empty // Typically empty for other lines in a lead merge
							}
							for k := startCol + 1; k < startCol+span; k++ {
								if k < len(result[visualLine]) {
									result[visualLine][k] = tw.Empty
								}
							}
						}
					}

					// Set merge state for the starting column
					startState := mergeMap[startCol]
					startState.Horizontal = tw.MergeStateOption{Present: true, Span: span, Start: true, End: (span == 1)}
					mergeMap[startCol] = startState
					horzMergeMap[startCol] = true // Mark this column as processed by a merge

					// Set merge state for subsequent columns in the span
					for k := startCol + 1; k < startCol+span; k++ {
						colState := mergeMap[k]
						colState.Horizontal = tw.MergeStateOption{Present: true, Span: span, Start: false, End: k == startCol+span-1}
						mergeMap[k] = colState
						horzMergeMap[k] = true // Mark as processed
					}
				}
			}
		}

		// Standard horizontal merge logic based on full logical cell content
		col := 0
		for col < numCols {
			if horzMergeMap[col] { // If already part of a footer lead-merge, skip
				col++
				continue
			}

			// Get full content of logical cell 'col'
			var currentLogicalCellContentBuilder strings.Builder
			for lineIdx := 0; lineIdx < len(content); lineIdx++ {
				if col < len(content[lineIdx]) {
					currentLogicalCellContentBuilder.WriteString(content[lineIdx][col])
				}
			}

			currentLogicalCellTrimmed := t.Trimmer(currentLogicalCellContentBuilder.String())
			if currentLogicalCellTrimmed == "" || currentLogicalCellTrimmed == "-" {
				col++
				continue
			}

			span := 1
			for nextCol := col + 1; nextCol < numCols; nextCol++ {
				if horzMergeMap[nextCol] { // Don't merge into an already merged (e.g. footer lead) column
					break
				}
				var nextLogicalCellContentBuilder strings.Builder
				for lineIdx := 0; lineIdx < len(content); lineIdx++ {
					if nextCol < len(content[lineIdx]) {
						nextLogicalCellContentBuilder.WriteString(content[lineIdx][nextCol])
					}
				}

				nextLogicalCellTrimmed := t.Trimmer(nextLogicalCellContentBuilder.String())
				if currentLogicalCellTrimmed == nextLogicalCellTrimmed && nextLogicalCellTrimmed != "-" {
					span++
				} else {
					break
				}
			}

			if span > 1 {
				t.logger.Debugf("Standard horizontal merge (logical cell): startCol %d, span %d for content '%s'", col, span, currentLogicalCellTrimmed)
				startState := mergeMap[col]
				startState.Horizontal = tw.MergeStateOption{Present: true, Span: span, Start: true, End: (span == 1)}
				mergeMap[col] = startState
				horzMergeMap[col] = true

				// For all visual lines, clear out the content of the merged-over cells
				for lineIdx := 0; lineIdx < len(result); lineIdx++ {
					for k := col + 1; k < col+span; k++ {
						if k < len(result[lineIdx]) {
							result[lineIdx][k] = tw.Empty
						}
					}
				}

				// Set merge state for subsequent columns in the span
				for k := col + 1; k < col+span; k++ {
					colState := mergeMap[k]
					colState.Horizontal = tw.MergeStateOption{Present: true, Span: span, Start: false, End: k == col+span-1}
					mergeMap[k] = colState
					horzMergeMap[k] = true
				}
				col += span
			} else {
				col++
			}
		}
	}

	t.logger.Debugf("PrepareWithMerges END: position=%s, lines=%d, mergeMapH: %v", position, len(result), func() map[int]tw.MergeStateOption {
		m := make(map[int]tw.MergeStateOption)
		for k, v := range mergeMap {
			m[k] = v.Horizontal
		}
		return m
	}())
	return result, mergeMap, horzMergeMap
}

// render generates the table output using the configured renderer.
// No parameters are required.
// Returns an error if rendering fails in any section.
func (t *Table) render() error {
	t.ensureInitialized()

	// Save the original writer and schedule its restoration upon function exit.
	// This guarantees the table's writer is restored even if errors occur.
	originalWriter := t.writer
	defer func() {
		t.writer = originalWriter
	}()

	// If a counter is active, wrap the writer in a MultiWriter.
	if len(t.counters) > 0 {
		// The slice must be of type io.Writer.
		// Start it with the original destination writer.
		allWriters := []io.Writer{originalWriter}

		// Append each counter to the slice of writers.
		for _, c := range t.counters {
			allWriters = append(allWriters, c)
		}

		// Create a MultiWriter that broadcasts to the original writer AND all counters.
		t.writer = io.MultiWriter(allWriters...)
	}

	if t.config.Stream.Enable {
		t.logger.Warn("Render() called in streaming mode. Use Start/Append/Close methods instead.")
		return errors.New("render called in streaming mode; use Start/Append/Close")
	}

	// Calculate and cache the column count for this specific batch render pass.
	t.batchRenderNumCols = t.maxColumns()
	t.isBatchRenderNumColsSet = true
	defer func() {
		t.isBatchRenderNumColsSet = false
		t.logger.Debugf("Render(): Cleared isBatchRenderNumColsSet to false (batchRenderNumCols was %d).", t.batchRenderNumCols)
	}()

	hasCaption := t.caption.Text != "" && t.caption.Spot != tw.SpotNone
	isTopOrBottomCaption := hasCaption &&
		(t.caption.Spot >= tw.SpotTopLeft && t.caption.Spot <= tw.SpotBottomRight)

	var tableStringBuffer *strings.Builder
	targetWriter := t.writer // Can be the original writer or the MultiWriter.

	// If a caption is present, the main table content must be rendered to an
	// in-memory buffer first to calculate its final width.
	if isTopOrBottomCaption {
		tableStringBuffer = &strings.Builder{}
		targetWriter = tableStringBuffer
		t.logger.Debugf("Top/Bottom caption detected. Rendering table core to buffer first.")
	} else {
		t.logger.Debugf("No caption detected. Rendering table core directly to writer.")
	}

	// Point the table's writer to the target (either the final destination or the buffer).
	t.writer = targetWriter
	ctx, mctx, err := t.prepareContexts()
	if err != nil {
		t.logger.Errorf("prepareContexts failed: %v", err)
		return errors.Newf("failed to prepare table contexts").Wrap(err)
	}

	if err := ctx.renderer.Start(t.writer); err != nil {
		t.logger.Errorf("Renderer Start() error: %v", err)
		return errors.Newf("renderer start failed").Wrap(err)
	}

	renderError := false
	var firstRenderErr error
	renderFuncs := []func(*renderContext, *mergeContext) error{
		t.renderHeader,
		t.renderRow,
		t.renderFooter,
	}
	for i, renderFn := range renderFuncs {
		sectionName := []string{"Header", "Row", "Footer"}[i]
		if renderErr := renderFn(ctx, mctx); renderErr != nil {
			t.logger.Errorf("Renderer section error (%s): %v", sectionName, renderErr)
			if !renderError {
				firstRenderErr = errors.Newf("failed to render %s section", sectionName).Wrap(renderErr)
			}
			renderError = true
			break
		}
	}

	if closeErr := ctx.renderer.Close(); closeErr != nil {
		t.logger.Errorf("Renderer Close() error: %v", closeErr)
		if !renderError {
			firstRenderErr = errors.Newf("renderer close failed").Wrap(closeErr)
		}
		renderError = true
	}

	// Restore the writer to the original for the caption-handling logic.
	// This is necessary because the caption must be written to the final
	// destination, not the temporary buffer used for the table body.
	t.writer = originalWriter

	if renderError {
		return firstRenderErr
	}

	// Caption Handling & Final Output
	if isTopOrBottomCaption {
		renderedTableContent := tableStringBuffer.String()
		t.logger.Debugf("[Render] Table core buffer length: %d", len(renderedTableContent))

		// Handle edge case where table is empty but should have borders.
		shouldHaveBorders := t.renderer != nil && (t.renderer.Config().Borders.Top.Enabled() || t.renderer.Config().Borders.Bottom.Enabled())
		if len(renderedTableContent) == 0 && shouldHaveBorders {
			var sb strings.Builder
			if t.renderer.Config().Borders.Top.Enabled() {
				sb.WriteString("+--+")
				sb.WriteString(t.newLine)
			}
			if t.renderer.Config().Borders.Bottom.Enabled() {
				sb.WriteString("+--+")
			}
			renderedTableContent = sb.String()
			t.logger.Warnf("[Render] Table buffer was empty despite enabled borders. Manually generated minimal output: %q", renderedTableContent)
		}

		actualTableWidth := 0
		trimmedBuffer := strings.TrimRight(renderedTableContent, "\r\n \t")
		for _, line := range strings.Split(trimmedBuffer, "\n") {
			w := twwidth.Width(line)
			if w > actualTableWidth {
				actualTableWidth = w
			}
		}
		t.logger.Debugf("[Render] Calculated actual table width: %d (from content: %q)", actualTableWidth, renderedTableContent)

		isTopCaption := t.caption.Spot >= tw.SpotTopLeft && t.caption.Spot <= tw.SpotTopRight

		if isTopCaption {
			t.logger.Debugf("[Render] Printing Top Caption.")
			t.printTopBottomCaption(t.writer, actualTableWidth)
		}

		if len(renderedTableContent) > 0 {
			t.logger.Debugf("[Render] Printing table content (length %d) to final writer.", len(renderedTableContent))
			t.writer.Write([]byte(renderedTableContent))
			if !isTopCaption && t.caption.Text != "" && !strings.HasSuffix(renderedTableContent, t.newLine) {
				t.writer.Write([]byte(tw.NewLine))
				t.logger.Debugf("[Render] Added trailing newline after table content before bottom caption.")
			}
		} else {
			t.logger.Debugf("[Render] No table content (original buffer or generated) to print.")
		}

		if !isTopCaption {
			t.logger.Debugf("[Render] Calling printTopBottomCaption for Bottom Caption. Width: %d", actualTableWidth)
			t.printTopBottomCaption(t.writer, actualTableWidth)
			t.logger.Debugf("[Render] Returned from printTopBottomCaption for Bottom Caption.")
		}
	}

	t.hasPrinted = true
	t.logger.Info("Render() completed.")
	return nil
}

// renderFooter renders the table's footer section with borders and padding.
// Parameters ctx and mctx hold rendering and merge state.
// Returns an error if rendering fails.
func (t *Table) renderFooter(ctx *renderContext, mctx *mergeContext) error {
	if !ctx.footerPrepared {
		t.prepareFooter(ctx, mctx)
	}

	f := ctx.renderer
	cfg := ctx.cfg

	hasContent := len(ctx.footerLines) > 0
	hasTopPadding := t.config.Footer.Padding.Global.Top != tw.Empty
	hasBottomPaddingConfig := t.config.Footer.Padding.Global.Bottom != tw.Empty || t.hasPerColumnBottomPadding()
	hasAnyFooterElement := hasContent || hasTopPadding || hasBottomPaddingConfig

	if !hasAnyFooterElement {
		hasContentAbove := len(ctx.rowLines) > 0 || len(ctx.headerLines) > 0
		if hasContentAbove && cfg.Borders.Bottom.Enabled() && cfg.Settings.Lines.ShowBottom.Enabled() {
			ctx.logger.Debugf("Footer is empty, rendering table bottom border based on last row/header")
			var lastLineAboveCtx *helperContext
			var lastLineAligns map[int]tw.Align
			var lastLinePadding map[int]tw.Padding

			if len(ctx.rowLines) > 0 {
				lastRowIdx := len(ctx.rowLines) - 1
				lastRowLineIdx := -1
				var lastRowLine []string
				if lastRowIdx >= 0 && len(ctx.rowLines[lastRowIdx]) > 0 {
					lastRowLineIdx = len(ctx.rowLines[lastRowIdx]) - 1
					lastRowLine = padLine(ctx.rowLines[lastRowIdx][lastRowLineIdx], ctx.numCols)
				} else {
					lastRowLine = make([]string, ctx.numCols)
				}
				lastLineAboveCtx = &helperContext{
					position: tw.Row,
					rowIdx:   lastRowIdx,
					lineIdx:  lastRowLineIdx,
					line:     lastRowLine,
					location: tw.LocationEnd,
				}
				lastLineAligns = t.buildAligns(t.config.Row)
				lastLinePadding = t.buildPadding(t.config.Row.Padding)
			} else {
				lastHeaderLineIdx := -1
				var lastHeaderLine []string
				if len(ctx.headerLines) > 0 {
					lastHeaderLineIdx = len(ctx.headerLines) - 1
					lastHeaderLine = padLine(ctx.headerLines[lastHeaderLineIdx], ctx.numCols)
				} else {
					lastHeaderLine = make([]string, ctx.numCols)
				}
				lastLineAboveCtx = &helperContext{
					position: tw.Header,
					rowIdx:   0,
					lineIdx:  lastHeaderLineIdx,
					line:     lastHeaderLine,
					location: tw.LocationEnd,
				}
				lastLineAligns = t.buildAligns(t.config.Header)
				lastLinePadding = t.buildPadding(t.config.Header.Padding)
			}

			resp := t.buildCellContexts(ctx, mctx, lastLineAboveCtx, lastLineAligns, lastLinePadding)
			ctx.logger.Debugf("Bottom border: Using Widths=%v", ctx.widths[tw.Row])
			f.Line(tw.Formatting{
				Row: tw.RowContext{
					Widths:       ctx.widths[tw.Row],
					Current:      resp.cells,
					Previous:     resp.prevCells,
					Position:     lastLineAboveCtx.position,
					Location:     tw.LocationEnd,
					ColMaxWidths: t.getColMaxWidths(tw.Footer),
				},
				Level:    tw.LevelFooter,
				IsSubRow: false,
			})
		} else {
			ctx.logger.Debugf("Footer is empty and no content above or borders disabled, skipping footer render")
		}
		return nil
	}

	ctx.logger.Debugf("Rendering footer section (has elements)")
	hasContentAbove := len(ctx.rowLines) > 0 || len(ctx.headerLines) > 0
	colAligns := t.buildAligns(t.config.Footer)
	colPadding := t.buildPadding(t.config.Footer.Padding)
	hctx := &helperContext{position: tw.Footer}
	// Declare paddingLineContentForContext with a default value
	paddingLineContentForContext := make([]string, ctx.numCols)

	if hasContentAbove && cfg.Settings.Lines.ShowFooterLine.Enabled() && !hasTopPadding && len(ctx.footerLines) > 0 {
		ctx.logger.Debugf("Rendering footer separator line")
		var lastLineAboveCtx *helperContext
		var lastLineAligns map[int]tw.Align
		var lastLinePadding map[int]tw.Padding
		var lastLinePosition tw.Position

		if len(ctx.rowLines) > 0 {
			lastRowIdx := len(ctx.rowLines) - 1
			lastRowLineIdx := -1
			var lastRowLine []string
			if lastRowIdx >= 0 && len(ctx.rowLines[lastRowIdx]) > 0 {
				lastRowLineIdx = len(ctx.rowLines[lastRowIdx]) - 1
				lastRowLine = padLine(ctx.rowLines[lastRowIdx][lastRowLineIdx], ctx.numCols)
			} else {
				lastRowLine = make([]string, ctx.numCols)
			}
			lastLineAboveCtx = &helperContext{
				position: tw.Row,
				rowIdx:   lastRowIdx,
				lineIdx:  lastRowLineIdx,
				line:     lastRowLine,
				location: tw.LocationMiddle,
			}
			lastLineAligns = t.buildAligns(t.config.Row)
			lastLinePadding = t.buildPadding(t.config.Row.Padding)
			lastLinePosition = tw.Row
		} else {
			lastHeaderLineIdx := -1
			var lastHeaderLine []string
			if len(ctx.headerLines) > 0 {
				lastHeaderLineIdx = len(ctx.headerLines) - 1
				lastHeaderLine = padLine(ctx.headerLines[lastHeaderLineIdx], ctx.numCols)
			} else {
				lastHeaderLine = make([]string, ctx.numCols)
			}
			lastLineAboveCtx = &helperContext{
				position: tw.Header,
				rowIdx:   0,
				lineIdx:  lastHeaderLineIdx,
				line:     lastHeaderLine,
				location: tw.LocationMiddle,
			}
			lastLineAligns = t.buildAligns(t.config.Header)
			lastLinePadding = t.buildPadding(t.config.Header.Padding)
			lastLinePosition = tw.Header
		}

		resp := t.buildCellContexts(ctx, mctx, lastLineAboveCtx, lastLineAligns, lastLinePadding)
		var nextCells map[int]tw.CellContext
		if hasContent {
			nextCells = make(map[int]tw.CellContext)
			for j, cellData := range padLine(ctx.footerLines[0], ctx.numCols) {
				mergeState := tw.MergeState{}
				if mctx.footerMerges != nil {
					mergeState = mctx.footerMerges[j]
				}
				nextCells[j] = tw.CellContext{Data: cellData, Merge: mergeState, Width: ctx.widths[tw.Footer].Get(j)}
			}
		}
		ctx.logger.Debugf("Footer separator: Using Widths=%v", ctx.widths[tw.Row])
		f.Line(tw.Formatting{
			Row: tw.RowContext{
				Widths:       ctx.widths[tw.Row],
				Current:      resp.cells,
				Previous:     resp.prevCells,
				Next:         nextCells,
				Position:     lastLinePosition,
				Location:     tw.LocationMiddle,
				ColMaxWidths: t.getColMaxWidths(tw.Footer),
			},
			Level:     tw.LevelFooter,
			IsSubRow:  false,
			HasFooter: true,
		})
	}

	if hasTopPadding {
		hctx.rowIdx = 0
		hctx.lineIdx = -1
		if !hasContentAbove || !cfg.Settings.Lines.ShowFooterLine.Enabled() {
			hctx.location = tw.LocationFirst
		} else {
			hctx.location = tw.LocationMiddle
		}
		hctx.line = t.buildPaddingLineContents(t.config.Footer.Padding.Global.Top, ctx.widths[tw.Footer], ctx.numCols, mctx.footerMerges)
		ctx.logger.Debugf("Calling renderPadding for Footer Top Padding line: %v (loc: %v)", hctx.line, hctx.location)
		if err := t.renderPadding(ctx, mctx, hctx, t.config.Footer.Padding.Global.Top); err != nil {
			return err
		}
	}

	lastRenderedLineIdx := -2
	if hasTopPadding {
		lastRenderedLineIdx = -1
	}
	for i, line := range ctx.footerLines {
		hctx.rowIdx = 0
		hctx.lineIdx = i
		hctx.line = padLine(line, ctx.numCols)
		isFirstContentLine := i == 0
		isLastContentLine := i == len(ctx.footerLines)-1
		if isFirstContentLine && !hasTopPadding && (!hasContentAbove || !cfg.Settings.Lines.ShowFooterLine.Enabled()) {
			hctx.location = tw.LocationFirst
		} else if isLastContentLine && !hasBottomPaddingConfig {
			hctx.location = tw.LocationEnd
		} else {
			hctx.location = tw.LocationMiddle
		}
		ctx.logger.Debugf("Rendering footer content line %d with location %v", i, hctx.location)
		if err := t.renderLine(ctx, mctx, hctx, colAligns, colPadding); err != nil {
			return err
		}
		lastRenderedLineIdx = i
	}

	if hasBottomPaddingConfig {
		paddingLineContentForContext = make([]string, ctx.numCols)
		formattedPaddingCells := make([]string, ctx.numCols)
		representativePadChar := " "
		ctx.logger.Debugf("Constructing Footer Bottom Padding line content strings")
		for j := 0; j < ctx.numCols; j++ {
			colWd := ctx.widths[tw.Footer].Get(j)
			mergeState := tw.MergeState{}
			if mctx.footerMerges != nil {
				if state, ok := mctx.footerMerges[j]; ok {
					mergeState = state
				}
			}
			if mergeState.Horizontal.Present && !mergeState.Horizontal.Start {
				paddingLineContentForContext[j] = ""
				formattedPaddingCells[j] = ""
				continue
			}
			padChar := " "
			if j < len(t.config.Footer.Padding.PerColumn) && t.config.Footer.Padding.PerColumn[j].Bottom != tw.Empty {
				padChar = t.config.Footer.Padding.PerColumn[j].Bottom
			} else if t.config.Footer.Padding.Global.Bottom != tw.Empty {
				padChar = t.config.Footer.Padding.Global.Bottom
			}
			paddingLineContentForContext[j] = padChar
			if j == 0 || representativePadChar == " " {
				representativePadChar = padChar
			}
			padWidth := max(twwidth.Width(padChar), 1)
			repeatCount := 0
			if colWd > 0 && padWidth > 0 {
				repeatCount = colWd / padWidth
			}
			if colWd > 0 && repeatCount < 1 && padChar != " " {
				repeatCount = 1
			}
			if colWd == 0 {
				repeatCount = 0
			}
			rawPaddingContent := strings.Repeat(padChar, repeatCount)
			currentWd := twwidth.Width(rawPaddingContent)
			if currentWd < colWd {
				rawPaddingContent += strings.Repeat(" ", colWd-currentWd)
			}
			if currentWd > colWd && colWd > 0 {
				rawPaddingContent = twwidth.Truncate(rawPaddingContent, colWd)
			}
			if colWd == 0 {
				rawPaddingContent = ""
			}
			formattedPaddingCells[j] = rawPaddingContent
		}
		ctx.logger.Debugf("Manually rendering Footer Bottom Padding line (char like '%s')", representativePadChar)
		var paddingLineOutput strings.Builder
		if cfg.Borders.Left.Enabled() {
			paddingLineOutput.WriteString(cfg.Symbols.Column())
		}
		for colIdx := 0; colIdx < ctx.numCols; {
			if colIdx > 0 && cfg.Settings.Separators.BetweenColumns.Enabled() {
				shouldAddSeparator := true
				if prevMergeState, ok := mctx.footerMerges[colIdx-1]; ok {
					if prevMergeState.Horizontal.Present && !prevMergeState.Horizontal.End {
						shouldAddSeparator = false
					}
				}
				if shouldAddSeparator {
					paddingLineOutput.WriteString(cfg.Symbols.Column())
				}
			}
			if colIdx < len(formattedPaddingCells) {
				paddingLineOutput.WriteString(formattedPaddingCells[colIdx])
			}
			currentMergeState := tw.MergeState{}
			if mctx.footerMerges != nil {
				if state, ok := mctx.footerMerges[colIdx]; ok {
					currentMergeState = state
				}
			}
			if currentMergeState.Horizontal.Present && currentMergeState.Horizontal.Start {
				colIdx += currentMergeState.Horizontal.Span
			} else {
				colIdx++
			}
		}
		if cfg.Borders.Right.Enabled() {
			paddingLineOutput.WriteString(cfg.Symbols.Column())
		}
		paddingLineOutput.WriteString(t.newLine)
		t.writer.Write([]byte(paddingLineOutput.String()))
		ctx.logger.Debugf("Manually rendered Footer Bottom Padding line: %s", strings.TrimSuffix(paddingLineOutput.String(), t.newLine))
		hctx.rowIdx = 0
		hctx.lineIdx = len(ctx.footerLines)
		hctx.line = paddingLineContentForContext
		hctx.location = tw.LocationEnd
		lastRenderedLineIdx = hctx.lineIdx
	}

	if cfg.Borders.Bottom.Enabled() && cfg.Settings.Lines.ShowBottom.Enabled() {
		ctx.logger.Debugf("Rendering final table bottom border")
		if lastRenderedLineIdx == len(ctx.footerLines) {
			hctx.rowIdx = 0
			hctx.lineIdx = lastRenderedLineIdx
			hctx.line = paddingLineContentForContext
			hctx.location = tw.LocationEnd
			ctx.logger.Debugf("Setting border context based on bottom padding line")
		} else if lastRenderedLineIdx >= 0 {
			hctx.rowIdx = 0
			hctx.lineIdx = lastRenderedLineIdx
			hctx.line = padLine(ctx.footerLines[hctx.lineIdx], ctx.numCols)
			hctx.location = tw.LocationEnd
			ctx.logger.Debugf("Setting border context based on last content line idx %d", hctx.lineIdx)
		} else if lastRenderedLineIdx == -1 {
			hctx.rowIdx = 0
			hctx.lineIdx = -1
			hctx.line = paddingLineContentForContext
			hctx.location = tw.LocationEnd
			ctx.logger.Debugf("Setting border context based on top padding line")
		} else {
			hctx.rowIdx = 0
			hctx.lineIdx = -2
			hctx.line = make([]string, ctx.numCols)
			hctx.location = tw.LocationEnd
			ctx.logger.Debugf("Warning: Cannot determine context for bottom border")
		}
		resp := t.buildCellContexts(ctx, mctx, hctx, colAligns, colPadding)
		ctx.logger.Debugf("Bottom border: Using Widths=%v", ctx.widths[tw.Row])
		f.Line(tw.Formatting{
			Row: tw.RowContext{
				Widths:       ctx.widths[tw.Row],
				Current:      resp.cells,
				Previous:     resp.prevCells,
				Position:     tw.Footer,
				Location:     tw.LocationEnd,
				ColMaxWidths: t.getColMaxWidths(tw.Footer),
			},
			Level:    tw.LevelFooter,
			IsSubRow: false,
		})
	}

	return nil
}

// renderHeader renders the table's header section with borders and padding.
// Parameters ctx and mctx hold rendering and merge state.
// Returns an error if rendering fails.
func (t *Table) renderHeader(ctx *renderContext, mctx *mergeContext) error {
	if len(ctx.headerLines) == 0 {
		return nil
	}
	ctx.logger.Debug("Rendering header section")

	f := ctx.renderer
	cfg := ctx.cfg
	colAligns := t.buildAligns(t.config.Header)
	colPadding := t.buildPadding(t.config.Header.Padding)
	hctx := &helperContext{position: tw.Header}

	if cfg.Borders.Top.Enabled() && cfg.Settings.Lines.ShowTop.Enabled() {
		ctx.logger.Debug("Rendering table top border")
		nextCells := make(map[int]tw.CellContext)
		if len(ctx.headerLines) > 0 {
			for j, cell := range ctx.headerLines[0] {
				nextCells[j] = tw.CellContext{Data: cell, Merge: mctx.headerMerges[j]}
			}
		}
		f.Line(tw.Formatting{
			Row: tw.RowContext{
				Widths:   ctx.widths[tw.Header],
				Next:     nextCells,
				Position: tw.Header,
				Location: tw.LocationFirst,
			},
			Level:    tw.LevelHeader,
			IsSubRow: false,
		})
	}

	if t.config.Header.Padding.Global.Top != tw.Empty {
		hctx.location = tw.LocationFirst
		hctx.line = t.buildPaddingLineContents(t.config.Header.Padding.Global.Top, ctx.widths[tw.Header], ctx.numCols, mctx.headerMerges)
		if err := t.renderPadding(ctx, mctx, hctx, t.config.Header.Padding.Global.Top); err != nil {
			return err
		}
	}

	for i, line := range ctx.headerLines {
		hctx.rowIdx = 0
		hctx.lineIdx = i
		hctx.line = padLine(line, ctx.numCols)
		hctx.location = t.determineLocation(i, len(ctx.headerLines), t.config.Header.Padding.Global.Top, t.config.Header.Padding.Global.Bottom)

		if t.config.Header.Callbacks.Global != nil {
			ctx.logger.Debug("Executing global header callback for line %d", i)
			t.config.Header.Callbacks.Global()
		}
		for colIdx, cb := range t.config.Header.Callbacks.PerColumn {
			if colIdx < ctx.numCols && cb != nil {
				ctx.logger.Debug("Executing per-column header callback for line %d, col %d", i, colIdx)
				cb()
			}
		}

		if err := t.renderLine(ctx, mctx, hctx, colAligns, colPadding); err != nil {
			return err
		}
	}

	if t.config.Header.Padding.Global.Bottom != tw.Empty {
		hctx.location = tw.LocationEnd
		hctx.line = t.buildPaddingLineContents(t.config.Header.Padding.Global.Bottom, ctx.widths[tw.Header], ctx.numCols, mctx.headerMerges)
		if err := t.renderPadding(ctx, mctx, hctx, t.config.Header.Padding.Global.Bottom); err != nil {
			return err
		}
	}

	if cfg.Settings.Lines.ShowHeaderLine.Enabled() && (len(ctx.rowLines) > 0 || len(ctx.footerLines) > 0) {
		ctx.logger.Debug("Rendering header separator line")
		resp := t.buildCellContexts(ctx, mctx, hctx, colAligns, colPadding)

		var nextSectionCells map[int]tw.CellContext
		var nextSectionWidths tw.Mapper[int, int]

		if len(ctx.rowLines) > 0 {
			nextSectionWidths = ctx.widths[tw.Row]
			rowColAligns := t.buildAligns(t.config.Row)
			rowColPadding := t.buildPadding(t.config.Row.Padding)
			firstRowHctx := &helperContext{
				position: tw.Row,
				rowIdx:   0,
				lineIdx:  0,
			}
			if len(ctx.rowLines[0]) > 0 {
				firstRowHctx.line = padLine(ctx.rowLines[0][0], ctx.numCols)
			} else {
				firstRowHctx.line = make([]string, ctx.numCols)
			}
			firstRowResp := t.buildCellContexts(ctx, mctx, firstRowHctx, rowColAligns, rowColPadding)
			nextSectionCells = firstRowResp.cells
		} else if len(ctx.footerLines) > 0 {
			nextSectionWidths = ctx.widths[tw.Row]
			footerColAligns := t.buildAligns(t.config.Footer)
			footerColPadding := t.buildPadding(t.config.Footer.Padding)
			firstFooterHctx := &helperContext{
				position: tw.Footer,
				rowIdx:   0,
				lineIdx:  0,
			}
			if len(ctx.footerLines) > 0 {
				firstFooterHctx.line = padLine(ctx.footerLines[0], ctx.numCols)
			} else {
				firstFooterHctx.line = make([]string, ctx.numCols)
			}
			firstFooterResp := t.buildCellContexts(ctx, mctx, firstFooterHctx, footerColAligns, footerColPadding)
			nextSectionCells = firstFooterResp.cells
		} else {
			nextSectionWidths = ctx.widths[tw.Header]
			nextSectionCells = nil
		}

		f.Line(tw.Formatting{
			Row: tw.RowContext{
				Widths:   nextSectionWidths,
				Current:  resp.cells,
				Previous: resp.prevCells,
				Next:     nextSectionCells,
				Position: tw.Header,
				Location: tw.LocationMiddle,
			},
			Level:    tw.LevelBody,
			IsSubRow: false,
		})
	}
	return nil
}

// renderLine renders a single line with callbacks and normalized widths.
// Parameters include ctx, mctx, hctx, aligns, and padding for rendering.
// Returns an error if rendering fails.
func (t *Table) renderLine(ctx *renderContext, mctx *mergeContext, hctx *helperContext, aligns map[int]tw.Align, padding map[int]tw.Padding) error {
	resp := t.buildCellContexts(ctx, mctx, hctx, aligns, padding)
	f := ctx.renderer

	isPaddingLine := false
	sectionConfig := t.config.Row
	switch hctx.position {
	case tw.Header:
		sectionConfig = t.config.Header
		isPaddingLine = (hctx.lineIdx == -1 && sectionConfig.Padding.Global.Top != tw.Empty) ||
			(hctx.lineIdx == len(ctx.headerLines) && sectionConfig.Padding.Global.Bottom != tw.Empty)
	case tw.Footer:
		sectionConfig = t.config.Footer
		isPaddingLine = (hctx.lineIdx == -1 && sectionConfig.Padding.Global.Top != tw.Empty) ||
			(hctx.lineIdx == len(ctx.footerLines) && (sectionConfig.Padding.Global.Bottom != tw.Empty || t.hasPerColumnBottomPadding()))
	case tw.Row:
		if hctx.rowIdx >= 0 && hctx.rowIdx < len(ctx.rowLines) {
			isPaddingLine = (hctx.lineIdx == -1 && sectionConfig.Padding.Global.Top != tw.Empty) ||
				(hctx.lineIdx == len(ctx.rowLines[hctx.rowIdx]) && sectionConfig.Padding.Global.Bottom != tw.Empty)
		}
	}

	sectionWidths := ctx.widths[hctx.position]
	normalizedWidths := ctx.widths[tw.Row]

	formatting := tw.Formatting{
		Row: tw.RowContext{
			Widths:       sectionWidths,
			ColMaxWidths: t.getColMaxWidths(hctx.position),
			Current:      resp.cells,
			Previous:     resp.prevCells,
			Next:         resp.nextCells,
			Position:     hctx.position,
			Location:     hctx.location,
		},
		Level:            t.getLevel(hctx.position),
		IsSubRow:         hctx.lineIdx > 0 || isPaddingLine,
		NormalizedWidths: normalizedWidths,
	}

	if hctx.position == tw.Row {
		formatting.HasFooter = len(ctx.footerLines) > 0
	}

	switch hctx.position {
	case tw.Header:
		f.Header([][]string{hctx.line}, formatting)
	case tw.Row:
		f.Row(hctx.line, formatting)
	case tw.Footer:
		f.Footer([][]string{hctx.line}, formatting)
	}
	return nil
}

// renderPadding renders padding lines for a section.
// Parameters include ctx, mctx, hctx, and padChar for padding content.
// Returns an error if rendering fails.
func (t *Table) renderPadding(ctx *renderContext, mctx *mergeContext, hctx *helperContext, padChar string) error {
	ctx.logger.Debug("Rendering padding line for %s (using char like '%s')", hctx.position, padChar)

	colAligns := t.buildAligns(t.config.Row)
	colPadding := t.buildPadding(t.config.Row.Padding)

	switch hctx.position {
	case tw.Header:
		colAligns = t.buildAligns(t.config.Header)
		colPadding = t.buildPadding(t.config.Header.Padding)
	case tw.Footer:
		colAligns = t.buildAligns(t.config.Footer)
		colPadding = t.buildPadding(t.config.Footer.Padding)
	}

	return t.renderLine(ctx, mctx, hctx, colAligns, colPadding)
}

// renderRow renders the table's row section with borders and padding.
// Parameters ctx and mctx hold rendering and merge state.
// Returns an error if rendering fails.
func (t *Table) renderRow(ctx *renderContext, mctx *mergeContext) error {
	if len(ctx.rowLines) == 0 {
		return nil
	}
	ctx.logger.Debugf("Rendering row section (total rows: %d)", len(ctx.rowLines))

	f := ctx.renderer
	cfg := ctx.cfg
	colAligns := t.buildAligns(t.config.Row)
	colPadding := t.buildPadding(t.config.Row.Padding)
	hctx := &helperContext{position: tw.Row}

	footerIsEmptyOrNonExistent := !t.hasFooterElements()
	if len(ctx.headerLines) == 0 && footerIsEmptyOrNonExistent && cfg.Borders.Top.Enabled() && cfg.Settings.Lines.ShowTop.Enabled() {
		ctx.logger.Debug("Rendering table top border (rows only table)")
		nextCells := make(map[int]tw.CellContext)
		if len(ctx.rowLines) > 0 && len(ctx.rowLines[0]) > 0 && len(mctx.rowMerges) > 0 {
			firstLine := ctx.rowLines[0][0]
			firstMerges := mctx.rowMerges[0]
			for j, cell := range padLine(firstLine, ctx.numCols) {
				mergeState := tw.MergeState{}
				if firstMerges != nil {
					mergeState = firstMerges[j]
				}
				nextCells[j] = tw.CellContext{Data: cell, Merge: mergeState, Width: ctx.widths[tw.Row].Get(j)}
			}
		}
		f.Line(tw.Formatting{
			Row: tw.RowContext{
				Widths:   ctx.widths[tw.Row],
				Next:     nextCells,
				Position: tw.Row,
				Location: tw.LocationFirst,
			},
			Level:    tw.LevelHeader,
			IsSubRow: false,
		})
	}

	for i, lines := range ctx.rowLines {
		rowHasTopPadding := t.config.Row.Padding.Global.Top != tw.Empty
		if rowHasTopPadding {
			hctx.rowIdx = i
			hctx.lineIdx = -1
			if i == 0 && len(ctx.headerLines) == 0 {
				hctx.location = tw.LocationFirst
			} else {
				hctx.location = tw.LocationMiddle
			}
			hctx.line = t.buildPaddingLineContents(t.config.Row.Padding.Global.Top, ctx.widths[tw.Row], ctx.numCols, mctx.rowMerges[i])
			ctx.logger.Debug("Calling renderPadding for Row Top Padding (row %d): %v (loc: %v)", i, hctx.line, hctx.location)
			if err := t.renderPadding(ctx, mctx, hctx, t.config.Row.Padding.Global.Top); err != nil {
				return err
			}
		}

		footerExists := t.hasFooterElements()
		rowHasBottomPadding := t.config.Row.Padding.Global.Bottom != tw.Empty
		isLastRow := i == len(ctx.rowLines)-1

		for j, visualLineData := range lines {
			hctx.rowIdx = i
			hctx.lineIdx = j
			hctx.line = padLine(visualLineData, ctx.numCols)

			if t.config.Behavior.TrimLine.Enabled() {
				if j > 0 {
					visualLineHasActualContent := false
					for kCellIdx, cellContentInVisualLine := range hctx.line {
						if t.Trimmer(cellContentInVisualLine) != "" {
							visualLineHasActualContent = true
							ctx.logger.Debug("Visual line [%d][%d] has content in cell %d: '%s'. Not skipping.", i, j, kCellIdx, cellContentInVisualLine)
							break
						}
					}

					if !visualLineHasActualContent {
						ctx.logger.Debug("Skipping visual line [%d][%d] as it's entirely blank after trimming. Line: %q", i, j, hctx.line)
						continue
					}
				}
			}

			isFirstRow := i == 0
			isLastLineOfRow := j == len(lines)-1

			if isFirstRow && j == 0 && !rowHasTopPadding && len(ctx.headerLines) == 0 {
				hctx.location = tw.LocationFirst
			} else if isLastRow && isLastLineOfRow && !rowHasBottomPadding && !footerExists {
				hctx.location = tw.LocationEnd
			} else {
				hctx.location = tw.LocationMiddle
			}

			ctx.logger.Debugf("Rendering row %d line %d with location %v. Content: %q", i, j, hctx.location, hctx.line)
			if err := t.renderLine(ctx, mctx, hctx, colAligns, colPadding); err != nil {
				return err
			}
		}

		if rowHasBottomPadding {
			hctx.rowIdx = i
			hctx.lineIdx = len(lines)
			if isLastRow && !footerExists {
				hctx.location = tw.LocationEnd
			} else {
				hctx.location = tw.LocationMiddle
			}
			hctx.line = t.buildPaddingLineContents(t.config.Row.Padding.Global.Bottom, ctx.widths[tw.Row], ctx.numCols, mctx.rowMerges[i])
			ctx.logger.Debug("Calling renderPadding for Row Bottom Padding (row %d): %v (loc: %v)", i, hctx.line, hctx.location)
			if err := t.renderPadding(ctx, mctx, hctx, t.config.Row.Padding.Global.Bottom); err != nil {
				return err
			}
		}

		if cfg.Settings.Separators.BetweenRows.Enabled() && !isLastRow {
			ctx.logger.Debug("Rendering between-rows separator after logical row %d", i)
			respCurrent := t.buildCellContexts(ctx, mctx, hctx, colAligns, colPadding)

			var nextCellsForSeparator map[int]tw.CellContext = nil
			nextRowIdx := i + 1
			if nextRowIdx < len(ctx.rowLines) && nextRowIdx < len(mctx.rowMerges) {
				hctxNext := &helperContext{position: tw.Row, rowIdx: nextRowIdx, location: tw.LocationMiddle}
				nextRowActualLines := ctx.rowLines[nextRowIdx]
				nextRowMerges := mctx.rowMerges[nextRowIdx]

				if t.config.Row.Padding.Global.Top != tw.Empty {
					hctxNext.lineIdx = -1
					hctxNext.line = t.buildPaddingLineContents(t.config.Row.Padding.Global.Top, ctx.widths[tw.Row], ctx.numCols, nextRowMerges)
				} else if len(nextRowActualLines) > 0 {
					hctxNext.lineIdx = 0
					hctxNext.line = padLine(nextRowActualLines[0], ctx.numCols)
				} else {
					hctxNext.lineIdx = 0
					hctxNext.line = make([]string, ctx.numCols)
				}
				respNext := t.buildCellContexts(ctx, mctx, hctxNext, colAligns, colPadding)
				nextCellsForSeparator = respNext.cells
			} else {
				ctx.logger.Debug("Separator context: No next logical row for separator after row %d.", i)
			}

			f.Line(tw.Formatting{
				Row: tw.RowContext{
					Widths:       ctx.widths[tw.Row],
					Current:      respCurrent.cells,
					Previous:     respCurrent.prevCells,
					Next:         nextCellsForSeparator,
					Position:     tw.Row,
					Location:     tw.LocationMiddle,
					ColMaxWidths: t.getColMaxWidths(tw.Row),
				},
				Level:     tw.LevelBody,
				IsSubRow:  false,
				HasFooter: footerExists,
			})
		}
	}
	return nil
}