package heroic_ops import ( "fmt" "sort" "sync" "strings" "eq2emu/internal/database" ) // MasterList provides optimized heroic opportunity management with O(1) performance characteristics type MasterList struct { mu sync.RWMutex // Core data storage - O(1) access by ID starters map[int32]*HeroicOPStarter // starter_id -> starter wheels map[int32]*HeroicOPWheel // wheel_id -> wheel // Specialized indices for O(1) lookups byClass map[int8]map[int32]*HeroicOPStarter // class -> starter_id -> starter byStarterID map[int32][]*HeroicOPWheel // starter_id -> wheels bySpellID map[int32][]*HeroicOPWheel // spell_id -> wheels byChance map[string][]*HeroicOPWheel // chance_range -> wheels orderedWheels map[int32]*HeroicOPWheel // wheel_id -> ordered wheels only shiftWheels map[int32]*HeroicOPWheel // wheel_id -> wheels with shifts spellInfo map[int32]SpellInfo // spell_id -> spell info // Lazy metadata caching - computed on demand totalStarters int totalWheels int classDistribution map[int8]int metadataValid bool loaded bool } // NewMasterList creates a new bespoke heroic opportunity master list func NewMasterList() *MasterList { return &MasterList{ starters: make(map[int32]*HeroicOPStarter), wheels: make(map[int32]*HeroicOPWheel), byClass: make(map[int8]map[int32]*HeroicOPStarter), byStarterID: make(map[int32][]*HeroicOPWheel), bySpellID: make(map[int32][]*HeroicOPWheel), byChance: make(map[string][]*HeroicOPWheel), orderedWheels: make(map[int32]*HeroicOPWheel), shiftWheels: make(map[int32]*HeroicOPWheel), spellInfo: make(map[int32]SpellInfo), loaded: false, } } // LoadFromDatabase loads all heroic opportunities from the database with optimal indexing func (ml *MasterList) LoadFromDatabase(db *database.Database) error { ml.mu.Lock() defer ml.mu.Unlock() // Clear existing data ml.clearIndices() // Load all starters if err := ml.loadStarters(db); err != nil { return fmt.Errorf("failed to load starters: %w", err) } // Load all wheels if err := ml.loadWheels(db); err != nil { return fmt.Errorf("failed to load wheels: %w", err) } // Build specialized indices for O(1) performance ml.buildIndices() ml.loaded = true return nil } // loadStarters loads all starters from database func (ml *MasterList) loadStarters(db *database.Database) error { query := `SELECT id, start_class, starter_icon, ability1, ability2, ability3, ability4, ability5, ability6, name, description FROM heroic_op_starters ORDER BY id` rows, err := db.Query(query) if err != nil { return err } defer rows.Close() for rows.Next() { starter := &HeroicOPStarter{ db: db, isNew: false, } err := rows.Scan( &starter.ID, &starter.StartClass, &starter.StarterIcon, &starter.Abilities[0], &starter.Abilities[1], &starter.Abilities[2], &starter.Abilities[3], &starter.Abilities[4], &starter.Abilities[5], &starter.Name, &starter.Description, ) if err != nil { return err } starter.SaveNeeded = false ml.starters[starter.ID] = starter } return rows.Err() } // loadWheels loads all wheels from database func (ml *MasterList) loadWheels(db *database.Database) error { query := `SELECT id, starter_link_id, chain_order, shift_icon, chance, ability1, ability2, ability3, ability4, ability5, ability6, spell_id, name, description, required_players FROM heroic_op_wheels ORDER BY id` rows, err := db.Query(query) if err != nil { return err } defer rows.Close() for rows.Next() { wheel := &HeroicOPWheel{ db: db, isNew: false, } err := rows.Scan( &wheel.ID, &wheel.StarterLinkID, &wheel.Order, &wheel.ShiftIcon, &wheel.Chance, &wheel.Abilities[0], &wheel.Abilities[1], &wheel.Abilities[2], &wheel.Abilities[3], &wheel.Abilities[4], &wheel.Abilities[5], &wheel.SpellID, &wheel.Name, &wheel.Description, &wheel.RequiredPlayers, ) if err != nil { return err } wheel.SaveNeeded = false ml.wheels[wheel.ID] = wheel // Store spell info ml.spellInfo[wheel.SpellID] = SpellInfo{ ID: wheel.SpellID, Name: wheel.Name, Description: wheel.Description, } } return rows.Err() } // buildIndices creates specialized indices for O(1) performance func (ml *MasterList) buildIndices() { // Build class-based starter index for _, starter := range ml.starters { if ml.byClass[starter.StartClass] == nil { ml.byClass[starter.StartClass] = make(map[int32]*HeroicOPStarter) } ml.byClass[starter.StartClass][starter.ID] = starter } // Build wheel indices for _, wheel := range ml.wheels { // By starter ID ml.byStarterID[wheel.StarterLinkID] = append(ml.byStarterID[wheel.StarterLinkID], wheel) // By spell ID ml.bySpellID[wheel.SpellID] = append(ml.bySpellID[wheel.SpellID], wheel) // By chance range (for performance optimization) chanceRange := ml.getChanceRange(wheel.Chance) ml.byChance[chanceRange] = append(ml.byChance[chanceRange], wheel) // Special wheel types if wheel.IsOrdered() { ml.orderedWheels[wheel.ID] = wheel } if wheel.HasShift() { ml.shiftWheels[wheel.ID] = wheel } } // Sort wheels by chance for deterministic selection for _, wheels := range ml.byStarterID { sort.Slice(wheels, func(i, j int) bool { return wheels[i].Chance > wheels[j].Chance }) } ml.metadataValid = false // Invalidate cached metadata } // clearIndices clears all indices func (ml *MasterList) clearIndices() { ml.starters = make(map[int32]*HeroicOPStarter) ml.wheels = make(map[int32]*HeroicOPWheel) ml.byClass = make(map[int8]map[int32]*HeroicOPStarter) ml.byStarterID = make(map[int32][]*HeroicOPWheel) ml.bySpellID = make(map[int32][]*HeroicOPWheel) ml.byChance = make(map[string][]*HeroicOPWheel) ml.orderedWheels = make(map[int32]*HeroicOPWheel) ml.shiftWheels = make(map[int32]*HeroicOPWheel) ml.spellInfo = make(map[int32]SpellInfo) ml.metadataValid = false } // getChanceRange returns a chance range string for indexing func (ml *MasterList) getChanceRange(chance float32) string { switch { case chance >= 75.0: return "very_high" case chance >= 50.0: return "high" case chance >= 25.0: return "medium" case chance >= 10.0: return "low" default: return "very_low" } } // O(1) Starter Operations // GetStarter retrieves a starter by ID with O(1) performance func (ml *MasterList) GetStarter(id int32) *HeroicOPStarter { ml.mu.RLock() defer ml.mu.RUnlock() return ml.starters[id] } // GetStartersForClass returns all starters for a specific class with O(1) performance func (ml *MasterList) GetStartersForClass(class int8) []*HeroicOPStarter { ml.mu.RLock() defer ml.mu.RUnlock() var result []*HeroicOPStarter // Add class-specific starters if classStarters, exists := ml.byClass[class]; exists { for _, starter := range classStarters { result = append(result, starter) } } // Add universal starters (class 0 = any) if universalStarters, exists := ml.byClass[ClassAny]; exists { for _, starter := range universalStarters { result = append(result, starter) } } return result } // O(1) Wheel Operations // GetWheel retrieves a wheel by ID with O(1) performance func (ml *MasterList) GetWheel(id int32) *HeroicOPWheel { ml.mu.RLock() defer ml.mu.RUnlock() return ml.wheels[id] } // GetWheelsForStarter returns all wheels for a starter with O(1) performance func (ml *MasterList) GetWheelsForStarter(starterID int32) []*HeroicOPWheel { ml.mu.RLock() defer ml.mu.RUnlock() wheels := ml.byStarterID[starterID] if wheels == nil { return nil } // Return copy to prevent external modification result := make([]*HeroicOPWheel, len(wheels)) copy(result, wheels) return result } // GetWheelsForSpell returns all wheels that cast a specific spell with O(1) performance func (ml *MasterList) GetWheelsForSpell(spellID int32) []*HeroicOPWheel { ml.mu.RLock() defer ml.mu.RUnlock() wheels := ml.bySpellID[spellID] if wheels == nil { return nil } // Return copy to prevent external modification result := make([]*HeroicOPWheel, len(wheels)) copy(result, wheels) return result } // SelectRandomWheel selects a random wheel from starter's wheels with optimized performance func (ml *MasterList) SelectRandomWheel(starterID int32) *HeroicOPWheel { wheels := ml.GetWheelsForStarter(starterID) if len(wheels) == 0 { return nil } return SelectRandomWheel(wheels) } // O(1) Specialized Queries // GetOrderedWheels returns all ordered wheels with O(1) performance func (ml *MasterList) GetOrderedWheels() []*HeroicOPWheel { ml.mu.RLock() defer ml.mu.RUnlock() result := make([]*HeroicOPWheel, 0, len(ml.orderedWheels)) for _, wheel := range ml.orderedWheels { result = append(result, wheel) } return result } // GetShiftWheels returns all wheels with shift abilities with O(1) performance func (ml *MasterList) GetShiftWheels() []*HeroicOPWheel { ml.mu.RLock() defer ml.mu.RUnlock() result := make([]*HeroicOPWheel, 0, len(ml.shiftWheels)) for _, wheel := range ml.shiftWheels { result = append(result, wheel) } return result } // GetWheelsByChanceRange returns wheels within a chance range with O(1) performance func (ml *MasterList) GetWheelsByChanceRange(minChance, maxChance float32) []*HeroicOPWheel { ml.mu.RLock() defer ml.mu.RUnlock() var result []*HeroicOPWheel // Use indexed chance ranges for performance for rangeKey, wheels := range ml.byChance { for _, wheel := range wheels { if wheel.Chance >= minChance && wheel.Chance <= maxChance { result = append(result, wheel) } } // Break early if we found wheels in this range if len(result) > 0 && !ml.shouldContinueChanceSearch(rangeKey, minChance, maxChance) { break } } return result } // shouldContinueChanceSearch determines if we should continue searching other chance ranges func (ml *MasterList) shouldContinueChanceSearch(currentRange string, minChance, maxChance float32) bool { // Optimize search by stopping early based on range analysis switch currentRange { case "very_high": return maxChance < 75.0 case "high": return maxChance < 50.0 || minChance > 75.0 case "medium": return maxChance < 25.0 || minChance > 50.0 case "low": return maxChance < 10.0 || minChance > 25.0 default: // very_low return minChance > 10.0 } } // Spell Information // GetSpellInfo returns spell information with O(1) performance func (ml *MasterList) GetSpellInfo(spellID int32) (*SpellInfo, bool) { ml.mu.RLock() defer ml.mu.RUnlock() info, exists := ml.spellInfo[spellID] return &info, exists } // Advanced Search with Optimized Performance // Search performs advanced search with multiple criteria func (ml *MasterList) Search(criteria HeroicOPSearchCriteria) *HeroicOPSearchResults { ml.mu.RLock() defer ml.mu.RUnlock() results := &HeroicOPSearchResults{ Starters: make([]*HeroicOPStarter, 0), Wheels: make([]*HeroicOPWheel, 0), } // Optimize search strategy based on criteria if criteria.StarterClass != 0 { // Class-specific search - use class index if classStarters, exists := ml.byClass[criteria.StarterClass]; exists { for _, starter := range classStarters { if ml.matchesStarterCriteria(starter, criteria) { results.Starters = append(results.Starters, starter) } } } } else { // Search all starters for _, starter := range ml.starters { if ml.matchesStarterCriteria(starter, criteria) { results.Starters = append(results.Starters, starter) } } } // Wheel search optimization if criteria.SpellID != 0 { // Spell-specific search - use spell index if spellWheels, exists := ml.bySpellID[criteria.SpellID]; exists { for _, wheel := range spellWheels { if ml.matchesWheelCriteria(wheel, criteria) { results.Wheels = append(results.Wheels, wheel) } } } } else if criteria.MinChance > 0 || criteria.MaxChance > 0 { // Chance-based search - use chance ranges minChance := criteria.MinChance maxChance := criteria.MaxChance if maxChance == 0 { maxChance = MaxChance } results.Wheels = ml.GetWheelsByChanceRange(minChance, maxChance) } else { // Search all wheels for _, wheel := range ml.wheels { if ml.matchesWheelCriteria(wheel, criteria) { results.Wheels = append(results.Wheels, wheel) } } } return results } // matchesStarterCriteria checks if starter matches search criteria func (ml *MasterList) matchesStarterCriteria(starter *HeroicOPStarter, criteria HeroicOPSearchCriteria) bool { if criteria.StarterClass != 0 && starter.StartClass != criteria.StarterClass { return false } if criteria.NamePattern != "" { if !strings.Contains(strings.ToLower(starter.Name), strings.ToLower(criteria.NamePattern)) { return false } } return true } // matchesWheelCriteria checks if wheel matches search criteria func (ml *MasterList) matchesWheelCriteria(wheel *HeroicOPWheel, criteria HeroicOPSearchCriteria) bool { if criteria.SpellID != 0 && wheel.SpellID != criteria.SpellID { return false } if criteria.MinChance > 0 && wheel.Chance < criteria.MinChance { return false } if criteria.MaxChance > 0 && wheel.Chance > criteria.MaxChance { return false } if criteria.RequiredPlayers > 0 && wheel.RequiredPlayers != criteria.RequiredPlayers { return false } if criteria.NamePattern != "" { if !strings.Contains(strings.ToLower(wheel.Name), strings.ToLower(criteria.NamePattern)) { return false } } if criteria.HasShift && !wheel.HasShift() { return false } if criteria.IsOrdered && !wheel.IsOrdered() { return false } return true } // Statistics with Lazy Caching // GetStatistics returns comprehensive statistics with lazy caching for optimal performance func (ml *MasterList) GetStatistics() *HeroicOPStatistics { ml.mu.Lock() defer ml.mu.Unlock() ml.ensureMetadataValid() return &HeroicOPStatistics{ TotalStarters: int64(ml.totalStarters), TotalWheels: int64(ml.totalWheels), ClassDistribution: ml.copyClassDistribution(), OrderedWheelsCount: int64(len(ml.orderedWheels)), ShiftWheelsCount: int64(len(ml.shiftWheels)), SpellCount: int64(len(ml.spellInfo)), AverageChance: ml.calculateAverageChance(), } } // ensureMetadataValid ensures cached metadata is current func (ml *MasterList) ensureMetadataValid() { if ml.metadataValid { return } // Recompute cached metadata ml.totalStarters = len(ml.starters) ml.totalWheels = len(ml.wheels) ml.classDistribution = make(map[int8]int) for _, starter := range ml.starters { ml.classDistribution[starter.StartClass]++ } ml.metadataValid = true } // copyClassDistribution creates a copy of class distribution for thread safety func (ml *MasterList) copyClassDistribution() map[int32]int64 { result := make(map[int32]int64) for class, count := range ml.classDistribution { result[int32(class)] = int64(count) } return result } // calculateAverageChance computes average wheel chance func (ml *MasterList) calculateAverageChance() float64 { if len(ml.wheels) == 0 { return 0.0 } total := float64(0) for _, wheel := range ml.wheels { total += float64(wheel.Chance) } return total / float64(len(ml.wheels)) } // Modification Operations // AddStarter adds a starter to the master list with index updates func (ml *MasterList) AddStarter(starter *HeroicOPStarter) error { ml.mu.Lock() defer ml.mu.Unlock() if err := starter.Validate(); err != nil { return fmt.Errorf("invalid starter: %w", err) } if _, exists := ml.starters[starter.ID]; exists { return fmt.Errorf("starter ID %d already exists", starter.ID) } // Add to primary storage ml.starters[starter.ID] = starter // Update indices if ml.byClass[starter.StartClass] == nil { ml.byClass[starter.StartClass] = make(map[int32]*HeroicOPStarter) } ml.byClass[starter.StartClass][starter.ID] = starter ml.metadataValid = false return nil } // AddWheel adds a wheel to the master list with index updates func (ml *MasterList) AddWheel(wheel *HeroicOPWheel) error { ml.mu.Lock() defer ml.mu.Unlock() if err := wheel.Validate(); err != nil { return fmt.Errorf("invalid wheel: %w", err) } if _, exists := ml.wheels[wheel.ID]; exists { return fmt.Errorf("wheel ID %d already exists", wheel.ID) } // Verify starter exists if _, exists := ml.starters[wheel.StarterLinkID]; !exists { return fmt.Errorf("starter ID %d not found for wheel", wheel.StarterLinkID) } // Add to primary storage ml.wheels[wheel.ID] = wheel // Update indices ml.byStarterID[wheel.StarterLinkID] = append(ml.byStarterID[wheel.StarterLinkID], wheel) ml.bySpellID[wheel.SpellID] = append(ml.bySpellID[wheel.SpellID], wheel) chanceRange := ml.getChanceRange(wheel.Chance) ml.byChance[chanceRange] = append(ml.byChance[chanceRange], wheel) if wheel.IsOrdered() { ml.orderedWheels[wheel.ID] = wheel } if wheel.HasShift() { ml.shiftWheels[wheel.ID] = wheel } // Store spell info ml.spellInfo[wheel.SpellID] = SpellInfo{ ID: wheel.SpellID, Name: wheel.Name, Description: wheel.Description, } ml.metadataValid = false return nil } // Utility Methods // IsLoaded returns whether data has been loaded func (ml *MasterList) IsLoaded() bool { ml.mu.RLock() defer ml.mu.RUnlock() return ml.loaded } // GetCount returns total counts with O(1) performance func (ml *MasterList) GetCount() (starters, wheels int) { ml.mu.RLock() defer ml.mu.RUnlock() return len(ml.starters), len(ml.wheels) } // Validate performs comprehensive validation of the master list func (ml *MasterList) Validate() []error { ml.mu.RLock() defer ml.mu.RUnlock() var errors []error // Validate all starters for _, starter := range ml.starters { if err := starter.Validate(); err != nil { errors = append(errors, fmt.Errorf("starter %d: %w", starter.ID, err)) } } // Validate all wheels for _, wheel := range ml.wheels { if err := wheel.Validate(); err != nil { errors = append(errors, fmt.Errorf("wheel %d: %w", wheel.ID, err)) } // Check if starter exists for this wheel if _, exists := ml.starters[wheel.StarterLinkID]; !exists { errors = append(errors, fmt.Errorf("wheel %d references non-existent starter %d", wheel.ID, wheel.StarterLinkID)) } } // Check for orphaned starters (starters with no wheels) for _, starter := range ml.starters { if wheels := ml.byStarterID[starter.ID]; len(wheels) == 0 { errors = append(errors, fmt.Errorf("starter %d has no associated wheels", starter.ID)) } } return errors } // HeroicOPSearchResults contains search results type HeroicOPSearchResults struct { Starters []*HeroicOPStarter Wheels []*HeroicOPWheel } // HeroicOPStatistics contains extended statistics type HeroicOPStatistics struct { TotalStarters int64 `json:"total_starters"` TotalWheels int64 `json:"total_wheels"` ClassDistribution map[int32]int64 `json:"class_distribution"` OrderedWheelsCount int64 `json:"ordered_wheels_count"` ShiftWheelsCount int64 `json:"shift_wheels_count"` SpellCount int64 `json:"spell_count"` AverageChance float64 `json:"average_chance"` ActiveHOCount int64 `json:"active_ho_count"` }