001package com.studentgui.apppages;
002
003import java.awt.BorderLayout;
004import java.awt.Font;
005import java.awt.GridBagConstraints;
006import java.awt.GridBagLayout;
007import java.awt.Insets;
008import java.awt.event.ActionEvent;
009import java.awt.event.KeyEvent;
010import java.sql.SQLException;
011import java.time.LocalDate;
012import java.util.LinkedHashMap;
013import java.util.Map;
014
015import javax.swing.JButton;
016import javax.swing.JLabel;
017import javax.swing.JPanel;
018import javax.swing.JScrollPane;
019import javax.swing.SwingUtilities;
020
021import org.slf4j.Logger;
022import org.slf4j.LoggerFactory;
023
024import com.studentgui.uicomp.PhaseScoreField;
025
026/**
027 * iOS and iPadOS assistive technology proficiency assessment page.
028 *
029 * <p>Provides structured evaluation of iOS/iPadOS device usage skills across
030 * 41 competencies organized into 6 functional domains:</p>
031 *
032 * <ul>
033 *   <li><b>Phase 1 (P1_1–P1_9): Device Basics and VoiceOver Fundamentals</b>
034 *     <ul>
035 *       <li>Power management, VoiceOver activation/deactivation</li>
036 *       <li>Core gestures (tap, swipe, rotor) for icon navigation and interaction</li>
037 *       <li>Home screen management, document handling, keyboarding basics</li>
038 *       <li>Control Center, App Switcher, and system-level navigation</li>
039 *     </ul>
040 *   </li>
041 *   <li><b>Phase 2 (P2_1–P2_6): Word Processing and Document Creation</b>
042 *     <ul>
043 *       <li>Creating, editing, and saving text documents</li>
044 *       <li>Reading and navigating within documents using VoiceOver</li>
045 *       <li>Menu bar interaction, text/image copy-paste workflows</li>
046 *       <li>Proofreading and editing strategies with assistive technology</li>
047 *     </ul>
048 *   </li>
049 *   <li><b>Phase 3 (P3_1–P3_5): Spreadsheet and Data Visualization</b>
050 *     <ul>
051 *       <li>Spreadsheet concepts and terminology (rows, columns, cells, formulas)</li>
052 *       <li>Data entry, editing, and spreadsheet navigation with VoiceOver</li>
053 *       <li>Creating and interpreting charts/graphs from data</li>
054 *     </ul>
055 *   </li>
056 *   <li><b>Phase 4 (P4_1–P4_5): Presentation Software</b>
057 *     <ul>
058 *       <li>Creating and structuring presentations with accessible workflows</li>
059 *       <li>Editing slides, adding multimedia content (images, audio)</li>
060 *       <li>Presenting slides effectively using assistive technology</li>
061 *       <li>Sharing and exporting presentations</li>
062 *     </ul>
063 *   </li>
064 *   <li><b>Phase 5 (P5_1–P5_7): Digital Citizenship and Online Safety</b>
065 *     <ul>
066 *       <li>Acceptable Use Policies, digital citizenship principles</li>
067 *       <li>Online safety, privacy awareness, copyright/plagiarism concepts</li>
068 *       <li>Recognizing and responding to cyberbullying</li>
069 *     </ul>
070 *   </li>
071 *   <li><b>Phase 6 (P6_1–P6_11): Device Management and Connectivity</b>
072 *     <ul>
073 *       <li>App installation, updates, deletion, storage management</li>
074 *       <li>Accessibility settings configuration and customization</li>
075 *       <li>Screen Time controls, Parental Controls</li>
076 *       <li>Connectivity features: Bluetooth, Wi-Fi, AirDrop, Personal Hotspot</li>
077 *     </ul>
078 *   </li>
079 * </ul>
080 *
081 * <p><b>Data Management and Artifacts:</b></p>
082 * <ul>
083 *   <li>Scores captured via {@link PhaseScoreField} components (typically 0–4 integer range)</li>
084 *   <li>Persisted to normalized schema using {@link com.studentgui.apphelpers.Database#insertAssessmentResults}</li>
085 *   <li>JSON session export: {@code StudentDataFiles/<student>/Sessions/iOS/iOS-<sessionId>-<timestamp>.json}</li>
086 *   <li>Phase-grouped time-series PNG plots saved to {@code plots/} directory</li>
087 *   <li>Markdown and HTML reports generated with embedded plots and color-coded legends</li>
088 * </ul>
089 *
090 * <p>The shared {@link JLineGraph} visualizes recent session trends grouped by phase prefix
091 * to maintain chart readability. This page operates on static student/date parameters and
092 * does not implement listener interfaces for dynamic updates.</p>
093 *
094 * @see com.studentgui.apphelpers.Database
095 * @see JLineGraph
096 * @see PhaseScoreField
097 */
098public class IOS extends JPanel {
099    private static final Logger LOG = LoggerFactory.getLogger(IOS.class);
100    /** Mapping of iOS assessment part codes to their input components. */
101    private final Map<String, PhaseScoreField> inputs = new LinkedHashMap<>();
102
103    /** Selected student display name used for saves and plots (may be null). */
104    private final String studentNameParam;
105
106    /** Session date to associate with saved iOS progress entries. */
107    private final LocalDate dateParam;
108
109    /** Shared graph component for plotting recent iOS assessment sessions. */
110    private final JLineGraph graph;
111
112    /**
113     * Construct the iOS page for the given student and date.
114     *
115     * @param studentName selected student name (may be null)
116     * @param date session date to associate with saved progress
117     * @param graph shared graph used to visualize recent sessions
118     */
119    public IOS(String studentName, LocalDate date, JLineGraph graph) {
120    this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName;
121        this.dateParam = date;
122        this.graph = graph;
123        setLayout(new BorderLayout());
124
125    JPanel p = new JPanel(new GridBagLayout());
126    JPanel view = new JPanel(new BorderLayout());
127    view.add(p, BorderLayout.NORTH);
128    view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20));
129    JScrollPane scroll = new JScrollPane(view);
130    scroll.getAccessibleContext().setAccessibleName("iOS data entry scroll pane");
131        GridBagConstraints gbc = new GridBagConstraints(); gbc.insets=new Insets(2,2,2,2); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.anchor = GridBagConstraints.NORTHWEST; gbc.weightx = 1.0;
132
133    JLabel title = new JLabel("iOS / iPad OS Skills");
134    title.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 28));
135    title.getAccessibleContext().setAccessibleName("iOS Skills Title");
136    title.setHorizontalAlignment(JLabel.LEFT);
137    gbc.gridx=0; gbc.gridy=0; gbc.gridwidth=2; p.add(title, gbc);
138
139        String[][] parts = new String[][]{
140            {"P1_1","1.1 Turn Device On/Off"},{"P1_2","1.2 Turn VoiceOver On/Off"},{"P1_3","1.3 Gestures to Click Icons"},
141            {"P1_4","1.4 Home Screen Icons to Open Documents"},{"P1_5","1.5 Save Documents"},{"P1_6","1.6 Online Tools/Resources"},
142            {"P1_7","1.7 Keyboarding"},{"P1_8","1.8 Use Different Elements"},{"P1_9","1.9 Control Center, App Switcher..."},
143            {"P2_1","2.1 Write, edit save"},{"P2_2","2.2 Read, Navigate Document"},{"P2_3","2.3 Use Menubar"},
144            {"P2_4","2.4 Highlight text, copy and paste text"},{"P2_5","2.5 Copy and paste images"},{"P2_6","2.6 Proofread and edit"},
145            {"P3_1","3.1 Describe Spreadsheet"},{"P3_2","3.2 Explain terms and concepts"},{"P3_3","3.3 Enter/Edit data"},
146            {"P3_4","3.4 Navigate Spreadsheet"},{"P3_5","3.5 Create Graphs"},{"P4_1","4.1 Create Presentation"},
147            {"P4_2","4.2 Edit Slides"},{"P4_3","4.3 Add Images"},{"P4_4","4.4 Present Slides"},{"P4_5","4.5 Share Presentation"},
148            {"P5_1","5.1 Acceptable Use Policy"},{"P5_2","5.2 Digital Citizenship"},{"P5_3","5.3 Online Safety"},
149            {"P5_4","5.4 Copyright"},{"P5_5","5.5 Plagiarism"},{"P5_6","5.6 Privacy"},{"P5_7","5.7 Cyberbullying"},
150            {"P6_1","6.1 Install Apps"},{"P6_2","6.2 Update Apps"},{"P6_3","6.3Delete Apps"},{"P6_4","6.4 Manage Storage"},
151            {"P6_5","6.5 Accessibility Settings"},{"P6_6","6.6 Screen Time"},{"P6_7","6.7 Parental Controls"},{"P6_8","6.8 Bluetooth"},
152            {"P6_9","6.9 Wi-Fi"},{"P6_10","6.10 AirDrop"},{"P6_11","6.11 Hotspot"}
153        };
154
155    java.awt.Font labelFont = com.studentgui.uicomp.PhaseScoreField.getLabelFont();
156    String[] labels = java.util.Arrays.stream(parts).map(x->x[1]).toArray(String[]::new);
157        int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(labelFont, labels);
158        com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(360, Math.max(200, maxPx + 50)));
159    int row = 1;
160        for (String[] part : parts) {
161            gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 2;
162            PhaseScoreField tf = new PhaseScoreField(part[1], 0);
163            tf.setToolTipText("Enter whole number score for " + part[1]);
164            tf.getAccessibleContext().setAccessibleName(part[1]);
165            tf.setName("ios_" + part[0]);
166            p.add(tf, gbc);
167            inputs.put(part[0], tf);
168            row++;
169        }
170    // Place Save and Open Latest side-by-side (Braille style)
171    gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.WEST;
172    JButton save = new JButton("Save iOS Data");
173    save.setPreferredSize(new java.awt.Dimension(0, 32));
174    save.addActionListener((ActionEvent e) -> { save(); plot(); });
175    save.setToolTipText("Save iOS assessment for selected student");
176    save.setMnemonic(KeyEvent.VK_S);
177    save.getAccessibleContext().setAccessibleName("Save iOS Data");
178    p.add(save, gbc);
179
180    gbc.gridx = 1;
181    JButton openLatest = new JButton("Open Latest Plot");
182    openLatest.setPreferredSize(new java.awt.Dimension(0, 32));
183    openLatest.addActionListener((ActionEvent e) -> {
184        java.nio.file.Path pth = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "iOS");
185        if (pth == null) {
186            com.studentgui.apphelpers.UiNotifier.show("No iOS plot found for student");
187        } else {
188            try {
189                java.awt.Desktop.getDesktop().open(pth.toFile());
190            } catch (java.io.IOException | UnsupportedOperationException | SecurityException ex) {
191                com.studentgui.apphelpers.UiNotifier.show("Unable to open plot: " + pth.getFileName().toString());
192            }
193        }
194    });
195    p.add(openLatest, gbc);
196
197    // consume remaining columns (if any) so layout stays compact and buttons are not clipped
198    gbc.gridx = 2; gbc.gridwidth = GridBagConstraints.REMAINDER; gbc.anchor = GridBagConstraints.WEST;
199    p.add(new JPanel(), gbc);
200    row++;
201
202        add(scroll, BorderLayout.CENTER);
203        add(graph, BorderLayout.SOUTH);
204
205        SwingUtilities.invokeLater(()->{
206            view.setPreferredSize(view.getPreferredSize());
207            scroll.getViewport().setViewPosition(new java.awt.Point(0,0));
208            revalidate();
209        });
210
211        SwingUtilities.invokeLater(() -> {
212            for (var f: inputs.values()) LOG.debug("IOS field {} labelWidth={} spinnerX={} gap={}", f.getLabel(), f.getLabelWrapWidth(), f.getSpinnerX(), f.getActualGap());
213        });
214
215        com.studentgui.apphelpers.Helpers.createFolderHierarchy();
216        initParts();
217    }
218
219    /**
220     * Ensure the iOS progress-type and part rows exist in the normalized
221     * database schema.
222     */
223    private void initParts() {
224        try {
225            int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("iOS");
226                java.util.Set<String> keys = inputs.keySet();
227            String[] codes = new String[keys.size()];
228            int idx = 0;
229            for (String k : keys) {
230                codes[idx++] = k;
231            }
232            com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes);
233        } catch (SQLException ex) {
234            LOG.error("Error ensuring iOS assessment parts", ex);
235        }
236    }
237
238    /**
239     * Validate inputs and persist them as a new progress session for the
240     * selected student.
241     */
242    private void save() {
243        if (this.studentNameParam == null || this.studentNameParam.trim().isEmpty()) {
244            javax.swing.JOptionPane.showMessageDialog(this, "Please select a student before saving iOS data.", "Missing student", javax.swing.JOptionPane.WARNING_MESSAGE);
245            return;
246        }
247
248        try {
249            int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(this.studentNameParam);
250            int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("iOS");
251            int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, this.dateParam);
252            java.util.Set<String> keys = inputs.keySet();
253            String[] codes = new String[keys.size()]; int idx = 0; for (String k: keys) codes[idx++] = k;
254            int[] scores = new int[codes.length];
255            for (int i = 0; i < codes.length; i++) {
256                scores[i] = inputs.get(codes[i]).getValue();
257            }
258            com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores);
259            LOG.info("iOS data saved for {}", this.studentNameParam);
260            com.studentgui.apphelpers.UiNotifier.show("iOS data saved.");
261            com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores);
262            java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "iOS", payload, sessionId);
263            if (jsonOut == null) LOG.warn("Unable to save iOS session JSON for sessionId={}", sessionId);
264            try {
265                java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam);
266                java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam);
267                java.nio.file.Files.createDirectories(plotsOut);
268                java.nio.file.Files.createDirectories(reportsOut);
269                java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE;
270                String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString();
271                String baseName = "iOS-" + sessionId + "-" + dateStr;
272
273                com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "iOS", Integer.MAX_VALUE);
274                    java.util.Map<String, java.nio.file.Path> groups = null;
275                        String[] labels = new String[codes.length];
276                        for (int i = 0; i < codes.length; i++) {
277                            labels[i] = inputs.get(codes[i]).getLabel();
278                        }
279                // codes already built above as 'codes'
280                if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) {
281                    graph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels);
282                    groups = graph.saveGroupedCharts(plotsOut, baseName, 1000, 240);
283                    java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1);
284                    dateStr = headerDate.format(df);
285                } else {
286                    java.util.List<java.util.List<Integer>> rowsList = new java.util.ArrayList<>();
287                        java.util.List<Integer> latest = new java.util.ArrayList<>();
288                        for (String c : codes) {
289                            latest.add(inputs.get(c).getValue());
290                        }
291                        rowsList.add(latest);
292                    graph.updateWithGroupedData(rowsList, codes);
293                    groups = graph.saveGroupedCharts(plotsOut, baseName, 1000, 240);
294                }
295
296                if (groups == null) {
297                    groups = new java.util.LinkedHashMap<>();
298                }
299                StringBuilder md = new StringBuilder();
300                md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n");
301                for (java.util.Map.Entry<String, java.nio.file.Path> e : groups.entrySet()) {
302                    md.append("## ").append(e.getKey()).append("\n\n");
303                    md.append("![](../plots/").append(e.getValue().getFileName().toString()).append(")\n\n");
304                }
305                java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md");
306                java.nio.file.Files.writeString(mdFile, md.toString(), java.nio.charset.StandardCharsets.UTF_8);
307
308                try {
309                    String[] palette = JLineGraph.PALETTE_HEX;
310                    java.util.LinkedHashMap<String, java.util.List<Integer>> groupsIdx = new java.util.LinkedHashMap<>();
311                    for (int i = 0; i < codes.length; i++) {
312                        String code = codes[i];
313                        String grp = code != null && code.contains("_") ? code.split("_")[0] : code;
314                        groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i);
315                    }
316                    StringBuilder html = new StringBuilder();
317                    html.append("<!doctype html><html><head><meta charset=\"utf-8\"><title>");
318                    html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append("</title>");
319                    html.append("<style>body{font-family:sans-serif;margin:20px;} img{max-width:100%;height:auto;border:1px solid #ccc;margin-bottom:8px;} .legend{max-height:160px;overflow:auto;border:1px solid #ddd;padding:8px;margin-bottom:24px;} .legend-item{display:flex;align-items:center;gap:8px;padding:4px 0;} .swatch{width:18px;height:12px;border:1px solid #333;display:inline-block}</style>");
320                    html.append("</head><body>");
321                    html.append("<h1>").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("</h1>");
322                    for (java.util.Map.Entry<String, java.nio.file.Path> e2 : groups.entrySet()) {
323                        String grp = e2.getKey();
324                        String imgName = e2.getValue().getFileName().toString();
325                        html.append("<h2>").append(grp).append("</h2>");
326                        html.append("<div class=\"plot\"><img src=\"../plots/").append(imgName).append("\" alt=\"").append(grp).append("\"></div>");
327                        java.util.List<Integer> idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>());
328                        html.append("<div class=\"legend\">");
329                        for (int s = 0; s < idxs.size(); s++) {
330                            int itemIdx = idxs.get(s);
331                            String code = codes[itemIdx];
332                            String human = labels[itemIdx];
333                            String seriesName = code + " - " + human;
334                            String color = palette[s % palette.length];
335                            html.append("<div class=\"legend-item\">");
336                            html.append("<span class=\"swatch\" style=\"background:");
337                            html.append(color);
338                            html.append(";\"></span>");
339                            html.append("<div>");
340                            html.append(seriesName);
341                            html.append("</div></div>");
342                        }
343                        html.append("</div>");
344                    }
345                    html.append("</body></html>");
346                    java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html");
347                    java.nio.file.Files.writeString(htmlFile, html.toString(), java.nio.charset.StandardCharsets.UTF_8);
348                    LOG.info("Wrote iOS HTML session report {}", htmlFile);
349                } catch (java.io.IOException ioex) {
350                    LOG.warn("Unable to write iOS HTML report: {}", ioex.toString());
351                }
352            } catch (java.io.IOException ioe) {
353                LOG.warn("Unable to save iOS per-phase plots or markdown report: {}", ioe.toString());
354            }
355        } catch (SQLException ex) {
356            LOG.error("Error saving iOS data", ex);
357            javax.swing.JOptionPane.showMessageDialog(this, "Database error saving iOS data: " + ex.getMessage(), "Database error", javax.swing.JOptionPane.ERROR_MESSAGE);
358        }
359    }
360
361    /**
362     * Fetch recent iOS assessment sessions and update the shared graph view.
363     */
364    private void plot() {
365        LOG.info("Plot requested for {}", studentNameParam);
366        try {
367            java.util.List<java.util.List<Integer>> data = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults(this.studentNameParam, "iOS", 20);
368            if (data != null && !data.isEmpty()) {
369                // Build codes array in the same order as inputs were created
370                String[] codes = new String[inputs.size()];
371                int idx = 0; for (String k: inputs.keySet()) codes[idx++] = k;
372                graph.updateWithGroupedData(data, codes);
373                // Save static PNG
374                if (this.studentNameParam != null && !this.studentNameParam.trim().isEmpty()) {
375                    try {
376                        java.nio.file.Path out = com.studentgui.apphelpers.Helpers.APP_HOME.resolve("StudentDataFiles").resolve(com.studentgui.apphelpers.Helpers.safeName(this.studentNameParam)).resolve("plots");
377                        java.nio.file.Files.createDirectories(out);
378                        java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE;
379                        String dateStr = (this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString());
380                        java.nio.file.Path file = out.resolve("iOS-" + dateStr + ".png");
381                        graph.saveChart(file, 800, 400);
382                        LOG.info("Saved iOS plot to {}", file);
383                        // Do not auto-open the plot here; only save it. Opening is handled
384                        // by submit/save handlers or the Open Latest button.
385                        com.studentgui.apphelpers.UiNotifier.show("iOS plot saved to " + file.toString());
386                    } catch (java.io.IOException ex) { LOG.warn("Unable to save iOS plot image: {}", ex.toString()); }
387                }
388            } else {
389                LOG.info("No iOS data to plot for {}", studentNameParam);
390            }
391        } catch (SQLException ex) {
392            LOG.error("Error fetching iOS data for plot", ex);
393        }
394    }
395}