/*
 * Filename: LpqReader.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.service.lpr;

import java.io.*;
import java.util.*;
import java.util.regex.*;
import javax.print.*;
import javax.print.attribute.*;
import javax.print.attribute.standard.*;
import com.millipad.util.*;

class LpqReader {
    static final String REPORT_TERMINATOR = "REPORT_DONE";
    BlockingStack stack;
    String command;
    //end report
    Pattern endPat;
    //normal
    Pattern resourcesNotReadyPat, jobQueuedPat, jobOutgoingPat, jobPrintingPat, printerStoppedPat, jobCompletedPat;
    //additional
    Pattern printerOpenPat, percentPat, pagePat, donePat;
    //abnormal
    Pattern printerClosePat, errorPat, comPat, noSyncPat, printerErrorPat, stallPat;
    Map warningCodes, errorCodes, reportCodes;
    ResourceBundle bundle;
    String lastOther = "";
    
    LpqReader(String queue) throws Exception {
        stack = new BlockingStack();
        endPat = Pattern.compile(REPORT_TERMINATOR);
        //normal progression, reasons are exclusive, state is processing
        //10%
        resourcesNotReadyPat = Pattern.compile(" Status: subserver pid \\d+ starting at ");
        //20%
        jobQueuedPat = Pattern.compile(" Filter_status: using model ");
        //30%
        jobOutgoingPat = Pattern.compile(" Filter_status: transferring (\\d+) bytes at ");
        //80%
        jobPrintingPat = Pattern.compile(" Filter_status: data sent at ");
        //90%
        printerStoppedPat = Pattern.compile(" Filter_status: JOB detected pjl JOB END ");
        //95%
        jobCompletedPat = Pattern.compile(" Status: finished '[a-zA-Z0-9[@+-]]+', status '(\\w+)' at ");
        //100% pcd deducted
        
        //additional
        printerOpenPat = Pattern.compile(" Status: processing ");
        percentPat = Pattern.compile(" Filter_status: (\\d+) percent done at ");
        pagePat = Pattern.compile(" Filter_status: pagecounter \\d+, pages ");
        donePat = Pattern.compile(" Filter_status: done at ");
        
        //abnormal
        printerClosePat = Pattern.compile(" Status: cannot open ");
        errorPat = Pattern.compile(" Filter_status: code = ");
        comPat = Pattern.compile(" Filter_status: printer not detected, trying again at ");
        noSyncPat = Pattern.compile(" Filter_status: no sync response from printer at ");
        printerErrorPat = Pattern.compile(" Filter_status: error = ");
        stallPat = Pattern.compile("stalled\\(");
        
        command = "lpq -P"+queue+" -L";
        
        errorCodes = new HashMap();
        bundle = ResourceBundle.getBundle(
        getClass().getPackage().getName()+".resource"
        );
        StringTokenizer tokenizer = new StringTokenizer(bundle.getString("service.errors"));
        for(;tokenizer.hasMoreTokens();) {
            String code = tokenizer.nextToken();
            String type = tokenizer.nextToken();
            PrinterStateReason reason = (PrinterStateReason )PrinterStateReason.class.getDeclaredField(type).get(null);
            errorCodes.put(code, reason);
        }
        warningCodes = new HashMap();
        tokenizer = new StringTokenizer(bundle.getString("service.warnings"));
        for(;tokenizer.hasMoreTokens();) {
            String code = tokenizer.nextToken();
            String type = tokenizer.nextToken();
            PrinterStateReason reason = (PrinterStateReason )PrinterStateReason.class.getDeclaredField(type).get(null);
            warningCodes.put(code, reason);
        }
        reportCodes = new HashMap();
        tokenizer = new StringTokenizer(bundle.getString("service.reports"));
        for(;tokenizer.hasMoreTokens();) {
            String code = tokenizer.nextToken();
            String type = tokenizer.nextToken();
            PrinterStateReason reason = (PrinterStateReason )PrinterStateReason.class.getDeclaredField(type).get(null);
            reportCodes.put(code, reason);
        }
        
        //start thread
        for(int i=0; i>0; i--) {
            BufferedReader reader;
            OutputStream os;
            Process p = Runtime.getRuntime().exec(bundle.getString("shell"));
            Thread t = new Thread(new Emptier(p.getErrorStream()));
            t.setPriority(Thread.MIN_PRIORITY);
            t.start();
            os = p.getOutputStream();
            //                    stack.push(new Object[] {os, reader});
        }
    }
    
    PrintJobAttributeSet getJobAttributes(boolean isDuplex) throws Exception {
        Process p = Runtime.getRuntime().exec(command);
        BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
        
        PrintJobAttributeSet set = new HashPrintJobAttributeSet();
        JobStateReasons reasons = new JobStateReasons();
        String lastCode = null;
        boolean printerOpen = false;
        for(String line = reader.readLine(); line!=null; line = reader.readLine()) {
            Matcher m;
            //terminator
            //                if(endPat.matcher(line).matches()) break;
            //read all the status info first
            if(resourcesNotReadyPat.matcher(line).lookingAt()) {
                set.clear();
                reasons.clear();
                reasons.add(JobStateReason.RESOURCES_ARE_NOT_READY);
                printerOpen = false;
                continue;
            }
            m = jobCompletedPat.matcher(line);
            if(m.lookingAt()) {
                reasons.clear();
                if("JSUCC".equals(m.group(1))) reasons.add(JobStateReason.JOB_COMPLETED_SUCCESSFULLY);
                else reasons.add(JobStateReason.ABORTED_BY_SYSTEM);
                printerOpen = false;
                continue;
            }
            if(printerOpenPat.matcher(line).lookingAt()) {
                printerOpen = true;
                continue;
            }
            if(printerClosePat.matcher(line).lookingAt()) {
                printerOpen = false;
                reasons.add(JobStateReason.SERVICE_OFF_LINE);
                continue;
            }
            //need the page counts
            m = pagePat.matcher(line);
            if(m.lookingAt()) {
                StringTokenizer token;
                token = new StringTokenizer(line.substring(m.end()));
                int pages = Integer.parseInt(token.nextToken());
                if(isDuplex) pages = (pages+1)/2;
                set.add(new JobMediaSheets(pages));
                set.add(new JobMediaSheetsCompleted(pages));
                continue;
            }
            if(!printerOpen) continue;
            reasons.remove(JobStateReason.ABORTED_BY_SYSTEM);
            reasons.remove(JobStateReason.JOB_COMPLETED_SUCCESSFULLY);
            //read all the filter status info
            m = jobQueuedPat.matcher(line);
            if(m.lookingAt()) {
                reasons.remove(JobStateReason.RESOURCES_ARE_NOT_READY);
                reasons.remove(JobStateReason.JOB_OUTGOING);
                reasons.remove(JobStateReason.JOB_PRINTING);
                reasons.remove(JobStateReason.PRINTER_STOPPED);
                reasons.add(JobStateReason.JOB_QUEUED);
                set.clear();
                continue;
            }
            m = jobOutgoingPat.matcher(line);
            if(m.lookingAt()) {
                reasons.remove(JobStateReason.RESOURCES_ARE_NOT_READY);
                reasons.remove(JobStateReason.JOB_QUEUED);
                reasons.remove(JobStateReason.JOB_PRINTING);
                reasons.remove(JobStateReason.PRINTER_STOPPED);
                reasons.add(JobStateReason.JOB_OUTGOING);
                set.add(new JobKOctets(Integer.parseInt(m.group(1))/1024));
                set.add(new JobKOctetsProcessed(0));
            }
            m = jobPrintingPat.matcher(line);
            if(m.lookingAt()) {
                reasons.remove(JobStateReason.RESOURCES_ARE_NOT_READY);
                reasons.remove(JobStateReason.JOB_QUEUED);
                reasons.remove(JobStateReason.JOB_OUTGOING);
                reasons.remove(JobStateReason.PRINTER_STOPPED);
                reasons.add(JobStateReason.JOB_PRINTING);
                JobKOctets octets = (JobKOctets )set.get(JobKOctets.class);
                if(octets!=null) set.add(new JobKOctetsProcessed(octets.getValue()));
                continue;
            }
            m = printerStoppedPat.matcher(line);
            if(m.lookingAt()) {
                reasons.remove(JobStateReason.RESOURCES_ARE_NOT_READY);
                reasons.remove(JobStateReason.JOB_QUEUED);
                reasons.remove(JobStateReason.JOB_OUTGOING);
                reasons.remove(JobStateReason.JOB_PRINTING);
                reasons.add(JobStateReason.PRINTER_STOPPED);
                continue;
            }
            //additional
            m = percentPat.matcher(line);
            if(m.lookingAt()) {
                int percent = Integer.parseInt(m.group(1));
                JobKOctets octets = (JobKOctets )set.get(JobKOctets.class);
                if(octets==null) continue;
                set.add(new JobKOctetsProcessed(octets.getValue()*percent/100));
                continue;
            }
            //abnormal
            m = noSyncPat.matcher(line);
            if(m.lookingAt()) {
                set.add(JobState.PROCESSING_STOPPED);
                continue;
            }
            m = comPat.matcher(line);
            if(m.lookingAt()) {
                reasons.add(JobStateReason.SERVICE_OFF_LINE);
                continue;
            }
            m = printerErrorPat.matcher(line);
            if(m.lookingAt()) {
                reasons.add(JobStateReason.DOCUMENT_FORMAT_ERROR);
                continue;
            }
            m = errorPat.matcher(line);
            if(m.lookingAt()) {
                StringTokenizer token;
                token = new StringTokenizer(line.substring(m.end()), ",");
                String code = token.nextToken();
                //special handeling requires the printer to be rebooted
                if("30114".equals(code)) {
                    reasons.add(JobStateReason.ABORTED_BY_SYSTEM);
                    continue;
                }
                lastCode = code;
                Object type;
                type = errorCodes.get(code);
                if(type==null) continue;
                set.add(JobState.PROCESSING_STOPPED);
                continue;
            }
        }
        if(!reasons.isEmpty()) {
            set.add(reasons);
            //                    debug(reasons+"");
        }
        if(reasons.contains(JobStateReason.ABORTED_BY_SYSTEM)) set.add(JobState.ABORTED);
        if(reasons.contains(JobStateReason.JOB_COMPLETED_SUCCESSFULLY)) set.add(JobState.COMPLETED);
        JobMediaSheetsCompleted sheets = (JobMediaSheetsCompleted )set.get(JobMediaSheetsCompleted.class);
        JobState state = (JobState )set.get(JobState.class);
        if(sheets!=null && state!=null) {
            if(state.equals(JobState.COMPLETED) && sheets.getValue()==0) {
                //                    debug("no sheets");
                set.remove(JobState.COMPLETED);
                set.add(JobState.ABORTED);
            }
        }
        p.destroy();
        return set;
    }
    
    PrintServiceAttributeSet getPrinterAttributes() throws Exception {
        Process p = Runtime.getRuntime().exec(command);
        BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
        
        PrintServiceAttributeSet set = new HashPrintServiceAttributeSet();
        PrinterStateReasons reasons = new PrinterStateReasons();
        String lastCode = null;
        boolean printerOpen = true;
        int pages = -1;
        for(String line = reader.readLine(); line!=null; line = reader.readLine()) {
            Matcher m;
            //terminator
            //                if(endPat.matcher(line).matches()) break;
            //read all the status info first
            if(resourcesNotReadyPat.matcher(line).lookingAt()) {
                set.clear();
                reasons.clear();
                set.add(PrinterState.STOPPED);
                printerOpen = false;
                continue;
            }
            m = jobCompletedPat.matcher(line);
            if(m.lookingAt()) {
                reasons.clear();
                set.add(PrinterState.IDLE);
                if("JTIMEOUT".equals(m.group(1))) reasons.put(PrinterStateReason.TIMED_OUT, Severity.ERROR);
                if("JFAIL".equals(m.group(1))) reasons.put(PrinterStateReason.OTHER, Severity.ERROR);
                printerOpen = false;
                continue;
            }
            if(printerOpenPat.matcher(line).lookingAt()) {
                printerOpen = true;
                set.add(PrinterState.STOPPED);
                continue;
            }
            if(printerClosePat.matcher(line).lookingAt()) {
                printerOpen = false;
                set.add(PrinterState.STOPPED);
                reasons.put(PrinterStateReason.CONNECTING_TO_DEVICE, Severity.ERROR);
                continue;
            }
            m = pagePat.matcher(line);
            if(m.lookingAt() && !printerOpen) {
                StringTokenizer token;
                token = new StringTokenizer(line.substring(m.end()));
                pages = Integer.parseInt(token.nextToken());
                continue;
            }
            //read all the filter status info
            if(!printerOpen) continue;
            set.add(PrinterState.PROCESSING);
            m = jobQueuedPat.matcher(line);
            if(m.lookingAt()) {
                set.add(PrinterState.PROCESSING);
                reasons.clear();
                continue;
            }
            //additional
            //abnormal
            if(comPat.matcher(line).lookingAt()) {
                set.add(PrinterState.STOPPED);
                reasons.put(PrinterStateReason.CONNECTING_TO_DEVICE, Severity.ERROR);
                continue;
            }
            if(noSyncPat.matcher(line).lookingAt()) {
                set.add(PrinterState.STOPPED);
                reasons.put(PrinterStateReason.CONNECTING_TO_DEVICE, Severity.ERROR);
                continue;
            }
            if(printerErrorPat.matcher(line).lookingAt()) {
                reasons.put(PrinterStateReason.INTERPRETER_RESOURCE_UNAVAILABLE, Severity.ERROR);
                continue;
            }
            if(stallPat.matcher(line).lookingAt()) {
                reasons.put(PrinterStateReason.TIMED_OUT, Severity.WARNING);
                continue;
            }
            m = errorPat.matcher(line);
            if(m.lookingAt()) {
                StringTokenizer token;
                token = new StringTokenizer(line.substring(m.end()), ",");
                String code = token.nextToken();
                Object type = null;
                Severity severity = null;
                if(type==null) {
                    type = warningCodes.get(code);
                    severity = Severity.WARNING;
                }
                if(type==null) {
                    type = errorCodes.get(code);
                    severity = Severity.ERROR;
                }
                if(type==null) {
                    type = reportCodes.get(code);
                    severity = Severity.REPORT;
                }
                if(type!=null) {
                    reasons.put(type, severity);
                    if(type.equals(PrinterStateReason.OTHER)) {
                        if(!lastOther.equals(line)) {
                            lastOther = line+"";
                            System.out.println("OTHER:" + line);
                        }
                    }
                }
                else {
                    int c = Integer.parseInt(code);
                    if(c>4000) {
                        reasons.put(PrinterStateReason.OTHER, Severity.ERROR);
                        debug(line);
                    }
                }
                continue;
            }
        }
        if(pages==0) reasons.put(PrinterStateReason.INTERPRETER_RESOURCE_UNAVAILABLE, Severity.ERROR);
        if(!reasons.isEmpty()) set.add(reasons);
        p.destroy();
//javax.print.attribute.Attribute[] a=set.toArray();
//for(int i=a.length-1; i>=0; i--) {
//debug("ps evt "+a[i]);
//}
        return set;
    }
    
    void debug(Object in) {Debug.print(LpqReader.class, in);}
    
}

