package appearances import ( "fmt" "maps" "strings" "sync" "eq2emu/internal/database" ) // MasterList is a specialized appearance master list optimized for: // - Fast ID-based lookups (O(1)) // - Fast name-based searching (indexed) // - Fast client version filtering (O(1)) // - Efficient range queries and statistics type MasterList struct { // Core storage appearances map[int32]*Appearance // ID -> Appearance mutex sync.RWMutex // Specialized indices for O(1) lookups byMinClient map[int16][]*Appearance // MinClient -> appearances byNamePart map[string][]*Appearance // Name substring -> appearances (for common searches) // Cached metadata clientVersions []int16 // Unique client versions (cached) metaStale bool // Whether metadata cache needs refresh } // NewMasterList creates a new specialized appearance master list func NewMasterList() *MasterList { return &MasterList{ appearances: make(map[int32]*Appearance), byMinClient: make(map[int16][]*Appearance), byNamePart: make(map[string][]*Appearance), metaStale: true, } } // refreshMetaCache updates the client versions cache func (ml *MasterList) refreshMetaCache() { if !ml.metaStale { return } clientVersionSet := make(map[int16]struct{}) // Collect unique client versions for _, appearance := range ml.appearances { clientVersionSet[appearance.MinClient] = struct{}{} } // Clear existing cache and rebuild ml.clientVersions = ml.clientVersions[:0] for version := range clientVersionSet { ml.clientVersions = append(ml.clientVersions, version) } ml.metaStale = false } // updateNameIndices updates name-based indices for an appearance func (ml *MasterList) updateNameIndices(appearance *Appearance, add bool) { // Index common name patterns for fast searching name := strings.ToLower(appearance.Name) partsSet := make(map[string]struct{}) // Use set to avoid duplicates // Add full name partsSet[name] = struct{}{} // Add word-based indices for multi-word names if strings.Contains(name, " ") { words := strings.FieldsSeq(name) for word := range words { partsSet[word] = struct{}{} } } // Add prefix indices for common prefixes if len(name) >= 3 { partsSet[name[:3]] = struct{}{} } if len(name) >= 5 { partsSet[name[:5]] = struct{}{} } // Convert set to slice for part := range partsSet { if add { ml.byNamePart[part] = append(ml.byNamePart[part], appearance) } else { // Remove from name part index if namePartApps := ml.byNamePart[part]; namePartApps != nil { for i, app := range namePartApps { if app.ID == appearance.ID { ml.byNamePart[part] = append(namePartApps[:i], namePartApps[i+1:]...) break } } } } } } // AddAppearance adds an appearance with full indexing func (ml *MasterList) AddAppearance(appearance *Appearance) bool { if appearance == nil { return false } ml.mutex.Lock() defer ml.mutex.Unlock() // Check if exists if _, exists := ml.appearances[appearance.ID]; exists { return false } // Add to core storage ml.appearances[appearance.ID] = appearance // Update client version index ml.byMinClient[appearance.MinClient] = append(ml.byMinClient[appearance.MinClient], appearance) // Update name indices ml.updateNameIndices(appearance, true) // Invalidate metadata cache ml.metaStale = true return true } // GetAppearance retrieves by ID (O(1)) func (ml *MasterList) GetAppearance(id int32) *Appearance { ml.mutex.RLock() defer ml.mutex.RUnlock() return ml.appearances[id] } // GetAppearanceSafe retrieves an appearance by ID with existence check func (ml *MasterList) GetAppearanceSafe(id int32) (*Appearance, bool) { ml.mutex.RLock() defer ml.mutex.RUnlock() appearance, exists := ml.appearances[id] return appearance, exists } // HasAppearance checks if an appearance exists by ID func (ml *MasterList) HasAppearance(id int32) bool { ml.mutex.RLock() defer ml.mutex.RUnlock() _, exists := ml.appearances[id] return exists } // GetAppearanceClone retrieves a cloned copy of an appearance by ID func (ml *MasterList) GetAppearanceClone(id int32) *Appearance { ml.mutex.RLock() defer ml.mutex.RUnlock() appearance := ml.appearances[id] if appearance == nil { return nil } return appearance.Clone() } // GetAllAppearances returns a copy of all appearances map func (ml *MasterList) GetAllAppearances() map[int32]*Appearance { ml.mutex.RLock() defer ml.mutex.RUnlock() // Return a copy to prevent external modification result := make(map[int32]*Appearance, len(ml.appearances)) maps.Copy(result, ml.appearances) return result } // GetAllAppearancesList returns all appearances as a slice func (ml *MasterList) GetAllAppearancesList() []*Appearance { ml.mutex.RLock() defer ml.mutex.RUnlock() result := make([]*Appearance, 0, len(ml.appearances)) for _, appearance := range ml.appearances { result = append(result, appearance) } return result } // FindAppearancesByMinClient finds appearances with specific minimum client version (O(1)) func (ml *MasterList) FindAppearancesByMinClient(minClient int16) []*Appearance { ml.mutex.RLock() defer ml.mutex.RUnlock() return ml.byMinClient[minClient] } // GetCompatibleAppearances returns appearances compatible with the given client version func (ml *MasterList) GetCompatibleAppearances(clientVersion int16) []*Appearance { ml.mutex.RLock() defer ml.mutex.RUnlock() var result []*Appearance // Collect all appearances with MinClient <= clientVersion for minClient, appearances := range ml.byMinClient { if minClient <= clientVersion { result = append(result, appearances...) } } return result } // FindAppearancesByName finds appearances containing the given name substring (optimized) func (ml *MasterList) FindAppearancesByName(nameSubstring string) []*Appearance { ml.mutex.RLock() defer ml.mutex.RUnlock() searchKey := strings.ToLower(nameSubstring) // Try indexed lookup first for exact matches if indexedApps := ml.byNamePart[searchKey]; indexedApps != nil { // Return a copy to prevent external modification result := make([]*Appearance, len(indexedApps)) copy(result, indexedApps) return result } // Fallback to full scan for partial matches var result []*Appearance for _, appearance := range ml.appearances { if contains(strings.ToLower(appearance.Name), searchKey) { result = append(result, appearance) } } return result } // GetAppearancesByIDRange returns appearances within the given ID range (inclusive) func (ml *MasterList) GetAppearancesByIDRange(minID, maxID int32) []*Appearance { ml.mutex.RLock() defer ml.mutex.RUnlock() var result []*Appearance for id, appearance := range ml.appearances { if id >= minID && id <= maxID { result = append(result, appearance) } } return result } // GetAppearancesByClientRange returns appearances compatible within client version range func (ml *MasterList) GetAppearancesByClientRange(minClientVersion, maxClientVersion int16) []*Appearance { ml.mutex.RLock() defer ml.mutex.RUnlock() var result []*Appearance // Collect all appearances with MinClient in range for minClient, appearances := range ml.byMinClient { if minClient >= minClientVersion && minClient <= maxClientVersion { result = append(result, appearances...) } } return result } // GetClientVersions returns all unique client versions using cached results func (ml *MasterList) GetClientVersions() []int16 { ml.mutex.Lock() // Need write lock to potentially update cache defer ml.mutex.Unlock() ml.refreshMetaCache() // Return a copy to prevent external modification result := make([]int16, len(ml.clientVersions)) copy(result, ml.clientVersions) return result } // RemoveAppearance removes an appearance and updates all indices func (ml *MasterList) RemoveAppearance(id int32) bool { ml.mutex.Lock() defer ml.mutex.Unlock() appearance, exists := ml.appearances[id] if !exists { return false } // Remove from core storage delete(ml.appearances, id) // Remove from client version index clientApps := ml.byMinClient[appearance.MinClient] for i, app := range clientApps { if app.ID == id { ml.byMinClient[appearance.MinClient] = append(clientApps[:i], clientApps[i+1:]...) break } } // Remove from name indices ml.updateNameIndices(appearance, false) // Invalidate metadata cache ml.metaStale = true return true } // UpdateAppearance updates an existing appearance func (ml *MasterList) UpdateAppearance(appearance *Appearance) error { if appearance == nil { return fmt.Errorf("appearance cannot be nil") } ml.mutex.Lock() defer ml.mutex.Unlock() // Check if exists old, exists := ml.appearances[appearance.ID] if !exists { return fmt.Errorf("appearance %d not found", appearance.ID) } // Remove old appearance from indices (but not core storage yet) clientApps := ml.byMinClient[old.MinClient] for i, app := range clientApps { if app.ID == appearance.ID { ml.byMinClient[old.MinClient] = append(clientApps[:i], clientApps[i+1:]...) break } } // Remove from name indices ml.updateNameIndices(old, false) // Update core storage ml.appearances[appearance.ID] = appearance // Add new appearance to indices ml.byMinClient[appearance.MinClient] = append(ml.byMinClient[appearance.MinClient], appearance) ml.updateNameIndices(appearance, true) // Invalidate metadata cache ml.metaStale = true return nil } // GetAppearanceCount returns the number of appearances func (ml *MasterList) GetAppearanceCount() int { ml.mutex.RLock() defer ml.mutex.RUnlock() return len(ml.appearances) } // Size returns the total number of appearances func (ml *MasterList) Size() int { ml.mutex.RLock() defer ml.mutex.RUnlock() return len(ml.appearances) } // IsEmpty returns true if the master list is empty func (ml *MasterList) IsEmpty() bool { ml.mutex.RLock() defer ml.mutex.RUnlock() return len(ml.appearances) == 0 } // ClearAppearances removes all appearances from the list func (ml *MasterList) ClearAppearances() { ml.mutex.Lock() defer ml.mutex.Unlock() // Clear all maps ml.appearances = make(map[int32]*Appearance) ml.byMinClient = make(map[int16][]*Appearance) ml.byNamePart = make(map[string][]*Appearance) // Clear cached metadata ml.clientVersions = ml.clientVersions[:0] ml.metaStale = true } // Clear removes all appearances from the master list func (ml *MasterList) Clear() { ml.ClearAppearances() } // ForEach executes a function for each appearance func (ml *MasterList) ForEach(fn func(int32, *Appearance)) { ml.mutex.RLock() defer ml.mutex.RUnlock() for id, appearance := range ml.appearances { fn(id, appearance) } } // ValidateAppearances checks all appearances for consistency func (ml *MasterList) ValidateAppearances() []string { ml.mutex.RLock() defer ml.mutex.RUnlock() var issues []string for id, appearance := range ml.appearances { if appearance == nil { issues = append(issues, fmt.Sprintf("Appearance ID %d is nil", id)) continue } if appearance.GetID() != id { issues = append(issues, fmt.Sprintf("Appearance ID mismatch: map key %d != appearance ID %d", id, appearance.GetID())) } if len(appearance.GetName()) == 0 { issues = append(issues, fmt.Sprintf("Appearance ID %d has empty name", id)) } if appearance.GetMinClientVersion() < 0 { issues = append(issues, fmt.Sprintf("Appearance ID %d has negative min client version: %d", id, appearance.GetMinClientVersion())) } } return issues } // IsValid returns true if all appearances are valid func (ml *MasterList) IsValid() bool { issues := ml.ValidateAppearances() return len(issues) == 0 } // GetStatistics returns statistics about the appearance collection func (ml *MasterList) GetStatistics() map[string]any { ml.mutex.RLock() defer ml.mutex.RUnlock() stats := make(map[string]any) stats["total_appearances"] = len(ml.appearances) if len(ml.appearances) == 0 { return stats } // Count by minimum client version versionCounts := make(map[int16]int) var minID, maxID int32 first := true for id, appearance := range ml.appearances { versionCounts[appearance.GetMinClientVersion()]++ if first { minID = id maxID = id first = false } else { if id < minID { minID = id } if id > maxID { maxID = id } } } stats["appearances_by_min_client"] = versionCounts stats["min_id"] = minID stats["max_id"] = maxID stats["id_range"] = maxID - minID return stats } // LoadAllAppearances loads all appearances from the database into the master list func (ml *MasterList) LoadAllAppearances(db *database.Database) error { if db == nil { return fmt.Errorf("database connection is nil") } // Clear existing appearances ml.Clear() query := `SELECT appearance_id, name, min_client_version FROM appearances ORDER BY appearance_id` rows, err := db.Query(query) if err != nil { return fmt.Errorf("failed to query appearances: %w", err) } defer rows.Close() count := 0 for rows.Next() { appearance := &Appearance{ db: db, isNew: false, } err := rows.Scan(&appearance.ID, &appearance.Name, &appearance.MinClient) if err != nil { return fmt.Errorf("failed to scan appearance: %w", err) } if !ml.AddAppearance(appearance) { return fmt.Errorf("failed to add appearance %d to master list", appearance.ID) } count++ } if err := rows.Err(); err != nil { return fmt.Errorf("error iterating appearance rows: %w", err) } return nil } // LoadAllAppearancesFromDatabase is a convenience function that creates a master list and loads all appearances func LoadAllAppearancesFromDatabase(db *database.Database) (*MasterList, error) { masterList := NewMasterList() err := masterList.LoadAllAppearances(db) if err != nil { return nil, err } return masterList, nil } // contains checks if a string contains a substring (case-sensitive) func contains(str, substr string) bool { if len(substr) == 0 { return true } if len(str) < len(substr) { return false } for i := 0; i <= len(str)-len(substr); i++ { if str[i:i+len(substr)] == substr { return true } } return false }