external/gridded-data/grib2/hydrolang-grib2-parser.js

// HydroLang GRIB2 Parser
// Uses Gerard Llorach's GRIB2 research implementation from grib2utils.js
// This file integrates with the existing research parser without modifying it

// Import the research parser functions
import { decodeGRIB2File, decodeGRIB2Buffer } from './grib2utils.js';

// Test the integration
console.log('GRIB2 Research Parser Integration Loaded');
console.log('Available functions:', { decodeGRIB2File, decodeGRIB2Buffer });
console.log('Ready to parse real GRIB2 meteorological data');

// GRIB2 Section parsing functions based on research implementation
class HydroLangGRIB2Parser {

    /**
     * Parse GRIB2 file buffer
     * @param {ArrayBuffer} buffer - GRIB2 file buffer
     * @returns {Promise<Object>} Parsed GRIB2 data
     */
    async parseBuffer(buffer, options = {}) {
        const { maxMessages = 50, targetVariable = null, memoryLimit = 500 * 1024 * 1024 } = options; // 500MB default limit

        console.log(`Parsing GRIB2 file with research implementation (${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB)...`);
        console.log(`Memory limits: maxMessages=${maxMessages}, memoryLimit=${(memoryLimit / 1024 / 1024).toFixed(1)}MB`);

        try {
            // Validate GRIB2 file format first
            const view = new DataView(buffer);
            const magic = String.fromCharCode(view.getUint8(0), view.getUint8(1), view.getUint8(2), view.getUint8(3));
            if (magic !== 'GRIB') {
                throw new Error('Invalid file format - not a GRIB2 file');
            }

            const version = view.getUint8(7);
            if (version !== 2) {
                throw new Error(`Unsupported GRIB version: ${version} - only GRIB2 is supported`);
            }

            console.log('GRIB2 file format validated');

            // Check if buffer is too large and needs partitioning
            if (buffer.byteLength > memoryLimit) {
                console.warn(`Large GRIB2 file detected (${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB). Using streaming parser...`);
                return await this.parseBufferStreaming(buffer, options);
            }

            // For files larger than 100MB, use streaming even if under memory limit
            if (buffer.byteLength > 100 * 1024 * 1024) {
                console.warn(`Large GRIB2 file (${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB). Using streaming to prevent stack overflow...`);
                return await this.parseBufferStreaming(buffer, options);
            }

            // Use the research implementation's decodeGRIB2File function
            console.log('Decoding GRIB2 messages...');
            const decodedData = decodeGRIB2File(buffer);

            if (!decodedData || decodedData.length === 0) {
                throw new Error('No GRIB2 messages found in file');
            }

            console.log(`Found ${decodedData.length} GRIB2 message(s). Processing first ${Math.min(maxMessages, decodedData.length)}...`);

            // Limit the number of messages to prevent memory issues
            const limitedData = decodedData.slice(0, maxMessages);

            // If looking for specific variable, find first matching message and stop
            let filteredData = limitedData;
            if (targetVariable) {
                filteredData = [];
                for (let index = 0; index < limitedData.length; index++) {
                    try {
                        const msg = limitedData[index];

                        // Check if this message contains precipitation data
                        if (msg && msg.data && msg.data.values) {
                            // Check only first 1000 values to avoid stack overflow
                            const sampleSize = Math.min(1000, msg.data.values.length);
                            const sampleValues = msg.data.values.slice(0, sampleSize);
                            const hasNonZeroValues = sampleValues.some(v => v !== null && v !== 0 && !isNaN(v));

                            console.log(`Message ${index}: Has ${msg.data.values.length} values, sample non-zero: ${hasNonZeroValues}`);

                            if (hasNonZeroValues) {
                                console.log(`Found message with actual data in message ${index} - STOPPING SEARCH`);
                                filteredData.push(msg);
                                break; // Stop after finding first message with real data
                            }
                        } else {
                            console.log(`Message ${index}: No data values found`);
                        }
                    } catch (e) {
                        console.warn(`Could not check data in message ${index}:`, e.message);
                    }
                }
                console.log(`Found ${filteredData.length} messages with actual meteorological data`);
            }

            if (filteredData.length === 0) {
                console.warn('No messages found matching criteria');
                filteredData = limitedData.slice(0, 1); // Take first message as fallback
            }

            console.log(`Successfully parsed ${filteredData.length} GRIB2 message(s) using research implementation`);

            // Return the parsed data with memory usage info
            return {
                version: 2,
                totalLength: view.getUint32(8, false),
                sections: [],
                messages: filteredData,
                buffer: buffer,
                rawData: buffer,
                decodedData: filteredData,
                metadata: {
                    discipline: view.getUint8(6),
                    edition: version,
                    messageCount: filteredData.length,
                    totalMessages: decodedData.length,
                    totalSize: buffer.byteLength,
                    memoryOptimized: filteredData.length < decodedData.length,
                    parsingImplemented: true,
                    parserUsed: 'Research Implementation (Gerard Llorach)',
                    warning: filteredData.length < decodedData.length ?
                        `Loaded ${filteredData.length}/${decodedData.length} messages to prevent memory issues` : null
                }
            };

        } catch (error) {
            console.error('GRIB2 parsing failed:', error);
            throw new Error(`GRIB2 parsing failed: ${error.message}`);
        }
    }

    /**
     * Extract GRIB metadata without decoding full data arrays (prevents stack overflow)
     */
    async extractGRIBMetadataOnly(messageBuffer) {
        try {
            // Create a minimal GRIB2 parser that stops before decoding data values
            const view = new DataView(messageBuffer);

            // Basic GRIB2 validation
            const identifier = new TextDecoder().decode(messageBuffer.slice(0, 4));
            if (identifier !== 'GRIB') {
                throw new Error('Not a valid GRIB file');
            }

            // Find section boundaries
            let offset = 16; // Skip GRIB header (sections 0)
            const sections = {};

            while (offset < messageBuffer.byteLength - 4) {
                const sectionLength = view.getUint32(offset, false);
                const sectionNumber = view.getUint8(offset + 4);

                if (sectionLength === 0 || sectionLength > messageBuffer.byteLength) break;

                sections[sectionNumber] = {
                    start: offset,
                    length: sectionLength,
                    data: messageBuffer.slice(offset, offset + sectionLength)
                };

                offset += sectionLength;

                // Stop after we have sections 3 (grid) and 4 (product) - we don't need section 7 (data)
                if (sections[3] && sections[4]) {
                    break;
                }
            }

            // Extract grid information from Section 3
            const gridInfo = this.parseGridSection(sections[3]);

            // Extract parameter information from Section 4  
            const paramInfo = this.parseProductSection(sections[4]);

            return {
                data: {
                    grid: gridInfo,
                    parameter: paramInfo,
                    parameterName: this.getParameterName(paramInfo),
                    hasDataSection: !!sections[7],
                    sections: Object.keys(sections),
                    bounds: this.calculateGridBounds(gridInfo),
                    dataAvailable: false, // We're not extracting actual data values
                    metadataOnly: true,
                    values: [] // Empty values array for metadata-only
                },
                grid: gridInfo, // Also keep at root level for compatibility
                parameter: paramInfo,
                parameterName: this.getParameterName(paramInfo),
                bounds: this.calculateGridBounds(gridInfo),
                metadataOnly: true
            };

        } catch (error) {
            console.warn('Failed to extract GRIB metadata:', error.message);
            return null;
        }
    }

    /**
     * Parse Grid Definition Section (Section 3) for coordinates and dimensions
     */
    parseGridSection(section3) {
        if (!section3) return {};

        const view = new DataView(section3.data);
        let offset = 5; // Skip section header

        const sourceGridDef = view.getUint8(offset); offset += 1;
        const numPoints = view.getUint32(offset, false); offset += 4;
        const gridDefTemplateNum = view.getUint16(offset + 7, false); // Template number

        // Simplified logging
        console.log(`GRIB2 metadata: ${Math.round(Math.sqrt(numPoints))} x ${Math.round(Math.sqrt(numPoints))} grid`);

        let gridInfo = {
            numPoints: numPoints,
            templateNumber: gridDefTemplateNum,
            sourceGridDef: sourceGridDef
        };

        // Simplified grid handling - use basic approximation to avoid parsing errors
        // Calculate approximate grid dimensions from total points
        const approxGridSize = Math.sqrt(numPoints);
        gridInfo.numLongPoints = Math.round(approxGridSize);
        gridInfo.numLatPoints = Math.round(approxGridSize);
        gridInfo.latStart = 21.0;
        gridInfo.lonStart = -130.0;

        return gridInfo;
    }

    /**
     * Parse Product Definition Section (Section 4) for parameter information
     */
    parseProductSection(section4) {
        if (!section4) return {};

        const view = new DataView(section4.data);
        let offset = 5; // Skip section header

        const numCoords = view.getUint16(offset, false); offset += 2;
        const productDefTemplateNum = view.getUint16(offset, false); offset += 2;

        let paramInfo = {
            templateNumber: productDefTemplateNum,
            numCoords: numCoords
        };

        // Template 4.0 - Analysis/forecast at horizontal level
        if (productDefTemplateNum === 0) {
            const paramCategory = view.getUint8(offset); offset += 1;
            const paramNumber = view.getUint8(offset); offset += 1;
            const processType = view.getUint8(offset); offset += 1;

            paramInfo.category = paramCategory;
            paramInfo.number = paramNumber;
            paramInfo.processType = processType;
            paramInfo.parameter = `${paramCategory},${paramNumber}`;

            // Store parameter info without excessive logging
        }

        return paramInfo;
    }

    /**
     * Get human-readable parameter name
     */
    getParameterName(paramInfo) {
        if (!paramInfo.category && !paramInfo.number) return 'Unknown';

        // Common HRRR parameters (expanded mapping)
        const paramMap = {
            // Meteorological Products (Category 0)
            '0,0': 'TMP',    // Temperature
            '0,1': 'APCP',   // Accumulated Precipitation
            '0,2': 'UGRD',   // U-component of wind
            '0,3': 'VGRD',   // V-component of wind
            '0,8': 'APCP',   // Total precipitation
            '0,10': 'TCDC',  // Total cloud cover
            '0,11': 'SNOD',  // Snow depth
            '0,22': 'CLWMR', // Cloud mixing ratio

            // Hydrological Products (Category 1)
            '1,0': 'RH',     // Relative humidity
            '1,1': 'SPFH',   // Specific humidity
            '1,8': 'APCPsfc', // Total precipitation rate

            // Momentum (Category 2)
            '2,2': 'UGRD',   // U-component of wind
            '2,3': 'VGRD',   // V-component of wind
            '2,22': 'GUST',  // Wind speed (gust)

            // Mass (Category 3)
            '3,0': 'PRES',   // Pressure
            '3,1': 'PRMSL',  // Pressure reduced to MSL
            '3,5': 'HGT',    // Geopotential height

            // Short-wave Radiation (Category 4)
            '4,7': 'DSWRF',  // Downward short-wave rad. flux

            // Long-wave Radiation (Category 5)  
            '5,3': 'DLWRF',  // Downward long-wave rad. flux

            // Cloud (Category 6)
            '6,1': 'TCDC',   // Total cloud cover
            '6,22': 'CDCON', // Cloud condensation nuclei concentration

            // Thermodynamic Stability indices (Category 7)
            '7,6': 'CAPE',   // Convective available potential energy
            '7,7': 'CIN',    // Convective inhibition

            // Atmospheric Chemistry (Category 14)
            '14,192': 'PMTF', // Particulate matter (fine)

            // Forecast Radar Imagery (Category 16)
            '16,195': 'REFD', // Reflectivity
            '16,196': 'REFC', // Composite reflectivity

            // Electrodynamics (Category 17)
            '17,192': 'LTNG', // Lightning

            // Physical Properties of Atmosphere (Category 19)
            '19,0': 'VIS',   // Visibility
            '19,1': 'ALBDO', // Albedo
        };

        const key = `${paramInfo.category},${paramInfo.number}`;
        return paramMap[key] || `PARAM_${key}`;
    }

    /**
     * Calculate approximate grid bounds for Lambert Conformal projection
     */
    calculateGridBounds(gridInfo) {
        if (!gridInfo.templateNumber) return null;

        // For Lambert Conformal (HRRR), use approximate CONUS bounds
        if (gridInfo.templateNumber === 30) {
            return {
                north: 47.8,
                south: 21.0,
                east: -60.9,
                west: -134.1,
                approximation: true,
                projection: 'Lambert Conformal'
            };
        }

        // For regular lat-lon grid (template 0)
        if (gridInfo.templateNumber === 0 && gridInfo.La1 !== undefined && gridInfo.Lo1 !== undefined) {
            // Use actual grid bounds from GRIB2 file
            return {
                north: gridInfo.La1 + (gridInfo.numLatPoints * 0.025), // Approximate increment
                south: gridInfo.La1,
                east: gridInfo.Lo1 + (gridInfo.numLongPoints * 0.025), // Approximate increment
                west: gridInfo.Lo1,
                approximation: true,
                projection: 'Regular lat-lon',
                actualBounds: true
            };
        }

        return null;
    }

    /**
     * Generate basic coordinates for HRRR grid (CONUS coverage)
     */
    generateBasicCoordinates(grid, bbox) {
        const numLat = grid.numLatPoints || 1381;
        const numLon = grid.numLongPoints || 1381;

        // HRRR CONUS approximate bounds
        const latStart = 21.0;
        const latEnd = 47.8;
        const lonStart = -134.1;
        const lonEnd = -60.9;

        // Generate coordinate arrays
        const latitudes = [];
        const longitudes = [];

        for (let i = 0; i < numLat; i++) {
            const lat = latStart + (i / (numLat - 1)) * (latEnd - latStart);
            latitudes.push(lat);
        }

        for (let j = 0; j < numLon; j++) {
            const lon = lonStart + (j / (numLon - 1)) * (lonEnd - lonStart);
            longitudes.push(lon);
        }

        return {
            latitude: latitudes,
            longitude: longitudes
        };
    }

    /**
     * Create metadata-only response for bbox intersection checking
     */
    createGridMetadataResponse(data, bbox) {
        const grid = data.grid;
        const bounds = data.bounds || this.calculateGridBounds(grid);

        // Check if requested bbox intersects with grid bounds
        let intersects = true;
        if (bbox && bounds && !bounds.approximation) {
            const [west, south, east, north] = bbox;
            intersects = !(east < bounds.west || west > bounds.east ||
                north < bounds.south || south > bounds.north);
        }

        return {
            success: true,
            data: {
                parameter: data.parameterName || 'Unknown',
                grid: {
                    Nx: grid.numLongPoints || grid.Nx,
                    Ny: grid.numLatPoints || grid.Ny,
                    templateNumber: grid.templateNumber,
                    totalPoints: grid.numPoints,
                    projection: bounds?.projection || 'Unknown'
                },
                bounds: bounds,
                bbox: bbox,
                intersects: intersects,
                values: [], // Empty for metadata-only
                coordinates: {
                    latitude: [],
                    longitude: []
                },
                metadata: {
                    source: 'HRRR GRIB2',
                    format: 'GRIB2',
                    spatialResolution: 3000, // 3km for HRRR
                    coordinateSystem: bounds?.projection === 'Lambert Conformal' ? 'Lambert Conformal' : 'EPSG:4326',
                    parsingMethod: 'Metadata-only (prevents memory overflow)',
                    compression: 'Complex Packing (5.3)',
                    metadataOnly: true,
                    rawDataAvailable: !!data.rawDataBuffer,
                    dataStats: {
                        minValue: null,
                        maxValue: null,
                        validPoints: 0,
                        totalPoints: grid.numPoints,
                        dataAvailable: false,
                        reason: 'Metadata-only parsing to prevent stack overflow'
                    }
                }
            }
        };
    }

    /**
     * Parse large GRIB2 files using streaming approach
     */
    async parseBufferStreaming(buffer, options = {}) {
        const { maxMessages = 20, targetVariable = null } = options; // Increased from 10 to 20

        console.log('Using streaming parser for large GRIB2 file...');

        try {
            // Parse GRIB headers first to identify messages
            const messageHeaders = this.parseGRIBHeaders(buffer);
            console.log(`Found ${messageHeaders.length} GRIB message headers`);

            // Search extensively for precipitation data - HRRR often has it later in the file
            let messagesToProcess = Math.min(100, messageHeaders.length); // Search up to 100 messages
            console.log(`Searching ${messagesToProcess} messages for precipitation data (APCP)`);

            const processedMessages = [];

            for (let i = 0; i < messagesToProcess; i++) {
                try {
                    const header = messageHeaders[i];
                    const messageBuffer = buffer.slice(header.start, header.end);

                    // METADATA-ONLY: Extract grid info without decoding massive data arrays
                    const metadata = await this.extractGRIBMetadataOnly(messageBuffer);
                    if (metadata) {
                        // Store raw buffer reference for potential future use
                        metadata.rawDataBuffer = messageBuffer;
                        metadata.messageIndex = i;
                        processedMessages.push(metadata);
                        // Minimal logging
                    }

                    // Break if we found our target variable
                    if (targetVariable && processedMessages.length > 0) {
                        const lastMsg = processedMessages[processedMessages.length - 1];
                        // Check for APCP precipitation match
                        if ((targetVariable === 'APCP' && lastMsg.parameterName === 'APCP') ||
                            (targetVariable.includes('precipitation') && lastMsg.parameterName === 'APCP') ||
                            (targetVariable.includes('APCP') && lastMsg.parameterName === 'APCP') ||
                            this.getVariableFromMessage(lastMsg) === targetVariable) {
                            console.log(`Found target variable ${targetVariable} (${lastMsg.parameterName}) in message ${i}, stopping processing`);
                            break;
                        }
                    }
                } catch (msgError) {
                    console.warn(`Failed to process message ${i}:`, msgError.message);
                }
            }

            // Check for precipitation data and log findings
            const precipitationMessages = processedMessages.filter(msg =>
                msg.parameterName === 'APCP' ||
                (msg.parameter?.category === 0 && msg.parameter?.number === 1) ||
                (msg.parameter?.category === 0 && msg.parameter?.number === 8)
            );

            console.log(`Found ${precipitationMessages.length} precipitation message(s) stored as ArrayBuffer(s)`);
            if (precipitationMessages.length > 0) {
                precipitationMessages.forEach((msg, idx) => {
                    console.log(`APCP message ${idx + 1}: ${msg.rawDataBuffer?.byteLength || 0} bytes at index ${msg.messageIndex}`);
                });
            }

            // If no precipitation found, log some found parameters for debugging
            if (precipitationMessages.length === 0 && processedMessages.length > 0) {
                const sampleParams = processedMessages.slice(0, 10).map(msg =>
                    `${msg.parameterName}(${msg.parameter?.category},${msg.parameter?.number})`
                ).join(', ');
                console.log(`No precipitation found. Sample parameters: ${sampleParams}...`);
                console.log(`Suggestion: APCP might be in messages ${messagesToProcess + 1}-${messageHeaders.length}`);
            }

            return {
                version: 2,
                totalLength: buffer.byteLength,
                sections: [],
                messages: processedMessages,
                buffer: buffer,
                rawData: buffer,
                decodedData: processedMessages,
                metadata: {
                    discipline: 0,
                    edition: 2,
                    messageCount: processedMessages.length,
                    totalMessages: messageHeaders.length,
                    totalSize: buffer.byteLength,
                    precipitationMessagesFound: precipitationMessages.length,
                    precipitationBuffers: precipitationMessages.map(msg => ({
                        messageIndex: msg.messageIndex,
                        bufferSize: msg.rawDataBuffer?.byteLength || 0,
                        parameter: msg.parameterName
                    })),
                    streamingParsed: true,
                    memoryOptimized: true,
                    parsingImplemented: true,
                    parserUsed: 'Research Implementation (Streaming)'
                }
            };

        } catch (error) {
            console.error('Streaming GRIB2 parsing failed:', error);
            throw new Error(`Streaming GRIB2 parsing failed: ${error.message}`);
        }
    }

    /**
     * Parse GRIB message headers without full decoding
     */
    parseGRIBHeaders(buffer) {
        const headers = [];
        let offset = 0;

        while (offset < buffer.byteLength - 16) {
            try {
                const view = new DataView(buffer, offset);
                const magic = String.fromCharCode(view.getUint8(0), view.getUint8(1), view.getUint8(2), view.getUint8(3));

                if (magic === 'GRIB') {
                    const messageLength = view.getUint32(12, false); // Big-endian
                    headers.push({
                        start: offset,
                        end: offset + messageLength,
                        length: messageLength
                    });
                    offset += messageLength;
                } else {
                    offset++;
                }
            } catch (e) {
                offset++;
            }
        }

        return headers;
    }

    /**
     * Extract variable name from GRIB2 message
     */
    getVariableFromMessage(message) {
        try {
            if (message && message.data && message.data.product) {
                // Try to extract parameter information
                const product = message.data.product;
                return product['Parameter category'] || product['Parameter'] || 'unknown';
            }
            return 'unknown';
        } catch (e) {
            return 'unknown';
        }
    }

    /**
     * Extract data in HydroLang format from parsed GRIB2
     * @param {Object} message - Parsed GRIB2 message
     * @param {Object} options - Extraction options
     * @returns {Object} HydroLang formatted data
     */
    extractParsedData(message, options) {
        // The research parser should have provided parsed GRIB2 data
        if (!message.data) {
            throw new Error('No parsed data available from GRIB2 research parser');
        }

        const data = message.data;

        // Check if we have the expected data structure from research parser
        if (!data.grid) {
            throw new Error('GRIB2 research parser did not provide grid information');
        }

        // Skip validation for metadata-only parsing
        if (!data.metadataOnly && (!data.values || !Array.isArray(data.values))) {
            throw new Error('GRIB2 research parser did not provide meteorological values array');
        }

        // Handle metadata-only parsing (prevents stack overflow)
        if (data.metadataOnly) {
            const isPrecipitation = data.parameterName === 'APCP' ||
                (data.parameter?.category === 0 && data.parameter?.number === 1) ||
                (data.parameter?.category === 0 && data.parameter?.number === 8);

            // Return metadata structure with ArrayBuffer preserved
            return {
                success: true,
                data: {
                    parameter: data.parameterName || 'Unknown',
                    values: [], // Empty - data stored as ArrayBuffer for on-demand parsing
                    coordinates: this.generateBasicCoordinates(data.grid, options.bbox),
                    rawDataBuffer: data.rawDataBuffer, // ArrayBuffer preserved for targeted extraction
                    messageIndex: data.messageIndex,
                    isPrecipitation: isPrecipitation,
                    shape: [data.grid.numLatPoints || 1381, data.grid.numLongPoints || 1381],
                    grid: {
                        Nx: data.grid.numLongPoints || 1381,
                        Ny: data.grid.numLatPoints || 1381,
                        totalPoints: data.grid.numPoints
                    },
                    bbox: options.bbox,
                    metadata: {
                        source: 'HRRR GRIB2',
                        format: 'GRIB2',
                        metadataOnly: true,
                        dataStoredAs: 'ArrayBuffer',
                        bufferSize: data.rawDataBuffer?.byteLength || 0,
                        canExtractOnDemand: true,
                        extractionMethod: 'Parse specific locations/steps when requested',
                        gridDefinition: {
                            Nx: data.grid.numLongPoints || 1381,
                            Ny: data.grid.numLatPoints || 1381,
                            projection: 'HRRR CONUS'
                        }
                    }
                }
            };
        }

        // Legacy full parsing (only for small files)
        const hasValues = data.values && data.values.length > 0;
        let validValues = [];
        let minValue = 'N/A';
        let maxValue = 'N/A';

        if (hasValues) {
            // Sample only first 10000 values to avoid stack overflow
            const sampleSize = Math.min(10000, data.values.length);
            const sampleValues = data.values.slice(0, sampleSize);
            validValues = sampleValues.filter(v => v !== null && !isNaN(v));

            if (validValues.length > 0) {
                minValue = Math.min(...validValues);
                maxValue = Math.max(...validValues);
            }
        }

        console.log('Extracting GRIB2 data from research parser:', {
            gridPoints: data.grid.numPoints,
            latPoints: data.grid.numLatPoints,
            lonPoints: data.grid.numLongPoints,
            valueCount: hasValues ? data.values.length : 0,
            sampleValues: hasValues ? data.values.slice(0, 5) : [],
            validValueCount: validValues.length,
            minValue: minValue,
            maxValue: maxValue,
            hasData: hasValues,
            sampled: hasValues && data.values.length > 10000
        });

        // Convert linear values array to 2D grid (research parser stores as 1D array)
        const gridValues = [];
        const numLat = Math.round(data.grid.numLatPoints) || 1;
        const numLon = Math.round(data.grid.numLongPoints) || 1;

        // Validate that we have data values
        if (!data.values || data.values.length === 0) {
            console.warn('No meteorological data values found in GRIB2 message');
            // Create a minimal grid with null values
            for (let lat = 0; lat < numLat; lat++) {
                const row = [];
                for (let lon = 0; lon < numLon; lon++) {
                    row.push(null);
                }
                gridValues.push(row);
            }
        } else {
            // Convert 1D array to 2D grid
            for (let lat = 0; lat < numLat; lat++) {
                const row = [];
                for (let lon = 0; lon < numLon; lon++) {
                    const index = lat * numLon + lon;
                    if (index < data.values.length) {
                        row.push(data.values[index]);
                    } else {
                        row.push(null); // Fill missing values with null
                    }
                }
                gridValues.push(row);
            }
        }

        // Extract data in HydroLang format
        const gridData = {
            parameter: this.getParameterFromParsedData(data),
            level: 'surface',
            bbox: options.bbox || this.getFullGridBounds(data.grid),
            timeRange: { start: options.startDate, end: options.endDate },
            data: {
                values: gridValues, // 2D array [lat][lon]
                shape: [numLat, numLon],
                coordinates: this.generateCoordinatesFromParsedData(data.grid, options.bbox)
            },
            metadata: {
                units: this.getUnitsFromParsedData(data),
                missingValue: null, // Research parser handles nulls
                scaleFactor: 1.0,
                gridType: 'latlon',
                parameterName: this.getParameterNameFromParsedData(data),
                parameterDescription: this.getParameterDescriptionFromParsedData(data),
                gridDefinition: data.grid,
                originalShape: [numLat, numLon],
                subsetShape: [numLat, numLon],
                spatialResolution: Math.abs(data.grid.incJ || 0.03),
                coordinateSystem: 'EPSG:4326',
                source: 'GRIB2 Research Parser',
                parsingMethod: 'Gerard Llorach Research Implementation',
                compression: data.compression || null,
                dataStats: {
                    minValue: minValue !== 'N/A' ? minValue : null,
                    maxValue: maxValue !== 'N/A' ? maxValue : null,
                    validPoints: validValues.length,
                    totalPoints: hasValues ? data.values.length : 0,
                    dataAvailable: hasValues,
                    sampled: hasValues && data.values.length > 10000
                }
            }
        };

        const logData = {
            parameter: gridData.parameter,
            shape: gridData.data.shape,
            units: gridData.metadata.units,
            validDataPoints: gridData.metadata.dataStats.validPoints,
            dataAvailable: gridData.metadata.dataStats.dataAvailable
        };

        if (gridData.metadata.dataStats.minValue !== null && gridData.metadata.dataStats.maxValue !== null) {
            logData.valueRange = `${gridData.metadata.dataStats.minValue.toFixed(3)} - ${gridData.metadata.dataStats.maxValue.toFixed(3)}`;
        } else {
            logData.valueRange = 'No valid data values';
        }

        console.log('Successfully extracted GRIB2 meteorological data:', logData);

        return gridData;
    }

    /**
     * Get parameter information from parsed data
     * @param {Object} data - Parsed GRIB2 data
     * @returns {string} Parameter code
     */
    getParameterFromData(data) {
        return 'unknown';
    }

    /**
     * Get parameter name from parsed data
     * @param {Object} data - Parsed GRIB2 data
     * @returns {string} Parameter name
     */
    getParameterNameFromData(data) {
        return 'Unknown Parameter';
    }

    /**
     * Get parameter description from parsed data
     * @param {Object} data - Parsed GRIB2 data
     * @returns {string} Parameter description
     */
    getParameterDescriptionFromData(data) {
        return 'Parameter from GRIB2 meteorological data';
    }

    /**
     * Get units from parsed data
     * @param {Object} data - Parsed GRIB2 data
     * @returns {string} Units
     */
    getUnitsFromData(data) {
        return 'unknown';
    }

    /**
     * Get full grid bounds
     * @param {Object} grid - Grid definition
     * @returns {Array} Bounding box [west, south, east, north]
     */
    getFullGridBounds(grid) {
        if (grid && grid.lonStart !== undefined && grid.latStart !== undefined) {
            return [grid.lonStart, grid.latStart, grid.lonEnd || grid.lonStart + 10, grid.latEnd || grid.latStart + 10];
        }
        return [-180, -90, 180, 90]; // Default world bounds
    }

    /**
     * Generate coordinates from parsed GRIB2 data
     * @param {Object} grid - Grid definition from parsed data
     * @param {Array} bbox - Bounding box (optional)
     * @returns {Object} Coordinate arrays
     */
    generateCoordinatesFromParsedData(grid, bbox) {
        if (!grid) {
            return { latitude: [], longitude: [] };
        }

        // Use proper grid dimensions
        const numLat = Math.round(grid.numLatPoints) || 10;
        const numLon = Math.round(grid.numLongPoints) || 10;

        if (bbox) {
            // Generate coordinates for bbox subset
            const [west, south, east, north] = bbox;
            const latStep = (north - south) / Math.max(1, numLat - 1);
            const lonStep = (east - west) / Math.max(1, numLon - 1);

            const latitudes = [];
            const longitudes = [];

            for (let i = 0; i < numLat; i++) {
                latitudes.push(south + i * latStep);
            }
            for (let j = 0; j < numLon; j++) {
                longitudes.push(west + j * lonStep);
            }

            return {
                latitude: latitudes,
                longitude: longitudes,
                bounds: { north, south, east, west }
            };
        } else {
            // For Lambert Conformal grids (HRRR), we need to handle coordinates differently
            if (grid.lambertLat1 !== undefined) {
                // This is a Lambert Conformal grid - coordinates are complex to calculate
                // For now, generate a simple approximation
                const latitudes = [];
                const longitudes = [];

                // Use approximate bounds for HRRR (continental US)
                const approxLatStart = 21.0;
                const approxLatEnd = 47.8;
                const approxLonStart = -134.1;
                const approxLonEnd = -60.9;

                const latStep = (approxLatEnd - approxLatStart) / Math.max(1, numLat - 1);
                const lonStep = (approxLonEnd - approxLonStart) / Math.max(1, numLon - 1);

                for (let i = 0; i < numLat; i++) {
                    latitudes.push(approxLatStart + i * latStep);
                }
                for (let j = 0; j < numLon; j++) {
                    longitudes.push(approxLonStart + j * lonStep);
                }

                return {
                    latitude: latitudes,
                    longitude: longitudes,
                    bounds: {
                        north: approxLatEnd,
                        south: approxLatStart,
                        east: approxLonEnd,
                        west: approxLonStart
                    },
                    projection: 'Lambert Conformal',
                    note: 'Approximate coordinates - exact projection conversion needed'
                };
            } else {
                // Regular latitude-longitude grid
                const latitudes = [];
                const longitudes = [];

                const latStart = grid.latStart || 0;
                const lonStart = grid.lonStart || 0;
                const latInc = grid.incJ || 0.1;
                const lonInc = grid.incI || 0.1;

                for (let i = 0; i < numLat; i++) {
                    latitudes.push(latStart + i * latInc);
                }
                for (let j = 0; j < numLon; j++) {
                    longitudes.push(lonStart + j * lonInc);
                }

                return {
                    latitude: latitudes,
                    longitude: longitudes,
                    bounds: {
                        north: grid.latEnd || latStart + numLat * latInc,
                        south: latStart,
                        east: grid.lonEnd || lonStart + numLon * lonInc,
                        west: lonStart
                    }
                };
            }
        }
    }

    /**
     * Get parameter from parsed data
     * @param {Object} data - Parsed GRIB2 data
     * @returns {string} Parameter code
     */
    getParameterFromParsedData(data) {
        if (data.product && data.product['Parameter number']) {
            return `0,${data.product['Parameter category'] || 1},${data.product['Parameter number']}`;
        }
        return '0,1,8'; // Default to precipitation
    }

    /**
     * Get parameter name from parsed data
     * @param {Object} data - Parsed GRIB2 data
     * @returns {string} Parameter name
     */
    getParameterNameFromParsedData(data) {
        if (data.product && data.product['Parameter number (see Code table 4.2)']) {
            return data.product['Parameter number (see Code table 4.2)'];
        }
        return 'Total Precipitation';
    }

    /**
     * Get parameter description from parsed data
     * @param {Object} data - Parsed GRIB2 data
     * @returns {string} Parameter description
     */
    getParameterDescriptionFromParsedData(data) {
        const paramName = this.getParameterNameFromParsedData(data);
        return `${paramName} from GRIB2 meteorological data`;
    }

    /**
     * Get units from parsed data
     * @param {Object} data - Parsed GRIB2 data
     * @returns {string} Units
     */
    getUnitsFromParsedData(data) {
        const paramName = this.getParameterNameFromParsedData(data);

        // Common parameter units
        if (paramName.toLowerCase().includes('precipitation') ||
            paramName.toLowerCase().includes('rain')) {
            return 'kg/m²';
        }
        if (paramName.toLowerCase().includes('temperature')) {
            return 'K';
        }
        if (paramName.toLowerCase().includes('wind') ||
            paramName.toLowerCase().includes('speed')) {
            return 'm/s';
        }
        if (paramName.toLowerCase().includes('pressure')) {
            return 'Pa';
        }

        return 'unknown';
    }
}

// Main GRIB2 processing class
class GRIB2Processor {

    constructor() {
        this.parser = new HydroLangGRIB2Parser();
    }

    /**
     * Parse GRIB2 buffer and extract grid data
     * @param {ArrayBuffer} buffer - GRIB2 file buffer
     * @param {Object} options - Extraction options
     * @returns {Promise<Object>} Parsed grid data
     */
    async parseGRIB2Buffer(buffer, options = {}) {
        console.log('Starting GRIB2 processing...');

        try {
            // Use our parser
            const parsedData = await this.parser.parseBuffer(buffer, options);

            console.log('GRIB2 processing completed');
            return parsedData;

        } catch (error) {
            console.error('GRIB2 processing failed:', error);
            throw new Error(`GRIB2 processing error: ${error.message}`);
        }
    }

    /**
     * Extract GRIB2 data with proper error handling
     * @param {Object} grib2Data - GRIB2 data object
     * @param {Object} options - Extraction options
     * @returns {Promise<Object>} Extracted data
     */
    async extractGRIB2Data(grib2Data, options = {}) {
        const { parameter, level, bbox, startDate, endDate } = options;

        console.log('Extracting GRIB2 data...', { parameter, level, bbox });

        try {
            // Check if we have parsed messages from the research parser
            if (grib2Data.messages && grib2Data.messages.length > 0) {
                // Use the first message (most GRIB2 files contain one message)
                const firstMessage = grib2Data.messages[0];
                const extractedData = this.parser.extractParsedData(firstMessage, options);

                // Skip spatial subsetting for metadata-only responses
                if (bbox && bbox.length === 4 && !extractedData.metadata?.metadataOnly) {
                    // Only apply subsetting if we have actual grid data
                    if (extractedData.metadata && extractedData.metadata.gridDefinition) {
                        extractedData.data = this.applySpatialSubsetting(extractedData.data, extractedData.metadata.gridDefinition, bbox);
                    }
                    extractedData.bbox = bbox;
                }

                console.log('GRIB2 data extraction completed successfully using parsed messages');
                return extractedData;
            }

            // If no parsed messages, try to get buffer for parsing
            let buffer = grib2Data?.buffer || grib2Data?.rawData;

            if (!buffer) {
                throw new Error('No GRIB2 file buffer available for parsing. The data structure does not contain parsed messages or original buffer.');
            }

            // Parse the buffer and then extract
            const parsedData = await this.parseGRIB2Buffer(buffer, options);

            if (parsedData.messages && parsedData.messages.length > 0) {
                const extractedData = this.extractParsedData(parsedData.messages[0], options);

                // Apply spatial subsetting if requested
                if (bbox && bbox.length === 4) {
                    extractedData.data = this.applySpatialSubsetting(extractedData.data, extractedData.metadata.gridDefinition, bbox);
                    extractedData.bbox = bbox;
                }

                console.log('GRIB2 data extraction completed successfully');
                return extractedData;
            } else {
                throw new Error('GRIB2 parsing completed but no messages were found');
            }

        } catch (error) {
            console.error('GRIB2 data extraction failed:', error);

            // Provide clear error about the limitation
            throw new Error(
                'GRIB2 data extraction failed. This application successfully downloads real HRRR GRIB2 files ' +
                'from NOAA containing actual meteorological data, but the parsing implementation encountered an error. ' +
                'The downloaded files contain real weather data values, but the current implementation could not extract them. ' +
                'Error: ' + error.message
            );
        }
    }

    /**
     * Apply spatial subsetting to GRIB2 data
     * @param {Object} data - Grid data
     * @param {Object} gridDef - Grid definition
     * @param {Array} bbox - Bounding box [west, south, east, north]
     * @returns {Object} Subsetted data
     */
    applySpatialSubsetting(data, gridDef, bbox) {
        if (!bbox || bbox.length !== 4) {
            return data;
        }

        // For now, return the full data since we can't actually parse it
        console.log('Spatial subsetting requested but GRIB2 parsing not implemented');
        return data;
    }
}

// Export the processor
export default GRIB2Processor;

// Also export individual functions for compatibility
export { HydroLangGRIB2Parser };
export const parseGRIB2 = async (buffer) => {
    const processor = new GRIB2Processor();
    return await processor.parseGRIB2Buffer(buffer);
};

export const extractGRIB2Data = async (grib2Data, options) => {
    const processor = new GRIB2Processor();
    return await processor.extractGRIB2Data(grib2Data, options);
};