/**
 * ASCIIBarChart - A dynamic ASCII-based vertical bar chart renderer
 * 
 * Creates real-time animated bar charts using monospaced characters (X)
 * with automatic scaling, labels, and responsive font sizing.
 */
class ASCIIBarChart {
    /**
     * Create a new ASCII bar chart
     * @param {string} containerId - The ID of the HTML element to render the chart in
     * @param {Object} options - Configuration options
     * @param {number} [options.maxHeight=20] - Maximum height of the chart in rows
     * @param {number} [options.maxDataPoints=30] - Maximum number of data columns before scrolling
     * @param {string} [options.title=''] - Chart title (displayed centered at top)
     * @param {string} [options.xAxisLabel=''] - X-axis label (displayed centered at bottom)
     * @param {string} [options.yAxisLabel=''] - Y-axis label (displayed vertically on left)
     * @param {boolean} [options.autoFitWidth=true] - Automatically adjust font size to fit container width
     * @param {boolean} [options.useBinMode=false] - Enable time bin mode for data aggregation
     * @param {number} [options.binDuration=10000] - Duration of each time bin in milliseconds (10 seconds default)
     * @param {string} [options.xAxisLabelFormat='elapsed'] - X-axis label format: 'elapsed', 'bins', 'timestamps', 'ranges'
     * @param {boolean} [options.debug=false] - Enable debug logging
     */
    constructor(containerId, options = {}) {
        this.container = document.getElementById(containerId);
        this.data = [];
        this.maxHeight = options.maxHeight || 20;
        this.maxDataPoints = options.maxDataPoints || 30;
        this.totalDataPoints = 0; // Track total number of data points added
        this.title = options.title || '';
        this.xAxisLabel = options.xAxisLabel || '';
        this.yAxisLabel = options.yAxisLabel || '';
        this.autoFitWidth = options.autoFitWidth !== false; // Default to true
        this.debug = options.debug || false; // Debug logging option

        // Time bin configuration
        this.useBinMode = options.useBinMode !== false; // Default to true
        this.binDuration = options.binDuration || 4000; // 4 seconds default
        this.xAxisLabelFormat = options.xAxisLabelFormat || 'elapsed';

        // Time bin data structures
        this.bins = [];
        this.currentBinIndex = -1;
        this.binStartTime = null;
        this.binCheckInterval = null;
        this.chartStartTime = Date.now();

        // Set up resize observer if auto-fit is enabled
        if (this.autoFitWidth) {
            this.resizeObserver = new ResizeObserver(() => {
                this.adjustFontSize();
            });
            this.resizeObserver.observe(this.container);
        }

        // Initialize first bin if bin mode is enabled
        if (this.useBinMode) {
            this.initializeBins();
        }
    }

    /**
     * Add a new data point to the chart
     * @param {number} value - The numeric value to add
     */
    addValue(value) {
        // Time bin mode: add value to current active bin count
        this.checkBinRotation(); // Ensure we have an active bin
        this.bins[this.currentBinIndex].count += value;  // Changed from ++ to += value
        this.totalDataPoints++;

        this.render();
        this.updateInfo();
    }

    /**
     * Clear all data from the chart
     */
    clear() {
        this.data = [];
        this.totalDataPoints = 0;

        if (this.useBinMode) {
            this.bins = [];
            this.currentBinIndex = -1;
            this.binStartTime = null;
            this.initializeBins();
        }

        this.render();
        this.updateInfo();
    }

    /**
     * Calculate the width of the chart in characters
     * @returns {number} The chart width in characters
     * @private
     */
    getChartWidth() {
        let dataLength = this.maxDataPoints; // Always use maxDataPoints for consistent width

        if (dataLength === 0) return 50; // Default width for empty chart

        const yAxisPadding = this.yAxisLabel ? 2 : 0;
        const yAxisNumbers = 3; // Width of Y-axis numbers
        const separator = 1; // The '|' character
        // const dataWidth = dataLength * 2; // Each column is 2 characters wide  // TEMP: commented for no-space test
        const dataWidth = dataLength; // Each column is 1 character wide  // TEMP: adjusted for no-space columns
        const padding = 1; // Extra padding

        const totalWidth = yAxisPadding + yAxisNumbers + separator + dataWidth + padding;

        // Only log when width changes
        if (this.debug && this.lastChartWidth !== totalWidth) {
            console.log('getChartWidth changed:', { dataLength, totalWidth, previous: this.lastChartWidth });
            this.lastChartWidth = totalWidth;
        }

        return totalWidth;
    }

    /**
     * Adjust font size to fit container width
     * @private
     */
    adjustFontSize() {
        if (!this.autoFitWidth) return;

        const containerWidth = this.container.clientWidth;
        const chartWidth = this.getChartWidth();

        if (chartWidth === 0) return;

        // Calculate optimal font size
        // For monospace fonts, character width is approximately 0.6 * font size
        // Use a slightly smaller ratio to fit more content
        const charWidthRatio = 0.7;
        const padding = 30; // Reduce padding to fit more content
        const availableWidth = containerWidth - padding;
        const optimalFontSize = Math.floor((availableWidth / chartWidth) / charWidthRatio);

        // Set reasonable bounds (min 4px, max 20px)
        const fontSize = Math.max(4, Math.min(20, optimalFontSize));

        // Only log when font size changes
        if (this.debug && this.lastFontSize !== fontSize) {
            console.log('fontSize changed:', { containerWidth, chartWidth, fontSize, previous: this.lastFontSize });
            this.lastFontSize = fontSize;
        }

        this.container.style.fontSize = fontSize + 'px';
        this.container.style.lineHeight = '1.0';
    }

    /**
     * Render the chart to the container
     * @private
     */
    render() {
        let dataToRender = [];
        let maxValue = 0;
        let minValue = 0;
        let valueRange = 0;

        if (this.useBinMode) {
            // Bin mode: render bin counts
            if (this.bins.length === 0) {
                this.container.textContent = 'No data yet. Click Start to begin.';
                return;
            }
            // Always create a fixed-length array filled with 0s, then overlay actual bin data
            dataToRender = new Array(this.maxDataPoints).fill(0);

            // Overlay actual bin data (most recent bins, reversed for left-to-right display)
            const startIndex = Math.max(0, this.bins.length - this.maxDataPoints);
            const recentBins = this.bins.slice(startIndex);

            // Reverse the bins so most recent is on the left, and overlay onto the fixed array
            recentBins.reverse().forEach((bin, index) => {
                if (index < this.maxDataPoints) {
                    dataToRender[index] = bin.count;
                }
            });

            if (this.debug) {
                console.log('render() dataToRender:', dataToRender, 'bins length:', this.bins.length);
            }
            maxValue = Math.max(...dataToRender);
            minValue = Math.min(...dataToRender);
            valueRange = maxValue - minValue;
        } else {
            // Legacy mode: render individual values
            if (this.data.length === 0) {
                this.container.textContent = 'No data yet. Click Start to begin.';
                return;
            }
            dataToRender = this.data;
            maxValue = Math.max(...this.data);
            minValue = Math.min(...this.data);
            valueRange = maxValue - minValue;
        }

        let output = '';
        const scale = this.maxHeight;

        // Calculate scaling factor: each X represents at least 1 count
        const maxCount = Math.max(...dataToRender);
        const scaleFactor = Math.max(1, Math.ceil(maxCount / scale)); // 1 X = scaleFactor counts
        const scaledMax = Math.ceil(maxCount / scaleFactor) * scaleFactor;

        // Calculate Y-axis label width (for vertical text)
        const yLabelWidth = this.yAxisLabel ? 2 : 0;
        const yAxisPadding = this.yAxisLabel ? '  ' : '';

        // Add title if provided (centered)
        if (this.title) {
            // const chartWidth = 4 + this.maxDataPoints * 2; // Y-axis numbers + data columns  // TEMP: commented for no-space test
            const chartWidth = 4 + this.maxDataPoints; // Y-axis numbers + data columns  // TEMP: adjusted for no-space columns
            const titlePadding = Math.floor((chartWidth - this.title.length) / 2);
            output += yAxisPadding + ' '.repeat(Math.max(0, titlePadding)) + this.title + '\n\n';
        }

        // Draw from top to bottom
        for (let row = scale; row > 0; row--) {
            let line = '';

            // Add vertical Y-axis label character
            if (this.yAxisLabel) {
                const L = this.yAxisLabel.length;
                const startRow = Math.floor((scale - L) / 2) + 1;
                const relativeRow = scale - row + 1; // 1 at top, scale at bottom
                if (relativeRow >= startRow && relativeRow < startRow + L) {
                    const labelIndex = relativeRow - startRow;
                    line += this.yAxisLabel[labelIndex] + ' ';
                } else {
                    line += '  ';
                }
            }

            // Calculate the actual count value this row represents (1 at bottom, increasing upward)
            const rowCount = (row - 1) * scaleFactor + 1;

            // Add Y-axis label (show actual count values)
            line += String(rowCount).padStart(3, ' ') + ' |';

            // Draw each column
            for (let i = 0; i < dataToRender.length; i++) {
                const count = dataToRender[i];
                const scaledHeight = Math.ceil(count / scaleFactor);

                if (scaledHeight >= row) {
                    // line += ' X';  // TEMP: commented out space between columns
                    line += 'X';  // TEMP: no space between columns
                } else {
                    // line += '  ';  // TEMP: commented out space between columns
                    line += ' ';  // TEMP: single space for empty columns
                }
            }

            output += line + '\n';
        }

        // Draw X-axis
        // output += yAxisPadding + '    +' + '-'.repeat(this.maxDataPoints * 2) + '\n';  // TEMP: commented out for no-space test
        output += yAxisPadding + '    +' + '-'.repeat(this.maxDataPoints) + '\n';  // TEMP: back to original length

        // Draw X-axis labels based on mode and format
        let xAxisLabels = yAxisPadding + '     ';  // Initial padding to align with X-axis

        // Determine label interval (every 5 columns)
        const labelInterval = 5;

        // Generate all labels first and store in array
        let labels = [];
        for (let i = 0; i < this.maxDataPoints; i++) {
            if (i % labelInterval === 0) {
                let label = '';
                if (this.useBinMode) {
                    // For bin mode, show labels for all possible positions
                    // i=0 is leftmost (most recent), i=maxDataPoints-1 is rightmost (oldest)
                    const elapsedSec = (i * this.binDuration) / 1000;
                    // Format with appropriate precision for sub-second bins
                    if (this.binDuration < 1000) {
                        // Show decimal seconds for sub-second bins
                        label = elapsedSec.toFixed(1) + 's';
                    } else {
                        // Show whole seconds for 1+ second bins
                        label = String(Math.round(elapsedSec)) + 's';
                    }
                } else {
                    // For legacy mode, show data point numbers
                    const startIndex = Math.max(1, this.totalDataPoints - this.maxDataPoints + 1);
                    label = String(startIndex + i);
                }
                labels.push(label);
            }
        }

        // Build the label string with calculated spacing
        for (let i = 0; i < labels.length; i++) {
            const label = labels[i];
            xAxisLabels += label;

            // Add spacing: labelInterval - label.length (except for last label)
            if (i < labels.length - 1) {
                const spacing = labelInterval - label.length;
                xAxisLabels += ' '.repeat(spacing);
            }
        }

        // Ensure the label line extends to match the X-axis dash line length
        // The dash line is this.maxDataPoints characters long, starting after "    +"
        const dashLineLength = this.maxDataPoints;
        const minLabelLineLength = yAxisPadding.length + 4 + dashLineLength; // 4 for "    "
        if (xAxisLabels.length < minLabelLineLength) {
            xAxisLabels += ' '.repeat(minLabelLineLength - xAxisLabels.length);
        }
        output += xAxisLabels + '\n';

        // Add X-axis label if provided
        if (this.xAxisLabel) {
            // const labelPadding = Math.floor((this.maxDataPoints * 2 - this.xAxisLabel.length) / 2);  // TEMP: commented for no-space test
            const labelPadding = Math.floor((this.maxDataPoints - this.xAxisLabel.length) / 2);  // TEMP: adjusted for no-space columns
            output += '\n' + yAxisPadding + '     ' + ' '.repeat(Math.max(0, labelPadding)) + this.xAxisLabel + '\n';
        }

        this.container.textContent = output;

        // Adjust font size to fit width (only once at initialization)
        if (this.autoFitWidth) {
            this.adjustFontSize();
        }

        // Update the external info display
        if (this.useBinMode) {
            const binCounts = this.bins.map(bin => bin.count);
            const scaleFactor = Math.max(1, Math.ceil(maxValue / scale));
            document.getElementById('values').textContent = `[${dataToRender.join(', ')}]`;
            document.getElementById('max-value').textContent = maxValue;
            document.getElementById('scale').textContent = `Min: ${minValue}, Max: ${maxValue}, 1X=${scaleFactor} counts`;
        } else {
            document.getElementById('values').textContent = `[${this.data.join(', ')}]`;
            document.getElementById('max-value').textContent = maxValue;
            document.getElementById('scale').textContent = `Min: ${minValue}, Max: ${maxValue}, Height: ${scale}`;
        }
    }

    /**
     * Update the info display
     * @private
     */
    updateInfo() {
        if (this.useBinMode) {
            const totalCount = this.bins.reduce((sum, bin) => sum + bin.count, 0);
            document.getElementById('count').textContent = totalCount;
        } else {
            document.getElementById('count').textContent = this.data.length;
        }
    }

    /**
     * Initialize the bin system
     * @private
     */
    initializeBins() {
        this.bins = [];
        this.currentBinIndex = -1;
        this.binStartTime = null;
        this.chartStartTime = Date.now();

        // Create first bin
        this.rotateBin();

        // Set up automatic bin rotation check
        this.binCheckInterval = setInterval(() => {
            this.checkBinRotation();
        }, 100); // Check every 100ms for responsiveness
    }

    /**
     * Check if current bin should rotate and create new bin if needed
     * @private
     */
    checkBinRotation() {
        if (!this.useBinMode || !this.binStartTime) return;

        const now = Date.now();
        if ((now - this.binStartTime) >= this.binDuration) {
            this.rotateBin();
        }
    }

    /**
     * Rotate to a new bin, finalizing the current one
     */
    rotateBin() {
        // Finalize current bin if it exists
        if (this.currentBinIndex >= 0) {
            this.bins[this.currentBinIndex].isActive = false;
        }

        // Create new bin
        const newBin = {
            startTime: Date.now(),
            count: 0,
            isActive: true
        };

        this.bins.push(newBin);
        this.currentBinIndex = this.bins.length - 1;
        this.binStartTime = newBin.startTime;

        // Keep only the most recent bins
        if (this.bins.length > this.maxDataPoints) {
            this.bins.shift();
            this.currentBinIndex--;
        }

        // Ensure currentBinIndex points to the last bin (the active one)
        this.currentBinIndex = this.bins.length - 1;

        // Force a render to update the display immediately
        this.render();
        this.updateInfo();
    }

    /**
     * Format X-axis label for a bin based on the configured format
     * @param {number} binIndex - Index of the bin
     * @returns {string} Formatted label
     * @private
     */
    formatBinLabel(binIndex) {
        const bin = this.bins[binIndex];
        if (!bin) return '  ';

        switch (this.xAxisLabelFormat) {
            case 'bins':
                return String(binIndex + 1).padStart(2, ' ');

            case 'timestamps':
                const time = new Date(bin.startTime);
                return time.toLocaleTimeString('en-US', {
                    hour12: false,
                    hour: '2-digit',
                    minute: '2-digit',
                    second: '2-digit'
                }).replace(/:/g, '');

            case 'ranges':
                const startSec = Math.floor((bin.startTime - this.chartStartTime) / 1000);
                const endSec = startSec + Math.floor(this.binDuration / 1000);
                return `${startSec}-${endSec}`;

            case 'elapsed':
            default:
                // For elapsed time, always show time relative to the first bin (index 0)
                // This keeps the leftmost label as 0s and increases to the right
                const firstBinTime = this.bins[0] ? this.bins[0].startTime : this.chartStartTime;
                const elapsedSec = Math.floor((bin.startTime - firstBinTime) / 1000);
                return String(elapsedSec).padStart(2, ' ') + 's';
        }
    }
}