/*
 * PrintText.java - Short Description
 * Copyright (C) 1999 Scot Bellamy, ACTS, Inc.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
package org.jext.print;
import java.util.*;
import java.awt.print.*;
import java.awt.*;
import java.awt.geom.*;
import java.awt.font.*;
import javax.swing.*;
import javax.swing.text.*;

/**
 * A simple printing class to handle basic text printing.
 * Accepts an array of Strings or a PlainDocument and
 * prints all the lines contained there in.  Each String
 * in the array is assumed to be a separate line.
 */
public class PrintText {
  private int numberOfpages_ = 0; // The number of pages
  private Book pages_ = new Book(); // This holds each page
  private int wrapOffset_ = 0; // Used to determine where to begin a wrapped line.
  private String docTitle_; // Used for document title (i.e. file name) when including the page header
  private String[] text_; // Text to print.
  private PrintingOptions printOptions_; // Print options (i.e. font, print header, etc.)
  private boolean softTabs_ = true; // Indicates whether soft or hard tabs are used.
  private int tabSize_ = 4; // Tab stop if hard tabs are used.

  /**
   * Constructor - Accepts a plain document and uses default font.
   * No header information will be printed.
   */
  public PrintText(PlainDocument document) {
    this(document, "", new PrintingOptions(), false, 4);
  }

  /**
   * Constructor - Accepts a plain document as well as  other print options,
   * including font, page title, and header indicator (true if printing header, false otherwise).
   */
  public PrintText(PlainDocument document, String docTitle, PrintingOptions printOptions, boolean softTabs, int tabSize) {
    printOptions_ = printOptions;
    softTabs_ = softTabs;
    tabSize_ = tabSize;
    if(docTitle != null) {
      docTitle_ = docTitle;
    } else {
      // If a new doc and no title, set docTitle to "New Document"
      docTitle_ = "New Document";
    }
    //  Get Root element of the document
    Element root = document.getDefaultRootElement();
    //get the number of lines (i.e. child elements)
    int count = root.getElementCount();
    //Allocate the array
    String lines[] = new String[count];
    Segment segment = new Segment();
    // Get each line element, get its text and put it in the string array
    for(int i = 0; i < count; i++) {
      Element lineElement = (Element) root.getElement(i);
      try {
        document.getText(lineElement.getStartOffset(), lineElement.getEndOffset() - lineElement.getStartOffset(), segment);
        lines[i] = segment.toString();
      }
      catch (BadLocationException ble) {} // Nothing gets added to the Array if there is a bad location
    }
    text_ = lines;
    printTextArray();
  }

  /**
   * Constructor - accepts an array of Strings, uses the default font, no header.
   */
  PrintText(String[] text) {
    printOptions_ = new PrintingOptions();
    text_ = text;
    printTextArray();
  }

  /**
   * Constructor - accepts an array of Strings and a font, no header.
   */
  PrintText(String[] text, Font font) {
    printOptions_ = new PrintingOptions();
    text_ = text;
    printTextArray();
  }

  /**
   * Where the print processing begins.
   */
  void printTextArray() {
    PageFormat pgfmt = printOptions_.getPageFormat();
    Font pageFont = printOptions_.getPageFont();
    try {
      PrinterJob job = PrinterJob.getPrinterJob(); // create a printjob
      //            pgfmt = job.pageDialog(pgfmt);
      // set a page format. Comment this if you do not want this to show
      //            pgfmt = job.validatePage(pgfmt);
      // make sure the pageformat is ok
      text_ = removeEOLChar();
      if(printOptions_.getPrintLineNumbers() == true) {
        text_ = addLineNumbers();
      }
      if(printOptions_.getWrapText() == true) {
        text_ = wrapText();
      }
      pages_ = pageinateText(); // do the pagination
      try {
        job.setPageable(pages_); // set the book pageable so the printjob knows we are printing more than one page (maybe)
        if(job.printDialog()) {
          job.print(); // print.  This calls each Page object's print method
        }
      }
      // catch any errors and be as ambiguous about them as possible :)
      catch (Exception e) {
        JOptionPane.showMessageDialog(null, "Printer Error", "Error", JOptionPane.OK_OPTION);
      }
    }
    catch (Exception e) {
      JOptionPane.showMessageDialog(null, "Printer Error", "Error", JOptionPane.OK_OPTION);
    }
  }

  /**
   * Eliminates end of line characters
   */
  private String[] removeEOLChar() {
    String temp1, temp2, temp3;
    int lineCount = text_.length;
    String [] newText = new String[lineCount];
    int offset = 0;
    for(int i = 0; i < lineCount; i++) {
      if(text_[i].length() == 1) {
        newText[i] = " ";
      } else {
        temp1 = text_[i].substring(text_[i].length() - 2, text_[i].length() - 1);
        temp2 = text_[i].substring(text_[i].length() - 1, text_[i].length());
        if(temp1.compareTo("\r") == 0 || temp1.compareTo("\n") == 0) {
          offset = 2;
        } else if(temp2.compareTo("\r") == 0 || temp2.compareTo("\n") == 0) {
          offset = 1;
        } else {
          offset = 0;
        }
        temp3 = text_[i].substring(0, text_[i].length() - offset);
        // Process tabs.  Assume tab stops.
        StringBuffer temp4 = new StringBuffer();
        int length = temp3.length();
        for(int j = 0; j < length; j++) {
          if("\t".equals(temp3.substring(j, j + 1)) == true) {
            // Calcualte the numbe of spaces to the tab stop.
            int numSpaces = (temp4.length()) % tabSize_;
            if(numSpaces == 0) {
              numSpaces = tabSize_;
            }
            for(int x = 0; x < numSpaces; x++) {
              temp4.append(" ");
            }
          } else {
            temp4.append(temp3.substring(j, j + 1));
          }
        }
        newText[i] = temp4.toString();
      }
    }
    return newText;
  }

  /**
   * Addes line numbers to the beginning of each line.
   */
  private String[] addLineNumbers() {
    int numLines = text_.length;
    int totalNumSpaces = 0;
    String temp;
    String [] newText = new String[numLines];
    // Get the total number of digits in last line number
    // So that spacing and alignment can be done properly.
    Integer lines = new Integer(numLines);
    temp = lines.toString();
    totalNumSpaces = temp.length();
    // Set the wrap offset so that we can start wrapped lines in the proper place.
    wrapOffset_ = totalNumSpaces + 3;
    for(int i = 0; i < numLines; i++) {
      StringBuffer num = new StringBuffer();
      num.append(i + 1);
      int numLen = num.length();
      StringBuffer lineNum = new StringBuffer();
      for(int j = 0; j < (totalNumSpaces - numLen); j++) {
        lineNum.append(' ');
      }
      lineNum.append(num.toString());
      newText[i] = lineNum.toString() + ".  " + text_[i];
    }
    return newText;
  }

  /**
   * Creates a new array of lines that all fit the width of the page.
   */
  private String[] wrapText() {
    String currentLine = null;
    String tempString = null;
    Vector temp = new Vector();
    int lineCount = text_.length;
    int newLineCount = 0;
    StringBuffer wrapSpaces = new StringBuffer("");
    int i = 0;
    PageFormat pgfmt = printOptions_.getPageFormat();
    Font pageFont = printOptions_.getPageFont();
    double pageWidth = pgfmt.getImageableWidth();
    for(i = 0; i < wrapOffset_; i++) {
      wrapSpaces.append(' ');
    }
    for(i = 0; i < lineCount; i++) {
      currentLine = text_[i];
      while (pageFont.getStringBounds(currentLine,
                                      new FontRenderContext(pageFont.getTransform(), false, false)).getWidth() > pageWidth) {
        int numChars = (int)(currentLine.length() * pageWidth / pageFont.getStringBounds(currentLine,
                                                                                         new FontRenderContext(pageFont.getTransform(), false, false)).getWidth());
        temp.add(currentLine.substring(0, numChars));
        currentLine = wrapSpaces.toString() + currentLine.substring(numChars, currentLine.length());
      }
      temp.add(currentLine);
    }
    newLineCount = temp.size();
    String [] newText = new String[newLineCount];
    for(int j = 0; j < newLineCount; j++) {
      newText[j] = (String) temp.get(j);
    }
    return newText;
  }

  /**
   * The pagination method, Paginate the text onto Printable page objects
   */
  private Book pageinateText() {
    Book book = new Book();
    int linesPerPage = 0; // lines on one page
    int currentLine = 0; // line I am  currently reading
    int pageNum = 0; // page #
    PageFormat pgfmt = printOptions_.getPageFormat();
    Font pageFont = printOptions_.getPageFont();
    int height = (int) pgfmt.getImageableHeight(); // height of a page
    int pages = 0; // number of pages
    linesPerPage = height / (pageFont.getSize() + 2); // number of lines on a page
    pages = ((int) text_.length / linesPerPage); // set number of pages
    String[] pageText; // one page of text
    String readString; // a temporary string to read from master string
    convertUnprintables(); // method to keep out errors
    if(printOptions_.getPrintHeader() == true) {
      linesPerPage = linesPerPage - 2;
    }
    while (pageNum <= pages) {
      pageText = new String[linesPerPage]; // create a new page
      for(int x = 0; x < linesPerPage; x++) {
        try {
          readString = text_[currentLine]; // read the string
        }
        catch (ArrayIndexOutOfBoundsException e) {
          readString = " ";
        }
        pageText[x] = readString; // add to the page
        currentLine++;
      }
      pageNum++; // increase the page number I am on
      book.append(new Page(pageText, pageNum), pgfmt); // create a new page object with the text and add it to the book
    }
    return book; // return the completed book
  }

  /**
   * Converts unprintable things to a space.  stops some errors.
   */
  private void convertUnprintables() {
    String tempString;
    int i = text_.length;
    while (i > 0) {
      i--;
      tempString = text_[i];
      if(tempString == null || "".equals(tempString)) {
        text_[i] = " ";
      }
    }
  }

  /**
   * An inner class that defines one page of text based
   * on data about the PageFormat etc. from the book defined
   * in the parent class
   */
  class Page implements Printable {
    private String[] pageText_; // the text for the page
    private int pageNumber_ = 0;
    Page(String[] text, int pageNum) {
      this.pageText_ = text; // set the page's text
      this.pageNumber_ = pageNum; // set page number.
    }

    /**
     * Defines the Printable print method, for printing a Page
     */
    public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException {
      int pos;
      int posOffset = 1;
      double pageWidth = pageFormat.getImageableWidth();
      Font pageFont = printOptions_.getPageFont();
      if(printOptions_.getPrintHeader() == true) {
        StringBuffer header = new StringBuffer();
        StringBuffer pageNumText = new StringBuffer();
        int i = 0;
        int headerPos = 0;
        int numSpaces = 0;
        Calendar date = Calendar.getInstance();
        header.append(date.get(Calendar.DAY_OF_MONTH));
        header.append('/');
        header.append(date.get(Calendar.MONTH) + 1);
        header.append('/');
        header.append(date.get(Calendar.YEAR));
        pageNumText.append("Page ");
        pageNumText.append(pageNumber_);
        int xPos;
        double margin = (pageFormat.getWidth() - pageFormat.getImageableWidth()) / 2;
        graphics.setFont(printOptions_.getHeaderFont());
        graphics.setColor(Color.black);
        pos = (int) pageFormat.getImageableY() + (printOptions_.getHeaderFont().getSize() + 2);
        graphics.drawString(header.toString(), (int) pageFormat.getImageableX(), pos); // draw a line of text
        xPos = (int)((pageFormat.getWidth() / 2) - (graphics.getFontMetrics().stringWidth(docTitle_) / 2));
        graphics.drawString(docTitle_, xPos, pos);
        xPos = (int)(pageFormat.getWidth() - margin - graphics.getFontMetrics().stringWidth(pageNumText.toString()));
        graphics.drawString(pageNumText.toString(), xPos, pos);
        posOffset = 3;
      }
      graphics.setFont(pageFont); // Set the font
      graphics.setColor(Color.black); // set color
      for(int x = 0; x < (pageText_.length); x++) {
        pos = (int) pageFormat.getImageableY() + (pageFont.getSize() + 2) * (x + posOffset);
        graphics.drawString(this.pageText_[x], (int) pageFormat.getImageableX(), pos); // draw a line of text
      }
      return Printable.PAGE_EXISTS; // print the page
    }
  }

  /**
   * An inner class that defines one section of printable text.
   * This allows the flexability to assign different fonts to
   * individual words or phrases (i.e. for headers/footers or
   * Syntax highlighting (pretty print).
   */
  class PrintableText {
    private Font font_;
    private boolean newLine_ = true;
    private String text_;

    PrintableText() {
    }

    PrintableText(String text, Font font, boolean newLine) {
      text_ = text;
      font_ = font;
      newLine_ = newLine;
    }

    String getText() {
      return text_;
    }

    void setText(String text) {
      text_ = text;
    }

    Font getFont() {
      return font_;
    }

    void setFont(Font font) {
      font_ = font;
    }

    boolean isNewLine() {
      return newLine_;
    }

    void setNewLine(boolean newLine) {
      newLine_ = newLine;
    }
  }
}
// End of PrintText.java

import java.awt.*;
import java.awt.event.*;
import java.awt.font.*;
import java.awt.geom.*;
import java.awt.print.*;
import java.io.*;
import javax.swing.*;
import java.util.*;

/**
 * PrintTextDemo is a simple Swing program that
 * allows the user to select a text file, and then
 * allows him to print it at a particular font size.
 * It also puts a page heading on each page.
 */
public class PrintTextDemo extends JFrame {
  public static final String [] sizes={ "7", "8", "9", "10", "11", "12" };
  private JTextField filepathField;
  private JButton  browseButton;
  private JComboBox  sizeBox;
  private JPanel topPanel;
  private JPanel bottomPanel;
  private JButton  printButton;
  private JButton  quitButton;
  private JFileChooser filer;
  private PageFormat pageformat;

  /**
   * Create the PrintTextDemo object.  This constructor
   * mainly builds the GUI.
   */
  public PrintTextDemo() {
    super("Print Text Demo");
    Container content;
    JLabel sizeLabel;
    content = getContentPane();
    content.setLayout(new BorderLayout());
    topPanel = new JPanel();
    bottomPanel = new JPanel();
    filepathField = new JTextField(48);
    browseButton = new JButton("Find..");
    sizeLabel = new JLabel("Font size:");
    sizeBox = new JComboBox(sizes);
    sizeBox.setSelectedIndex(1);
    printButton = new JButton("Print..");
    quitButton = new JButton("Quit");
    topPanel.add(filepathField);
    topPanel.add(browseButton);
    topPanel.add(sizeLabel);
    topPanel.add(sizeBox);
    bottomPanel.add(printButton);
    bottomPanel.add(quitButton);
    content.add(topPanel, BorderLayout.NORTH);
    content.add(bottomPanel, BorderLayout.SOUTH);
    filer = new JFileChooser();
    setHandlers();
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    pack();
  }

  /**
   * Utility method to pop up a dialog with an error message.
   */
  public void popupError(String msg) {
    JOptionPane.showMessageDialog(this, msg, "PrintTextDemo Error",
                                  JOptionPane.ERROR_MESSAGE);
  }
  /**
   * Utility method to pop up a dialog with an information message.
   */
  public void popupMessage(String msg) {
    JOptionPane.showMessageDialog(this, msg, "PrintTextDemo Message",
                                  JOptionPane.INFORMATION_MESSAGE);
  }

  /**
   * Action listener for the Print.. button; this starts
   * up the whole printing process: creating the PrinterJob
   * object, initializing our TextPageRenderer Printable
   * object, popping up the platform print dialog, and then
   * invoking the print.
   */
  class PrintListener implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      TextPageRenderer pager = null;
      java.util.List<String> ls = null;
      String msg = null;
      File filepath = null;
      try {
        filepath = new File(filepathField.getText());
        ls = readContentsOf(filepath);
      } catch (IOException ie) {
        msg = "Unable to handle file path '" + filepath + "'";
      }
      if(msg == null) {
        int size;
        size = Integer.parseInt(sizeBox.getSelectedItem().toString());
        PrinterJob pj;
        pj = PrinterJob.getPrinterJob();
        pager = new TextPageRenderer(filepath.getAbsolutePath(), 
                                     size, ls);
        pj.setJobName(filepath.getName());
        pj.setPrintable(pager);
        if(pj.printDialog()) {
          try {
            pj.print();
          } catch (PrinterException pe) {
            msg = "Print error: " + pe;
          }
        }
      }
      if(msg != null) {
        popupError(msg);
      } else if(pager != null) {
        popupMessage("Print job succeeded, " + 
                     pager.getPageCount() + " pages.");
      }
    }
  }

  /** 
   * Action listener for the Find.. button.  This pops up
   * the file chooser modal dialog and handles the response.
   */
  class BrowserListener implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      int ret;
      ret = filer.showOpenDialog(topPanel);
      if(ret == JFileChooser.APPROVE_OPTION) {
        filepathField.setText(filer.getSelectedFile().getAbsolutePath());
      }
    }
  }

  /** 
   * Utility method to quit the application.
   */
  protected void quitProgram() {
    setVisible(false);
    dispose();
    System.exit(0);
  }

  /**
   * Set the button action listeners.
   */
  protected void setHandlers() {
    printButton.addActionListener(new PrintListener());
    quitButton.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          quitProgram();
        }
      });
    browseButton.addActionListener(new BrowserListener());
  }

  /**
   * Utility method to read a text file into a List.
   * This is rather wasteful for very large text files,
   * because it stores the whole thing in memory as
   * 2-byte characters.
   */
  public java.util.List<String> readContentsOf(File f) 
    throws IOException  {
    FileReader fr;
    BufferedReader br;
    java.util.LinkedList<String> output;
    fr = new FileReader(f);
    br = new BufferedReader(fr);
    output = new java.util.LinkedList<String>();
    String line;
    for(line = br.readLine(); line != null; line = br.readLine()) {
      output.add(line);
    }
    br.close();
    return output;
  }

  /**
   * This class handles rendering the pages.  It
   * implements the java.awt.print.Printable interface,
   * which means it can be invoked from a PrinterJob.
   */
  class TextPageRenderer implements Printable {
    private boolean firstcall;
    private int pagecount;
    private int fontsize;
    private String title;
    private double fontascent;
    private double fontheight;
    private Font font;
    private Font hfFont;
    private java.util.List<String> lines;
    private int charsPerLine;
    private int linesPerPage;
    private AffineTransform paf;

    /**
     * This constructor stores some state that we'll need
     * for when the PrinterJob calls us to do printing.
     * Note that we cannot initialize anything about the
     * print rendering here, because we don't have a
     * Graphics object or a PageFormat object yet.  So,
     * we just store away the information for later.
     *
     * @param ttl Page header for the print job
     * @param fsiz Font size for the printed page (Courier)
     * @param content List of lines to be printed
     */
    public TextPageRenderer(String ttl, 
                            int fsiz, 
                            java.util.List<String> content) {
      title = ttl;
      lines = content;
      fontsize = fsiz;
      firstcall = true;
      pagecount = 0;
      pagecount = 0;
      charsPerLine = 68; // default
      linesPerPage = 50; // default
      font = null;
      paf = null;
    }

    /**
     * This method renders a specified page into the given
     * graphic surface.  The pageFormat object specifies the
     * size of page, based on the platform printer capabilities.
     * This method returns NO_SUCH_PAGE if the specified page
     * index is not printable, or PAGE_EXISTS otherwise.
     *
     * On the first time it is called, this method does a bunch
     * of computations about page capacity and such.
     *
     * Note that this method makes a number of assumptions
     * about the environment.  First, it assumes the Graphics
     * object supplied from the PrinterJob is actually a 
     * Graphics2D.  This should always be true for Java 1.4
     * and later.  
     */
    public int print(Graphics g,PageFormat pageFormat,int pageIndex) {
      Graphics2D g2 = (Graphics2D)g;
      if(firstcall) {
        firstcall = false;
        // check the page transform and apply
        paf = new AffineTransform(pageFormat.getMatrix());
        g2.transform(paf);
		
        // get font
        font = new Font("Monospaced", Font.PLAIN, fontsize);
        hfFont = new Font("Monospaced", Font.BOLD, fontsize);
        g2.setFont(font);
        // compute chars per line and lines per page
        double cw, lh, pw, ph;
        LineMetrics lm;
        Rectangle2D rx, sb;
        FontRenderContext frc = g2.getFontRenderContext();
        // proper call to use here would be getLineMetrics but
        // it always seems to fail.
        rx = font.getMaxCharBounds(frc);
        sb = font.getStringBounds("M", frc);
        cw = sb.getWidth();
        lh = rx.getHeight() ;
        fontascent = sb.getHeight();
        fontheight = lh;
        ph = pageFormat.getImageableHeight();
        pw = pageFormat.getImageableWidth();
        charsPerLine = (int)(pw / cw);
        linesPerPage = (int)(ph / lh) - 2;
                
        // adjust the lines List for wrapping
        adjustLines();
      } else {
        // don't forget to apply the page transform!
        g2.transform(paf);
      }
      if(pageIndex > (lines.size() / linesPerPage)) {
        return NO_SUCH_PAGE;
      }
      // do the printing       
      int lpos = pageIndex * linesPerPage;
      int lim = (pageIndex + 1) * linesPerPage;
      int i;
      String line;
      String heading;
      g2.translate(pageFormat.getImageableX(), 
                   pageFormat.getImageableY());
      heading = "Page " + (pageIndex+1) + " - " + title;
      g2.setFont(hfFont);
      g2.drawString(heading, 0, (int)fontascent);
      g2.setFont(font);
      for(i = 2; lpos < lim && lpos < lines.size(); i++, lpos++) {
        line = lines.get(lpos);
        g2.drawString(line, 0, (int)((i * fontheight) + fontascent));
      }
      if(pagecount < (pageIndex + 1)) pagecount= pageIndex+1;
      return PAGE_EXISTS;
    }

    /**
     * Adjust the stored set of lines for this TextPageRenderer
     * based on the calculated charsPerLine.  Basically, this
     * method splits up long lines into multiple lines, giving
     * the effect of wrapping them.  It returns void, all its
     * work is done by adjusting the internal state List lines.
     * Note that this method does not attempt to handle 
     * formfeed characters; it really should do so.
     */
    private void adjustLines() {
      ListIterator<String> it;
      String line;
      int pos;
      it = lines.listIterator();
      while(it.hasNext()) {
        line = it.next();
        if(line.length() > charsPerLine) {
          it.remove();
          for(pos = 0; pos < line.length(); pos += charsPerLine) {
            if(pos+charsPerLine > line.length()) {
              it.add(line.substring(pos));
            } else {
              it.add(line.substring(pos, pos+charsPerLine));
            }
          }
        }
      }
    }

    /**
     * Return the largest page number that
     * this Printable was asked to print.
     */
    public int getPageCount() {
      return pagecount;
    }
  }

  /**
   * Main method for the PrintTextDemo application.
   * This just creates the PrintTextDemo GUI and 
   * causes it to pop up.
   */
  public static void main(String [] args) {
    PrintTextDemo ptd;
    ptd = new PrintTextDemo();
    ptd.setVisible(true);
  }
}
