package messagewall import ( "fmt" "sort" ) const ( versionV1 = 1 unitGrid = "grid" fitModeCover = "cover" fitModeContain = "contain" defaultDuration = 20 defaultLoadTime = 15 defaultOnError = "skip" ) func Resolve(request ResolveRequest) (ResolveResult, error) { if err := Validate(request.Layout); err != nil { return ResolveResult{}, err } result := ResolveResult{ Version: request.Layout.Version, CoordinateSpace: request.Layout.CoordinateSpace, FitMode: request.Layout.FitMode, Scenes: make([]ResolvedScene, 0, len(request.Layout.Slots)), } duration := request.DurationSeconds if duration <= 0 { duration = defaultDuration } loadTimeout := request.LoadTimeoutSeconds if loadTimeout <= 0 { loadTimeout = defaultLoadTime } onError := request.OnError if onError == "" { onError = defaultOnError } for _, slot := range request.Layout.Slots { result.Scenes = append(result.Scenes, ResolvedScene{ SlotID: slot.SlotID, Source: request.Source, DurationSeconds: duration, LoadTimeoutSeconds: loadTimeout, OnError: onError, Crop: Crop{ X: slot.X, Y: slot.Y, Width: slot.Width, Height: slot.Height, Unit: request.Layout.CoordinateSpace.Unit, }, }) } sort.Slice(result.Scenes, func(i, j int) bool { return result.Scenes[i].SlotID < result.Scenes[j].SlotID }) return result, nil } func Validate(layout Layout) error { if layout.Version != versionV1 { return fmt.Errorf("unsupported layout version: %d", layout.Version) } if layout.CoordinateSpace.Width <= 0 || layout.CoordinateSpace.Height <= 0 { return fmt.Errorf("coordinate_space width and height must be positive") } if layout.CoordinateSpace.Unit != unitGrid { return fmt.Errorf("unsupported coordinate_space unit: %s", layout.CoordinateSpace.Unit) } if layout.FitMode != fitModeCover && layout.FitMode != fitModeContain { return fmt.Errorf("unsupported fit_mode: %s", layout.FitMode) } if len(layout.Slots) == 0 { return fmt.Errorf("layout must contain at least one slot") } seen := make(map[string]struct{}, len(layout.Slots)) for i, slot := range layout.Slots { if slot.SlotID == "" { return fmt.Errorf("slot %d has empty slot_id", i) } if _, ok := seen[slot.SlotID]; ok { return fmt.Errorf("duplicate slot_id: %s", slot.SlotID) } seen[slot.SlotID] = struct{}{} if slot.Width <= 0 || slot.Height <= 0 { return fmt.Errorf("slot %s must have positive width and height", slot.SlotID) } if slot.X < 0 || slot.Y < 0 { return fmt.Errorf("slot %s must not start outside coordinate space", slot.SlotID) } if slot.X+slot.Width > layout.CoordinateSpace.Width { return fmt.Errorf("slot %s exceeds coordinate_space width", slot.SlotID) } if slot.Y+slot.Height > layout.CoordinateSpace.Height { return fmt.Errorf("slot %s exceeds coordinate_space height", slot.SlotID) } } for i := range layout.Slots { for j := i + 1; j < len(layout.Slots); j++ { if overlaps(layout.Slots[i], layout.Slots[j]) { return fmt.Errorf("slots %s and %s overlap", layout.Slots[i].SlotID, layout.Slots[j].SlotID) } } } return nil } func overlaps(a, b Slot) bool { return a.X < b.X+b.Width && a.X+a.Width > b.X && a.Y < b.Y+b.Height && a.Y+a.Height > b.Y }