package valueobjects import ( "fmt" "strings" "knowfoolery/backend/shared/types" "knowfoolery/backend/shared/errors" ) // Theme represents a question theme/category type Theme struct { id types.ThemeID name string description string color string // Hex color code for UI icon string // Icon identifier parentID *types.ThemeID // For hierarchical themes isActive bool sortOrder int createdAt types.Timestamp updatedAt types.Timestamp } // NewTheme creates a new theme func NewTheme( name string, description string, ) (*Theme, error) { // Validate inputs if err := validateThemeName(name); err != nil { return nil, fmt.Errorf("invalid theme name: %w", err) } if err := validateThemeDescription(description); err != nil { return nil, fmt.Errorf("invalid theme description: %w", err) } now := types.NewTimestamp() theme := &Theme{ id: types.NewThemeID(), name: strings.TrimSpace(name), description: strings.TrimSpace(description), color: types.DefaultThemeColor, icon: types.DefaultThemeIcon, parentID: nil, isActive: true, sortOrder: 0, createdAt: now, updatedAt: now, } return theme, nil } // NewThemeWithID creates a theme with a specific ID (for loading from persistence) func NewThemeWithID( id types.ThemeID, name string, description string, createdAt types.Timestamp, ) (*Theme, error) { if id.IsEmpty() { return nil, errors.ErrValidationFailed("id", "theme ID cannot be empty") } theme, err := NewTheme(name, description) if err != nil { return nil, err } theme.id = id theme.createdAt = createdAt theme.updatedAt = createdAt return theme, nil } // Predefined themes factory functions // NewGeographyTheme creates a geography theme func NewGeographyTheme() *Theme { theme, _ := NewTheme("Geography", "Questions about countries, capitals, landmarks, and physical geography") theme.color = "#4CAF50" theme.icon = "globe" return theme } // NewHistoryTheme creates a history theme func NewHistoryTheme() *Theme { theme, _ := NewTheme("History", "Questions about historical events, figures, and periods") theme.color = "#795548" theme.icon = "history" return theme } // NewScienceTheme creates a science theme func NewScienceTheme() *Theme { theme, _ := NewTheme("Science", "Questions about biology, chemistry, physics, and general science") theme.color = "#2196F3" theme.icon = "science" return theme } // NewSportsTheme creates a sports theme func NewSportsTheme() *Theme { theme, _ := NewTheme("Sports", "Questions about sports, athletes, and sporting events") theme.color = "#FF5722" theme.icon = "sports" return theme } // NewEntertainmentTheme creates an entertainment theme func NewEntertainmentTheme() *Theme { theme, _ := NewTheme("Entertainment", "Questions about movies, music, TV shows, and celebrities") theme.color = "#E91E63" theme.icon = "movie" return theme } // NewLiteratureTheme creates a literature theme func NewLiteratureTheme() *Theme { theme, _ := NewTheme("Literature", "Questions about books, authors, and literary works") theme.color = "#9C27B0" theme.icon = "book" return theme } // NewGeneralKnowledgeTheme creates a general knowledge theme func NewGeneralKnowledgeTheme() *Theme { theme, _ := NewTheme("General Knowledge", "Miscellaneous questions covering various topics") theme.color = "#607D8B" theme.icon = "lightbulb" return theme } // Getters // ID returns the theme's unique identifier func (t *Theme) ID() types.ThemeID { return t.id } // Name returns the theme name func (t *Theme) Name() string { return t.name } // Description returns the theme description func (t *Theme) Description() string { return t.description } // Color returns the theme color func (t *Theme) Color() string { return t.color } // Icon returns the theme icon func (t *Theme) Icon() string { return t.icon } // ParentID returns the parent theme ID if this is a sub-theme func (t *Theme) ParentID() *types.ThemeID { return t.parentID } // IsActive returns true if the theme is active func (t *Theme) IsActive() bool { return t.isActive } // SortOrder returns the sort order func (t *Theme) SortOrder() int { return t.sortOrder } // CreatedAt returns the creation timestamp func (t *Theme) CreatedAt() types.Timestamp { return t.createdAt } // UpdatedAt returns the last update timestamp func (t *Theme) UpdatedAt() types.Timestamp { return t.updatedAt } // Business methods // UpdateName updates the theme name func (t *Theme) UpdateName(newName string) error { if err := validateThemeName(newName); err != nil { return fmt.Errorf("invalid theme name: %w", err) } t.name = strings.TrimSpace(newName) t.markUpdated() return nil } // UpdateDescription updates the theme description func (t *Theme) UpdateDescription(newDescription string) error { if err := validateThemeDescription(newDescription); err != nil { return fmt.Errorf("invalid theme description: %w", err) } t.description = strings.TrimSpace(newDescription) t.markUpdated() return nil } // SetColor sets the theme color func (t *Theme) SetColor(color string) error { color = strings.TrimSpace(color) if color == "" { t.color = types.DefaultThemeColor } else { if err := validateHexColor(color); err != nil { return fmt.Errorf("invalid color: %w", err) } t.color = color } t.markUpdated() return nil } // SetIcon sets the theme icon func (t *Theme) SetIcon(icon string) error { icon = strings.TrimSpace(icon) if icon == "" { t.icon = types.DefaultThemeIcon } else { if len(icon) > types.MaxIconNameLength { return errors.ErrValidationFailed("icon", fmt.Sprintf("icon name too long (max %d characters)", types.MaxIconNameLength)) } t.icon = icon } t.markUpdated() return nil } // SetParent sets the parent theme func (t *Theme) SetParent(parentID *types.ThemeID) error { // Prevent circular references if parentID != nil && *parentID == t.id { return errors.ErrValidationFailed("parent_id", "theme cannot be its own parent") } t.parentID = parentID t.markUpdated() return nil } // Activate activates the theme func (t *Theme) Activate() { if !t.isActive { t.isActive = true t.markUpdated() } } // Deactivate deactivates the theme func (t *Theme) Deactivate() { if t.isActive { t.isActive = false t.markUpdated() } } // SetSortOrder sets the sort order func (t *Theme) SetSortOrder(order int) { if order < 0 { order = 0 } if t.sortOrder != order { t.sortOrder = order t.markUpdated() } } // Query methods // IsRootTheme returns true if this is a root theme (no parent) func (t *Theme) IsRootTheme() bool { return t.parentID == nil } // IsSubTheme returns true if this is a sub-theme (has parent) func (t *Theme) IsSubTheme() bool { return t.parentID != nil } // HasParent checks if this theme has the specified parent func (t *Theme) HasParent(parentID types.ThemeID) bool { return t.parentID != nil && *t.parentID == parentID } // Equals checks if two themes are equal func (t *Theme) Equals(other *Theme) bool { if other == nil { return false } return t.id == other.id } // Validation methods // Validate performs comprehensive validation func (t *Theme) Validate() *types.ValidationResult { result := types.NewValidationResult() // Validate ID if t.id.IsEmpty() { result.AddError("theme ID cannot be empty") } // Validate name if err := validateThemeName(t.name); err != nil { result.AddErrorf("invalid name: %v", err) } // Validate description if err := validateThemeDescription(t.description); err != nil { result.AddErrorf("invalid description: %v", err) } // Validate color if err := validateHexColor(t.color); err != nil { result.AddErrorf("invalid color: %v", err) } // Validate icon if len(t.icon) > types.MaxIconNameLength { result.AddErrorf("icon name too long (max %d characters)", types.MaxIconNameLength) } // Validate sort order if t.sortOrder < 0 { result.AddError("sort order cannot be negative") } // Validate parent relationship if t.parentID != nil && *t.parentID == t.id { result.AddError("theme cannot be its own parent") } return result } // Helper methods // markUpdated updates the timestamp func (t *Theme) markUpdated() { t.updatedAt = types.NewTimestamp() } // ToSnapshot creates a read-only snapshot of the theme func (t *Theme) ToSnapshot() *ThemeSnapshot { return &ThemeSnapshot{ ID: t.id, Name: t.name, Description: t.description, Color: t.color, Icon: t.icon, ParentID: t.parentID, IsActive: t.isActive, SortOrder: t.sortOrder, CreatedAt: t.createdAt, UpdatedAt: t.updatedAt, } } // ToDisplayName returns a formatted display name func (t *Theme) ToDisplayName() string { return t.name } // String returns a string representation of the theme func (t *Theme) String() string { if t.IsSubTheme() { return fmt.Sprintf("%s (sub-theme)", t.name) } return t.name } // ThemeSnapshot represents a read-only view of theme data type ThemeSnapshot struct { ID types.ThemeID `json:"id"` Name string `json:"name"` Description string `json:"description"` Color string `json:"color"` Icon string `json:"icon"` ParentID *types.ThemeID `json:"parent_id,omitempty"` IsActive bool `json:"is_active"` SortOrder int `json:"sort_order"` CreatedAt types.Timestamp `json:"created_at"` UpdatedAt types.Timestamp `json:"updated_at"` } // GetDisplayName returns the display name func (ts *ThemeSnapshot) GetDisplayName() string { return ts.Name } // IsSubTheme returns true if this is a sub-theme func (ts *ThemeSnapshot) IsSubTheme() bool { return ts.ParentID != nil } // Validation helper functions func validateThemeName(name string) error { name = strings.TrimSpace(name) if name == "" { return errors.ErrValidationFailed("name", "theme name cannot be empty") } if len(name) < types.MinThemeNameLength { return errors.ErrValidationFailed("name", fmt.Sprintf("theme name too short (min %d characters)", types.MinThemeNameLength)) } if len(name) > types.MaxThemeNameLength { return errors.ErrValidationFailed("name", fmt.Sprintf("theme name too long (max %d characters)", types.MaxThemeNameLength)) } return nil } func validateThemeDescription(description string) error { description = strings.TrimSpace(description) if description == "" { return errors.ErrValidationFailed("description", "theme description cannot be empty") } if len(description) < types.MinThemeDescriptionLength { return errors.ErrValidationFailed("description", fmt.Sprintf("theme description too short (min %d characters)", types.MinThemeDescriptionLength)) } if len(description) > types.MaxThemeDescriptionLength { return errors.ErrValidationFailed("description", fmt.Sprintf("theme description too long (max %d characters)", types.MaxThemeDescriptionLength)) } return nil } func validateHexColor(color string) error { if color == "" { return nil // Empty is allowed, will use default } if !strings.HasPrefix(color, "#") { return errors.ErrValidationFailed("color", "color must start with #") } if len(color) != 7 { return errors.ErrValidationFailed("color", "color must be in format #RRGGBB") } // Check if all characters after # are valid hex for i, char := range color[1:] { if !isHexChar(char) { return errors.ErrValidationFailed("color", fmt.Sprintf("invalid hex character at position %d", i+1)) } } return nil } func isHexChar(char rune) bool { return (char >= '0' && char <= '9') || (char >= 'A' && char <= 'F') || (char >= 'a' && char <= 'f') } // Theme collections for easy access // GetDefaultThemes returns a list of default themes func GetDefaultThemes() []*Theme { return []*Theme{ NewGeographyTheme(), NewHistoryTheme(), NewScienceTheme(), NewSportsTheme(), NewEntertainmentTheme(), NewLiteratureTheme(), NewGeneralKnowledgeTheme(), } } // GetThemeByName returns a default theme by name func GetThemeByName(name string) *Theme { themes := GetDefaultThemes() name = strings.TrimSpace(strings.ToLower(name)) for _, theme := range themes { if strings.ToLower(theme.Name()) == name { return theme } } return nil }