001package jmri.jmrit.operations.trains;
002
003import java.awt.*;
004import java.io.*;
005import java.nio.charset.StandardCharsets;
006import java.util.ArrayList;
007import java.util.List;
008
009import javax.print.attribute.standard.Sides;
010import javax.swing.ImageIcon;
011import javax.swing.JLabel;
012
013import org.slf4j.Logger;
014import org.slf4j.LoggerFactory;
015
016import jmri.jmrit.operations.setup.Setup;
017import jmri.jmrit.operations.trains.trainbuilder.TrainCommon;
018import jmri.util.davidflanagan.CompatibleHardcopyWriter;
019
020/**
021 * Used for printing train Manifests and switch lists. Text can have color, bold
022 * and italic control characters.
023 *
024 * @author Daniel Boudreau (C) 2025, 2026
025 */
026public class TrainPrintManifest extends TrainCommon {
027
028    protected static final char SPACE_CHAR = ' ';
029    private static boolean isPrintingStyleDone = false;
030    private static boolean isPrintingColor = false;
031    private static boolean isTextSizeDone = false;
032    private static Color color;
033    private static int _fontSize;
034
035    /**
036     * Print or preview a train Manifest or switch list.
037     *
038     * @param file          File to be printed or previewed
039     * @param name          Title of document
040     * @param isPreview     true if preview
041     * @param fontName      optional font to use when printing document
042     * @param logoURL       optional pathname for logo
043     * @param printerName   optional default printer name
044     * @param orientation   Setup.LANDSCAPE, Setup.PORTRAIT, or Setup.HANDHELD
045     * @param fontSize      font size
046     * @param isPrintHeader when true print page header
047     * @param sides         two sides long or short can be null
048     */
049    public static void printReport(File file, String name, boolean isPreview, String fontName, String logoURL,
050            String printerName, String orientation, int fontSize, boolean isPrintHeader, Sides sides) {
051
052        double leftmargin = .5;
053        double rightmargin = .5;
054        double topmargin = .5;
055        double bottommargin = .5;
056
057        // get hand held or half page dimensions in DPI
058        Dimension pageSize = getFullPageSizeDPI(orientation);
059
060        if (orientation.equals(Setup.RECEIPT)) {
061            leftmargin = .2;
062            rightmargin = .2;
063        }
064
065        try (CompatibleHardcopyWriter writer = new CompatibleHardcopyWriter(new Frame(), name, fontSize, leftmargin,
066                rightmargin, topmargin, bottommargin, isPreview, printerName, orientation.equals(Setup.LANDSCAPE),
067                isPrintHeader, sides, pageSize);
068                BufferedReader in = new BufferedReader(new InputStreamReader(
069                        new FileInputStream(file), StandardCharsets.UTF_8));) {
070
071            // set font
072            if (!fontName.isEmpty()) {
073                writer.setFontName(fontName);
074            }
075
076            _fontSize = fontSize;
077
078            if (logoURL != null && !logoURL.equals(Setup.NONE)) {
079                ImageIcon icon = new ImageIcon(logoURL);
080                if (icon.getIconWidth() == -1) {
081                    log.error("Logo not found: {}", logoURL);
082                } else {
083                    writer.write(icon.getImage(), new JLabel(icon));
084                }
085            }
086
087            List<String> lines = new ArrayList<>();
088            String line;
089            while (true) {
090                line = in.readLine();
091                if (line == null) {
092                    if (isPreview) {
093                        // need to do this in case the input file was empty to create preview
094                        writer.write(" ");
095                    }
096                    break;
097                }
098                lines.add(line);
099                if (line.isBlank()) {
100                    print(writer, lines, false);
101                }
102            }
103            print(writer, lines, true); // last block
104        } catch (FileNotFoundException e) {
105            log.error("Build file doesn't exist", e);
106        } catch (CompatibleHardcopyWriter.PrintCanceledException ex) {
107            log.debug("Print canceled");
108        } catch (IOException e) {
109            log.warn("Exception printing: {}", e.getLocalizedMessage());
110        }
111    }
112
113    // this routine checks to see if the text between line spaces will fit on the page
114    private static void print(CompatibleHardcopyWriter writer, List<String> lines, boolean lastBlock)
115            throws IOException {
116        int lineSize = getNumberOfLines(writer, lines);
117        if (Setup.isPrintNoPageBreaksEnabled() &&
118                writer.getCurrentLineNumber() != 0 &&
119                writer.getLinesPerPage() - writer.getCurrentLineNumber() < (lastBlock ? lineSize : lineSize - 1)) {
120            writer.pageBreak();
121        }
122        // check for exact page break
123        if (writer.getLinesPerPage() - writer.getCurrentLineNumber() == lineSize - 1) {
124            // eliminate blank line after page break
125            String s = lines.get(lines.size() - 1);
126            if (s.isBlank()) {
127                lines.remove(lines.size() - 1);
128            }
129        }
130        // use line feed for all lines?
131        if (lastBlock && writer.getLinesPerPage() - writer.getCurrentLineNumber() < lineSize) {
132            lastBlock = false; // yes
133        }
134
135        isPrintingColor = false;
136        color = null;
137
138        for (String line : lines) {
139            // determine if there's a line separator
140            if (printHorizontalLineSeparator(writer, line)) {
141                color = null;
142                continue;
143            }
144
145            // font size change?
146            line = setFontSize(writer, line);
147
148            // bold or italic text?
149            line = printStyle(writer, line);
150
151            // color text without bold or italic text?
152            line = printColor(writer, line);
153
154            line = printVerticalLineSeparator(writer, line);
155
156            // color can be null
157            writer.write(color, line);
158
159            // no line feed if last line of file, eliminates blank page
160            if (!lastBlock || writer.getCurrentLineNumber() < writer.getLinesPerPage() - 1) {
161                writer.write(NEW_LINE);
162            }
163
164            // done text size change?
165            if (isTextSizeDone) {
166                writer.setFont(null, null, _fontSize);
167                isTextSizeDone = false;
168            }
169
170            // done bold or italic text?
171            if (isPrintingStyleDone) {
172                writer.setFontStyle(Font.PLAIN);
173                isPrintingStyleDone = false;
174            }
175        }
176        lines.clear();
177    }
178
179    /*
180     * When determining the number of lines to print, we need to ignore any
181     * horizontal lines.
182     */
183    private static int getNumberOfLines(CompatibleHardcopyWriter writer, List<String> lines) {
184        int numberLines = lines.size();
185        for (String line : lines) {
186            if (isHorizontalLineSpearator(writer, line)) {
187                numberLines--;
188            }
189        }
190        return numberLines;
191    }
192
193    /*
194     * Returns true if horizontal line was printed
195     */
196    private static boolean printHorizontalLineSeparator(CompatibleHardcopyWriter writer, String line) {
197        boolean horizontalLineSeparatorFound = isHorizontalLineSpearator(writer, line);
198        if (horizontalLineSeparatorFound) {
199            int lineOffset = Setup.getHorizontalLineAdjustment();
200            float vStart = writer.getCurrentVPos() + lineOffset;
201            float hEnd = writer.getPrintablePagesizePoints().width;
202            writer.writeLine(vStart, 0, vStart, hEnd);
203        }
204        return horizontalLineSeparatorFound;
205    }
206
207    /*
208     * Determines if horizontal line. Requires the number of horizontal line
209     * characters equal to the page width and no other characters in the line.
210     * The smallest horizontal line is when the 2.25 wide paper is selected and
211     * largest font 18. About 12 horizontal line characters.
212     */
213    private static boolean isHorizontalLineSpearator(CompatibleHardcopyWriter writer, String line) {
214        int count = 0;
215        for (char c : line.toCharArray()) {
216            if (c == HORIZONTAL_LINE_CHAR) {
217                count++;
218            } else {
219                count = 0;
220                break; // all characters need to be horizontal char
221            }
222        }
223        // one less character most likely not necessary
224        return count >= writer.getCharactersPerLine() - 1;
225    }
226
227    private static String printVerticalLineSeparator(CompatibleHardcopyWriter writer, String line) {
228        if (line.contains(Character.toString(VERTICAL_LINE_CHAR))) {
229            // make a frame (two column format)
230            int lineOffset = Setup.getHorizontalLineAdjustment();
231            float vStart = writer.getCurrentVPos() + lineOffset;
232            float hEnd = writer.getPrintablePagesizePoints().width;
233            writer.writeLine(vStart, 0, vStart + writer.getLineHeight(), 0);
234            writer.writeLine(vStart, hEnd / 2, vStart + writer.getLineHeight(), hEnd / 2);
235            writer.writeLine(vStart, hEnd, vStart + writer.getLineHeight(), hEnd);
236            line = line.replace(VERTICAL_LINE_CHAR, SPACE_CHAR);
237        }
238        return line;
239    }
240
241    private static String printStyle(CompatibleHardcopyWriter writer, String line) throws IOException {
242        // If monospaced font, it is possible to style or color a subset of words in the line.
243        if (writer.isMonospaced() &&
244                (line.contains(TEXT_BOLD) ||
245                        line.contains(TEXT_BOLD_END))) {
246            line = printingStyleWords(writer, line, TEXT_BOLD, TEXT_BOLD_END, Font.BOLD);
247        } else if (writer.isMonospaced() &&
248                (line.contains(TEXT_ITALIC) ||
249                        line.contains(TEXT_ITALIC_END))) {
250            line = printingStyleWords(writer, line, TEXT_ITALIC, TEXT_ITALIC_END, Font.ITALIC);
251        } else {
252            if (line.contains(TEXT_ITALIC)) {
253                writer.setFontStyle(Font.ITALIC); // italicize the entire line
254            }
255            if (line.contains(TEXT_BOLD)) {
256                writer.setFontStyle(Font.BOLD); // bold the entire line
257            }
258            if (line.contains(TEXT_BOLD_END) || line.contains(TEXT_ITALIC_END)) {
259                isPrintingStyleDone = true;
260            }
261            if (line.contains(TEXT_ITALIC) || line.contains(TEXT_ITALIC_END)) {
262                line = getTextItalicString(line); // strip the italic characters
263            }
264            if (line.contains(TEXT_BOLD) || line.contains(TEXT_BOLD_END)) {
265                line = getTextBoldString(line); // strip the bold characters
266            }
267        }
268        return line;
269    }
270
271    // text has bold or italic control characters and possibly color control characters
272    private static String printingStyleWords(CompatibleHardcopyWriter writer, String line, String startStyle,
273            String endStyle, int style) throws IOException {
274        if (!isPrintingColor) {
275            color = null;
276        }
277        offset = 0;
278        printStyleWords(writer, line, startStyle, endStyle, style);
279        return ""; // done
280    }
281
282    // where in the line to add words
283    private static int offset;
284
285    private static void printStyleWords(CompatibleHardcopyWriter writer, String line, String startStyle,
286            String endStyle, int style) throws IOException {
287        // determine how many bold or italic words to print
288        List<String> words = getSytleWords(line, startStyle, endStyle);
289        for (String s : words) {
290            if (s.contains(startStyle)) {
291                writer.setFontStyle(style);
292                s = s.substring(s.indexOf(startStyle) + startStyle.length());
293            }
294            if (s.contains(endStyle)) {
295                writer.setFontStyle(style);
296                String text = s.substring(0, s.indexOf(endStyle));
297                printWords(writer, text);
298                writer.setFontStyle(Font.PLAIN);
299                
300                s = s.substring(s.indexOf(endStyle) + endStyle.length());
301            }
302            // special case where the line contains both bold and italic words
303            if (s.contains(TEXT_ITALIC)) {
304                printStyleWords(writer, s, TEXT_ITALIC, TEXT_ITALIC_END, Font.ITALIC);
305            } else {
306                printWords(writer, s);
307            }
308        }
309    }
310
311    private static List<String> getSytleWords(String line, String startStyle, String endStyle) {
312        ArrayList<String> list = new ArrayList<>();
313        String s;
314        while (line.length() > 0) {
315            if (line.contains(startStyle)) {
316                s = line.substring(0, line.indexOf(startStyle));
317                if (s.length() > 0) {
318                    list.add(s);
319                    line = line.substring(line.indexOf(startStyle));
320                }
321                if (line.contains(endStyle)) {
322                    s = line.substring(line.indexOf(startStyle),
323                            line.indexOf(endStyle, line.indexOf(startStyle)) + endStyle.length());
324                    list.add(s);
325                    line = line.substring(line.indexOf(endStyle, line.indexOf(startStyle)) + endStyle.length());
326                } else {
327                    list.add(line);
328                    break;
329                }
330            } else {
331                list.add(line);
332                break; //done
333            }
334        }
335        return list;
336    }
337    
338    private static void printWords(CompatibleHardcopyWriter writer, String text) throws IOException {
339        if (text.contains(TEXT_COLOR_START)) {
340            printColorWords(writer, text);
341        } else if (text.contains(TEXT_COLOR_END)) {
342            printColorEnd(writer, text);
343        } else {
344            writeColorWords(writer, text); // bold or italic text
345        }
346    }
347
348    private static String printColor(CompatibleHardcopyWriter writer, String line) throws IOException {
349        offset = 0;
350        if (line.contains(TEXT_COLOR_START)) {
351            color = getTextColor(line);
352            // if no TEXT_COLOR_END then printing multiple lines in color
353            isPrintingColor = !line.contains(TEXT_COLOR_END);
354            // could be a color change when using two column format
355            if (line.contains(Character.toString(VERTICAL_LINE_CHAR))) {
356                String s = line.substring(0, line.indexOf(VERTICAL_LINE_CHAR));
357                s = getOnlyText(s);
358                writer.write(color, s); // 1st half of line printed
359                // get the new color and text
360                line = line.substring(line.indexOf(VERTICAL_LINE_CHAR));
361                color = getTextColor(line);
362                // pad out string
363                line = tabString(getOnlyText(line), s.length());
364            } else if (writer.isMonospaced()) {
365                printColorWords(writer, line);
366                line = ""; // done
367            } else {
368                // simple case only one color
369                line = getOnlyText(line);
370            }
371        } else if (line.contains(TEXT_COLOR_END)) {
372            isPrintingColor = false;
373            if (writer.isMonospaced()) {
374                printColorEnd(writer, line);
375                line = ""; //done
376            } else {
377                line = getOnlyText(line);
378            }
379        } else if (!isPrintingColor) {
380            color = null;
381        }
382        return line;
383    }
384
385    private static void printColorEnd(CompatibleHardcopyWriter writer, String line) throws IOException {
386        String s = line.substring(0, line.indexOf(TEXT_COLOR_END));
387        writeColorWords(writer, s);
388        s = line.substring(line.indexOf(TEXT_COLOR_END) + TEXT_COLOR_END.length());
389        isPrintingColor = false;
390        color = null;
391        writeColorWords(writer, s);
392    }
393
394    // If monospaced font, it is possible to only color subset of words in the line
395    private static void printColorWords(CompatibleHardcopyWriter writer, String line) throws IOException {
396        for (String words : getColorWords(line)) {
397            color = getTextColor(words);
398            if (words.contains(TEXT_COLOR_START)) {
399                isPrintingColor = true;
400            }
401            if (words.contains(TEXT_COLOR_END)) {
402                isPrintingColor = false;
403            }
404            words = getOnlyText(words);
405            writeColorWords(writer, words);
406            if (!isPrintingColor) {
407                color = null;
408            }
409        }
410    }
411
412    private static List<String> getColorWords(String line) {
413        ArrayList<String> list = new ArrayList<>();
414        String s;
415        while (line.length() > 0) {
416            if (line.contains(TEXT_COLOR_START)) {
417                s = line.substring(0, line.indexOf(TEXT_COLOR_START));
418                if (s.length() > 0) {
419                    list.add(s);
420                    line = line.substring(line.indexOf(TEXT_COLOR_START));
421                }
422                if (line.contains(TEXT_COLOR_END)) {
423                    s = line.substring(line.indexOf(TEXT_COLOR_START),
424                            line.indexOf(TEXT_COLOR_END, line.indexOf(TEXT_COLOR_START)) + TEXT_COLOR_END.length());
425                    list.add(s);
426                    line = line.substring(
427                            line.indexOf(TEXT_COLOR_END, line.indexOf(TEXT_COLOR_START)) + TEXT_COLOR_END.length());
428                } else {
429                    list.add(line);
430                    break;
431                }
432            } else {
433                list.add(line);
434                break; //done
435            }
436        }
437        return list;
438    }
439
440    private static void writeColorWords(CompatibleHardcopyWriter writer, String s) throws IOException {
441        String text = tabString(s, offset);
442        writer.write(color, text);
443        offset = +text.length();
444    }
445
446    private static String setFontSize(CompatibleHardcopyWriter writer, String line) {
447        if (line.contains(TEXT_SIZE_START)) {
448            int size = getFontSize(line);
449            writer.setFont(null, null, size);
450        }
451        if (line.contains(TEXT_SIZE_END)) {
452            isTextSizeDone = true;
453        }
454        return getTextSizeString(line);
455    }
456
457    private static final Logger log = LoggerFactory.getLogger(TrainPrintManifest.class);
458}