package collections import ( "fmt" "maps" "strings" "sync" "eq2emu/internal/database" ) // MasterList is a specialized collection master list optimized for: // - Fast ID-based lookups (O(1)) // - Fast category filtering (O(1)) // - Fast level range queries (indexed) // - Fast item requirement lookups (O(1)) // - Efficient completion status filtering // - Name-based searching with indexing type MasterList struct { // Core storage collections map[int32]*Collection // ID -> Collection mutex sync.RWMutex // Specialized indices for O(1) lookups byCategory map[string][]*Collection // Category -> collections byLevel map[int8][]*Collection // Level -> collections byItemNeeded map[int32][]*Collection // ItemID -> collections that need it byNameLower map[string]*Collection // Lowercase name -> collection byCompletion map[bool][]*Collection // Completion status -> collections // Cached metadata categories []string // Unique categories (cached) levels []int8 // Unique levels (cached) itemsNeeded []int32 // Unique items needed (cached) categoryStats map[string]int // Category -> count metaStale bool // Whether metadata cache needs refresh } // NewMasterList creates a new specialized collection master list func NewMasterList() *MasterList { return &MasterList{ collections: make(map[int32]*Collection), byCategory: make(map[string][]*Collection), byLevel: make(map[int8][]*Collection), byItemNeeded: make(map[int32][]*Collection), byNameLower: make(map[string]*Collection), byCompletion: make(map[bool][]*Collection), categoryStats: make(map[string]int), metaStale: true, } } // refreshMetaCache updates the cached metadata func (ml *MasterList) refreshMetaCache() { if !ml.metaStale { return } // Clear and rebuild category stats ml.categoryStats = make(map[string]int) categorySet := make(map[string]struct{}) levelSet := make(map[int8]struct{}) itemSet := make(map[int32]struct{}) // Collect unique values and stats for _, collection := range ml.collections { category := collection.GetCategory() ml.categoryStats[category]++ categorySet[category] = struct{}{} levelSet[collection.GetLevel()] = struct{}{} // Collect items needed by this collection for _, item := range collection.CollectionItems { if item.Found == ItemNotFound { itemSet[item.ItemID] = struct{}{} } } } // Clear and rebuild cached slices ml.categories = ml.categories[:0] for category := range categorySet { ml.categories = append(ml.categories, category) } ml.levels = ml.levels[:0] for level := range levelSet { ml.levels = append(ml.levels, level) } ml.itemsNeeded = ml.itemsNeeded[:0] for itemID := range itemSet { ml.itemsNeeded = append(ml.itemsNeeded, itemID) } ml.metaStale = false } // updateCollectionIndices updates all indices for a collection func (ml *MasterList) updateCollectionIndices(collection *Collection, add bool) { if add { // Add to category index category := collection.GetCategory() ml.byCategory[category] = append(ml.byCategory[category], collection) // Add to level index level := collection.GetLevel() ml.byLevel[level] = append(ml.byLevel[level], collection) // Add to name index ml.byNameLower[strings.ToLower(collection.GetName())] = collection // Add to completion index completed := collection.Completed ml.byCompletion[completed] = append(ml.byCompletion[completed], collection) // Add to item needed index for _, item := range collection.CollectionItems { if item.Found == ItemNotFound { ml.byItemNeeded[item.ItemID] = append(ml.byItemNeeded[item.ItemID], collection) } } } else { // Remove from category index category := collection.GetCategory() categoryCollections := ml.byCategory[category] for i, coll := range categoryCollections { if coll.ID == collection.ID { ml.byCategory[category] = append(categoryCollections[:i], categoryCollections[i+1:]...) break } } // Remove from level index level := collection.GetLevel() levelCollections := ml.byLevel[level] for i, coll := range levelCollections { if coll.ID == collection.ID { ml.byLevel[level] = append(levelCollections[:i], levelCollections[i+1:]...) break } } // Remove from name index delete(ml.byNameLower, strings.ToLower(collection.GetName())) // Remove from completion index completed := collection.Completed completionCollections := ml.byCompletion[completed] for i, coll := range completionCollections { if coll.ID == collection.ID { ml.byCompletion[completed] = append(completionCollections[:i], completionCollections[i+1:]...) break } } // Remove from item needed index for _, item := range collection.CollectionItems { if item.Found == ItemNotFound { itemCollections := ml.byItemNeeded[item.ItemID] for i, coll := range itemCollections { if coll.ID == collection.ID { ml.byItemNeeded[item.ItemID] = append(itemCollections[:i], itemCollections[i+1:]...) break } } } } } } // AddCollection adds a collection with full indexing func (ml *MasterList) AddCollection(collection *Collection) bool { if collection == nil { return false } ml.mutex.Lock() defer ml.mutex.Unlock() // Check if exists if _, exists := ml.collections[collection.ID]; exists { return false } // Add to core storage ml.collections[collection.ID] = collection // Update all indices ml.updateCollectionIndices(collection, true) // Invalidate metadata cache ml.metaStale = true return true } // GetCollection retrieves by ID (O(1)) func (ml *MasterList) GetCollection(id int32) *Collection { ml.mutex.RLock() defer ml.mutex.RUnlock() return ml.collections[id] } // GetCollectionSafe retrieves a collection by ID with existence check func (ml *MasterList) GetCollectionSafe(id int32) (*Collection, bool) { ml.mutex.RLock() defer ml.mutex.RUnlock() collection, exists := ml.collections[id] return collection, exists } // HasCollection checks if a collection exists by ID func (ml *MasterList) HasCollection(id int32) bool { ml.mutex.RLock() defer ml.mutex.RUnlock() _, exists := ml.collections[id] return exists } // GetCollectionClone retrieves a cloned copy of a collection by ID func (ml *MasterList) GetCollectionClone(id int32) *Collection { ml.mutex.RLock() defer ml.mutex.RUnlock() collection := ml.collections[id] if collection == nil { return nil } return collection.Clone() } // RemoveCollection removes a collection and updates all indices func (ml *MasterList) RemoveCollection(id int32) bool { ml.mutex.Lock() defer ml.mutex.Unlock() collection, exists := ml.collections[id] if !exists { return false } // Remove from core storage delete(ml.collections, id) // Update all indices ml.updateCollectionIndices(collection, false) // Invalidate metadata cache ml.metaStale = true return true } // GetAllCollections returns a copy of all collections map func (ml *MasterList) GetAllCollections() map[int32]*Collection { ml.mutex.RLock() defer ml.mutex.RUnlock() // Return a copy to prevent external modification result := make(map[int32]*Collection, len(ml.collections)) maps.Copy(result, ml.collections) return result } // GetAllCollectionsList returns all collections as a slice func (ml *MasterList) GetAllCollectionsList() []*Collection { ml.mutex.RLock() defer ml.mutex.RUnlock() result := make([]*Collection, 0, len(ml.collections)) for _, collection := range ml.collections { result = append(result, collection) } return result } // GetCollectionCount returns the number of collections func (ml *MasterList) GetCollectionCount() int { ml.mutex.RLock() defer ml.mutex.RUnlock() return len(ml.collections) } // Size returns the total number of collections func (ml *MasterList) Size() int { ml.mutex.RLock() defer ml.mutex.RUnlock() return len(ml.collections) } // IsEmpty returns true if the master list is empty func (ml *MasterList) IsEmpty() bool { ml.mutex.RLock() defer ml.mutex.RUnlock() return len(ml.collections) == 0 } // ClearCollections removes all collections from the list func (ml *MasterList) ClearCollections() { ml.mutex.Lock() defer ml.mutex.Unlock() // Clear all maps ml.collections = make(map[int32]*Collection) ml.byCategory = make(map[string][]*Collection) ml.byLevel = make(map[int8][]*Collection) ml.byItemNeeded = make(map[int32][]*Collection) ml.byNameLower = make(map[string]*Collection) ml.byCompletion = make(map[bool][]*Collection) // Clear cached metadata ml.categories = ml.categories[:0] ml.levels = ml.levels[:0] ml.itemsNeeded = ml.itemsNeeded[:0] ml.categoryStats = make(map[string]int) ml.metaStale = true } // Clear removes all collections from the master list func (ml *MasterList) Clear() { ml.ClearCollections() } // FindCollectionsByCategory finds collections in a specific category (O(1)) func (ml *MasterList) FindCollectionsByCategory(category string) []*Collection { ml.mutex.RLock() defer ml.mutex.RUnlock() return ml.byCategory[category] } // FindCollectionsByLevel finds collections for a specific level range func (ml *MasterList) FindCollectionsByLevel(minLevel, maxLevel int8) []*Collection { ml.mutex.RLock() defer ml.mutex.RUnlock() var result []*Collection for level := minLevel; level <= maxLevel; level++ { result = append(result, ml.byLevel[level]...) } return result } // GetCollectionsByExactLevel returns collections with specific level (O(1)) func (ml *MasterList) GetCollectionsByExactLevel(level int8) []*Collection { ml.mutex.RLock() defer ml.mutex.RUnlock() return ml.byLevel[level] } // GetCollectionByName retrieves a collection by name (case-insensitive, O(1)) func (ml *MasterList) GetCollectionByName(name string) *Collection { ml.mutex.RLock() defer ml.mutex.RUnlock() return ml.byNameLower[strings.ToLower(name)] } // NeedsItem checks if any collection needs the specified item (O(1)) func (ml *MasterList) NeedsItem(itemID int32) bool { ml.mutex.RLock() defer ml.mutex.RUnlock() collections := ml.byItemNeeded[itemID] return len(collections) > 0 } // GetCollectionsNeedingItem returns all collections that need a specific item (O(1)) func (ml *MasterList) GetCollectionsNeedingItem(itemID int32) []*Collection { ml.mutex.RLock() defer ml.mutex.RUnlock() return ml.byItemNeeded[itemID] } // GetCompletedCollections returns all completed collections (O(1)) func (ml *MasterList) GetCompletedCollections() []*Collection { ml.mutex.RLock() defer ml.mutex.RUnlock() return ml.byCompletion[true] } // GetIncompleteCollections returns all incomplete collections (O(1)) func (ml *MasterList) GetIncompleteCollections() []*Collection { ml.mutex.RLock() defer ml.mutex.RUnlock() return ml.byCompletion[false] } // GetReadyToTurnInCollections returns collections ready to be turned in func (ml *MasterList) GetReadyToTurnInCollections() []*Collection { ml.mutex.RLock() defer ml.mutex.RUnlock() var result []*Collection for _, collection := range ml.collections { if !collection.Completed && collection.GetIsReadyToTurnIn() { result = append(result, collection) } } return result } // GetCollectionsByLevelRange returns collections within level range using indices func (ml *MasterList) GetCollectionsByLevelRange(minLevel, maxLevel int8) []*Collection { return ml.FindCollectionsByLevel(minLevel, maxLevel) } // GetCategories returns all unique collection categories using cached results func (ml *MasterList) GetCategories() []string { 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([]string, len(ml.categories)) copy(result, ml.categories) return result } // GetLevels returns all unique collection levels using cached results func (ml *MasterList) GetLevels() []int8 { 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([]int8, len(ml.levels)) copy(result, ml.levels) return result } // GetItemsNeeded returns all unique items needed by collections func (ml *MasterList) GetItemsNeeded() []int32 { 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([]int32, len(ml.itemsNeeded)) copy(result, ml.itemsNeeded) return result } // UpdateCollection updates an existing collection and refreshes indices func (ml *MasterList) UpdateCollection(collection *Collection) error { if collection == nil { return fmt.Errorf("collection cannot be nil") } ml.mutex.Lock() defer ml.mutex.Unlock() // Check if exists old, exists := ml.collections[collection.ID] if !exists { return fmt.Errorf("collection %d not found", collection.ID) } // Remove old collection from indices (but not core storage yet) ml.updateCollectionIndices(old, false) // Update core storage ml.collections[collection.ID] = collection // Add new collection to indices ml.updateCollectionIndices(collection, true) // Invalidate metadata cache ml.metaStale = true return nil } // RefreshCollectionIndices refreshes indices for a collection (used when items found/completion changes) func (ml *MasterList) RefreshCollectionIndices(collection *Collection) { ml.mutex.Lock() defer ml.mutex.Unlock() // Remove from old indices ml.updateCollectionIndices(collection, false) // Add to new indices ml.updateCollectionIndices(collection, true) // Invalidate metadata cache ml.metaStale = true } // ForEach executes a function for each collection func (ml *MasterList) ForEach(fn func(int32, *Collection)) { ml.mutex.RLock() defer ml.mutex.RUnlock() for id, collection := range ml.collections { fn(id, collection) } } // ValidateCollections checks all collections for consistency func (ml *MasterList) ValidateCollections() []string { ml.mutex.RLock() defer ml.mutex.RUnlock() var issues []string for id, collection := range ml.collections { if collection == nil { issues = append(issues, fmt.Sprintf("Collection ID %d is nil", id)) continue } if collection.GetID() != id { issues = append(issues, fmt.Sprintf("Collection ID mismatch: map key %d != collection ID %d", id, collection.GetID())) } if len(collection.GetName()) == 0 { issues = append(issues, fmt.Sprintf("Collection ID %d has empty name", id)) } if len(collection.GetCategory()) == 0 { issues = append(issues, fmt.Sprintf("Collection ID %d has empty category", id)) } if collection.GetLevel() < 0 { issues = append(issues, fmt.Sprintf("Collection ID %d has negative level: %d", id, collection.GetLevel())) } if len(collection.CollectionItems) == 0 { issues = append(issues, fmt.Sprintf("Collection ID %d has no collection items", id)) } if len(collection.GetName()) > MaxCollectionNameLength { issues = append(issues, fmt.Sprintf("Collection ID %d name too long: %d > %d", id, len(collection.GetName()), MaxCollectionNameLength)) } if len(collection.GetCategory()) > MaxCollectionCategoryLength { issues = append(issues, fmt.Sprintf("Collection ID %d category too long: %d > %d", id, len(collection.GetCategory()), MaxCollectionCategoryLength)) } // Check for duplicate item indices indexMap := make(map[int8]bool) for _, item := range collection.CollectionItems { if indexMap[item.Index] { issues = append(issues, fmt.Sprintf("Collection ID %d has duplicate item index: %d", id, item.Index)) } indexMap[item.Index] = true } } return issues } // IsValid returns true if all collections are valid func (ml *MasterList) IsValid() bool { issues := ml.ValidateCollections() return len(issues) == 0 } // GetStatistics returns statistics about the collection collection using cached data func (ml *MasterList) GetStatistics() map[string]any { ml.mutex.Lock() // Need write lock to potentially update cache defer ml.mutex.Unlock() ml.refreshMetaCache() stats := make(map[string]any) stats["total_collections"] = len(ml.collections) if len(ml.collections) == 0 { return stats } // Use cached category stats stats["collections_by_category"] = ml.categoryStats // Calculate additional stats var totalItems, totalRewards int var completedCount, readyCount int var minLevel, maxLevel int8 = 127, 0 var minID, maxID int32 first := true for id, collection := range ml.collections { totalItems += len(collection.CollectionItems) totalRewards += len(collection.RewardItems) + len(collection.SelectableRewardItems) if collection.Completed { completedCount++ } else if collection.GetIsReadyToTurnIn() { readyCount++ } level := collection.GetLevel() if level < minLevel { minLevel = level } if level > maxLevel { maxLevel = level } if first { minID = id maxID = id first = false } else { if id < minID { minID = id } if id > maxID { maxID = id } } } stats["total_collection_items"] = totalItems stats["total_rewards"] = totalRewards stats["completed_collections"] = completedCount stats["ready_to_turn_in"] = readyCount stats["min_level"] = minLevel stats["max_level"] = maxLevel stats["min_id"] = minID stats["max_id"] = maxID stats["id_range"] = maxID - minID stats["average_items_per_collection"] = float64(totalItems) / float64(len(ml.collections)) return stats } // LoadAllCollections loads all collections from the database into the master list func (ml *MasterList) LoadAllCollections(db *database.Database) error { if db == nil { return fmt.Errorf("database connection is nil") } // Clear existing collections ml.Clear() query := `SELECT id, collection_name, collection_category, level FROM collections ORDER BY id` rows, err := db.Query(query) if err != nil { return fmt.Errorf("failed to query collections: %w", err) } defer rows.Close() count := 0 for rows.Next() { collection := &Collection{ db: db, isNew: false, CollectionItems: make([]CollectionItem, 0), RewardItems: make([]CollectionRewardItem, 0), SelectableRewardItems: make([]CollectionRewardItem, 0), } err := rows.Scan(&collection.ID, &collection.Name, &collection.Category, &collection.Level) if err != nil { return fmt.Errorf("failed to scan collection: %w", err) } // Load collection items for this collection itemQuery := `SELECT item_id, item_index, found FROM collection_items WHERE collection_id = ? ORDER BY item_index` itemRows, err := db.Query(itemQuery, collection.ID) if err != nil { return fmt.Errorf("failed to load collection items for collection %d: %w", collection.ID, err) } for itemRows.Next() { var item CollectionItem if err := itemRows.Scan(&item.ItemID, &item.Index, &item.Found); err != nil { itemRows.Close() return fmt.Errorf("failed to scan collection item: %w", err) } collection.CollectionItems = append(collection.CollectionItems, item) } itemRows.Close() // Load rewards for this collection rewardQuery := `SELECT reward_type, reward_value, reward_quantity FROM collection_rewards WHERE collection_id = ?` rewardRows, err := db.Query(rewardQuery, collection.ID) if err != nil { return fmt.Errorf("failed to load rewards for collection %d: %w", collection.ID, err) } for rewardRows.Next() { var rewardType, rewardValue string var quantity int8 if err := rewardRows.Scan(&rewardType, &rewardValue, &quantity); err != nil { rewardRows.Close() return fmt.Errorf("failed to scan collection reward: %w", err) } switch rewardType { case "coin": fmt.Sscanf(rewardValue, "%d", &collection.RewardCoin) case "xp": fmt.Sscanf(rewardValue, "%d", &collection.RewardXP) case "item": var itemID int32 fmt.Sscanf(rewardValue, "%d", &itemID) collection.RewardItems = append(collection.RewardItems, CollectionRewardItem{ ItemID: itemID, Quantity: quantity, }) case "selectable_item": var itemID int32 fmt.Sscanf(rewardValue, "%d", &itemID) collection.SelectableRewardItems = append(collection.SelectableRewardItems, CollectionRewardItem{ ItemID: itemID, Quantity: quantity, }) } } rewardRows.Close() if !ml.AddCollection(collection) { return fmt.Errorf("failed to add collection %d to master list", collection.ID) } count++ } if err := rows.Err(); err != nil { return fmt.Errorf("error iterating collection rows: %w", err) } return nil } // LoadAllCollectionsFromDatabase is a convenience function that creates a master list and loads all collections func LoadAllCollectionsFromDatabase(db *database.Database) (*MasterList, error) { masterList := NewMasterList() err := masterList.LoadAllCollections(db) if err != nil { return nil, err } return masterList, nil }