001package com.studentgui.apppages;
002
003import java.awt.BorderLayout;
004import java.awt.Dimension;
005import java.awt.Font;
006import java.awt.GridBagConstraints;
007import java.awt.GridBagLayout;
008import java.awt.Insets;
009import java.awt.event.ActionEvent;
010import java.awt.event.KeyEvent;
011import java.sql.SQLException;
012import java.time.LocalDate;
013
014import javax.swing.JButton;
015import javax.swing.JLabel;
016import javax.swing.JOptionPane;
017import javax.swing.JPanel;
018import javax.swing.JScrollPane;
019import javax.swing.JTextField;
020import javax.swing.SwingUtilities;
021
022import org.slf4j.Logger;
023import org.slf4j.LoggerFactory;
024
025/**
026 * Touch-typing and keyboarding skills assessment page.
027 *
028 * <p>Unlike other assessment pages that use phase-score grids, this page captures
029 * structured performance metrics for keyboarding practice sessions:</p>
030 *
031 * <ul>
032 *   <li><b>Program:</b> Name of the typing curriculum or software (e.g., TypingClub, KeyBlaze, Braille2000)</li>
033 *   <li><b>Topic:</b> Specific lesson, module, or exercise completed (e.g., "Home Row Mastery", "Lesson 12")</li>
034 *   <li><b>Speed (WPM):</b> Words per minute achieved during the timed exercise</li>
035 *   <li><b>Accuracy (%):</b> Percentage of characters typed correctly</li>
036 * </ul>
037 *
038 * <p><b>Data Persistence:</b></p>
039 * <ul>
040 *   <li>Values persisted via {@link com.studentgui.apphelpers.Database#insertKeyboardingResult} to the {@code KeyboardingResult} table</li>
041 *   <li>JSON export: {@code StudentDataFiles/<student>/Sessions/Keyboarding/Keyboarding-<sessionId>-<timestamp>.json}</li>
042 *   <li>Metadata-only reports (no plots): Markdown and HTML files in {@code reports/} with session details</li>
043 * </ul>
044 *
045 * <p><b>Validation and Error Handling:</b></p>
046 * <ul>
047 *   <li>Speed and Accuracy fields must contain whole numbers (non-negative integers)</li>
048 *   <li>Empty speed/accuracy fields default to 0 for leniency</li>
049 *   <li>Invalid input triggers error dialogs and field focus for correction</li>
050 * </ul>
051 *
052 * <p>The shared {@link JLineGraph} component is present for UI consistency but is not populated
053 * with keyboarding data (keyboarding does not use assessment parts). Implements
054 * {@link com.studentgui.app.DateChangeListener} and {@link com.studentgui.app.StudentChangeListener}
055 * for title updates when global selections change.</p>
056 *
057 * @see com.studentgui.apphelpers.Database#insertKeyboardingResult
058 * @see com.studentgui.apphelpers.dto.KeyboardingPayload
059 */
060public class Keyboarding extends JPanel implements com.studentgui.app.DateChangeListener, com.studentgui.app.StudentChangeListener {
061    private static final Logger LOG = LoggerFactory.getLogger(Keyboarding.class);
062    /** Text field for the program or curriculum name. */
063    private final JTextField programField, topicField, speedField, accuracyField;
064
065    /** Shared graph component (present but not used for keyboarding plotting). */
066    private final JLineGraph lineGraph;
067
068    /** Selected student's display name for saves/refreshes (may be null). */
069    private String studentNameParam;
070    /** Page header label. */
071    private JLabel titleLabel;
072    /** Base title text for the Keyboarding page; date suffix appended in UI. */
073    private final String baseTitle = "Keyboarding Skills";
074
075    /** Session date associated with persisted keyboarding results. */
076    private LocalDate dateParam;
077
078    /**
079     * Construct the Keyboarding page for a specific student and session date.
080     *
081     * @param studentName selected student's display name (may be null)
082     * @param date session date used for persisted results
083     * @param lineGraph shared graph component (unused for keyboarding results)
084     */
085    public Keyboarding(String studentName, LocalDate date, JLineGraph lineGraph) {
086    this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName;
087        this.dateParam = date;
088        this.lineGraph = lineGraph;
089        setLayout(new BorderLayout());
090
091    JPanel p = new JPanel(new GridBagLayout());
092    JPanel view = new JPanel(new BorderLayout());
093    view.add(p, BorderLayout.NORTH);
094    view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20));
095    JScrollPane scroll = new JScrollPane(view);
096    scroll.getAccessibleContext().setAccessibleName("Keyboarding data entry scroll pane");
097    p.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20));
098        GridBagConstraints gbc = new GridBagConstraints(); gbc.insets=new Insets(2,2,2,2); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.anchor = GridBagConstraints.NORTHWEST;
099    this.titleLabel = new JLabel(baseTitle, JLabel.LEFT);
100    this.titleLabel.setFont(this.titleLabel.getFont().deriveFont(Font.BOLD,28f));
101    this.titleLabel.getAccessibleContext().setAccessibleName("Keyboarding Skills Title");
102    gbc.gridx=0; gbc.gridy=0; gbc.gridwidth=2; p.add(this.titleLabel, gbc);
103
104    gbc.gridwidth=1;
105    // Normalize label width to the PhaseScoreField global width so inputs align
106    int globalLabel = com.studentgui.uicomp.PhaseScoreField.getGlobalLabelWidth();
107    gbc.gridy=1; gbc.gridx=0; JLabel programLabel = new JLabel("Program:"); programLabel.setPreferredSize(new Dimension(globalLabel, programLabel.getPreferredSize().height)); p.add(programLabel, gbc); gbc.gridx=1; programField = new JTextField(); programField.setPreferredSize(new Dimension(300,24)); programField.setToolTipText("Name of the program or curriculum"); programField.getAccessibleContext().setAccessibleName("Program"); p.add(programField, gbc); programLabel.setLabelFor(programField);
108    gbc.gridy=2; gbc.gridx=0; JLabel topicLabel = new JLabel("Topic:"); topicLabel.setPreferredSize(new Dimension(globalLabel, topicLabel.getPreferredSize().height)); p.add(topicLabel, gbc); gbc.gridx=1; topicField = new JTextField(); topicField.setPreferredSize(new Dimension(300,24)); topicField.setToolTipText("Topic or lesson name"); topicField.getAccessibleContext().setAccessibleName("Topic"); p.add(topicField, gbc); topicLabel.setLabelFor(topicField);
109    gbc.gridy=3; gbc.gridx=0; JLabel speedLabel = new JLabel("Speed (WPM):"); speedLabel.setPreferredSize(new Dimension(globalLabel, speedLabel.getPreferredSize().height)); p.add(speedLabel, gbc); gbc.gridx=1; speedField = new JTextField("0"); speedField.setPreferredSize(new Dimension(100,24)); speedField.setToolTipText("Words per minute"); speedField.getAccessibleContext().setAccessibleName("Speed (WPM)"); p.add(speedField, gbc); speedLabel.setLabelFor(speedField);
110    gbc.gridy=4; gbc.gridx=0; JLabel accuracyLabel = new JLabel("Accuracy (%):"); accuracyLabel.setPreferredSize(new Dimension(globalLabel, accuracyLabel.getPreferredSize().height)); p.add(accuracyLabel, gbc); gbc.gridx=1; accuracyField = new JTextField("0"); accuracyField.setPreferredSize(new Dimension(100,24)); accuracyField.setToolTipText("Accuracy percentage"); accuracyField.getAccessibleContext().setAccessibleName("Accuracy (%)"); p.add(accuracyField, gbc); accuracyLabel.setLabelFor(accuracyField);
111
112    gbc.gridy=5; gbc.gridx=0; gbc.gridwidth=GridBagConstraints.REMAINDER;
113    JButton submit = new JButton("Submit Data");
114    submit.setPreferredSize(new java.awt.Dimension(0, 32));
115    submit.addActionListener((ActionEvent e)-> { submitData(); refreshGraph(); });
116    submit.setToolTipText("Save keyboarding result for selected student");
117    submit.setMnemonic(KeyEvent.VK_S);
118    submit.getAccessibleContext().setAccessibleName("Submit Keyboarding Data");
119    p.add(submit, gbc);
120    gbc.gridwidth = 1;
121    // Removed separate Refresh Graph button; Submit Data now triggers refreshGraph
122
123    add(scroll, BorderLayout.CENTER);
124    add(this.lineGraph, BorderLayout.SOUTH);
125
126    SwingUtilities.invokeLater(()->{ p.setPreferredSize(p.getPreferredSize()); updateTitleDate(); revalidate(); });
127
128        com.studentgui.apphelpers.Helpers.createFolderHierarchy();
129        initDatabase();
130        refreshGraph();
131    }
132
133    /**
134     * Ensure the keyboarding progress type exists in the canonical schema.
135     */
136    private void initDatabase() {
137        try {
138            com.studentgui.apphelpers.Database.getOrCreateProgressType("Keyboarding");
139        } catch (SQLException ex) {
140            LOG.error("Error ensuring Keyboarding progress type", ex);
141        }
142    }
143
144    /**
145     * Validate keyboarding inputs (speed and accuracy as integers) and
146     * persist a keyboarding result record for the selected student.
147     */
148    private void submitData() {
149        if (this.studentNameParam == null || this.studentNameParam.trim().isEmpty()) {
150            JOptionPane.showMessageDialog(this, "Please select a student before saving keyboarding data.", "Missing student", JOptionPane.WARNING_MESSAGE);
151            return;
152        }
153
154        try {
155            int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(this.studentNameParam);
156            int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Keyboarding");
157            int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, this.dateParam);
158
159            String program = programField.getText().trim();
160            String topic = topicField.getText().trim();
161            int speed;
162            int accuracy;
163            try {
164                String sp = speedField.getText().trim(); speed = sp.isEmpty() ? 0 : Integer.parseInt(sp);
165            } catch (NumberFormatException nfe) {
166                JOptionPane.showMessageDialog(this, "Please enter a whole number for Speed (WPM)", "Invalid input", JOptionPane.ERROR_MESSAGE);
167                speedField.requestFocusInWindow();
168                return;
169            }
170            try {
171                String ac = accuracyField.getText().trim(); accuracy = ac.isEmpty() ? 0 : Integer.parseInt(ac);
172            } catch (NumberFormatException nfe) {
173                JOptionPane.showMessageDialog(this, "Please enter a whole number for Accuracy (%)", "Invalid input", JOptionPane.ERROR_MESSAGE);
174                accuracyField.requestFocusInWindow();
175                return;
176            }
177
178            com.studentgui.apphelpers.Database.insertKeyboardingResult(sessionId, program, topic, speed, accuracy);
179            LOG.info("Keyboarding data saved for {}", this.studentNameParam);
180            com.studentgui.apphelpers.UiNotifier.show("Keyboarding data saved.");
181            com.studentgui.apphelpers.dto.KeyboardingPayload payload = new com.studentgui.apphelpers.dto.KeyboardingPayload(sessionId, program, topic, speed, accuracy);
182            java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "Keyboarding", payload, sessionId);
183            if (jsonOut == null) {
184                LOG.warn("Unable to save Keyboarding session JSON for sessionId={}", sessionId);
185            }
186            try {
187                java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam);
188                java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam);
189                java.nio.file.Files.createDirectories(plotsOut);
190                java.nio.file.Files.createDirectories(reportsOut);
191                java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE;
192                String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString();
193                String baseName = "Keyboarding-" + sessionId + "-" + dateStr;
194
195                // Keyboarding doesn't have grouped codes; produce a small HTML/MD with metadata
196                StringBuilder md = new StringBuilder();
197                md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n");
198                md.append("**Program:** ").append(program == null || program.isEmpty() ? "(none)" : program).append("  \n\n");
199                md.append("**Topic:** ").append(topic == null || topic.isEmpty() ? "(none)" : topic).append("  \n\n");
200                md.append("**Speed (WPM):** ").append(String.valueOf(speed)).append("  \n\n");
201                md.append("**Accuracy (%):** ").append(String.valueOf(accuracy)).append("  \n\n");
202                java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md");
203                java.nio.file.Files.writeString(mdFile, md.toString(), java.nio.charset.StandardCharsets.UTF_8);
204
205                try {
206                    StringBuilder html = new StringBuilder();
207                    html.append("<!doctype html><html><head><meta charset=\"utf-8\"><title>");
208                    html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append("</title>");
209                    html.append("<style>body{font-family:sans-serif;margin:20px;} .meta{margin-bottom:12px;} .swatch{width:18px;height:12px;border:1px solid #333;display:inline-block;vertical-align:middle;margin-right:8px;}</style>");
210                    html.append("</head><body>");
211                    html.append("<h1>").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("</h1>");
212                    html.append("<div class=\"meta\">\n");
213                    html.append("<p><strong>Program:</strong> ").append(program == null || program.isEmpty() ? "(none)" : program).append("</p>");
214                    html.append("<p><strong>Topic:</strong> ").append(topic == null || topic.isEmpty() ? "(none)" : topic).append("</p>");
215                    html.append("<p><strong>Speed (WPM):</strong> ").append(String.valueOf(speed)).append("</p>");
216                    html.append("<p><strong>Accuracy (%):</strong> ").append(String.valueOf(accuracy)).append("</p>");
217                    html.append("</div>");
218                    html.append("</body></html>");
219                    java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html");
220                    java.nio.file.Files.writeString(htmlFile, html.toString(), java.nio.charset.StandardCharsets.UTF_8);
221                    LOG.info("Wrote Keyboarding session report {}", htmlFile);
222                } catch (java.io.IOException ioex) {
223                    LOG.warn("Unable to write Keyboarding HTML report: {}", ioex.toString());
224                }
225            } catch (java.io.IOException ioe) {
226                LOG.warn("Unable to save Keyboarding report: {}", ioe.toString());
227            }
228        } catch (SQLException ex) {
229            LOG.error("DB error saving keyboarding data", ex);
230            JOptionPane.showMessageDialog(this, "Database error saving keyboarding data: " + ex.getMessage(), "Database error", JOptionPane.ERROR_MESSAGE);
231        }
232    }
233
234    /**
235     * Refresh the keyboarding visualization. Currently keyboarding results are
236     * stored in a separate table and this method logs the request.
237     */
238    private void refreshGraph() {
239        LOG.info("Keyboarding refresh requested for {}", studentNameParam);
240    }
241
242    @Override
243    /**
244     * Update the displayed date for the Keyboarding page and refresh content.
245     *
246     * Stores the provided `dateParam` and schedules a refresh on the Swing EDT
247     * so the UI (graph and title) reflects the new date.
248     *
249     * @param newDate the date to display (may be null to use current date)
250     */
251
252    public void dateChanged(LocalDate newDate) {
253        this.dateParam = newDate;
254        SwingUtilities.invokeLater(() -> {
255            refreshGraph();
256            updateTitleDate();
257        });
258    }
259
260    @Override
261    /**
262     * Update the selected student for the Keyboarding page and refresh content.
263     *
264     * Sets `studentNameParam` and schedules a UI update on the Swing EDT to
265     * reload page data and update the title.
266     *
267     * @param newStudent student identifier (name or id) to display; may be null
268     */
269
270    public void studentChanged(String newStudent) {
271        this.studentNameParam = newStudent;
272        SwingUtilities.invokeLater(() -> {
273            refreshGraph();
274            updateTitleDate();
275        });
276    }
277
278    private void updateTitleDate() {
279        try {
280            String dateStr = this.dateParam != null ? this.dateParam.toString() : java.time.LocalDate.now().toString();
281            this.titleLabel.setText(baseTitle + " - " + dateStr);
282        } catch (Exception ex) {
283            this.titleLabel.setText(baseTitle);
284        }
285    }
286}