Source: zpt/parameters/ParameterNormalizer.js

/**
 * Normalizes and standardizes ZPT navigation parameters
 */
export default class ParameterNormalizer {
    constructor() {
        this.defaults = this.initializeDefaults();
    }

    /**
     * Initialize default values for parameters
     */
    initializeDefaults() {
        return {
            pan: {},
            transform: {
                maxTokens: 4000,
                format: 'structured',
                tokenizer: 'cl100k_base',
                includeMetadata: true,
                chunkStrategy: 'semantic'
            }
        };
    }

    /**
     * Normalize complete parameter object
     * @param {Object} params - Raw navigation parameters
     * @returns {Object} Normalized parameters
     */
    normalize(params) {
        if (!params || typeof params !== 'object') {
            throw new Error('Parameters must be an object');
        }

        const normalized = {
            zoom: this.normalizeZoom(params.zoom),
            pan: this.normalizePan(params.pan),
            tilt: this.normalizeTilt(params.tilt),
            transform: this.normalizeTransform(params.transform)
        };

        // Add computed metadata
        normalized._metadata = {
            normalizedAt: new Date().toISOString(),
            hasFilters: this.hasFilters(normalized.pan),
            tokenBudget: normalized.transform.maxTokens,
            complexity: this.calculateComplexity(normalized)
        };

        return normalized;
    }

    /**
     * Normalize zoom parameter
     */
    normalizeZoom(zoom) {
        if (!zoom) {
            throw new Error('Zoom parameter is required');
        }

        return {
            level: zoom,
            granularity: this.getZoomGranularity(zoom),
            targetTypes: this.getTargetTypes(zoom)
        };
    }

    /**
     * Normalize pan parameter with filters
     */
    normalizePan(pan = {}) {
        const normalized = {
            topic: this.normalizeTopicFilter(pan.topic),
            entity: this.normalizeEntityFilter(pan.entity),
            temporal: this.normalizeTemporalFilter(pan.temporal),
            geographic: this.normalizeGeographicFilter(pan.geographic)
        };

        // Remove undefined filters
        Object.keys(normalized).forEach(key => {
            if (normalized[key] === undefined) {
                delete normalized[key];
            }
        });

        return normalized;
    }

    /**
     * Normalize tilt parameter
     */
    normalizeTilt(tilt) {
        if (!tilt) {
            throw new Error('Tilt parameter is required');
        }

        return {
            representation: tilt,
            outputFormat: this.getTiltOutputFormat(tilt),
            processingType: this.getTiltProcessingType(tilt)
        };
    }

    /**
     * Normalize transform parameter with defaults
     */
    normalizeTransform(transform = {}) {
        const normalized = {
            ...this.defaults.transform,
            ...transform
        };

        // Validate and adjust token limits
        normalized.maxTokens = Math.max(100, Math.min(128000, normalized.maxTokens));

        // Add computed transform properties
        normalized.tokenBudget = {
            content: Math.floor(normalized.maxTokens * 0.8),
            metadata: Math.floor(normalized.maxTokens * 0.15),
            overhead: Math.floor(normalized.maxTokens * 0.05)
        };

        // Set chunk size based on strategy
        normalized.chunkSize = this.calculateChunkSize(normalized);

        return normalized;
    }

    /**
     * Normalize topic filter
     */
    normalizeTopicFilter(topic) {
        if (!topic) return undefined;

        return {
            value: topic.toLowerCase().trim(),
            pattern: topic.includes('*') ? 'wildcard' : 'exact',
            namespace: this.extractNamespace(topic)
        };
    }

    /**
     * Normalize entity filter
     */
    normalizeEntityFilter(entities) {
        if (!entities || !Array.isArray(entities)) return undefined;

        return {
            values: entities.map(e => e.trim()),
            count: entities.length,
            type: entities.length === 1 ? 'single' : 'multiple'
        };
    }

    /**
     * Normalize temporal filter
     */
    normalizeTemporalFilter(temporal) {
        if (!temporal) return undefined;

        const normalized = {};

        if (temporal.start) {
            normalized.start = new Date(temporal.start).toISOString();
        }

        if (temporal.end) {
            normalized.end = new Date(temporal.end).toISOString();
        }

        // Calculate duration if both dates are provided
        if (normalized.start && normalized.end) {
            const startDate = new Date(normalized.start);
            const endDate = new Date(normalized.end);
            normalized.duration = endDate.getTime() - startDate.getTime();
            normalized.durationDays = Math.ceil(normalized.duration / (1000 * 60 * 60 * 24));
        }

        return normalized;
    }

    /**
     * Normalize geographic filter
     */
    normalizeGeographicFilter(geographic) {
        if (!geographic) return undefined;

        const normalized = {};

        if (geographic.bbox) {
            const [minLon, minLat, maxLon, maxLat] = geographic.bbox;
            normalized.bbox = {
                minLon, minLat, maxLon, maxLat,
                width: maxLon - minLon,
                height: maxLat - minLat,
                area: (maxLon - minLon) * (maxLat - minLat)
            };
        }

        if (geographic.center) {
            normalized.center = {
                lat: geographic.center.lat,
                lon: geographic.center.lon
            };
        }

        // Add radius if specified
        if (geographic.radius) {
            normalized.radius = geographic.radius;
        }

        return normalized;
    }

    /**
     * Get zoom granularity level
     */
    getZoomGranularity(zoom) {
        const granularityMap = {
            'corpus': 1,    // Highest level overview
            'community': 2, // Community-level aggregation
            'unit': 3,      // Semantic unit level
            'entity': 4,    // Individual entities
            'text': 5       // Full text detail
        };
        return granularityMap[zoom] || 3;
    }

    /**
     * Get target element types for zoom level
     */
    getTargetTypes(zoom) {
        const typeMap = {
            'corpus': ['ragno:Corpus'],
            'community': ['ragno:Community'],
            'unit': ['ragno:SemanticUnit', 'ragno:Unit'],
            'entity': ['ragno:Entity'],
            'text': ['ragno:TextElement', 'ragno:Text']
        };
        return typeMap[zoom] || [];
    }

    /**
     * Get tilt output format
     */
    getTiltOutputFormat(tilt) {
        const formatMap = {
            'embedding': 'vector',
            'keywords': 'text',
            'graph': 'structured',
            'temporal': 'sequence'
        };
        return formatMap[tilt] || 'text';
    }

    /**
     * Get tilt processing type
     */
    getTiltProcessingType(tilt) {
        const processingMap = {
            'embedding': 'vectorization',
            'keywords': 'extraction',
            'graph': 'relationship',
            'temporal': 'chronological'
        };
        return processingMap[tilt] || 'extraction';
    }

    /**
     * Calculate chunk size based on strategy
     */
    calculateChunkSize(transform) {
        const baseSize = transform.tokenBudget.content;
        
        switch (transform.chunkStrategy) {
            case 'fixed':
                return Math.floor(baseSize / 4); // Fixed chunks
            case 'adaptive':
                return { min: Math.floor(baseSize / 8), max: Math.floor(baseSize / 2) };
            case 'semantic':
            default:
                return { target: Math.floor(baseSize / 3), overlap: 50 };
        }
    }

    /**
     * Extract namespace from topic string
     */
    extractNamespace(topic) {
        if (topic.includes(':')) {
            return topic.split(':')[0];
        }
        return 'default';
    }

    /**
     * Check if pan has any active filters
     */
    hasFilters(pan) {
        return Object.keys(pan).length > 0;
    }

    /**
     * Calculate parameter complexity score
     */
    calculateComplexity(params) {
        let complexity = 0;

        // Base complexity from zoom level
        complexity += params.zoom.granularity;

        // Add complexity for filters
        if (params.pan.topic) complexity += 1;
        if (params.pan.entity) complexity += params.pan.entity.count;
        if (params.pan.temporal) complexity += 2;
        if (params.pan.geographic) complexity += 3;

        // Add complexity for tilt processing
        const tiltComplexity = {
            'keywords': 1,
            'temporal': 2,
            'graph': 3,
            'embedding': 4
        };
        complexity += tiltComplexity[params.tilt.representation] || 1;

        return Math.min(complexity, 10); // Cap at 10
    }

    /**
     * Create a hash of normalized parameters for caching
     */
    createParameterHash(normalizedParams) {
        const hashSource = {
            zoom: normalizedParams.zoom.level,
            pan: normalizedParams.pan,
            tilt: normalizedParams.tilt.representation,
            transform: {
                maxTokens: normalizedParams.transform.maxTokens,
                format: normalizedParams.transform.format,
                chunkStrategy: normalizedParams.transform.chunkStrategy
            }
        };

        return this.simpleHash(JSON.stringify(hashSource));
    }

    /**
     * Simple hash function for parameter caching
     */
    simpleHash(str) {
        let hash = 0;
        for (let i = 0; i < str.length; i++) {
            const char = str.charCodeAt(i);
            hash = ((hash << 5) - hash) + char;
            hash = hash & hash; // Convert to 32-bit integer
        }
        return Math.abs(hash).toString(36);
    }

    /**
     * Validate that normalized parameters are complete
     */
    validateNormalized(normalizedParams) {
        const required = ['zoom', 'pan', 'tilt', 'transform'];
        const missing = required.filter(field => !normalizedParams[field]);
        
        if (missing.length > 0) {
            throw new Error(`Missing required normalized fields: ${missing.join(', ')}`);
        }

        return true;
    }

    /**
     * Get parameter summary for logging
     */
    getParameterSummary(normalizedParams) {
        return {
            zoom: normalizedParams.zoom.level,
            filters: Object.keys(normalizedParams.pan).length,
            tilt: normalizedParams.tilt.representation,
            complexity: normalizedParams._metadata.complexity,
            tokenBudget: normalizedParams.transform.maxTokens
        };
    }
}