1/* ARCHITECTURE §5: mesin keadaan pemotretan sebagai fungsi murni (`reduce`).
2 Tanpa kamera dan tanpa timer: uji unit menjalankan seluruh tabel transisi. */
3
4export const COUNTDOWN_FROM = 3
5export const COUNTDOWN_STEP_MS = 1000
6export const PAUSE_MS = 300
7export const FLASH_MS = 160
8
9export type CountdownValue = 3 | 2 | 1
10export type DeniedReason = "denied" | "unavailable"
11
12export type CapturePhase =
13 | "idle"
14 | "requesting"
15 | "denied"
16 | "ready"
17 | "holding"
18 | "countdown"
19 | "capturing"
20 | "pause"
21 | "complete"
22
23export interface CaptureState {
24 phase: CapturePhase
25 countdownValue: CountdownValue | null
26 /** Slot yang sedang diganti lewat retake; null berarti slot kosong pertama. */
27 targetSlot: number | null
28 deniedReason: DeniedReason | null
29 /** Kilat sekali per foto (DESIGN-SYSTEM §7); tampilannya ditentukan lapisan UI. */
30 flash: boolean
31}
32
33export type CaptureEvent =
34 | { type: "PERMISSION_REQUEST" }
35 | { type: "PERMISSION_GRANTED" }
36 | { type: "PERMISSION_DENIED"; reason: DeniedReason }
37 | { type: "SHOOT"; target?: number }
38 | { type: "HOLD_START" }
39 | { type: "HOLD_CANCEL" }
40 | { type: "HOLD_COMPLETE" }
41 | { type: "TICK" }
42 | { type: "SHOT_SAVED" }
43 | { type: "FLASH_DONE" }
44 | { type: "PAUSE_DONE"; allFilled: boolean }
45
46export const INITIAL_CAPTURE_STATE: CaptureState = {
47 phase: "idle",
48 countdownValue: null,
49 targetSlot: null,
50 deniedReason: null,
51 flash: false,
52}
53
54/* Fase yang mengunci kontrol: baki foto dan tombol jepret tidak bisa dipakai
55 selama izin diminta, hitung mundur, pengambilan, dan jeda. */
56const BUSY_PHASES: Record<CapturePhase, boolean> = {
57 idle: false,
58 requesting: true,
59 denied: false,
60 ready: false,
61 holding: false,
62 countdown: true,
63 capturing: true,
64 pause: true,
65 complete: false,
66}
67
68export function isBusy(phase: CapturePhase): boolean {
69 return BUSY_PHASES[phase]
70}
71
72/* Tombol jepret hanya hidup saat kamera siap. `complete` sengaja mati:
73 mengganti foto saat semua slot terisi hanya lewat baki (FR-07). */
74const SHOOTABLE_PHASES: Record<CapturePhase, boolean> = {
75 idle: false,
76 requesting: false,
77 denied: false,
78 ready: true,
79 holding: false,
80 countdown: false,
81 capturing: false,
82 pause: false,
83 complete: false,
84}
85
86export function canShoot(phase: CapturePhase): boolean {
87 return SHOOTABLE_PHASES[phase]
88}
89
90export function reduce(state: CaptureState, event: CaptureEvent): CaptureState {
91 switch (state.phase) {
92 case "idle":
93 case "denied":
94 if (event.type === "PERMISSION_REQUEST") {
95 return { ...state, phase: "requesting" }
96 }
97 return state
98
99 case "requesting":
100 if (event.type === "PERMISSION_GRANTED") {
101 return { ...state, phase: "ready", deniedReason: null }
102 }
103 if (event.type === "PERMISSION_DENIED") {
104 return { ...state, phase: "denied", deniedReason: event.reason }
105 }
106 return state
107
108 case "ready":
109 if (event.type === "SHOOT") {
110 return {
111 ...state,
112 phase: "countdown",
113 countdownValue: COUNTDOWN_FROM,
114 targetSlot: event.target ?? null,
115 }
116 }
117 if (event.type === "HOLD_START") {
118 return { ...state, phase: "holding" }
119 }
120 return state
121
122 case "holding":
123 if (event.type === "HOLD_CANCEL") {
124 return { ...state, phase: "ready" }
125 }
126 /* GS-02: tahanan penuh memulai hitung mundur. GS-05: tombol kamera dan
127 Spasi tetap memotret walau gesture sedang ditahan. */
128 if (event.type === "HOLD_COMPLETE" || event.type === "SHOOT") {
129 return {
130 ...state,
131 phase: "countdown",
132 countdownValue: COUNTDOWN_FROM,
133 targetSlot: null,
134 }
135 }
136 return state
137
138 case "countdown":
139 if (event.type !== "TICK" || state.countdownValue === null) {
140 return state
141 }
142 if (state.countdownValue === 1) {
143 return {
144 ...state,
145 phase: "capturing",
146 countdownValue: null,
147 flash: true,
148 }
149 }
150 return { ...state, countdownValue: (state.countdownValue - 1) as CountdownValue }
151
152 case "capturing":
153 if (event.type === "SHOT_SAVED") {
154 return { ...state, phase: "pause" }
155 }
156 if (event.type === "FLASH_DONE") {
157 return { ...state, flash: false }
158 }
159 return state
160
161 case "pause":
162 if (event.type === "PAUSE_DONE") {
163 return {
164 ...state,
165 phase: event.allFilled ? "complete" : "ready",
166 targetSlot: null,
167 }
168 }
169 if (event.type === "FLASH_DONE") {
170 return { ...state, flash: false }
171 }
172 return state
173
174 case "complete":
175 /* Retake hanya lewat slot tertentu; tanpa target tidak ada yang diganti. */
176 if (event.type === "SHOOT" && event.target !== undefined) {
177 return {
178 ...state,
179 phase: "countdown",
180 countdownValue: COUNTDOWN_FROM,
181 targetSlot: event.target,
182 }
183 }
184 return state
185
186 default:
187 return state
188 }
189}
190