package org.autoplot.pngwalk;

import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.FileNotFoundException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import org.das2.components.DasProgressPanel;
import org.das2.datum.Datum;
import org.das2.datum.DatumRange;
import org.das2.datum.DatumRangeUtil;
import org.das2.util.DasPNGConstants;
import org.das2.util.DasPNGEncoder;
import org.das2.datum.TimeParser;
import org.das2.datum.Units;
import org.das2.datum.UnitsUtil;
import org.das2.datum.format.DatumFormatter;
import org.das2.datum.format.FormatStringFormatter;
import org.das2.util.ArgumentList;
import org.das2.util.ExceptionHandler;
import org.das2.util.FileUtil;
import org.das2.util.monitor.NullProgressMonitor;
import org.das2.util.monitor.ProgressMonitor;
import org.autoplot.ApplicationModel;
import org.autoplot.AutoplotUtil;
import org.autoplot.ScriptContext;
import org.autoplot.dom.Application;
import org.autoplot.dom.Plot;
import org.autoplot.state.StatePersistence;
import org.das2.qds.DataSetOps;
import org.das2.qds.DataSetUtil;
import org.das2.qds.QDataSet;
import org.das2.qds.SemanticOps;
import org.autoplot.datasource.URISplit;
import org.das2.qds.ops.Ops;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.BufferedWriter;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

/**
 * CreatePngWalk makes a sequence of images from a .vap file or the current state.
 * This is used with PngWalkTool to quickly flip through the images once they
 * are created.  This was once a Python script, but it got complex enough that it was useful to
 * rewrite it in Java.
 * @author jbf
 */
public class CreatePngWalk {
    
    /**
     * Get the list of times, which can be one of:<ul>
     *   <li> rank 2 bins datasets  T[index;min,max]
     *   <li> dataset with rank 2 bins datasets   Event[ T[index;min,max] ]
     * </ul>
     * This uses params.batchUri to get the URI that is resolved to control the times.  These
     * times then need to be formatted to filenames, or if params.batchUriName is "$o" then
     * the output filename is explicitly specified in the last column.
     * 
     * @param params
     * @return array of strings: filename: timeRange
     * @throws IllegalArgumentException
     * @throws ParseException 
     */
    private static String[] getListOfTimes( Params params, List<String> warnings ) throws IllegalArgumentException, ParseException {
        String[] times;
        if ( params.useBatchUri ) {
            try {
                String uri= params.batchUri;
                QDataSet timesds= org.autoplot.jythonsupport.Util.getDataSet( uri );
                times= new String[timesds.length()];
                
                if ( params.batchUriName.equals("") ) {
                    if ( !UnitsUtil.isTimeLocation( SemanticOps.getUnits(timesds) ) ) {
                        if ( (QDataSet) timesds.property(QDataSet.DEPEND_0)!=null ) {
                            timesds= (QDataSet) timesds.property(QDataSet.DEPEND_0);
                        } else if ( SemanticOps.isBundle(timesds) ) { // See EventsRenderer.makeCanonical
                            timesds= Ops.bundle( DataSetOps.unbundle(timesds,0), DataSetOps.unbundle(timesds,1) ); 
                        } else {
                            throw new IllegalArgumentException("expected events list URI");
                        }
                    }
                    if ( timesds.rank()!=2 ) {
                        timesds= Ops.createEvents( timesds );
                    }
                    if ( timesds.rank()!=2 ) {
                        throw new IllegalArgumentException("expected bins dataset for times");
                    }
                
                    TimeParser tp= TimeParser.create(params.timeFormat);
                    for ( int i=0; i<times.length; i++ ) {
                        times[i]= tp.format( DataSetUtil.asDatumRange( timesds.slice(i) ) ) + ": "+DataSetUtil.asDatumRange( timesds.slice(i) ).toString();
                    }
                } else {
                    timesds= Ops.createEvents(timesds); 
                    Units tu= ((Units)((QDataSet)timesds.property(QDataSet.BUNDLE_1)).property( QDataSet.UNITS, 0 ));
                    Units eu= ((Units)((QDataSet)timesds.property(QDataSet.BUNDLE_1)).property( QDataSet.UNITS, 3 ));
                    if ( uri.endsWith(".txt" ) ) { // hey it's just an orbits file...
                        logger.fine("reading events file to preserve identity of orbits.");
                        for ( int i=0; i<times.length; i++ ) {
                            String s1= eu.createDatum( timesds.slice(i).value(3) ).toString(); // orbit name
                            String s= s1 + ": " + "orbit:"+uri + ":" + s1;
                            times[i]= s;
                        }
                    } else {
                        logger.fine("reading events file as start/stop times.");
                        for ( int i=0; i<times.length; i++ ) {
                            times[i]= eu.createDatum( timesds.slice(i).value(3) ).toString() + ": " + DatumRange.newDatumRange( timesds.slice(i).value(0), timesds.slice(i).value(1), tu ); // TODO: this should be easier to code
                        }
                    }
                }
            } catch (Exception ex) {
                if ( ex instanceof IllegalArgumentException ){
                    throw (IllegalArgumentException)ex;
                } else {
                    throw new IllegalArgumentException(ex);
                }
            }
        } else {
            times = ScriptContext.generateTimeRanges(params.timeFormat, params.timeRangeStr);
        }
        return times;
    }
    
    /**
     * parameters specifying the creation of a pngwalk.
     */
    public static class Params {

        /**
         *  output folder for the walk, e.g. /home/user/pngwalk/
         */
        public String outputFolder=null;

        /**
         *  timerange to cover for the walk, e.g. 2012 through 2014.
         */
        public String timeRangeStr=null;

        /**
         *  rescale to show context for each step, e.g. "0%,100%" or "0%-1hr,100%+1hr"
         */
        public String rescalex= "0%,100%";

        /**
         * autorange dependent dimensions
         */
        public boolean autorange= true;

        /**
         * consider autorange flags for each axis.
         */
        public boolean autorangeFlags= true;

        /**
         * clone the dom and run on the copy, so the original Autoplot is free.
         */
        public boolean runOnCopy= true;
        
        /**
         *  version tag to apply to each image, if non-null
         */
        public String version= null;

        /**
         * product name for the walk, e.g. product
         */
        public String product=null;

        /**
         * timeformat for the walk, e.g. $Y$m$d
         */
        public String timeFormat=null;

        /**
         * Uri that creates an events dataset, like 
         * 'http://emfisis.physics.uiowa.edu/events/rbsp-a/burst/rbsp-a_burst_times_20130201.txt?eventListColumn=3'
         * or null for automatically generating names based on template.
         */
        public String batchUri=null;
        
        /**
         * if true, use the URI to source the list of events.
         */
        public boolean useBatchUri= false;
        
        /**
         * if non-null, use the name in the event list column instead of the 
         * product and timeFormat.  For example,
         * the batch file contains lines like:
         * 2015-03-05T02:10 2015-03-05T02:14 marsex/event1
         * so this png will have the name outputFolder + "marsex/event1" + ".png"
         * This must be either empty "" or "$o" for now.
         */
        public String batchUriName= "";
        
        /*
         * if true, the also create thumbs.
         */
        public boolean createThumbs= true;

        /**
         * if true, skip over products that appear to be created already.
         */
        public boolean update= false;

        /**
         * presently this is png or pdf
         */
        public String outputFormat = "png";
        
        /**
         * also write a .vap file
         */
        public boolean writeVap= true;
        
        @Override
        public String toString() {
            return String.format( "outputFolder=%s\ntimeRange=%s\nrescalex=%s\nautorange=%s\nversion=%s\nproduct=%s\ntimeFormat=%s\ncreateThumbs=%s\nupdate=%s\n",
                    outputFolder, timeRangeStr, rescalex, autorange, version, product, timeFormat, createThumbs, update  );
        }
    }

    private static final Logger logger= org.das2.util.LoggerManager.getLogger("autoplot.pngwalk");

    private static BufferedImage myWriteToPng(String filename, Application ldom, int width, int height) throws InterruptedException, FileNotFoundException, IOException {
        OutputStream out=null;
        BufferedImage image=null;
        try {
            File outf= new File( filename );
            File parentf= outf.getParentFile();
            if ( parentf!=null && !parentf.exists() ) {
                if ( !parentf.mkdirs() ) {
                    throw new IllegalArgumentException("failed to make directories "+parentf);
                }
            }
            out= new java.io.FileOutputStream(filename);
            image = (BufferedImage) ldom.getCanvases(0).getController().getDasCanvas().getImage(width, height);
            DasPNGEncoder encoder = new DasPNGEncoder(); // 20120525: tested against ImageIO.write comparable time and space.
            encoder.addText(DasPNGConstants.KEYWORD_CREATION_TIME, new java.util.Date().toString());
            encoder.addText(DasPNGConstants.KEYWORD_SOFTWARE, "Autoplot" );
            encoder.addText(DasPNGConstants.KEYWORD_PLOT_INFO, ldom.getCanvases(0).getController().getDasCanvas().getImageMetadata() );        

            encoder.write(image, out);
        } finally {
            if ( out!=null ) out.close();
        }
        if ( image==null ) throw new IllegalArgumentException("image not assigned, this shouldn't happen.");
        return image;
    }

    private static int returnCode1= 0;
    
    /**
     * run the pngwalk for the list of times.  The dom argument is copied so the
     * scientist can continue working while the pngwalk is run.
     * @param times list of times to run.  If a time contains a ": ", then the first part is the label and after is the exact time.
     * @param readOnlyDom the dom to render for each time.
     * @param params outputFolder and spec.
     * @param mon progress monitor to provide feedback about the run.
     * @return 0 if any were successful, 10 otherwise.
     * @throws IOException
     * @throws InterruptedException 
     */
    public static int doBatch( String[] times, Application readOnlyDom, Params params, ProgressMonitor mon ) throws IOException, InterruptedException {
    
        final ArrayList<String> pngFilenameArrayThumbs= new ArrayList();
        final ArrayList<String> pngFilenameArrayBig= new ArrayList();
        final ArrayList<String> timeLabels= new ArrayList();
    
        int returnCodeAll= 10;
        
        logger.log( Level.CONFIG, "CreatePngWalk.doBatch with params {0}", params);
        if ( !( params.outputFolder.endsWith("/") || params.outputFolder.endsWith("\\") ) ) {
            params.outputFolder= params.outputFolder + "/";
        }
        File outputFolder=  new java.io.File(params.outputFolder);
        if ( !outputFolder.exists() && !outputFolder.mkdirs() ) {
            throw new IOException( "failed mkdirs: "+outputFolder);
        }
        if ( !outputFolder.canWrite() ) {
            throw new IOException( "unable to write to folder "+outputFolder );
        }

        if (params.createThumbs) {
            File thumbsFolder= new java.io.File(params.outputFolder,"thumbs400/" );
            if ( !thumbsFolder.exists() && !( thumbsFolder.mkdirs() ) ) {
                throw new IOException( "failed mkdirs: "+thumbsFolder );
            }
            if ( !thumbsFolder.canWrite() ) {
                throw new IOException( "unable to write to folder "+thumbsFolder );
            }
        } else {
            File thumbsFolder= new java.io.File(params.outputFolder,"thumbs400/" );
            if ( thumbsFolder.exists() ) {
                System.err.println("warning: thumbs folder already exists!");
            }
        }

        int n = times.length;
        mon.setTaskSize(n);
        mon.started();

        try {
            mon.setProgressMessage("initializing child application");

            TimeParser tp = TimeParser.create(params.timeFormat);
            Application dom= (Application) readOnlyDom.copy();
            dom.getOptions().syncToAll( readOnlyDom.getOptions(), new ArrayList() );

            try {
                String atime= times[0];
                int ic= atime.indexOf(": ");
                String exactTime; 
                if ( ic>-1 ) { // rfe batchfile time.
                    exactTime= atime.substring(ic+2);
                } else {
                    exactTime= atime;
                }
                // set the initial timerange to avoid an extraneous load.
                if ( params.useBatchUri ) {
                    DatumRange tr1= DatumRangeUtil.parseTimeRange(exactTime);
                    dom.setTimeRange(tr1);        
                } else {
                    DatumRange tr1= tp.parse(exactTime).getTimeRange(); 
                    dom.setTimeRange(tr1);        
                }
            } catch ( ParseException ex ) {
                throw new RuntimeException(ex);
            }

            Application dom2;
            int w0,h0;

            if ( params.runOnCopy ) {
                ApplicationModel appmodel = new ApplicationModel();
                appmodel.addDasPeersToAppAndWait();

                dom2= appmodel.getDocumentModel();

                mon.setProgressMessage("synchronize to this application");

                dom2.getCanvases(0).setHeight( dom.getCanvases(0).getHeight() );
                dom2.getCanvases(0).setWidth( dom.getCanvases(0).getWidth() );
                w0 = dom2.getCanvases(0).getWidth();
                h0 = dom2.getCanvases(0).getHeight();
                dom2.getCanvases(0).getController().getDasCanvas().setSize( w0, h0 );
                dom2.getCanvases(0).getController().getDasCanvas().revalidate();

                dom2.syncTo( dom, java.util.Arrays.asList("id") );
                dom2.getController().waitUntilIdle();
                dom2.syncTo( dom, java.util.Arrays.asList("id") ); // work around bug where someone resets the margin column http://jfaden.net:8080/hudson/job/autoplot-test033/5786/artifact/
                dom2.getOptions().syncToAll( readOnlyDom.getOptions(), new ArrayList() ); // 1165 grid overlay

            } else {
                dom2= readOnlyDom;
                w0= dom2.getCanvases(0).getWidth();
                h0 = dom2.getCanvases(0).getHeight();
            }

            ApplicationModel appmodel= dom2.getController().getApplicationModel();

            int thumbSize = 400;

            int thumbH = 0, thumbW = 0;
            if (params.createThumbs) {
                double aspect = 1. * w0 / h0;
                thumbH = (int) (Math.sqrt(Math.pow(thumbSize, 2) / (aspect * aspect + 1.)));
                thumbW = (int) (thumbH * aspect);
            }

            // Write out the vap file to product.vap
            if ( params.writeVap ) {
                mon.setProgressMessage("write " + params.product + ".vap");
                logger.log(Level.FINE, "write {0}.vap", params.product);
                StatePersistence.saveState(new java.io.File( outputFolder, params.product + ".vap"), dom2, "");
            }

            String vap= new java.io.File( outputFolder, params.product + ".vap").toString();
            StringBuilder build= new StringBuilder();
            build.append( String.format( "JAVA -cp autoplot.jar org.autoplot.pngwalk.CreatePngWalk " ) );

            try ( PrintWriter ff = new PrintWriter( new FileWriter( new java.io.File( outputFolder, params.product + ".pngwalk" ) ) ) ) {  // Write out the parameters used to create this pngwalk in product.pngwalk

                build.append("--vap=").append(vap).append( " ");
                build.append("--outputFolder=").append(params.outputFolder).append( " ");

                ff.println( "# set the following line to the location of the pngwalk");
                ff.println( "baseurl=." );
                ff.println( "product=" + params.product );
                build.append("--product=").append(params.product).append( " ");
                ff.println( "timeFormat=" + params.timeFormat );
                build.append("--timeFormat='").append(params.timeFormat).append( "' ");

                if ( params.useBatchUri==false ) {
                    ff.println( "timeRange=" + params.timeRangeStr );
                    build.append("--timeRange='").append(params.timeRangeStr).append( "' ");
                }

                if ( params.batchUriName.equals("$o") ) {
                    ff.println( "# the filePattern may need editing, depending on extension and subdirectories.");
                    ff.println( "filePattern=*.png");
                }

                if ( params.useBatchUri ) {
                    if ( params.batchUri!=null && !params.batchUri.equals("") ) {
                        ff.println( "batchUri=" + params.batchUri );
                        build.append("--batchUri=").append(params.batchUri).append(" ");
                    }
                    if ( !params.batchUriName.equals("") ) {
                        ff.println( "batchUriName=" + params.batchUri );
                        build.append("--batchUriName=").append(params.batchUri).append(" ");
                    }
                }

                if ( params.rescalex!=null && !params.rescalex.equals("0%,100%") ) {
                    ff.println( "rescalex="+ params.rescalex );
                    build.append("--rescalex=").append(params.rescalex).append(" ");
                }
                if ( params.autorange ) {
                    ff.println( "autorange="+ params.autorange );
                    build.append("--autorange=").append(params.autorange).append(" ");
                }
                if ( params.autorangeFlags ) {
                    ff.println( "autorangeFlags="+ params.autorangeFlags );
                    build.append("--autorangeFlags=").append(params.autorangeFlags).append(" ");
                }
                if ( params.version!=null && params.version.trim().length()>0 ) {
                    ff.println( "version="+ params.version );
                    build.append("--version=").append( params.version);
                }

                if ( !params.outputFormat.equals("png") ) {
                    ff.println( "outputFormat="+ params.outputFormat );
                    build.append("--outputFormat=").append( params.outputFormat );
                }

            }

            if ( !( mon instanceof NullProgressMonitor ) ) { // only show in interactive session
                System.err.println( build.toString() );
            }

            dom2.getController().waitUntilIdle();

            mon.setProgressMessage("making images");

            long t0 = java.lang.System.currentTimeMillis();
            int count = 0;

            appmodel.setExceptionHandler( new ExceptionHandler() {
                @Override
                public void handle(Throwable t) {
                    logger.log( Level.WARNING, null, t );
                    returnCode1= 11;
                }
                @Override
                public void handleUncaught(Throwable t) {
                    logger.log( Level.WARNING, null, t );
                    returnCode1= 12;
                }
            });

            //LoggerManager.setEnableTimers(true);
            //LoggerManager.setTimerLogfile("/tmp/foo.autoplot.txt");

            String currentTimeLabel;
            for ( String atime : times ) {

                //LoggerManager.resetTimer();

                returnCode1= 0;

                int ic= atime.indexOf(": ");
                String exactTime= null; 
                if ( ic>-1 ) { // rfe batchfile time.
                    exactTime= atime.substring(ic+2);
                    atime= atime.substring(0,ic);
                }

                //LoggerManager.markTime("455");

                String filename= getFilename( params, "", atime );

                /**
                * Code for adding images into global arrayList for use in HTML method
                * @author Armond Luthens
                * @date 09/21/2015
                */
                pngFilenameArrayThumbs.add( getRelativeFilename( params, "thumbs100", atime ) );
                pngFilenameArrayBig.add( getRelativeFilename( params, "", atime ) );

                //LoggerManager.markTime("469");

                count = count + 1;
                if (mon.isCancelled()) {
                    break;
                }
                mon.setTaskProgress(count);

                if ( params.update ) {
                    File out= new File( filename );
                    if ( out.exists() ) {
                        mon.setProgressMessage( String.format("skipping %s", filename ) );
                        logger.log( Level.FINE, String.format("skipping %s", filename ) );
                        continue;
                    }
                }

                //LoggerManager.markTime("486");

                try {
                    DatumRange dr;
                    if ( exactTime==null ) {
                        dr= tp.parse(atime).getTimeRange();
                    } else {
                        dr= DatumRangeUtil.parseTimeRange(exactTime);
                    }
                    if ( params.rescalex!=null ) {
                        String rescalex= params.rescalex.trim();
                        if ( rescalex.length()>0 && !params.rescalex.equals("0%,100%") ) {
                            dr= DatumRangeUtil.rescale( dr,params.rescalex );
                        }
                    }
                    currentTimeLabel= dr.toString();
                    timeLabels.add(currentTimeLabel);

                    if ( !dom2.getTimeRange().equals(dr) ) { // don't even call it for one png--I don't think it matters.
                        dom2.setTimeRange(dr);
                    }

                } catch (ParseException ex) {
                    logger.log(Level.SEVERE, ex.getMessage(), ex);
                }
                mon.setProgressMessage( String.format("write %s", filename ) );
                logger.log( Level.FINE, String.format("write %s", filename ) );

                //LoggerManager.markTime("514");
                appmodel.waitUntilIdle();
                //LoggerManager.markTime("516");

                if ( params.autorange ) {
                    if (params.autorangeFlags) {
                        for ( Plot p: dom2.getPlots() ) {
                            if ( p.getYaxis().isAutoRange() ) {
                                AutoplotUtil.resetZoomY(dom2,p);
                            }
                            if ( p.getZaxis().isAutoRange() ) {
                                AutoplotUtil.resetZoomZ(dom2,p);
                            }
                        }
                    } else {
                        for ( Plot p: dom2.getPlots() ) {
                            dom2.getController().setPlot(p);
                            AutoplotUtil.resetZoomY(dom2);
                            AutoplotUtil.resetZoomZ(dom2);
                        }
                    }
                }
                //LoggerManager.markTime("526");
                appmodel.waitUntilIdle();

                //LoggerManager.markTime("529");

                if ( atime.equals(times[0]) ) { // resetting zoomY and zoomZ can cause the labels and bounds to change.  Turn off autoranging.
                    dom2.getOptions().setAutolayout(false);
                    appmodel.waitUntilIdle();
                }

                BufferedImage image = null;

                //LoggerManager.markTime("538");

                try {
                    if ( params.outputFormat.equals("png") ) {
                        image= myWriteToPng(filename, dom2, w0, h0);
                    } else {
                        dom2.getCanvases(0).getController().getDasCanvas().writeToPDF(filename);
                    }
                } catch ( IOException ex ) {
                    logger.log( Level.SEVERE, "unable to write file "+filename, ex );
                    throw new IOException("unable to write file "+filename,ex);
                }
                //LoggerManager.markTime("548");

                if ( returnCode1==0 ) {
                    returnCodeAll= 0;
                } else if ( returnCodeAll==10 ) {
                    returnCodeAll= returnCode1;
                }

                if ( params.createThumbs && params.outputFormat.equals("png") ) {
                    BufferedImage thumb400 = ImageResize.getScaledInstance(image, thumbW, thumbH, RenderingHints.VALUE_INTERPOLATION_BILINEAR, true);
                    File outf= new java.io.File( getFilename(params, "thumbs400", atime ) );
                    File parentf= outf.getParentFile();
                    if ( parentf!=null && !parentf.exists() ) {
                        if ( !parentf.mkdirs() ) {
                            throw new IllegalArgumentException("failed to make directories: "+parentf);
                        }
                    }
                    if ( !ImageIO.write(thumb400, "png", outf ) ) {
                        throw new IllegalArgumentException("no appropriate writer is found");
                    }
                    BufferedImage thumb100 = ImageResize.getScaledInstance(thumb400, thumbW/4, thumbH/4, RenderingHints.VALUE_INTERPOLATION_BILINEAR, true);
                    outf= new java.io.File( getFilename( params, "thumbs100", atime ) );
                    parentf= outf.getParentFile();
                    if ( parentf!=null && !parentf.exists() ) {
                        if ( !parentf.mkdirs() ) {
                            throw new IllegalArgumentException("failed to make directories: "+parentf);
                        }
                    }
                    if ( !ImageIO.write(thumb100, "png", outf ) ) {
                        throw new IllegalArgumentException("no appropriate writer is found");
                    }
                }

                //LoggerManager.markTime("581");

                double imagesPerSec = count * 1000. / (java.lang.System.currentTimeMillis() - t0);
                double etaSec= (n-count) / imagesPerSec;
                String etaStr= "";
                if ( count>3 ) {
                    Datum eta= org.das2.datum.DatumUtil.asOrderOneUnits( Units.seconds.createDatum(etaSec) );
                    DatumFormatter df;
                    df= new FormatStringFormatter("%.1f",true);
                    etaStr= String.format( Locale.US, ", eta %s", df.format(eta) ); 
                }
                if ( imagesPerSec<1.0 ) {
                    mon.setAdditionalInfo(String.format( Locale.US, "(%.1f/min%s)", imagesPerSec*60, etaStr ) );
                } else {
                    mon.setAdditionalInfo(String.format( Locale.US, "(%.1f/sec%s)", imagesPerSec, etaStr ) );
                }
                //LoggerManager.markTime("597");
            }

            //LoggerManager.setEnableTimers(false);

            if ( !mon.isCancelled() ) {
                writeHTMLFile( params, pngFilenameArrayThumbs, pngFilenameArrayBig, timeLabels );
            }
        
        } finally {
            if ( !mon.isFinished() ) mon.finished();
        }
        return returnCodeAll;

    }

    /**
     * create the filename for the time.
     * @param params the parameters
     * @param thumbdir "" or "thumbs100" or "thumbs400"
     * @param atime the time "20150822"
     * @return
     * @throws IllegalArgumentException 
     */
    private static String getFilename(Params params, String thumbdir, String atime) throws IllegalArgumentException {
        String filename;
        if ( thumbdir.length()>0 && !thumbdir.endsWith("/") ) {
            thumbdir= thumbdir + "/";
        }
        if ( params.useBatchUri && params.batchUriName.equals("$o") ) {
            String name= atime; // really?
            // sometimes we want capitalized extention.
            String outputFormat= params.outputFormat;
            if ( name.toLowerCase().endsWith(params.outputFormat) ) {
                outputFormat= name.substring(name.length()-outputFormat.length());
                name= name.substring(0,name.length()-(params.outputFormat.length()+1));
            }
            filename= String.format("%s%s%s.%s", params.outputFolder, thumbdir, name, outputFormat );
        } else if ( params.useBatchUri && !params.batchUriName.equals("") ) {
            throw new IllegalArgumentException("batchUriName must be \"\" or \"$o\"");
        } else {
            String vers= ( params.version==null || params.version.trim().length()==0 ) ? "" : "_"+params.version.trim();
            filename= String.format("%s%s%s_%s%s.%s", params.outputFolder, thumbdir, params.product, atime, vers, params.outputFormat );
        }
        return filename;
    }

    /**
     * create the filename for the time.
     * @param params the parameters
     * @param thumbdir "" or "thumbs100" or "thumbs400"
     * @param atime the time "20150822"
     * @return
     * @throws IllegalArgumentException 
     */
    private static String getRelativeFilename(Params params, String thumbdir, String atime) throws IllegalArgumentException {
        String filename;
        if ( thumbdir.length()>0 && !thumbdir.endsWith("/") ) {
            thumbdir= thumbdir + "/";
        }
        if ( params.useBatchUri && params.batchUriName.equals("$o") ) {
            String name= atime; // really?
            // sometimes we want capitalized extention.
            String outputFormat= params.outputFormat;
            if ( name.toLowerCase().endsWith(params.outputFormat) ) {
                outputFormat= name.substring(name.length()-outputFormat.length());
                name= name.substring(0,name.length()-(params.outputFormat.length()+1));
            }
            filename= String.format("%s%s.%s", thumbdir, name, outputFormat );
        } else if ( params.useBatchUri && !params.batchUriName.equals("") ) {
            throw new IllegalArgumentException("batchUriName must be \"\" or \"$o\"");
        } else {
            String vers= ( params.version==null || params.version.trim().length()==0 ) ? "" : "_"+params.version.trim();
            filename= String.format("%s%s_%s%s.%s", thumbdir, params.product, atime, vers, params.outputFormat );
        }
        return filename;
    }

    /**
     * run the pngwalk. If the params are null, then prompt the user with a GUI.
     * The pngwalk is run by resetting the timeRange field of the vap to each step
     * of the sequence.
     * @param dom the state from which a pngwalk is to be produced.
     * @param params a parameters structure (e.g. batch processing) or null.
     * @return an integer exit code where 0=success, 10=bad time format, 11=caught exception, 12=uncaught exception
     * @throws ParseException
     * @throws IOException
     * @throws InterruptedException 
     */
    public static int doIt(Application dom, Params params) throws ParseException, IOException, InterruptedException {
        int status= 0;
        
        if (params == null) {

            CreatePngWalkDialog p = new CreatePngWalkDialog();

            if ( AutoplotUtil.showConfirmDialog(ScriptContext.getViewWindow(), p, "Create PngWalk Options", JOptionPane.OK_CANCEL_OPTION) == JOptionPane.OK_OPTION) {
                
                p.writeDefaults();

                params= p.getParams();

                File ff= new File( params.outputFolder );
                if ( p.getOverwriteCb().isSelected() && ff.exists() ) {
                    FileUtil.deleteFileTree(ff);
                }

                ProgressMonitor mon;
                if (ScriptContext.getViewWindow() == null) {
                    mon = new NullProgressMonitor();
                    System.err.println("ScriptContext.getViewWindow is null, running quietly in the background.");
                } else {
                    mon = DasProgressPanel.createFramed(ScriptContext.getViewWindow(), "running batch");
                }

                if ( params.timeFormat.length()>0 ) {
                    TimeParser tp= TimeParser.create(params.timeFormat);
                    if ( !tp.isNested() ) {
                        JOptionPane.showMessageDialog( ScriptContext.getViewWindow(), "<html>Time spec must have fields nested: $Y,$m,$d, etc,<br>not "+params.timeFormat + " ." );
                        return -1;
                    }
                }

                String[] times = getListOfTimes( params, new ArrayList() );

                status= doBatch( times, dom, params, mon );

                String url;
                if (!mon.isCancelled()) {
                    if (params.outputFolder.charAt(1) == ':') {
                        url = "file:/" + params.outputFolder;
                    } else {
                        url = "file:" + params.outputFolder;
                    }

                    if ( ScriptContext.getViewWindow() != null && params.outputFormat.equals("png" ) ) {
                        logger.log(Level.FINE, "version=\"{0}\"", String.valueOf(params.version));
                        String vers= ( params.version==null || params.version.trim().length()==0 ) ? "" : "_"+params.version.trim();
                        String st1;
                        if ( params.batchUriName.length()==0 ) {
                            st1= url + params.product + "_" + params.timeFormat + vers + ".png";
                        } else {
                            st1= url + "*.png";
                        }
                        final String st=st1;
                        SwingUtilities.invokeLater( new Runnable() {
                            @Override
                            public void run() {
                                PngWalkTool.start( st, ScriptContext.getViewWindow() );
                            }
                        } );
                    } else if ( ScriptContext.getViewWindow() != null ) {
                        String vers= ( params.version==null || params.version.trim().length()==0 ) ? "" : "_"+params.version.trim();
                        final String st= url + params.product + "_" + params.timeFormat + vers + "." + params.outputFormat;
                        JOptionPane.showMessageDialog( ScriptContext.getViewWindow(), "<html>Files created:<br>"+st );
                    }
                }
            }

        } else {
            
            String[] times = getListOfTimes( params, new ArrayList() );
            
            ProgressMonitor mon;
            if (ScriptContext.getViewWindow() == null) {
                if ( "true".equals( System.getProperty("java.awt.headless","false") ) ) {
                    mon = new NullProgressMonitor();
                } else {
                    mon = DasProgressPanel.createFramed( "running batch" );
                }
            } else {
                mon = DasProgressPanel.createFramed(ScriptContext.getViewWindow(), "running batch");
            }

            status= doBatch(times, dom, params, mon);

        }
        return status;
    }
    
    /**
     * Method to write HTML file of all the pictures to give a gallery view
     * @author Armond Luthens
     * @param params
     * @param pngFilenameArrayThumbs
     * @param pngFilenameArrayBig
     * @param timeLabels
     *
     */
    public static void writeHTMLFile( Params params, ArrayList<String> pngFilenameArrayThumbs, ArrayList<String> pngFilenameArrayBig, ArrayList<String> timeLabels ){
        
        if ( params.update || ( timeLabels.size()!=pngFilenameArrayBig.size() ) ) {
            logger.info("skipping create HTML step because of partial run");
            return;
        }
        
        String filePath= params.outputFolder+""+ params.product + ".html";
        //String filePath = "pngImagePage2.html";
        File f= new File(filePath);
        
        String htmlOpen= "<html>\n";
        String htmlHead="\t<head><title>PNG Gallery "+params.product+"</title></head>\n";
        String htmlBody="\t<body style=\"background-color: #6B6B6B; margin:0;\">\n";
        String htmlClose1= "\t\t</div>\n";
        String htmlClose2= "\t</body>\n";
        String htmlClose3= "</html>";
        
        //String pageHeaderOpen= "\t\t<div style=\"padding:20px; top: 0px; margin-right=0px; background-color:black; color:white;height:30px;\">\n\t\t\t"
        //            + "<strong>" + params.product + "_"+ params.timeFormat + "</strong>\n" + "\t\t</div>\n";
        
        String pageHeaderOpen= "\t\t<div style=\"padding:20px; top: 0px; margin-right:0px; background-color:black; color:white;height:30px;\">\n\t\t\t"
                    + "<strong>" + "PNG WALK" + "</strong>\n" + "\t\t</div>\n";
        String addImageString;
        String htmlAnchorStringOpen = "\t\t\t\t<a href=\"";
        String htmlAnchorStringClose = "\">\n";
        String htmlImageStringOpen = "\t\t\t\t<img src=\"";
        String htmlImageStringClose = "\" style=\"width:72px;height:68px;margin-left:10px;margin-bottom:10px;\"></a>\n";
        String htmlImageCaptionOpen = "\t\t\t\t<figcaption style=\"color: white; text-align:center;\">";
        String htmlImageCaptionClose = "\t\t\t\t</figcaption>\n";
        String htmlImageContainer = "\t\t<div style=\"background-color: #6B6B6B;margin-left:20px;\">\n";
        String htmlFigureOpen = "\t\t\t<figure style=\"width:78px; float:left;\">\n";
        String htmlFigureClose = "\t\t\t</figure>\n";
        
        String currentPngFilename;
        String currentPngFilenameBIG;
        String fileNameToDisplay;
        String bigImageLink;
        String fullImageCaption;
        int count=0;
        
        try ( BufferedWriter bw = new BufferedWriter(new FileWriter(f)) ) {
            bw.write(htmlOpen);
            bw.write(htmlHead);
            bw.write(htmlBody);
            bw.write(pageHeaderOpen);
            bw.write(htmlImageContainer);
            
            for (String pngFilenameArray1 : pngFilenameArrayThumbs) {
                
                currentPngFilename = pngFilenameArray1;
                //System.out.println("image file path: " + currentPngFilename);
                
                currentPngFilenameBIG = pngFilenameArrayBig.get(count);
                fileNameToDisplay= timeLabels.get(count);
                count++;
                
                bigImageLink= htmlAnchorStringOpen + currentPngFilenameBIG + htmlAnchorStringClose;
                addImageString= htmlImageStringOpen+currentPngFilename+htmlImageStringClose; //insert image into html code
                fullImageCaption= htmlImageCaptionOpen + fileNameToDisplay + htmlImageCaptionClose; //insert corresponding date for image into html code
                
                bw.write(htmlFigureOpen);
                bw.write(bigImageLink);
                bw.write(addImageString);
                bw.write(fullImageCaption);
                bw.write(htmlFigureClose);
            }
            bw.write(htmlClose1);
            bw.write(htmlClose2);
            bw.write(htmlClose3);
        } catch (IOException e) {
            logger.log( Level.WARNING, e.getMessage(), e );
        }
    }

    /**
     * command-line support for creating PNGWalks.  When PNGWalks are created 
     * interactively in Autoplot, this is used as well.
     * @param args see the code for the argument list.
     * @throws InterruptedException
     * @throws ParseException
     * @throws IOException 
     */
    public static void main( String[] args ) throws InterruptedException, ParseException, IOException {
        
        System.err.println("CreatePngWalk 20180725");
        final ArgumentList alm = new ArgumentList("CreatePngWalk");
        alm.addOptionalSwitchArgument( "timeFormat", "f", "timeFormat", "$Y$m$d", "timeformat for png files, e.g. $Y is year, $j is day of year");
        alm.addOptionalSwitchArgument( "timeRange", "r", "timeRange", "", "time range to cover, e.g. 2011 through 2012" );
        alm.requireOneOf( new String[] { "timeRange","batchUri" } );
        alm.addOptionalSwitchArgument( "batchUri", "b", "batchUri", "", "optionally provide list of timeranges" );
        alm.addOptionalSwitchArgument( "batchUriName", null, "batchUriName", "", "use $o to use the filename in the batch file" );
        alm.addOptionalSwitchArgument( "createThumbs", "t", "createThumbs", "y", "create thumbnails, y (default) or n" );
        alm.addOptionalSwitchArgument( "product", "n", "product", "product", "product name in each filename (default=product)");
        alm.addOptionalSwitchArgument( "outputFolder", "o", "outputFolder", "pngwalk", "location of root of pngwalk");
        alm.addOptionalSwitchArgument( "outputFormat", null, "outputFormat", "png", "output format png or pdf");
        alm.addSwitchArgument( "vap", "v", "vap", "vap file or URI to plot");
        alm.addOptionalSwitchArgument( "rescalex", null, "rescalex", "0%,100%", "rescale factor, such as '0%-1hr,100%+1hr', to provide context to each image");
        alm.addOptionalSwitchArgument( "version", null, "version", null, "additional version string to add to each filename, like v1.0");
        alm.addBooleanSwitchArgument( "autorange", null, "autorange", "rerange dependent dimensions Y and Z");
        alm.addBooleanSwitchArgument( "autorangeFlags", null, "autorangeFlags", "only autorange axes with autorange=true");
        alm.addBooleanSwitchArgument( "update", null, "update", "only calculate missing images");
        alm.addBooleanSwitchArgument( "testException", null, "testException", "throw a runtime exception to test exit code");
        alm.process(args);

        if ( alm.getBooleanValue("testException") ) {
            throw new RuntimeException("--textException on command line, throwing exception");
            // verified, 20130627.
            // java -cp autoplot.jar org.autoplot.pngwalk.CreatePngWalk --vap=x --testException
            // echo $? -> 1
            // Note, no files found does not yeild non-zero exit code!
        }
        
        if ( System.getProperty( "noCheckCertificate","true").equals("true") ) {
            logger.fine("disabling HTTP certificate checks.");
            try {
                TrustManager[] trustAllCerts = new TrustManager[]{
                    new X509TrustManager() {
                        @Override
                        public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                            return new java.security.cert.X509Certificate[0];
                        }
                        
                        @Override
                        public void checkClientTrusted(X509Certificate[] certs, String authType) {  }
                        
                        @Override
                        public void checkServerTrusted(X509Certificate[] certs, String authType) {  }
                        
                    }
                };
                
                SSLContext sc = SSLContext.getInstance("SSL");
                sc.init(null, trustAllCerts, new java.security.SecureRandom());
                HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
                
                // Create all-trusting host name verifier
                HostnameVerifier allHostsValid = new HostnameVerifier() {
                    @Override
                    public boolean verify(String hostname, SSLSession session) {
                        return true;
                    }
                };
                
                HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid);
                
            } catch (NoSuchAlgorithmException | KeyManagementException ex) {
                logger.log(Level.SEVERE, ex.getMessage(), ex);
            }
        }

        Params params= new Params();
        params.createThumbs= alm.getValue("createThumbs").equals("y");
        params.outputFolder= alm.getValue("outputFolder");
        params.product= alm.getValue("product");
        params.timeFormat= alm.getValue("timeFormat");
        params.timeRangeStr= alm.getValue("timeRange");
        params.rescalex= alm.getValue("rescalex");
        params.version= alm.getValue("version");
        params.autorange= alm.getBooleanValue("autorange");
        params.autorangeFlags= alm.getBooleanValue("autorangeFlags");
        params.update= alm.getBooleanValue("update");
        params.batchUri= alm.getValue("batchUri");
        if ( params.batchUri!=null && params.batchUri.length()>0 ) {
            params.useBatchUri= true;
            params.batchUriName= alm.getValue("batchUriName");
        }
        params.outputFormat= alm.getValue("outputFormat");
        String vap= alm.getValue("vap");
        if ( ( vap.length()>2 && vap.charAt(1)==':' ) ) {
            logger.fine("reference appears to be absolute (Windows)");
        } else {
            vap= URISplit.makeAbsolute(new File(".").getAbsolutePath(),vap);
        }

        Application dom= (Application) StatePersistence.restoreState(new File(vap));
        if ( vap.contains(params.outputFolder) ) {
            params.writeVap= false;
        }
        
        int status= doIt( dom, params );
        
        System.exit(status); // something starts up thread that prevents java from exiting.
    }
}