/*
 * Filename: LprRequester.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.io.*;
import java.net.*;
import java.nio.*;
import java.nio.channels.*;

/**
 * The LprRequester takes cf and df files from the spool directory of a lpr queue
 * and generates a print request from that information.  The df file will be
 * treated as the raw data for the Doc.  The cf file will be used to generate
 * attributes for printing the Doc.
 * @author <a href="mailto:jw168@columbia.edu">Joe Wang</a>
 * @since 1.0
 */
public class LprRequester extends Requester {
    
    ResourceBundle bundle;
    OutputDeviceAssigned inputDevice;
    FileInputStream jobIS;
    Set jobs;
    Set services;
    Map mapping;
    Requester[] postscript;
    BlockingStack resultStack;
    long maxLength = 1L;
    
    public LprRequester() throws Exception {
        bundle = ResourceBundle.getBundle(getClass().getPackage().getName()+".resource");
        
        mapping = new HashMap();
        resultStack = new BlockingStack();
        setInputDevice("");
        postscript = new Requester[Integer.parseInt(bundle.getString("LprRequester.threads"))];
        for(int i=postscript.length-1; i>=0; i--) {
            postscript[i] = new PostscriptRequester();
        }
        AttributeSet attr = new HashPrintServiceAttributeSet();
        jobs = new HashSet();
        Thread t = new Thread(new Dequeue(resultStack));
        t.setPriority(Thread.MIN_PRIORITY);
        t.start();
    }
    
    public void setInputDevice(String device) {
        inputDevice = new OutputDeviceAssigned(device, null);
    }

    void debug(Object in) {Debug.print(LprRequester.class, in);}
    
    /**
     * add a printService to direct produced requests
     * @param service the print service that will be accepting requests
     */
    public void addPrintService(net.jini.print.service.PrintService service) {
        for(int i=postscript.length-1; i>=0; i--) {
            postscript[i].addPrintService(service);
        }
    }
    
    synchronized void waitTillComplete(File file) throws IOException {
        long size = 0;
        for(int i=4;; i--) {
            if(file.exists()) break;
            if(i==0) throw new IOException(file.getName()+" non-existant");
            try {wait(200);}
            catch(Exception e) {
                debug(e);
                continue;
            }
        }
        RandomAccessFile raf = new RandomAccessFile(file, "r");
        FileChannel channel = raf.getChannel();
        for(;;) {
            try {
                wait(100);
                long newSize = channel.size();
                if(newSize!=size) break;
            }
            catch(Exception e) {
                e.printStackTrace();
                continue;
            }
        }
        raf.close();
    }
    
    /**
     * sort by file size
     * attempt to create a PrintRequest given a CF file
     * @param cf The cf file to parse for information
     * @param attr The existing set of attributes
     * @param required The attributes required to successfully complete
     * @return REQUEST_FAILED if we could not produce a valid request else REQUEST_DISPATCHED
     */
    public int produceRequest(File in, PrintRequestAttributeSet attr, Class[] required, BlockingStack result) throws IOException {
        String name = in.getName();
        int index = name.indexOf("hf");
        if(index!=0) {
            return REQUEST_FAILED;
        }
        File dir = in.getParentFile();
        try {
            //get the attributes for the job
            waitTillComplete(in);
            //getDF
            String[] dfs = parse(in, attr);
            if(missing(attr, required, this)) {
                for(int i=dfs.length-1; i>=0; i--) {
                    File spl = new File(dir, dfs[i]);
                    waitTillComplete(spl);
                }
                int res = REQUEST_DISPATCHED;
                if(dfs.length==0) {
                    return REQUEST_FAILED;
                }
                //select here
                File out = new File(dir, dfs[0]);
                if(out.length()>maxLength) maxLength=out.length();
                int bucket = (int )((postscript.length-1)*out.length()/maxLength);
                mapping.put(out, new Object[] {
                    name.substring(3,6),
                    result, in
                });
                if(postscript[bucket].produceRequest(out, attr, required, resultStack)!=REQUEST_DISPATCHED) {
                    res = REQUEST_FAILED;
                }
                if(!attr.containsKey(Destination.class)) {
                    res = REQUEST_FAILED;
                }
                //deque junk
                URI uri = ((URISyntax )attr.get(Destination.class)).getURI();
                return res;
            }
//            assert true : "Obtained all attributes without df";
        }
//        catch(FileNotFoundException e) {
            //e.printStackTrace();
//        }
        catch(Exception e) {
//            debug(e);
        }
        return REQUEST_FAILED;
    }
    
    String getParam(String in) {
        if (in == null) return null;

        String[] kvPair = in.split("=");
        if (kvPair.length >= 2) return kvPair[1];
        
        return null;
    }
    
    String[] parse(File shd, PrintRequestAttributeSet set) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(
        new FileInputStream(shd)));
        String user = "";
        String host = "";
        String z = "";
        Set dfs = new HashSet();
        for(;;) {
            String line = reader.readLine();
            if(line==null) break;
            if(line.startsWith("H")) host = getParam(line);
            else if(line.startsWith("Z")) z = getParam(line);
            else if(line.startsWith("P")) user = getParam(line);
            else if(line.startsWith("J")) {
                JobName name;
                name = new JobName(getParam(line), null);
                set.add(name);
            }
            else if(line.startsWith("Q")) {
                try {
                    set.add(new Destination(
                    new URI("printservice://"+inputDevice.getValue())
                    //new URI("printservice://"+getParam(line))
                    ));
                }
                catch(Exception e) {debug(e);}
            }
            else if(line.startsWith("datafiles")) dfs.add(getParam(line));
        }
        set.add(new RequestingUserName(user+"@"+host, null));
        if(z.indexOf("duplex")>=0) set.add(Sides.TWO_SIDED_LONG_EDGE);
        reader.close();
        return (String[] )dfs.toArray(new String[dfs.size()]);
    }
    
    //send lpr command
    class Dequeue implements Runnable {
        BlockingStack stack;
        OutputStream os;
        Dequeue(BlockingStack stack) throws Exception {
            this.stack = stack;
            Process p = Runtime.getRuntime().exec(bundle.getString("LprRequester.shell"));
            Thread t;
            t = new Thread(new Emptier(p.getErrorStream()));
            t.setPriority(Thread.MIN_PRIORITY);
            t.start();
            t = new Thread(new Emptier(p.getInputStream()));
            t.setPriority(Thread.MIN_PRIORITY);
            t.start();
            os = p.getOutputStream();
        }
        public synchronized void run() {
            for(;;) {
                try {
                    File file = (File )resultStack.pop();
                    Object[] v = (Object[] )mapping.get(file);
                    if(v==null) continue;
                    //            debug("remove "+file+" "+v[0]);
                    os.write(("lprm -P"+inputDevice.getValue()+" "+v[0]+"\n").getBytes());
                    //debug("lprm -P"+v[0]+" "+v[1]+"\n");
                    os.flush();
                    //wait(3*1000);
                    BlockingStack result = (BlockingStack )v[1];
                    result.push(v[2]);
                    result.push(file);
                    file.delete();
                }
                catch(Exception e) {
                    debug(e);
                }
            }
        }
    }
    
    class Emptier implements Runnable {
        InputStream is;
        Emptier(InputStream is) { this.is = is; }
        public void run() {
            try {
                for(int b = 0; b>=0;) b=is.read();
                is.close();
            }
            catch(IOException e) {
                debug(e);
            }
            //system.exit ?
        }
    }
    
}

