/**
 * Wake Word Detector
 *
 * Detects wake words in audio stream using Porcupine (Picovoice) engine
 * with fallback to text-match mode when no access key is available.
 */
import { EventEmitter } from 'events';
import { DEFAULT_WAKE_WORD_CONFIG } from './types.js';
/**
 * Built-in Porcupine keyword mapping.
 * Maps friendly wake word names to Porcupine built-in keyword identifiers.
 */
const BUILTIN_KEYWORD_MAP = {
    'hey buddy': 'COMPUTER',
    'ok code': 'PICOVOICE',
    'computer': 'COMPUTER',
    'picovoice': 'PICOVOICE',
    'alexa': 'ALEXA',
    'hey google': 'HEY_GOOGLE',
    'hey siri': 'HEY_SIRI',
    'ok google': 'OK_GOOGLE',
    'jarvis': 'JARVIS',
    'bumblebee': 'BUMBLEBEE',
    'porcupine': 'PORCUPINE',
    'terminator': 'TERMINATOR',
    'blueberry': 'BLUEBERRY',
    'grapefruit': 'GRAPEFRUIT',
    'grasshopper': 'GRASSHOPPER',
    'americano': 'AMERICANO',
};
/**
 * Wake word detector with Porcupine engine and text-match fallback
 */
export class WakeWordDetector extends EventEmitter {
    config;
    running = false;
    porcupine = null;
    engine = 'text-match';
    lastDetection = null;
    cooldownMs = 1000;
    keywordLabels = [];
    constructor(config = {}) {
        super();
        this.config = { ...DEFAULT_WAKE_WORD_CONFIG, ...config };
    }
    async start() {
        if (this.running)
            return;
        const accessKey = this.config.accessKey || process.env.PICOVOICE_ACCESS_KEY;
        const requestedEngine = this.config.engine;
        if (requestedEngine === 'text-match' || !accessKey) {
            this.engine = 'text-match';
            if (!accessKey && requestedEngine !== 'text-match') {
                console.warn('[WakeWord] No PICOVOICE_ACCESS_KEY found, falling back to text-match mode');
            }
        }
        else {
            try {
                await this.initPorcupine(accessKey);
                this.engine = 'porcupine';
            }
            catch (error) {
                const msg = error instanceof Error ? error.message : String(error);
                console.warn(`[WakeWord] Porcupine init failed (${msg}), falling back to text-match mode`);
                this.engine = 'text-match';
            }
        }
        this.running = true;
        this.emit('started');
    }
    async initPorcupine(accessKey) {
        const { Porcupine, BuiltinKeyword, getBuiltinKeywordPath } = await import('@picovoice/porcupine-node');
        if (this.config.keywordPaths && this.config.keywordPaths.length > 0) {
            // Custom .ppn keyword files
            const sensitivities = this.config.keywordPaths.map(() => this.config.sensitivity);
            this.porcupine = new Porcupine(accessKey, this.config.keywordPaths, sensitivities);
            this.keywordLabels = this.config.keywordPaths.map((p, i) => this.config.wakeWords[i] || `keyword_${i}`);
        }
        else {
            // Map configured wake words to built-in keyword paths
            const keywordPaths = [];
            const labels = [];
            for (const word of this.config.wakeWords) {
                const builtinName = BUILTIN_KEYWORD_MAP[word.toLowerCase()];
                if (builtinName) {
                    const builtinEnum = BuiltinKeyword[builtinName];
                    if (builtinEnum !== undefined) {
                        keywordPaths.push(getBuiltinKeywordPath(builtinEnum));
                        labels.push(word);
                    }
                }
            }
            if (keywordPaths.length === 0) {
                // Default to COMPUTER if no mapping found
                keywordPaths.push(getBuiltinKeywordPath(BuiltinKeyword.COMPUTER));
                labels.push(this.config.wakeWords[0] || 'hey buddy');
            }
            const sensitivities = keywordPaths.map(() => this.config.sensitivity);
            this.porcupine = new Porcupine(accessKey, keywordPaths, sensitivities);
            this.keywordLabels = labels;
        }
    }
    async stop() {
        if (!this.running)
            return;
        this.running = false;
        if (this.porcupine) {
            this.porcupine.release();
            this.porcupine = null;
        }
        this.emit('stopped');
    }
    processFrame(frame) {
        if (!this.running)
            return null;
        // Cooldown check
        if (this.lastDetection && Date.now() - this.lastDetection.getTime() < this.cooldownMs) {
            return null;
        }
        if (this.engine === 'porcupine' && this.porcupine) {
            return this.processPorcupineFrame(frame);
        }
        // text-match mode: no-op for raw audio frames
        // Text matching is handled via detectWakeWordText()
        return null;
    }
    processPorcupineFrame(frame) {
        if (!this.porcupine)
            return null;
        let int16Frame;
        if (frame instanceof Int16Array) {
            int16Frame = frame;
        }
        else {
            // Convert Buffer to Int16Array
            int16Frame = new Int16Array(frame.buffer, frame.byteOffset, frame.length / 2);
        }
        const keywordIndex = this.porcupine.process(int16Frame);
        if (keywordIndex >= 0) {
            const detection = {
                wakeWord: this.keywordLabels[keywordIndex] || `keyword_${keywordIndex}`,
                confidence: 1.0, // Porcupine is binary (detected or not)
                timestamp: new Date(),
                frameIndex: keywordIndex,
            };
            this.lastDetection = new Date();
            this.emit('detected', detection);
            return detection;
        }
        return null;
    }
    /**
     * Text-based wake word detection (fallback for when Porcupine is not available).
     * Call this with transcribed text instead of raw audio frames.
     */
    detectWakeWordText(text) {
        if (!this.running)
            return null;
        // Cooldown check
        if (this.lastDetection && Date.now() - this.lastDetection.getTime() < this.cooldownMs) {
            return null;
        }
        const normalized = text.toLowerCase().trim();
        for (const word of this.config.wakeWords) {
            if (normalized.includes(word.toLowerCase())) {
                const detection = {
                    wakeWord: word,
                    confidence: 0.85,
                    timestamp: new Date(),
                };
                this.lastDetection = new Date();
                this.emit('detected', detection);
                return detection;
            }
        }
        return null;
    }
    /**
     * Get the active detection engine
     */
    getEngine() {
        return this.engine;
    }
    /**
     * Get Porcupine frame length (samples per frame required)
     */
    get frameLength() {
        return this.porcupine?.frameLength ?? this.config.frameSize;
    }
    /**
     * Get Porcupine sample rate
     */
    get sampleRate() {
        return this.porcupine?.sampleRate ?? this.config.sampleRate;
    }
    isRunning() {
        return this.running;
    }
    getConfig() {
        return { ...this.config };
    }
    updateConfig(config) {
        this.config = { ...this.config, ...config };
    }
    /**
     * Add a custom wake word
     */
    addWakeWord(wakeWord) {
        if (!this.config.wakeWords.includes(wakeWord)) {
            this.config.wakeWords.push(wakeWord);
        }
    }
    /**
     * Remove a wake word
     */
    removeWakeWord(wakeWord) {
        const index = this.config.wakeWords.indexOf(wakeWord);
        if (index >= 0) {
            this.config.wakeWords.splice(index, 1);
        }
    }
    /**
     * Get current wake words
     */
    getWakeWords() {
        return [...this.config.wakeWords];
    }
    /**
     * Set sensitivity
     */
    setSensitivity(sensitivity) {
        this.config.sensitivity = Math.max(0, Math.min(1, sensitivity));
    }
    /**
     * Clear audio buffer (no-op, kept for API compatibility)
     */
    clearBuffer() {
        // Porcupine processes frames individually, no buffer to clear
    }
}
/**
 * Factory function for creating wake word detector
 */
export function createWakeWordDetector(config) {
    return new WakeWordDetector(config);
}
export default WakeWordDetector;
//# sourceMappingURL=wake-word.js.map