package chat import ( "fmt" "maps" "strings" "sync" "eq2emu/internal/database" ) // MasterList is a specialized chat channel master list optimized for: // - Fast ID-based lookups (O(1)) // - Fast channel type filtering (O(1)) // - Fast name-based searching (indexed) // - Fast member count filtering // - Efficient channel compatibility queries type MasterList struct { // Core storage channels map[int32]*Channel // ID -> Channel mutex sync.RWMutex // Specialized indices for O(1) lookups byType map[int][]*Channel // ChannelType -> channels byMemberCt map[int][]*Channel // Member count -> channels (active/empty) byNameLower map[string]*Channel // Lowercase name -> channel byRestrict map[int32][]*Channel // Restriction level -> channels // Cached metadata memberCounts []int // Unique member counts (cached) typeStats map[int]int // Channel type -> count metaStale bool // Whether metadata cache needs refresh } // NewMasterList creates a new specialized chat channel master list func NewMasterList() *MasterList { return &MasterList{ channels: make(map[int32]*Channel), byType: make(map[int][]*Channel), byMemberCt: make(map[int][]*Channel), byNameLower: make(map[string]*Channel), byRestrict: make(map[int32][]*Channel), typeStats: make(map[int]int), metaStale: true, } } // refreshMetaCache updates the member counts cache and type stats func (ml *MasterList) refreshMetaCache() { if !ml.metaStale { return } // Clear and rebuild type stats ml.typeStats = make(map[int]int) memberCountSet := make(map[int]struct{}) // Collect unique member counts and type stats for _, channel := range ml.channels { ml.typeStats[channel.GetType()]++ memberCount := channel.GetNumClients() memberCountSet[memberCount] = struct{}{} } // Clear and rebuild member counts cache ml.memberCounts = ml.memberCounts[:0] for count := range memberCountSet { ml.memberCounts = append(ml.memberCounts, count) } ml.metaStale = false } // updateChannelIndices updates all indices for a channel func (ml *MasterList) updateChannelIndices(channel *Channel, add bool) { if add { // Add to type index ml.byType[channel.GetType()] = append(ml.byType[channel.GetType()], channel) // Add to member count index memberCount := channel.GetNumClients() ml.byMemberCt[memberCount] = append(ml.byMemberCt[memberCount], channel) // Add to name index ml.byNameLower[strings.ToLower(channel.GetName())] = channel // Add to restriction index ml.byRestrict[channel.LevelRestriction] = append(ml.byRestrict[channel.LevelRestriction], channel) } else { // Remove from type index typeChannels := ml.byType[channel.GetType()] for i, ch := range typeChannels { if ch.ID == channel.ID { ml.byType[channel.GetType()] = append(typeChannels[:i], typeChannels[i+1:]...) break } } // Remove from member count index memberCount := channel.GetNumClients() memberChannels := ml.byMemberCt[memberCount] for i, ch := range memberChannels { if ch.ID == channel.ID { ml.byMemberCt[memberCount] = append(memberChannels[:i], memberChannels[i+1:]...) break } } // Remove from name index delete(ml.byNameLower, strings.ToLower(channel.GetName())) // Remove from restriction index restrChannels := ml.byRestrict[channel.LevelRestriction] for i, ch := range restrChannels { if ch.ID == channel.ID { ml.byRestrict[channel.LevelRestriction] = append(restrChannels[:i], restrChannels[i+1:]...) break } } } } // RefreshChannelIndices refreshes the indices for a channel (used when member count changes) func (ml *MasterList) RefreshChannelIndices(channel *Channel, oldMemberCount int) { ml.mutex.Lock() defer ml.mutex.Unlock() // Remove from old member count index oldMemberChannels := ml.byMemberCt[oldMemberCount] for i, ch := range oldMemberChannels { if ch.ID == channel.ID { ml.byMemberCt[oldMemberCount] = append(oldMemberChannels[:i], oldMemberChannels[i+1:]...) break } } // Add to new member count index newMemberCount := channel.GetNumClients() ml.byMemberCt[newMemberCount] = append(ml.byMemberCt[newMemberCount], channel) // Invalidate metadata cache ml.metaStale = true } // AddChannel adds a channel with full indexing func (ml *MasterList) AddChannel(channel *Channel) bool { if channel == nil { return false } ml.mutex.Lock() defer ml.mutex.Unlock() // Check if exists if _, exists := ml.channels[channel.ID]; exists { return false } // Add to core storage ml.channels[channel.ID] = channel // Update all indices ml.updateChannelIndices(channel, true) // Invalidate metadata cache ml.metaStale = true return true } // GetChannel retrieves by ID (O(1)) func (ml *MasterList) GetChannel(id int32) *Channel { ml.mutex.RLock() defer ml.mutex.RUnlock() return ml.channels[id] } // GetChannelSafe retrieves a channel by ID with existence check func (ml *MasterList) GetChannelSafe(id int32) (*Channel, bool) { ml.mutex.RLock() defer ml.mutex.RUnlock() channel, exists := ml.channels[id] return channel, exists } // HasChannel checks if a channel exists by ID func (ml *MasterList) HasChannel(id int32) bool { ml.mutex.RLock() defer ml.mutex.RUnlock() _, exists := ml.channels[id] return exists } // RemoveChannel removes a channel and updates all indices func (ml *MasterList) RemoveChannel(id int32) bool { ml.mutex.Lock() defer ml.mutex.Unlock() channel, exists := ml.channels[id] if !exists { return false } // Remove from core storage delete(ml.channels, id) // Update all indices ml.updateChannelIndices(channel, false) // Invalidate metadata cache ml.metaStale = true return true } // GetAllChannels returns a copy of all channels map func (ml *MasterList) GetAllChannels() map[int32]*Channel { ml.mutex.RLock() defer ml.mutex.RUnlock() // Return a copy to prevent external modification result := make(map[int32]*Channel, len(ml.channels)) maps.Copy(result, ml.channels) return result } // GetAllChannelsList returns all channels as a slice func (ml *MasterList) GetAllChannelsList() []*Channel { ml.mutex.RLock() defer ml.mutex.RUnlock() result := make([]*Channel, 0, len(ml.channels)) for _, channel := range ml.channels { result = append(result, channel) } return result } // GetChannelCount returns the number of channels func (ml *MasterList) GetChannelCount() int { ml.mutex.RLock() defer ml.mutex.RUnlock() return len(ml.channels) } // Size returns the total number of channels func (ml *MasterList) Size() int { ml.mutex.RLock() defer ml.mutex.RUnlock() return len(ml.channels) } // IsEmpty returns true if the master list is empty func (ml *MasterList) IsEmpty() bool { ml.mutex.RLock() defer ml.mutex.RUnlock() return len(ml.channels) == 0 } // ClearChannels removes all channels from the list func (ml *MasterList) ClearChannels() { ml.mutex.Lock() defer ml.mutex.Unlock() // Clear all maps ml.channels = make(map[int32]*Channel) ml.byType = make(map[int][]*Channel) ml.byMemberCt = make(map[int][]*Channel) ml.byNameLower = make(map[string]*Channel) ml.byRestrict = make(map[int32][]*Channel) // Clear cached metadata ml.memberCounts = ml.memberCounts[:0] ml.typeStats = make(map[int]int) ml.metaStale = true } // Clear removes all channels from the master list func (ml *MasterList) Clear() { ml.ClearChannels() } // FindChannelsByName finds channels containing the given name substring (optimized) func (ml *MasterList) FindChannelsByName(nameSubstring string) []*Channel { ml.mutex.RLock() defer ml.mutex.RUnlock() searchKey := strings.ToLower(nameSubstring) // Try exact match first for full channel names if exactChannel := ml.byNameLower[searchKey]; exactChannel != nil { return []*Channel{exactChannel} } // Fallback to substring search var result []*Channel for _, channel := range ml.channels { if contains(strings.ToLower(channel.GetName()), searchKey) { result = append(result, channel) } } return result } // FindChannelsByType finds channels of a specific type (O(1)) func (ml *MasterList) FindChannelsByType(channelType int) []*Channel { ml.mutex.RLock() defer ml.mutex.RUnlock() return ml.byType[channelType] } // GetWorldChannels returns all world channels (O(1)) func (ml *MasterList) GetWorldChannels() []*Channel { return ml.FindChannelsByType(ChannelTypeWorld) } // GetCustomChannels returns all custom channels (O(1)) func (ml *MasterList) GetCustomChannels() []*Channel { return ml.FindChannelsByType(ChannelTypeCustom) } // GetActiveChannels returns channels that have members func (ml *MasterList) GetActiveChannels() []*Channel { ml.mutex.RLock() defer ml.mutex.RUnlock() var result []*Channel for _, channel := range ml.channels { if !channel.IsEmpty() { result = append(result, channel) } } return result } // GetEmptyChannels returns channels that have no members func (ml *MasterList) GetEmptyChannels() []*Channel { ml.mutex.RLock() defer ml.mutex.RUnlock() var result []*Channel for _, channel := range ml.channels { if channel.IsEmpty() { result = append(result, channel) } } return result } // GetChannelByName retrieves a channel by name (case-insensitive, O(1)) func (ml *MasterList) GetChannelByName(name string) *Channel { ml.mutex.RLock() defer ml.mutex.RUnlock() return ml.byNameLower[strings.ToLower(name)] } // HasChannelByName checks if a channel exists by name (case-insensitive, O(1)) func (ml *MasterList) HasChannelByName(name string) bool { return ml.GetChannelByName(name) != nil } // GetCompatibleChannels returns channels compatible with player restrictions func (ml *MasterList) GetCompatibleChannels(level, race, class int32) []*Channel { ml.mutex.RLock() defer ml.mutex.RUnlock() var result []*Channel for _, channel := range ml.channels { if channel.CanJoinChannelByLevel(level) && channel.CanJoinChannelByRace(race) && channel.CanJoinChannelByClass(class) { result = append(result, channel) } } return result } // GetChannelsByMemberCount returns channels with specific member count func (ml *MasterList) GetChannelsByMemberCount(memberCount int) []*Channel { ml.mutex.RLock() defer ml.mutex.RUnlock() return ml.byMemberCt[memberCount] } // GetChannelsByLevelRestriction returns channels with specific level restriction func (ml *MasterList) GetChannelsByLevelRestriction(levelRestriction int32) []*Channel { ml.mutex.RLock() defer ml.mutex.RUnlock() return ml.byRestrict[levelRestriction] } // UpdateChannel updates an existing channel and refreshes indices func (ml *MasterList) UpdateChannel(channel *Channel) error { if channel == nil { return fmt.Errorf("channel cannot be nil") } ml.mutex.Lock() defer ml.mutex.Unlock() // Check if exists old, exists := ml.channels[channel.ID] if !exists { return fmt.Errorf("channel %d not found", channel.ID) } // Remove old channel from indices (but not core storage yet) ml.updateChannelIndices(old, false) // Update core storage ml.channels[channel.ID] = channel // Add new channel to indices ml.updateChannelIndices(channel, true) // Invalidate metadata cache ml.metaStale = true return nil } // ForEach executes a function for each channel func (ml *MasterList) ForEach(fn func(int32, *Channel)) { ml.mutex.RLock() defer ml.mutex.RUnlock() for id, channel := range ml.channels { fn(id, channel) } } // ValidateChannels checks all channels for consistency func (ml *MasterList) ValidateChannels() []string { ml.mutex.RLock() defer ml.mutex.RUnlock() var issues []string for id, channel := range ml.channels { if channel == nil { issues = append(issues, fmt.Sprintf("Channel ID %d is nil", id)) continue } if channel.GetID() != id { issues = append(issues, fmt.Sprintf("Channel ID mismatch: map key %d != channel ID %d", id, channel.GetID())) } if len(channel.GetName()) == 0 { issues = append(issues, fmt.Sprintf("Channel ID %d has empty name", id)) } if channel.GetType() < ChannelTypeNone || channel.GetType() > ChannelTypeCustom { issues = append(issues, fmt.Sprintf("Channel ID %d has invalid type: %d", id, channel.GetType())) } if len(channel.GetName()) > MaxChannelNameLength { issues = append(issues, fmt.Sprintf("Channel ID %d name too long: %d > %d", id, len(channel.GetName()), MaxChannelNameLength)) } if len(channel.Password) > MaxChannelPasswordLength { issues = append(issues, fmt.Sprintf("Channel ID %d password too long: %d > %d", id, len(channel.Password), MaxChannelPasswordLength)) } } return issues } // IsValid returns true if all channels are valid func (ml *MasterList) IsValid() bool { issues := ml.ValidateChannels() return len(issues) == 0 } // GetStatistics returns statistics about the channel 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_channels"] = len(ml.channels) if len(ml.channels) == 0 { return stats } // Use cached type stats stats["channels_by_type"] = ml.typeStats stats["world_channels"] = ml.typeStats[ChannelTypeWorld] stats["custom_channels"] = ml.typeStats[ChannelTypeCustom] // Calculate additional stats var totalMembers int var activeChannels int var minID, maxID int32 first := true for id, channel := range ml.channels { totalMembers += channel.GetNumClients() if !channel.IsEmpty() { activeChannels++ } if first { minID = id maxID = id first = false } else { if id < minID { minID = id } if id > maxID { maxID = id } } } stats["total_members"] = totalMembers stats["active_channels"] = activeChannels stats["min_id"] = minID stats["max_id"] = maxID stats["id_range"] = maxID - minID return stats } // LoadAllChannels loads all channels from the database into the master list func (ml *MasterList) LoadAllChannels(db *database.Database) error { if db == nil { return fmt.Errorf("database connection is nil") } // Clear existing channels ml.Clear() query := `SELECT id, name, password, type, level_restriction, race_restriction, class_restriction, discord_enabled, created_at, updated_at FROM channels ORDER BY id` rows, err := db.Query(query) if err != nil { return fmt.Errorf("failed to query channels: %w", err) } defer rows.Close() count := 0 for rows.Next() { channel := &Channel{ db: db, isNew: false, members: make([]int32, 0), } err := rows.Scan(&channel.ID, &channel.Name, &channel.Password, &channel.ChannelType, &channel.LevelRestriction, &channel.RaceRestriction, &channel.ClassRestriction, &channel.DiscordEnabled, &channel.Created, &channel.Updated) if err != nil { return fmt.Errorf("failed to scan channel: %w", err) } if !ml.AddChannel(channel) { return fmt.Errorf("failed to add channel %d to master list", channel.ID) } count++ } if err := rows.Err(); err != nil { return fmt.Errorf("error iterating channel rows: %w", err) } return nil } // LoadAllChannelsFromDatabase is a convenience function that creates a master list and loads all channels func LoadAllChannelsFromDatabase(db *database.Database) (*MasterList, error) { masterList := NewMasterList() err := masterList.LoadAllChannels(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 }