1// Package core owns pure absence policy. Call Handle on one owner goroutine;
2// adapters perform returned effects. Times are explicit, not inferred idle age.
3package core
4
5import (
6 "fmt"
7 "slices"
8 "time"
9
10 "github.com/bnema/neferafk/internal/ports"
11)
12
13type deadline struct {
14 after time.Duration
15 notification uint64
16 done, due bool
17}
18type registration struct {
19 id uint64
20 generation ports.Generation
21 after time.Duration
22 actions []ports.Action
23}
24type Policy struct {
25 cfg ports.Config
26 generation ports.Generation
27 nextNotification uint64
28 deadlines [4]deadline
29 registrations []registration
30 acquiring, protected bool
31 lockGeneration ports.Generation
32 faded, off, beforeSleep bool
33}
34type State struct {
35 Generation ports.Generation
36 Acquiring, Protected, Faded, Off bool
37}
38
39func (p *Policy) State() State { return State{p.generation, p.acquiring, p.protected, p.faded, p.off} }
40func afters(c ports.Config) [4]time.Duration {
41 return [4]time.Duration{c.LockAfter, c.FadeAfter, c.OffAfter, c.SleepAfter}
42}
43
44func New(cfg ports.Config, now time.Time) (*Policy, []ports.PolicyEffect, error) {
45 if err := cfg.Validate(); err != nil {
46 return nil, nil, err
47 }
48 p := &Policy{cfg: cfg, generation: 1}
49 for a, d := range afters(cfg) {
50 p.deadlines[a] = deadline{after: d}
51 }
52 return p, p.arm(now), nil
53}
54
55func (p *Policy) arm(_ time.Time) []ports.PolicyEffect {
56 old := p.registrations
57 p.registrations = nil
58 for a := range p.deadlines {
59 d := &p.deadlines[a]
60 if d.after == 0 || d.done || d.due {
61 continue
62 }
63 i := slices.IndexFunc(p.registrations, func(r registration) bool {
64 return d.notification != 0 && r.id == d.notification || d.notification == 0 && r.generation == p.generation && r.after == d.after
65 })
66 if i < 0 {
67 r := registration{after: d.after, generation: p.generation}
68 if d.notification != 0 {
69 j := slices.IndexFunc(old, func(r registration) bool { return r.id == d.notification })
70 if j >= 0 {
71 r = old[j]
72 r.actions = nil
73 }
74 }
75 if r.id == 0 {
76 p.nextNotification++
77 r.id = p.nextNotification
78 }
79 r.actions = []ports.Action{ports.Action(a)}
80 p.registrations = append(p.registrations, r)
81 d.notification = r.id
82 } else {
83 p.registrations[i].actions = append(p.registrations[i].actions, ports.Action(a))
84 d.notification = p.registrations[i].id
85 }
86 }
87 slices.SortFunc(p.registrations, func(a, b registration) int {
88 if a.after < b.after {
89 return -1
90 }
91 if a.after > b.after {
92 return 1
93 }
94 return 0
95 })
96 n := make([]ports.IdleNotification, 0, len(p.registrations))
97 for _, r := range p.registrations {
98 n = append(n, ports.IdleNotification{ID: r.id, After: r.after})
99 }
100 return []ports.PolicyEffect{ports.ArmIdle{Generation: p.generation, Notifications: n}}
101}
102
103func (p *Policy) acquire(out *[]ports.PolicyEffect) {
104 if p.acquiring || p.protected {
105 return
106 }
107 p.acquiring = true
108 p.lockGeneration = p.generation
109 *out = append(*out, ports.AcquireLock{Generation: p.lockGeneration})
110}
111
112func (p *Policy) dispatch() []ports.PolicyEffect {
113 var out []ports.PolicyEffect
114 // Due flags for one notification are established before dispatch; lock is
115 // first only for simultaneous due actions, never because enabled/later.
116 for a := ports.Lock; a <= ports.Sleep; a++ {
117 d := &p.deadlines[a]
118 if !d.due || d.done || d.after == 0 {
119 continue
120 }
121 if (a == ports.OutputOff || a == ports.Sleep) && p.acquiring {
122 continue
123 }
124 d.done = true
125 switch a {
126 case ports.Lock:
127 p.acquire(&out)
128 case ports.Fade:
129 p.faded = true
130 out = append(out, ports.SetFade{Black: true, Duration: p.cfg.FadeDuration})
131 case ports.OutputOff:
132 p.off = true
133 out = append(out, ports.SetOutputPower{On: false})
134 case ports.Sleep:
135 out = append(out, ports.Suspend{})
136 }
137 }
138 return out
139}
140
141func (p *Policy) Handle(now time.Time, event ports.PolicyEvent) ([]ports.PolicyEffect, error) {
142 switch v := event.(type) {
143 case ports.IdleDue:
144 i := slices.IndexFunc(p.registrations, func(r registration) bool {
145 return r.generation == v.Generation && (v.Notification != 0 && r.id == v.Notification || v.Notification == 0 && r.after == v.After)
146 })
147 if i < 0 {
148 return nil, nil
149 }
150 for _, a := range p.registrations[i].actions {
151 p.deadlines[a].due = true
152 }
153 return p.dispatch(), nil
154 case ports.Activity:
155 p.generation++
156 var out []ports.PolicyEffect
157 if p.faded {
158 p.faded = false
159 out = append(out, ports.SetFade{Black: false, Duration: p.cfg.FadeDuration})
160 }
161 if p.off {
162 p.off = false
163 out = append(out, ports.SetOutputPower{On: true})
164 }
165 for a, d := range afters(p.cfg) {
166 p.deadlines[a] = deadline{after: d}
167 }
168 return append(out, p.arm(now)...), nil
169 case ports.Reload:
170 if err := v.Config.Validate(); err != nil {
171 return nil, err
172 }
173 if v.Config == p.cfg {
174 return nil, nil
175 }
176 for a, d := range afters(v.Config) {
177 old := &p.deadlines[a]
178 if old.after != d {
179 // Enablement changes even for completed actions; completion is
180 // pinned for this cycle, but old due/proxy identity is retired.
181 *old = deadline{after: d, done: old.done}
182 }
183 }
184 p.cfg = v.Config
185 p.generation++
186 return p.arm(now), nil
187 case ports.LockConfirmed:
188 if !p.acquiring || v.Generation != p.lockGeneration {
189 return nil, nil
190 }
191 p.acquiring = false
192 p.protected = true
193 out := p.dispatch()
194 if p.beforeSleep {
195 p.beforeSleep = false
196 out = append(out, ports.SleepReady{})
197 }
198 return out, nil
199 case ports.LockReleased:
200 if !p.protected || v.Generation != p.lockGeneration {
201 return nil, nil
202 }
203 p.protected = false // only authoritative successful visual-owner release
204 return nil, nil
205 case ports.LockRequested:
206 var out []ports.PolicyEffect
207 p.acquire(&out)
208 return out, nil
209 case ports.BeforeSleep:
210 if p.protected {
211 return []ports.PolicyEffect{ports.SleepReady{}}, nil
212 }
213 if p.cfg.LockAfter == 0 && !p.acquiring {
214 return []ports.PolicyEffect{ports.SleepReady{}}, nil
215 }
216 p.beforeSleep = true
217 var out []ports.PolicyEffect
218 p.acquire(&out)
219 return out, nil
220 default:
221 return nil, fmt.Errorf("unknown policy event %T", event)
222 }
223}