'use strict';

var React = require('react');

/* ────────────────────────────────────────────────────────────────
   Gameplay Analytics — Telemetry, heatmaps & session recording
   
   Comprehensive gameplay analytics with:
   - Event-based telemetry pipeline with batching
   - Spatial heatmaps (death, traversal, interaction, combat)
   - Session recording and playback
   - Funnel analysis and retention tracking
   - Performance metrics collection
──────────────────────────────────────────────────────────────── */
const DEFAULT_TELEMETRY_CONFIG = {
    enabled: true,
    batchSize: 50,
    flushIntervalMs: 30000,
    maxBufferSize: 10000,
    categories: [],
    sampleRate: 1,
};
const DEFAULT_HEATMAP_CONFIG = {
    type: 'death',
    cellSize: 1,
    bounds: { minX: -50, minY: -50, maxX: 50, maxY: 50 },
    colorStops: [
        { value: 0, color: 'rgba(0, 0, 255, 0)' },
        { value: 0.25, color: 'rgba(0, 0, 255, 0.5)' },
        { value: 0.5, color: 'rgba(0, 255, 0, 0.7)' },
        { value: 0.75, color: 'rgba(255, 255, 0, 0.8)' },
        { value: 1, color: 'rgba(255, 0, 0, 1)' },
    ],
    maxValue: 0,
    blurRadius: 1,
};
const DEFAULT_RECORDING_CONFIG = {
    enabled: false,
    fps: 10,
    maxDurationMs: 600000, // 10 minutes
    recordTypes: ['position', 'action', 'state-change'],
    recordPosition: true,
    compress: true,
};
/* ══════════════════════════════════════════════════════════════
   TELEMETRY PIPELINE
   ══════════════════════════════════════════════════════════════ */
class TelemetryPipeline {
    constructor(eventBus, config = {}) {
        this.buffer = [];
        this.flushed = [];
        this.flushTimer = null;
        this.eventCounter = 0;
        this.config = { ...DEFAULT_TELEMETRY_CONFIG, ...config };
        this.eventBus = eventBus;
        this.sessionId = this.generateId();
        if (this.config.enabled)
            this.startFlushTimer();
    }
    generateId() {
        return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
    }
    /** Track a telemetry event */
    track(category, name, data = {}, position) {
        if (!this.config.enabled)
            return;
        if (this.config.categories.length > 0 && !this.config.categories.includes(category))
            return;
        if (this.config.sampleRate < 1 && Math.random() > this.config.sampleRate)
            return;
        const event = {
            id: `evt-${++this.eventCounter}`,
            category,
            name,
            timestamp: Date.now(),
            sessionId: this.sessionId,
            data,
            position,
        };
        this.buffer.push(event);
        if (this.buffer.length >= this.config.batchSize) {
            this.flush();
        }
        // Trim buffer if too large
        if (this.buffer.length > this.config.maxBufferSize) {
            this.buffer = this.buffer.slice(-this.config.maxBufferSize);
        }
    }
    /** Flush buffered events */
    flush() {
        if (this.buffer.length === 0)
            return [];
        const batch = [...this.buffer];
        this.buffer = [];
        this.flushed.push(batch);
        this.eventBus.emit('analytics:flush', { count: batch.length });
        return batch;
    }
    startFlushTimer() {
        this.flushTimer = setInterval(() => this.flush(), this.config.flushIntervalMs);
    }
    /** Get all events (current buffer + flushed) */
    getAllEvents() {
        return [...this.flushed.flat(), ...this.buffer];
    }
    /** Get events by category */
    getByCategory(category) {
        return this.getAllEvents().filter(e => e.category === category);
    }
    /** Get event count by name */
    countByName(name) {
        return this.getAllEvents().filter(e => e.name === name).length;
    }
    getSessionId() {
        return this.sessionId;
    }
    getBufferSize() {
        return this.buffer.length;
    }
    getTotalEvents() {
        return this.flushed.reduce((s, b) => s + b.length, 0) + this.buffer.length;
    }
    clear() {
        this.buffer = [];
        this.flushed = [];
        this.eventCounter = 0;
    }
    destroy() {
        if (this.flushTimer)
            clearInterval(this.flushTimer);
        this.flush();
    }
}
/* ══════════════════════════════════════════════════════════════
   HEATMAP GENERATOR
   ══════════════════════════════════════════════════════════════ */
class HeatmapGenerator {
    constructor(config = {}) {
        this.grid = new Map();
        this.totalEvents = 0;
        this.config = { ...DEFAULT_HEATMAP_CONFIG, ...config };
    }
    cellKey(cx, cy) {
        return `${cx},${cy}`;
    }
    worldToCell(wx, wy) {
        return [
            Math.floor((wx - this.config.bounds.minX) / this.config.cellSize),
            Math.floor((wy - this.config.bounds.minY) / this.config.cellSize),
        ];
    }
    /** Add an event at a world position */
    addEvent(x, y, weight = 1) {
        var _a;
        if (x < this.config.bounds.minX || x > this.config.bounds.maxX ||
            y < this.config.bounds.minY || y > this.config.bounds.maxY)
            return;
        const [cx, cy] = this.worldToCell(x, y);
        const key = this.cellKey(cx, cy);
        this.grid.set(key, ((_a = this.grid.get(key)) !== null && _a !== void 0 ? _a : 0) + weight);
        this.totalEvents++;
    }
    /** Add events from telemetry data */
    addFromTelemetry(events) {
        for (const event of events) {
            if (event.position) {
                this.addEvent(event.position.x, event.position.y);
            }
        }
    }
    /** Get the grid as an array of cells with normalized values */
    getCells() {
        const cells = [];
        let maxVal = this.config.maxValue;
        if (maxVal <= 0) {
            // Auto-detect max value
            maxVal = 0;
            for (const v of this.grid.values()) {
                if (v > maxVal)
                    maxVal = v;
            }
        }
        if (maxVal === 0)
            maxVal = 1;
        for (const [key, value] of this.grid) {
            const [cx, cy] = key.split(',').map(Number);
            cells.push({
                x: cx,
                y: cy,
                value: Math.min(value / maxVal, 1),
                events: value,
            });
        }
        return cells;
    }
    /** Apply Gaussian blur to the grid */
    getBlurredCells() {
        var _a;
        const cells = this.getCells();
        if (this.config.blurRadius <= 0)
            return cells;
        const radius = this.config.blurRadius;
        const cellMap = new Map();
        // Build raw map
        for (const cell of cells) {
            cellMap.set(this.cellKey(cell.x, cell.y), cell.events);
        }
        // Apply blur
        const blurred = [];
        const sigma = radius / 2;
        const kernel = [];
        for (let dy = -radius; dy <= radius; dy++) {
            const row = [];
            for (let dx = -radius; dx <= radius; dx++) {
                row.push(Math.exp(-(dx * dx + dy * dy) / (2 * sigma * sigma)));
            }
            kernel.push(row);
        }
        // Normalize kernel
        let kernelSum = 0;
        for (const row of kernel)
            for (const v of row)
                kernelSum += v;
        const visited = new Set();
        for (const cell of cells) {
            for (let dy = -radius; dy <= radius; dy++) {
                for (let dx = -radius; dx <= radius; dx++) {
                    const nx = cell.x + dx;
                    const ny = cell.y + dy;
                    const key = this.cellKey(nx, ny);
                    if (visited.has(key))
                        continue;
                    visited.add(key);
                    let sum = 0;
                    for (let ky = -radius; ky <= radius; ky++) {
                        for (let kx = -radius; kx <= radius; kx++) {
                            const srcKey = this.cellKey(nx + kx, ny + ky);
                            const srcVal = (_a = cellMap.get(srcKey)) !== null && _a !== void 0 ? _a : 0;
                            sum += srcVal * kernel[ky + radius][kx + radius];
                        }
                    }
                    const normalized = sum / kernelSum;
                    if (normalized > 0.001) {
                        blurred.push({ x: nx, y: ny, value: 0, events: normalized });
                    }
                }
            }
        }
        // Normalize values
        let maxVal = this.config.maxValue;
        if (maxVal <= 0) {
            maxVal = Math.max(...blurred.map(c => c.events), 1);
        }
        for (const cell of blurred) {
            cell.value = Math.min(cell.events / maxVal, 1);
        }
        return blurred;
    }
    /** Render heatmap to a canvas */
    renderToCanvas(ctx, width, height) {
        const cells = this.getBlurredCells();
        const { bounds, cellSize, colorStops } = this.config;
        const worldW = bounds.maxX - bounds.minX;
        const worldH = bounds.maxY - bounds.minY;
        const scaleX = width / worldW;
        const scaleY = height / worldH;
        const cellW = cellSize * scaleX;
        const cellH = cellSize * scaleY;
        ctx.clearRect(0, 0, width, height);
        for (const cell of cells) {
            const screenX = cell.x * cellSize * scaleX;
            const screenY = cell.y * cellSize * scaleY;
            // Interpolate color from stops
            const color = this.interpolateColor(cell.value, colorStops);
            ctx.fillStyle = color;
            ctx.fillRect(screenX, screenY, cellW + 1, cellH + 1);
        }
    }
    interpolateColor(value, stops) {
        if (stops.length === 0)
            return 'transparent';
        if (stops.length === 1)
            return stops[0].color;
        for (let i = 0; i < stops.length - 1; i++) {
            if (value >= stops[i].value && value <= stops[i + 1].value) {
                const t = (value - stops[i].value) / (stops[i + 1].value - stops[i].value);
                return this.lerpColor(stops[i].color, stops[i + 1].color, t);
            }
        }
        return stops[stops.length - 1].color;
    }
    lerpColor(a, b, t) {
        const parse = (c) => {
            const m = c.match(/[\d.]+/g);
            return m ? m.map(Number) : [0, 0, 0, 1];
        };
        const ca = parse(a);
        const cb = parse(b);
        const r = Math.round(ca[0] + (cb[0] - ca[0]) * t);
        const g = Math.round(ca[1] + (cb[1] - ca[1]) * t);
        const bl = Math.round(ca[2] + (cb[2] - ca[2]) * t);
        const al = ca[3] + (cb[3] - ca[3]) * t;
        return `rgba(${r}, ${g}, ${bl}, ${al.toFixed(2)})`;
    }
    getTotalEvents() {
        return this.totalEvents;
    }
    getConfig() {
        return this.config;
    }
    clear() {
        this.grid.clear();
        this.totalEvents = 0;
    }
}
/* ══════════════════════════════════════════════════════════════
   SESSION RECORDER
   ══════════════════════════════════════════════════════════════ */
class SessionRecorder {
    constructor(config = {}) {
        this.recording = false;
        this.frames = [];
        this.startedAt = 0;
        this.lastFrameTime = 0;
        this.pendingEvents = [];
        this.recordTimer = null;
        this.config = { ...DEFAULT_RECORDING_CONFIG, ...config };
        this.sessionId = `sess-${Date.now().toString(36)}`;
    }
    /** Start recording */
    start() {
        if (this.recording)
            return;
        this.recording = true;
        this.startedAt = Date.now();
        this.lastFrameTime = this.startedAt;
        this.frames = [];
        const intervalMs = 1000 / this.config.fps;
        this.recordTimer = setInterval(() => this.captureFrame(), intervalMs);
    }
    /** Stop recording and return the session */
    stop(sceneId = 'unknown', buildVersion = '0.0.0') {
        this.recording = false;
        if (this.recordTimer) {
            clearInterval(this.recordTimer);
            this.recordTimer = null;
        }
        // Capture any remaining events
        if (this.pendingEvents.length > 0)
            this.captureFrame();
        const endedAt = Date.now();
        const recording = {
            id: `rec-${Date.now().toString(36)}`,
            sessionId: this.sessionId,
            startedAt: this.startedAt,
            endedAt,
            durationMs: endedAt - this.startedAt,
            frameCount: this.frames.length,
            frames: this.frames,
            metadata: { sceneId, buildVersion, tags: [] },
            sizeBytes: 0,
        };
        // Estimate size
        recording.sizeBytes = JSON.stringify(recording.frames).length;
        return recording;
    }
    /** Add an event to the current frame batch */
    recordEvent(type, data) {
        if (!this.recording)
            return;
        if (this.config.recordTypes.length > 0 && !this.config.recordTypes.includes(type))
            return;
        this.pendingEvents.push({ type, data });
    }
    /** Record player position (convenience) */
    recordPosition(x, y, z) {
        if (!this.config.recordPosition)
            return;
        this.recordEvent('position', { x, y, z });
    }
    captureFrame() {
        const now = Date.now();
        // Check max duration
        if (this.config.maxDurationMs > 0 && now - this.startedAt > this.config.maxDurationMs) {
            this.stop();
            return;
        }
        const frame = {
            timestamp: now,
            delta: now - this.lastFrameTime,
            events: [...this.pendingEvents],
        };
        this.frames.push(frame);
        this.pendingEvents = [];
        this.lastFrameTime = now;
    }
    isRecording() {
        return this.recording;
    }
    getFrameCount() {
        return this.frames.length;
    }
    getDurationMs() {
        return this.recording ? Date.now() - this.startedAt : 0;
    }
}
/* ══════════════════════════════════════════════════════════════
   SESSION PLAYER (Playback)
   ══════════════════════════════════════════════════════════════ */
class SessionPlayer {
    constructor() {
        this.recording = null;
        this.currentFrame = 0;
        this.playing = false;
        this.speed = 1;
        this.playTimer = null;
        this.onFrame = null;
    }
    load(recording) {
        this.recording = recording;
        this.currentFrame = 0;
        this.stop();
    }
    /** Start playback */
    play(onFrame) {
        if (!this.recording || this.playing)
            return;
        this.playing = true;
        this.onFrame = onFrame;
        this.advanceFrame();
    }
    advanceFrame() {
        if (!this.recording || !this.playing || this.currentFrame >= this.recording.frames.length) {
            this.stop();
            return;
        }
        const frame = this.recording.frames[this.currentFrame];
        if (this.onFrame)
            this.onFrame(frame, this.currentFrame);
        this.currentFrame++;
        if (this.currentFrame < this.recording.frames.length) {
            const nextFrame = this.recording.frames[this.currentFrame];
            const delay = (nextFrame.delta || 100) / this.speed;
            this.playTimer = setTimeout(() => this.advanceFrame(), delay);
        }
        else {
            this.stop();
        }
    }
    stop() {
        this.playing = false;
        if (this.playTimer) {
            clearTimeout(this.playTimer);
            this.playTimer = null;
        }
    }
    pause() {
        this.playing = false;
        if (this.playTimer) {
            clearTimeout(this.playTimer);
            this.playTimer = null;
        }
    }
    seek(frameIndex) {
        if (!this.recording)
            return;
        this.currentFrame = Math.max(0, Math.min(frameIndex, this.recording.frameCount - 1));
    }
    setSpeed(speed) {
        this.speed = Math.max(0.1, Math.min(10, speed));
    }
    getProgress() {
        if (!this.recording || this.recording.frameCount === 0)
            return 0;
        return this.currentFrame / this.recording.frameCount;
    }
    isPlaying() {
        return this.playing;
    }
}
/* ══════════════════════════════════════════════════════════════
   FUNNEL TRACKER
   ══════════════════════════════════════════════════════════════ */
class FunnelTracker {
    constructor() {
        this.funnels = new Map();
        this.progress = new Map(); // funnelId → stepId → sessionIds
    }
    define(funnel) {
        this.funnels.set(funnel.id, funnel);
        const stepMap = new Map();
        for (const step of funnel.steps) {
            stepMap.set(step.id, new Set());
        }
        this.progress.set(funnel.id, stepMap);
    }
    /** Record that a session reached a funnel step */
    recordStep(funnelId, stepEventName, sessionId) {
        var _a;
        const funnel = this.funnels.get(funnelId);
        if (!funnel)
            return;
        const step = funnel.steps.find(s => s.eventName === stepEventName);
        if (!step)
            return;
        const stepMap = this.progress.get(funnelId);
        (_a = stepMap === null || stepMap === void 0 ? void 0 : stepMap.get(step.id)) === null || _a === void 0 ? void 0 : _a.add(sessionId);
    }
    /** Get funnel analysis */
    analyze(funnelId) {
        var _a, _b;
        const funnel = this.funnels.get(funnelId);
        const stepMap = this.progress.get(funnelId);
        if (!funnel || !stepMap)
            return [];
        const result = [];
        let prevCount = 0;
        for (const step of funnel.steps) {
            const count = (_b = (_a = stepMap.get(step.id)) === null || _a === void 0 ? void 0 : _a.size) !== null && _b !== void 0 ? _b : 0;
            const dropOff = prevCount > 0 ? 1 - count / prevCount : 0;
            result.push({
                id: step.id,
                name: step.name,
                count,
                dropOffRate: result.length === 0 ? 0 : dropOff,
            });
            prevCount = count;
        }
        return result;
    }
    getFunnels() {
        return Array.from(this.funnels.values());
    }
}
/* ══════════════════════════════════════════════════════════════
   GAMEPLAY ANALYTICS (Main class)
   ══════════════════════════════════════════════════════════════ */
class GameplayAnalytics {
    constructor(eventBus, telemetryConfig, recordingConfig) {
        this.heatmaps = new Map();
        this.eventBus = eventBus;
        this.telemetry = new TelemetryPipeline(eventBus, telemetryConfig);
        this.recorder = new SessionRecorder(recordingConfig);
        this.funnels = new FunnelTracker();
    }
    /** Create or get a heatmap */
    heatmap(id, config) {
        let hm = this.heatmaps.get(id);
        if (!hm) {
            hm = new HeatmapGenerator(config);
            this.heatmaps.set(id, hm);
        }
        return hm;
    }
    /** Track and automatically distribute to relevant heatmaps */
    trackEvent(category, name, data = {}, position) {
        this.telemetry.track(category, name, data, position);
        // Auto-feed heatmaps
        if (position) {
            for (const [_, hm] of this.heatmaps) {
                hm.addEvent(position.x, position.y);
            }
        }
        // Auto-feed session recorder
        this.recorder.recordEvent('action', { category, name, ...data });
        if (position) {
            this.recorder.recordPosition(position.x, position.y, position.z);
        }
        // Auto-feed funnel tracker
        for (const funnel of this.funnels.getFunnels()) {
            this.funnels.recordStep(funnel.id, name, this.telemetry.getSessionId());
        }
    }
    /** Get summary stats */
    getSummary() {
        return {
            totalEvents: this.telemetry.getTotalEvents(),
            sessionId: this.telemetry.getSessionId(),
            heatmapCount: this.heatmaps.size,
            isRecording: this.recorder.isRecording(),
            recordedFrames: this.recorder.getFrameCount(),
        };
    }
    destroy() {
        this.telemetry.destroy();
        if (this.recorder.isRecording())
            this.recorder.stop();
        this.heatmaps.clear();
    }
}
/* ══════════════════════════════════════════════════════════════
   REACT HOOK
   ══════════════════════════════════════════════════════════════ */
function useGameplayAnalytics(eventBus, telemetryConfig, recordingConfig) {
    const analyticsRef = React.useRef(null);
    const analytics = React.useMemo(() => {
        if (!analyticsRef.current) {
            analyticsRef.current = new GameplayAnalytics(eventBus, telemetryConfig, recordingConfig);
        }
        return analyticsRef.current;
    }, [eventBus]);
    React.useEffect(() => {
        return () => {
            analytics.destroy();
            analyticsRef.current = null;
        };
    }, [analytics]);
    const trackEvent = React.useCallback((category, name, data, position) => analytics.trackEvent(category, name, data, position), [analytics]);
    const [summary, setSummary] = React.useState(analytics.getSummary());
    const refreshSummary = React.useCallback(() => {
        setSummary(analytics.getSummary());
    }, [analytics]);
    return {
        analytics,
        trackEvent,
        summary,
        refreshSummary,
        startRecording: () => analytics.recorder.start(),
        stopRecording: (sceneId) => analytics.recorder.stop(sceneId),
        heatmap: (id, config) => analytics.heatmap(id, config),
    };
}
const AnalyticsDashboard = ({ analytics, className, style, }) => {
    const [summary, setSummary] = React.useState(analytics.getSummary());
    React.useEffect(() => {
        const timer = setInterval(() => setSummary(analytics.getSummary()), 1000);
        return () => clearInterval(timer);
    }, [analytics]);
    const statStyle = {
        display: 'flex', justifyContent: 'space-between', padding: '4px 0',
        borderBottom: '1px solid rgba(128,128,128,0.2)',
    };
    return React.createElement('div', {
        className: `nice-analytics-dashboard ${className !== null && className !== void 0 ? className : ''}`.trim(),
        style: { padding: 16, fontFamily: 'monospace', fontSize: 13, ...style },
    }, React.createElement('h3', { style: { margin: '0 0 12px 0' } }, 'Gameplay Analytics'), React.createElement('div', { style: statStyle }, React.createElement('span', null, 'Session ID'), React.createElement('code', null, summary.sessionId)), React.createElement('div', { style: statStyle }, React.createElement('span', null, 'Total Events'), React.createElement('span', null, String(summary.totalEvents))), React.createElement('div', { style: statStyle }, React.createElement('span', null, 'Heatmaps'), React.createElement('span', null, String(summary.heatmapCount))), React.createElement('div', { style: statStyle }, React.createElement('span', null, 'Recording'), React.createElement('span', null, summary.isRecording ? `ON (${summary.recordedFrames} frames)` : 'OFF')));
};
AnalyticsDashboard.displayName = 'AnalyticsDashboard';

exports.AnalyticsDashboard = AnalyticsDashboard;
exports.DEFAULT_HEATMAP_CONFIG = DEFAULT_HEATMAP_CONFIG;
exports.DEFAULT_RECORDING_CONFIG = DEFAULT_RECORDING_CONFIG;
exports.DEFAULT_TELEMETRY_CONFIG = DEFAULT_TELEMETRY_CONFIG;
exports.FunnelTracker = FunnelTracker;
exports.GameplayAnalytics = GameplayAnalytics;
exports.HeatmapGenerator = HeatmapGenerator;
exports.SessionPlayer = SessionPlayer;
exports.SessionRecorder = SessionRecorder;
exports.TelemetryPipeline = TelemetryPipeline;
exports.useGameplayAnalytics = useGameplayAnalytics;
//# sourceMappingURL=GameplayAnalytics.js.map
