//! Inline caching for optimized method invocation
//!
//! Inline caches store resolved method entries at call sites to avoid
//! repeated lookup through the class hierarchy. This significantly
//! improves performance for polymorphic method calls.

use crate::class_file::MethodInfo;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};

/// Cache entry for a resolved method
#[derive(Debug, Clone)]
pub struct InlineCacheEntry {
    /// The receiver class name (e.g., "java/lang/String")
    pub receiver_class: String,

    /// The method name (e.g., "toString")
    pub method_name: String,

    /// Method descriptor (e.g., "()Ljava/lang/String;")
    pub descriptor: String,

    /// The class where the method was found
    pub resolved_class: String,

    /// Reference to the resolved method
    pub method: Arc<MethodInfo>,

    /// Cache hit count
    pub hit_count: u64,
}

/// Inline cache for a specific call site
#[derive(Debug, Clone)]
pub struct CallSiteCache {
    /// Cache entries indexed by receiver class
    entries: Vec<InlineCacheEntry>,

    /// Maximum number of entries (monomorphic vs polymorphic)
    max_entries: usize,

    /// Total lookups
    total_lookups: u64,

    /// Cache hits
    cache_hits: u64,
}

impl CallSiteCache {
    /// Create a new inline cache
    pub fn new(max_entries: usize) -> Self {
        Self {
            entries: Vec::with_capacity(max_entries.min(4)),
            max_entries,
            total_lookups: 0,
            cache_hits: 0,
        }
    }

    /// Look up a method in the cache
    pub fn lookup(
        &self,
        receiver_class: &str,
        method_name: &str,
        descriptor: &str,
    ) -> Option<&InlineCacheEntry> {
        self.entries.iter().find(|entry| {
            entry.receiver_class == receiver_class
                && entry.method_name == method_name
                && entry.descriptor == descriptor
        })
    }

    /// Update cache with a resolved method
    pub fn update(&mut self, entry: InlineCacheEntry) {
        self.total_lookups += 1;

        // Check if we already have this entry
        if let Some(existing) = self.entries.iter_mut().find(|e| {
            e.receiver_class == entry.receiver_class
                && e.method_name == entry.method_name
                && e.descriptor == entry.descriptor
        }) {
            existing.hit_count += 1;
            self.cache_hits += 1;
            return;
        }

        // Add new entry if space available
        if self.entries.len() < self.max_entries {
            self.entries.push(entry);
        } else {
            // Evict least recently used (LRU) strategy
            let lru_idx = self
                .entries
                .iter()
                .enumerate()
                .min_by_key(|(_, e)| e.hit_count)
                .map(|(i, _)| i)
                .unwrap_or(0);
            self.entries[lru_idx] = entry;
        }
    }

    /// Get cache statistics
    pub fn stats(&self) -> CacheStats {
        CacheStats {
            total_lookups: self.total_lookups,
            cache_hits: self.cache_hits,
            hit_rate: if self.total_lookups > 0 {
                (self.cache_hits as f64 / self.total_lookups as f64) * 100.0
            } else {
                0.0
            },
            entry_count: self.entries.len(),
        }
    }

    /// Clear the cache
    pub fn clear(&mut self) {
        self.entries.clear();
        self.total_lookups = 0;
        self.cache_hits = 0;
    }
}

impl Default for CallSiteCache {
    fn default() -> Self {
        Self::new(4) // Default to 4 entries for polymorphic sites
    }
}

/// Cache statistics
#[derive(Debug, Clone, Copy)]
pub struct CacheStats {
    pub total_lookups: u64,
    pub cache_hits: u64,
    pub hit_rate: f64,
    pub entry_count: usize,
}

/// Global inline cache manager
pub struct InlineCacheManager {
    /// Call site caches indexed by (class_name, method_name, descriptor, bytecode_offset)
    caches: RwLock<HashMap<(String, String, String, usize), CallSiteCache>>,

    /// Enable/disable inline caching
    enabled: bool,
}

impl InlineCacheManager {
    /// Create a new cache manager
    pub fn new(enabled: bool) -> Self {
        Self {
            caches: RwLock::new(HashMap::new()),
            enabled,
        }
    }

    /// Get or create a cache for a call site
    pub fn get_cache(
        &self,
        class_name: &str,
        method_name: &str,
        descriptor: &str,
        bytecode_offset: usize,
    ) -> Option<CallSiteCache> {
        if !self.enabled {
            return None;
        }

        let key = (
            class_name.to_string(),
            method_name.to_string(),
            descriptor.to_string(),
            bytecode_offset,
        );

        self.caches
            .read()
            .ok()
            .and_then(|caches| caches.get(&key).cloned())
    }

    /// Update cache for a call site
    pub fn update_cache(
        &self,
        class_name: &str,
        method_name: &str,
        descriptor: &str,
        bytecode_offset: usize,
        entry: InlineCacheEntry,
    ) {
        if !self.enabled {
            return;
        }

        let key = (
            class_name.to_string(),
            method_name.to_string(),
            descriptor.to_string(),
            bytecode_offset,
        );

        if let Ok(mut caches) = self.caches.write() {
            caches.entry(key).or_default().update(entry);
        }
    }

    /// Get cache statistics
    pub fn get_stats(&self) -> Vec<((String, String, String, usize), CacheStats)> {
        self.caches
            .read()
            .map(|caches| {
                caches
                    .iter()
                    .map(|(key, cache)| (key.clone(), cache.stats()))
                    .collect()
            })
            .unwrap_or_default()
    }

    /// Clear all caches
    pub fn clear_all(&self) {
        if let Ok(mut caches) = self.caches.write() {
            caches.clear();
        }
    }

    /// Check if caching is enabled
    pub fn is_enabled(&self) -> bool {
        self.enabled
    }
}

impl Default for InlineCacheManager {
    fn default() -> Self {
        Self::new(true)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::class_file::{ClassFile, MethodInfo};

    fn create_test_method(name: &str, desc: &str) -> MethodInfo {
        MethodInfo {
            access_flags: 0,
            name_index: 0,
            descriptor_index: 0,
            attributes: vec![],
        }
    }

    #[test]
    fn test_call_site_cache_lookup() {
        let mut cache = CallSiteCache::new(4);

        let entry = InlineCacheEntry {
            receiver_class: "java/lang/String".to_string(),
            method_name: "toString".to_string(),
            descriptor: "()Ljava/lang/String;".to_string(),
            resolved_class: "java/lang/String".to_string(),
            method: Arc::new(create_test_method("toString", "()Ljava/lang/String;")),
            hit_count: 0,
        };

        cache.update(entry.clone());

        let found = cache.lookup("java/lang/String", "toString", "()Ljava/lang/String;");
        assert!(found.is_some());
        assert_eq!(found.unwrap().receiver_class, "java/lang/String");
    }

    #[test]
    fn test_call_site_cache_stats() {
        let mut cache = CallSiteCache::new(4);

        let entry = InlineCacheEntry {
            receiver_class: "java/lang/String".to_string(),
            method_name: "toString".to_string(),
            descriptor: "()Ljava/lang/String;".to_string(),
            resolved_class: "java/lang/String".to_string(),
            method: Arc::new(create_test_method("toString", "()Ljava/lang/String;")),
            hit_count: 0,
        };

        cache.update(entry.clone());
        cache.update(entry); // Second lookup should hit

        let stats = cache.stats();
        assert_eq!(stats.total_lookups, 2);
        assert_eq!(stats.cache_hits, 1);
        assert!(stats.hit_rate > 0.0);
    }

    #[test]
    fn test_inline_cache_manager() {
        let manager = InlineCacheManager::new(true);

        let entry = InlineCacheEntry {
            receiver_class: "java/lang/String".to_string(),
            method_name: "toString".to_string(),
            descriptor: "()Ljava/lang/String;".to_string(),
            resolved_class: "java/lang/String".to_string(),
            method: Arc::new(create_test_method("toString", "()Ljava/lang/String;")),
            hit_count: 0,
        };

        manager.update_cache("Test", "main", "([Ljava/lang/String;)V", 10, entry);

        let stats = manager.get_stats();
        assert!(!stats.is_empty());
    }

    #[test]
    fn test_cache_disabled() {
        let manager = InlineCacheManager::new(false);

        assert!(!manager.is_enabled());
        assert!(manager.get_cache("Test", "main", "()V", 0).is_none());
    }
}
