package chat import ( "fmt" "strings" "eq2emu/internal/common" "eq2emu/internal/database" ) // MasterList manages a collection of channels using the generic MasterList base type MasterList struct { *common.MasterList[int32, *Channel] } // NewMasterList creates a new channel master list func NewMasterList() *MasterList { return &MasterList{ MasterList: common.NewMasterList[int32, *Channel](), } } // AddChannel adds a channel to the master list func (ml *MasterList) AddChannel(channel *Channel) bool { return ml.Add(channel) } // GetChannel retrieves a channel by ID func (ml *MasterList) GetChannel(id int32) *Channel { return ml.Get(id) } // GetChannelSafe retrieves a channel by ID with existence check func (ml *MasterList) GetChannelSafe(id int32) (*Channel, bool) { return ml.GetSafe(id) } // HasChannel checks if a channel exists by ID func (ml *MasterList) HasChannel(id int32) bool { return ml.Exists(id) } // RemoveChannel removes a channel by ID func (ml *MasterList) RemoveChannel(id int32) bool { return ml.Remove(id) } // GetAllChannels returns all channels as a map func (ml *MasterList) GetAllChannels() map[int32]*Channel { return ml.GetAll() } // GetAllChannelsList returns all channels as a slice func (ml *MasterList) GetAllChannelsList() []*Channel { return ml.GetAllSlice() } // GetChannelCount returns the number of channels func (ml *MasterList) GetChannelCount() int { return ml.Size() } // ClearChannels removes all channels from the list func (ml *MasterList) ClearChannels() { ml.Clear() } // FindChannelsByName finds channels containing the given name substring func (ml *MasterList) FindChannelsByName(nameSubstring string) []*Channel { return ml.Filter(func(channel *Channel) bool { return contains(channel.GetName(), nameSubstring) }) } // FindChannelsByType finds channels of a specific type func (ml *MasterList) FindChannelsByType(channelType int) []*Channel { return ml.Filter(func(channel *Channel) bool { return channel.GetType() == channelType }) } // GetWorldChannels returns all world channels func (ml *MasterList) GetWorldChannels() []*Channel { return ml.FindChannelsByType(ChannelTypeWorld) } // GetCustomChannels returns all custom channels func (ml *MasterList) GetCustomChannels() []*Channel { return ml.FindChannelsByType(ChannelTypeCustom) } // GetActiveChannels returns channels that have members func (ml *MasterList) GetActiveChannels() []*Channel { return ml.Filter(func(channel *Channel) bool { return !channel.IsEmpty() }) } // GetEmptyChannels returns channels that have no members func (ml *MasterList) GetEmptyChannels() []*Channel { return ml.Filter(func(channel *Channel) bool { return channel.IsEmpty() }) } // GetChannelByName retrieves a channel by name (case-insensitive) func (ml *MasterList) GetChannelByName(name string) *Channel { name = strings.ToLower(name) var foundChannel *Channel ml.ForEach(func(id int32, channel *Channel) { if strings.ToLower(channel.GetName()) == name { foundChannel = channel } }) return foundChannel } // HasChannelByName checks if a channel exists by name (case-insensitive) 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 { return ml.Filter(func(channel *Channel) bool { return channel.CanJoinChannelByLevel(level) && channel.CanJoinChannelByRace(race) && channel.CanJoinChannelByClass(class) }) } // ValidateChannels checks all channels for consistency func (ml *MasterList) ValidateChannels() []string { var issues []string ml.ForEach(func(id int32, channel *Channel) { if channel == nil { issues = append(issues, fmt.Sprintf("Channel ID %d is nil", id)) return } 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 func (ml *MasterList) GetStatistics() map[string]any { stats := make(map[string]any) stats["total_channels"] = ml.Size() if ml.IsEmpty() { return stats } // Count by channel type typeCounts := make(map[int]int) var totalMembers int var activeChannels int var minID, maxID int32 first := true ml.ForEach(func(id int32, channel *Channel) { typeCounts[channel.GetType()]++ 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["channels_by_type"] = typeCounts stats["world_channels"] = typeCounts[ChannelTypeWorld] stats["custom_channels"] = typeCounts[ChannelTypeCustom] 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 }