/*
 * Filename: PostscriptRequester.java
 *
 * Author: Joe Wang
 *
 * Copyright (c) 2003-2005 
 * The Trustees of Columbia University in the City of New York
 * Columbia University Information Technology
 *
 * This file is part of NINJa.
 *
 * NINJa 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
 * (at your option) any later version.
 *
 * NINJa 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 NINJa; if not, write to the Free Software Foundation,
 * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 *
 */

package edu.columbia.print.ninja.prix.requester;

import edu.columbia.print.*;
import javax.print.*;
import javax.print.attribute.*;
import javax.print.attribute.standard.*;
import com.millipad.util.*;
import java.util.*;
import java.util.regex.*;
import java.io.*;
import java.nio.*;
import java.nio.channels.*;

/**
 * read the postscript file for additional attributes
 * @author Joe Wang <jw168@columbia.edu>
 * @since 1.0
 */
public class PostscriptRequester extends Requester implements Runnable {
    
    static final long TAIL_SIZE = 80*10;
    
    ResourceBundle bundle;
    OutputDeviceAssigned inputDevice;
    String outputDevice;
    String jobFile;
    FileInputStream jobIS;
    Set jobs;
    File dir;
    Set services;
    Pattern altDuplexPattern, duplexPattern, pagesPattern, pagePattern, driverPattern;
    Pattern titlePattern, headerPattern, atendPattern, copiesPattern, copies2Pattern, numberUpPattern;
    BlockingStack stack;
    
    public PostscriptRequester() throws Exception {
        bundle = ResourceBundle.getBundle(getClass().getPackage().getName()+".resource");
        services = new HashSet();
        stack = new BlockingStack();
        pagesPattern = Pattern.compile("%%Pages:", Pattern.CASE_INSENSITIVE);
        titlePattern = Pattern.compile("%%Title:", Pattern.CASE_INSENSITIVE);
        pagePattern = Pattern.compile("\\(?%%\\[?Page:\\s*([\\d]+)\\s*([\\d]*)", Pattern.CASE_INSENSITIVE);
        headerPattern = Pattern.compile(".{0,10}%!PS-", Pattern.CASE_INSENSITIVE);
        atendPattern = Pattern.compile(".*\\(atend\\).*", Pattern.CASE_INSENSITIVE);
        altDuplexPattern = Pattern.compile(".{0,10}/Duplex ([a-z]{1,10})", Pattern.CASE_INSENSITIVE);
        duplexPattern = Pattern.compile("%%BeginFeature: \\*Duplex", Pattern.CASE_INSENSITIVE);
        copiesPattern = Pattern.compile("%%BeginNonPPDFeature: NumCopies (\\d+)", Pattern.CASE_INSENSITIVE);
        copies2Pattern = Pattern.compile("/#copies (\\d+) def", Pattern.CASE_INSENSITIVE);
        driverPattern = Pattern.compile("%%TargetDevice: (.*)", Pattern.CASE_INSENSITIVE);
//-90 1 599 779 2 1 definenup
        numberUpPattern = Pattern.compile("[-]?\\d+ [-]?\\d+ [-]?\\d+ [-]?\\d+ (\\d+) (\\d+) definenup", Pattern.CASE_INSENSITIVE);
//%%BeginNonPPDFeature: NumCopies 12
        Thread t = new Thread(this);
        t.setPriority(Thread.MIN_PRIORITY);
        t.start();
    }
    
    void debug(Object in) {Debug.print(PostscriptRequester.class, in);}
    
    public void addPrintService(net.jini.print.service.PrintService service) {
        services.add(service);
    }

    public void setInputDevice(String device) {
    }

    private boolean setPages(String count, DocAttributeSet attr,
    PrintRequestAttributeSet reqAttr) {
        try {
            if(count==null || count.length()==0) {
                count=null;
                PageRanges ranges = (PageRanges )attr.get(PageRanges.class);
//                JobMediaSheets sheets = (JobMediaSheets )reqAttr.get(
//                JobMediaSheets.class
//                );
                if(ranges!=null) {
                    count = (ranges.getMembers()[0][1]-ranges.getMembers()[0][0]+1)+"";
                }
                //sometimes Page: is followed by nothing
                if(count==null) count = "1";
            }
            for(;count.startsWith("0")||count.startsWith(" ");) {
                count=count.substring(1, count.length());
            }
            int sheets = Integer.parseInt(count);
            attr.add(new PageRanges(1, sheets));

            NumberUp numberUp = (NumberUp )attr.get(NumberUp.class);
            if(numberUp!=null) {
                sheets = (sheets+numberUp.getValue()-1)/numberUp.getValue();
            }
            Sides sides = (Sides )attr.get(Sides.class);
            if(sides!=null && sides.equals(Sides.TWO_SIDED_LONG_EDGE)) {
                sheets = (sheets+1)/2;
            }
            else if(sides!=null && sides.equals(Sides.TWO_SIDED_SHORT_EDGE)) {
                sheets = (sheets+1)/2;
            }
            Copies copies = (Copies )reqAttr.get(Copies.class);
            if(copies!=null) {
                sheets *= copies.getValue();
            }
//            System.out.println("sheets:"+count+" "+sheets+" "+sides+" "+copies+" "+numberUp);
            reqAttr.add(new JobMediaSheets(sheets));
            return true;
        }
        catch(NumberFormatException e) { }
        catch(Exception e) {
            e.printStackTrace();
        }
        return false;
    }
    
    long readToLineTerminator(BufferedReader reader, int lineLen)
    throws IOException {
        char[] buffer = {0,0};
        reader.skip(lineLen);
        reader.mark(2);
        reader.read(buffer);
        int newLine = 0;
        if(buffer[0]=='\r' && buffer[1]=='\n') newLine=2;
        else if(buffer[0]=='\r' || buffer[0]=='\n') newLine=1;
        reader.reset();
        for(int i=newLine; i>0; i--) reader.read();
        return lineLen+newLine;
    }
    
    /**
     * Read the postscript file to find out the number of pages and wether the
     * pages are duplex or simplex.
     */
    public synchronized int produceRequest(File in, PrintRequestAttributeSet attr, Class[] required, BlockingStack result) throws IOException {
        stack.push(new Object[] {in, attr, required, result});
        return REQUEST_DISPATCHED;
    }
    
    public boolean containsPostscriptHeader(File in) {
        try {
            FileReader fr = new FileReader(in);
            BufferedReader reader = new BufferedReader(fr);
            boolean isPostscript = false;
            for(int i=20; i>=0; i--) {
                String line = reader.readLine();
                if(line==null) break;
                if(!headerPattern.matcher(line).lookingAt()) continue;
                isPostscript = true;
                break;
            }
            fr.close();
            return isPostscript;
        }
        catch(Exception e) {
            debug(e);
            return false;
        }
    }

    public void run() {
        for(;;) {
            Object[] v = (Object[] )stack.pop();
            File in = (File )v[0];
            PrintRequestAttributeSet attr = (PrintRequestAttributeSet )v[1];
            Class[] required = (Class[] )v[2];
            BlockingStack result = (BlockingStack )v[3];
            try {
                //get the pages
                FileReader fr = new FileReader(in);
                BufferedReader reader = new BufferedReader(fr);
                boolean isPostscript = false;
                String firstLine = "";
                isPostscript = containsPostscriptHeader(in);
/*                for(int i=20; i>=0; i--) {
//                    reader.mark(4096);
                    String line = reader.readLine();
                    if(line==null) break;
                    firstLine += line+"\n";
                    if(!headerPattern.matcher(line).lookingAt()) continue;
                    isPostscript = true;
                    break;
                }
*/                DocAttributeSet docAttr = new HashDocAttributeSet();
                if(!isPostscript) {
                    fr.close();
                    for(Iterator i=services.iterator(); i.hasNext();) {
                        net.jini.print.service.PrintService ps = (net.jini.print.service.PrintService )i.next();
                        FileInputStream fis = new FileInputStream(in);
                        net.jini.print.job.DocPrintRequest req = ps.createDocPrintRequest();
                        attr.add(new JobName("not a valid Postscript file", null));
                        req.setAttributes(attr);
                        req.setDoc(new org.jpwg.jini.print.data.InputStreamDoc(fis, DocFlavor.INPUT_STREAM.AUTOSENSE.getMimeType(), docAttr));
                        req.print(0L);
                    }
                    if(result!=null) result.push(in);
                    throw new Exception("not ps "+firstLine);
                }
                boolean hasPageCount = false;
                boolean hasSkipped = false;
                if(attr.containsKey(Sides.class)) docAttr.add(attr.get(Sides.class));
                int stage = 0;
                for(;;) {
//                    reader.mark(4096);
                    String line = reader.readLine();
                    if(line==null) break;
                    //find postscript markup
                    Matcher m;
                    switch(stage) {
                        case 0:
                            m = pagesPattern.matcher(line);
                            if(m.lookingAt()) {
                                String count = line.substring(m.end(), line.length());
                                hasPageCount=setPages(count, docAttr, attr);
                                stage=1;
                                break;
                            }
/* Does not work well with Win2000 lpr client
                                m = titlePattern.matcher(line);
                                if(m.lookingAt()) {
                                        String title = line.substring(m.end()+1);
                                        attr.add(new JobName(title, null));
                                        continue;
                                        }
 */
                        case 1:
/*                            m = driverPattern.matcher(line);
                            if(m.lookingAt()) {
                                String driver = m.group(1).trim();
                                JobName name = (JobName )attr.get(JobName.class);
                                if(name==null) name = new JobName("", null);
                                name = new JobName(name.getValue()+":"+driver,  null);
                                attr.add(name);
                                break;
                            }
*/
                            m = altDuplexPattern.matcher(line);
                            if(m.lookingAt()) {
                                String mode = m.group(1);
                                mode = mode.trim();
                                if(mode.equals("true")) {
                                    docAttr.add(Sides.TWO_SIDED_LONG_EDGE);
                                }
                                else {
                                    docAttr.add(Sides.ONE_SIDED);
                                }
//                                stage=2;
                                break;
                            }
                            m = duplexPattern.matcher(line);
                            if(m.lookingAt()) {
                                String mode = line.substring(m.end()+1);
                                mode = mode.trim();
                                if(mode.equals("None") && !attr.containsKey(Sides.class)) {
                                    docAttr.add(Sides.ONE_SIDED);
                                }
                                else if(mode.equals("DuplexNoTumble")) {
                                    docAttr.add(Sides.TWO_SIDED_LONG_EDGE);
                                }
                                else if(mode.equals("DuplexTumble")) {
                                    docAttr.add(Sides.TWO_SIDED_SHORT_EDGE);
                                }
                                else if(mode.equals("Off") && !attr.containsKey(Sides.class)) {
                                    docAttr.add(Sides.ONE_SIDED);
                                }
                                else if(mode.equals("Long-Edge Binding")) {
                                    docAttr.add(Sides.TWO_SIDED_LONG_EDGE);
                                }
                                else if(mode.equals("Short-Edge Binding")) {
                                    docAttr.add(Sides.TWO_SIDED_SHORT_EDGE);
                                }
                                setPages(null, docAttr, attr);
//                                stage=2;
                                break;
                            }
//    copiesPattern, numberUpPattern;
                        m = copiesPattern.matcher(line);
                            if(m.lookingAt()) {
                                String copies = m.group(1);
                                copies = copies.trim();
                                attr.add(new Copies(Integer.parseInt(copies.trim())));
                                setPages(null, docAttr, attr);
                            }
                        m = copies2Pattern.matcher(line);
                            if(m.lookingAt()) {
                                String copies = m.group(1);
                                copies = copies.trim();
                                attr.add(new Copies(Integer.parseInt(copies.trim())));
                                setPages(null, docAttr, attr);
                            }
                        m = numberUpPattern.matcher(line);
                            if(m.lookingAt()) {
                                String v1 = m.group(1);
                                String v2 = m.group(2);
                                int total = Integer.parseInt(v1) * Integer.parseInt(v2);
                                docAttr.add(new NumberUp(total));
                                setPages(null, docAttr, attr);
                            }
                        case 2:
                                /*
                                AdobeAcrobat will display an atend, but the page count
                                will not actually be at the end.
                                 */
                                /*
                                 we ignore the initial Pages: markup and
                                 skip to the end.  Read either the last Page: markup
                                 or the last Pages: markup
                                 */
                            if(!hasPageCount) {
                                m = pagePattern.matcher(line);
                                if(m.lookingAt()) {
                                    String pages = m.group(2);
                                    if(pages==null || pages.length()==0) pages = m.group(1);
                                    setPages(pages, docAttr, attr);
                                    break;
                                }
                            }
                    }
                    if(!hasSkipped) {
                        //first see if we have everything we need
                        if(docAttr.get(Sides.class)==null) continue;
                        if(!hasPageCount) continue;
                        break;
/*                        //move the reader
                        long skip = in.length()-TAIL_SIZE-position;
                        if(skip<0) continue;
                        long actual = reader.skip(skip);
                        if(actual==skip) hasSkipped = true;
                        position+=actual;
                        break;
*/                    }
                }
                DocFlavor flavor = null;
                if(missing(attr, required, this)) flavor = DocFlavor.INPUT_STREAM.AUTOSENSE;
                else flavor = DocFlavor.INPUT_STREAM.POSTSCRIPT;
                fr.close();
                for(Iterator i=services.iterator(); i.hasNext();) {
                    net.jini.print.service.PrintService ps = (net.jini.print.service.PrintService )i.next();
                    FileInputStream fis = new FileInputStream(in);
                    net.jini.print.job.DocPrintRequest req = ps.createDocPrintRequest();
                    req.setAttributes(attr);
                    req.setDoc(new org.jpwg.jini.print.data.InputStreamDoc(fis, flavor.getMimeType(), docAttr));
                    req.print(0L);
                }
                if(result!=null) result.push(in);
            }
            catch(FileNotFoundException e) {
                try {wait(500);}
                catch(Exception ex) {}
            }
            catch(Exception e) {
                debug(e);
            }
        }
    }
    public void main(String[] arg) {
//        copies2Pattern = Pattern.compile("%%Requirements:.*numcopies\\((\\d+)\\)", Pattern.CASE_INSENSITIVE);
        System.out.println(Pattern.matches("%%Requirements:.*numcopies\\((\\d+)\\)", "%%Requirements: numcopies(20) collate"));
    }
}

