package field import ( "fmt" "image/color" "slices" ) type Mark struct { Cell *Cell Change *Change color color.Color } func (m *Mark) GetColor() color.Color { if m == nil { return nil } return m.color } type ChangeAction int const ( ActionSetNumber ChangeAction = iota ActionSetNote ActionRemoveNumber ActionRemoveNote ) type ExternalChange struct { Cell *Cell Marks []Mark Action ChangeAction Value int From int TriggerdBy string } type Change struct { Cell *Cell marks []Mark action ChangeAction value int from int triggerdBy string } func (c *Change) GetMarks() []Mark { if c == nil { return nil } return slices.Clone(c.marks) } func (c *Change) GetAction() ChangeAction { if c == nil { return ActionSetNumber } return c.action } func (c *Change) GetTo() int { if c == nil { return 0 } return c.value } func (c *Change) GetFrom() int { if c == nil { return 0 } return c.from } func (c *Change) GetTriggeredBy() string { if c == nil { return "" } return c.triggerdBy } func (f *Field) AddChange(external *ExternalChange) error { if err := f.validateChange(external); err != nil { return err } cell := external.Cell switch external.Action { case ActionSetNumber: if cell.number == external.Value { return nil } cell.number = external.Value f.recordChange(external) f.clearNotes(cell, external.TriggerdBy) f.removePeerNote(cell, external.Value, external.TriggerdBy) case ActionSetNote: if cell.notes.Has(external.Value) { return nil } cell.notes.numbers = append(cell.notes.numbers, external.Value) slices.Sort(cell.notes.numbers) f.recordChange(external) case ActionRemoveNumber: if cell.number == 0 { return nil } cell.number = 0 f.recordChange(external) case ActionRemoveNote: for i, existing := range cell.notes.numbers { if existing == external.From { cell.notes.numbers = slices.Delete(cell.notes.numbers, i, i+1) f.recordChange(external) break } } } return nil } func (f *Field) AddChanges(changes []ExternalChange) error { if f == nil || !f.IsValid() { return ErrInvalidField } state := f.snapshot() for i := range changes { if err := f.AddChange(&changes[i]); err != nil { f.restore(state) return fmt.Errorf("apply change %d: %w", i, err) } } return nil } func (f *Field) GetChanges() []ExternalChange { if f == nil { return nil } result := make([]ExternalChange, len(f.changes)) for i := range f.changes { result[i] = ExternalChange{Cell: f.changes[i].Cell, Marks: slices.Clone(f.changes[i].marks), Action: f.changes[i].action, Value: f.changes[i].value, From: f.changes[i].from, TriggerdBy: f.changes[i].triggerdBy} } return result } func (f *Field) validateChange(change *ExternalChange) error { if f == nil || !f.IsValid() { return ErrInvalidField } if change == nil || change.Cell == nil || !f.ownsCell(change.Cell) { return fmt.Errorf("%w: cell does not belong to field", ErrInvalidChange) } cell := change.Cell if cell.notes == nil || cell.pos == nil { return fmt.Errorf("%w: incomplete cell", ErrInvalidChange) } switch change.Action { case ActionSetNumber: if change.Value < 1 || change.Value > f.props.Rows { return fmt.Errorf("%w: number %d is out of range", ErrInvalidChange, change.Value) } if change.From != cell.number { return fmt.Errorf("%w: expected previous value %d, got %d", ErrInvalidChange, cell.number, change.From) } if f.hasPeerNumber(cell, change.Value) { return fmt.Errorf("%w: number %d already exists in a peer", ErrInvalidChange, change.Value) } case ActionSetNote: if cell.number != 0 { return fmt.Errorf("%w: cannot add a note to a filled cell", ErrInvalidChange) } if change.Value < 1 || change.Value > f.props.Rows || f.hasPeerNumber(cell, change.Value) { return fmt.Errorf("%w: note %d is not a candidate", ErrInvalidChange, change.Value) } case ActionRemoveNumber: if change.From != cell.number { return fmt.Errorf("%w: expected previous value %d, got %d", ErrInvalidChange, cell.number, change.From) } case ActionRemoveNote: if change.From < 1 || change.From > f.props.Rows { return fmt.Errorf("%w: note %d is out of range", ErrInvalidChange, change.From) } default: return fmt.Errorf("%w: unknown action %d", ErrInvalidChange, change.Action) } return nil } func (f *Field) recordChange(external *ExternalChange) { f.changes = append(f.changes, Change{Cell: external.Cell, marks: slices.Clone(external.Marks), action: external.Action, value: external.Value, from: external.From, triggerdBy: external.TriggerdBy}) } func (f *Field) clearNotes(cell *Cell, trigger string) { for _, note := range slices.Clone(cell.notes.numbers) { cell.notes.numbers = removeNumber(cell.notes.numbers, note) f.recordChange(&ExternalChange{Cell: cell, Action: ActionRemoveNote, From: note, TriggerdBy: trigger}) } } func (f *Field) removePeerNote(cell *Cell, number int, trigger string) { seen := make(map[*Cell]struct{}) parts, err := f.GetEachPartAtPos(cell.pos) if err != nil { return } for _, part := range parts { part.ForEachCell(func(peer *Cell) { if peer == cell { return } if _, exists := seen[peer]; exists { return } seen[peer] = struct{}{} if peer.notes.Has(number) { peer.notes.numbers = removeNumber(peer.notes.numbers, number) f.recordChange(&ExternalChange{Cell: peer, Action: ActionRemoveNote, From: number, TriggerdBy: trigger}) } }) } } func removeNumber(numbers []int, number int) []int { for i, existing := range numbers { if existing == number { return slices.Delete(numbers, i, i+1) } } return numbers }