/**
 * Nap 0.2
 * Copyright 2007 Zach Scrivena
 * 2007-05-03
 * zachscrivena@gmail.com
 *
 * Pauses for a specified duration, and displays a countdown.
 *
 * TERMS & CONDITIONS:
 * 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
 * (at your option) 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., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

import java.io.InputStreamReader;
import java.util.regex.Pattern;
import java.util.regex.Matcher;

/**
 * Nap pauses for a specified duration, and displays a countdown.
 */
public class Nap
{   
    /**
    * PROGRAM PARAMETERS
    */
   
    /* Program title */
    private static final String programTitle =
            "Nap 0.2   Copyright 2007 Zach Scrivena   2007-05-03";
   
    /* Width of console display; used for erasing displayed output */
    private static final int consoleWidth = 80;
   
    /* Refresh interval in milliseconds */
    private static final int refreshIntervalMs = 150;
   
    /* Verbosity level; default is 2 */
    private static int verbosity = 2;
   
    /* Allow user to abort countdown */
    private static boolean checkUserAbort = false;
   
    /* Pause before exiting program */
    private static boolean pauseBeforeExit = false;
   
    /* User abort keys (ENTER) */
    private static final String userAbortKeys = "\n\r";
   
    /* Nap duration as specified on the command-line */
    private static Hmss duration = null;   
   
   
    /**
    * Main entry point for program.
    */
    public static void main(final String args[]) throws Exception
    {
        /* process command-line arguments */
        processArguments(args);
       
        /* Nap duration in milliseconds */     
        final long durationMs = hmssToMs(Nap.duration);
               
        /* backspaces to the first column of the console output */
        StringBuffer backspaces = new StringBuffer();
        for (int i = 0; i < consoleWidth; i++) backspaces.append('\b');
       
        /* print header */
        if (Nap.verbosity > 0)
        {
            StringBuffer header = new StringBuffer();
       
            header.append("\n");
           
            if (Nap.checkUserAbort)
                header.append("Press ENTER to abort countdown...\n\n");
           
            if (Nap.verbosity == 1)
            {
                header.append("Nap for " + Nap.duration + "...");
            }
            else if (Nap.verbosity == 4)
            {
                header.append(" Nap duration" +
                        "\n HH:mm:ss.SSS" +
                        "\n " + Nap.duration +
                        "\n" +
                        "\n   Elapsed       Countdown" +
                        "\n HH:mm:ss.SSS   HH:mm:ss.SSS" +
                        "\n");
            }
           
            System.out.print(header);
            System.out.flush();
        }          
       
        /* display countdown and time elapsed, in real-time */
        Hmss elapsedHmss, countdownHmss;
        long elapsedurationMs, countdownMs;
       
        /* start time, in milliseconds since the epoch */
        final long startMs = System.currentTimeMillis();
       
        /* buffered reader to get user input, if necessary */
        InputStreamReader userInput = null;
        if (Nap.checkUserAbort)
            userInput = new InputStreamReader(System.in);
       
        /* indicates if user has aborted the countdown */
        boolean abortNow = false;
               
        while (true)
        {
            /* refresh time counters */
            elapsedurationMs = System.currentTimeMillis() - startMs;
            elapsedHmss = msToHmss(elapsedurationMs);
            countdownMs = durationMs - elapsedurationMs;
            countdownHmss = msToHmss(countdownMs);
           
            /* time is up! */
            if (elapsedurationMs > durationMs)
                break;
               
            /* update display */
            if (Nap.verbosity == 2)
            {
                System.out.print(backspaces + "Nap for " + countdownHmss + "...     ");
                System.out.flush();
            }
            else if (Nap.verbosity == 3)
            {
                System.out.print(backspaces + "Nap for " + countdownHmss + " (" + elapsedHmss + " elapsed)...     ");
                System.out.flush();
            }
            else if (Nap.verbosity == 4)
            {
                System.out.print(backspaces + " " + elapsedHmss + "   " + countdownHmss + "     ");
                System.out.flush();
            }
           
            /* check if user has hit ENTER to abort countdown */
            if (Nap.checkUserAbort)
            {              
                try
                {
                    while (userInput.ready())
                    {
                        if (Nap.userAbortKeys.contains("" + ((char) userInput.read())))
                        {
                            abortNow = true;
                            break;
                        }
                    }
                }
                catch (Exception e)
                {
                    /* do nothing */
                }              
            }
               
            /* check if user has aborted the countdown */
            if (abortNow)
                break;
           
            /* pause the thread for a while (to prevent excessive CPU usage) */
            Thread.sleep(refreshIntervalMs);
        }
       
        if (!abortNow)
        {
            /* print the final time */
            if (Nap.verbosity == 1)
            {
                System.out.print("\n");
            }
            else if (Nap.verbosity == 2)
            {
                System.out.print(backspaces + "Nap for 00:00:00.000...     \n");
            }
            else if (Nap.verbosity == 3)
            {
                System.out.print(backspaces + "Nap for 00:00:00.000 (" + Nap.duration + " elapsed)...     \n");
            }
            else if (Nap.verbosity == 4)
            {
                System.out.print(backspaces + " " + Nap.duration + "   00:00:00.000     \n" );
            }
        }
       
        if (Nap.pauseBeforeExit)
        {
            if (verbosity > 0)
            {
                System.out.print("\nPress ENTER to continue...");
                System.out.flush();
            }
           
            (new InputStreamReader(System.in)).read();         
        }
       
        /* successful termination */
        if (verbosity > 0)
            System.out.print("\n");
       
        System.exit(0);    
    }
   
   
    /**
    * Convenience method for printing to standard output.
    *
    * @params o  object to be printed.
    */
    /*
    private static void p(
            final Object o)
    {
        System.out.print(o + "");
        System.out.flush();
    }
    */
   
   
    /**
    * Convert the specified time duration in milliseconds to a Hmss object.
    *
    * @param ms  duration in milliseconds to be converted
    * @return Hmss representation
    */
    private static Hmss msToHmss(
            long ms)
    {
        /* return value */
        Hmss h = new Hmss();
        h.HH = ms / (1000 * 60 * 60);
        ms %= (1000 * 60 * 60);
        h.mm = ms / (1000 * 60);
        ms %= (1000 * 60);
        h.ss = ms / (1000);
        ms %= (1000);
        h.SSS = ms;    
        return h;
    }
   
   
    /**
    * Convert the duration represented by the specified Hmss object
    * to milliseconds.
    *
    * @param h  Hmss object to be converted.
    * @return millisecond representation of the specified Hmss object
    */
    private static long hmssToMs(
            final Hmss h)
    {
        /* return value (number of milliseconds) */
        long ms = 0;       
        ms += (h.SSS);
        ms += (1000 * h.ss);
        ms += (1000 * 60 * h.mm);
        ms += (1000 * 60 * 60 * h.HH);
        return ms;
    }
   
   
    /**
    * Parses a specified string into a Hmss object.
    *
    * @param s  string to be parsed
    * @return Hmss object corresponding to the specified string; null on failure
    */
    private static Hmss parseStringToHmss(
            final String s)
    {
        /* return value */
        final Hmss h = new Hmss();
        h.HH  = 0;
        h.mm  = 0;
        h.ss  = 0;
        h.SSS = 0;
       
        /* Parse using the format HH:mm:ss.SSS */
        final Matcher m = Pattern.compile(
                "(?:(?:([0-9]*)\\:)?([0-9]*)\\:)?([0-9]*)(?:\\.([0-9]*))?").matcher(s);
       
        if (m.matches())
        {
            String HH = m.group(1);
            String mm = m.group(2);
            String ss = m.group(3);
            String SSS = m.group(4);
           
            /* Parse substrings */
            if ((HH != null) && (HH.length() > 0))
                h.HH = Long.parseLong(HH);
           
            if ((mm != null) && (mm.length() > 0))
                h.mm = Long.parseLong(mm);
           
            if ((ss != null) && (ss.length() > 0))
                h.ss = Long.parseLong(ss);
           
            if ((SSS != null) && (SSS.length() > 0))
                h.SSS = (long) (1000.0 * Double.parseDouble("0." + SSS));
           
            /* Handle overflows */
            h.ss  += (h.SSS / 1000);
            h.SSS %= 1000;
           
            h.mm  += (h.ss / 60);
            h.ss %= 60;
           
            h.HH  += (h.mm / 60);
            h.mm %= 60;
           
            return h;
        }
       
        /* unable to parse specified string */
        return null;
    }
   
   
    /**
    * Process command-line arguments.
    *
    * @param args  Array of command-line argument strings
    */
    private static void processArguments(
            final String args[])
    {
        /* error message */
        String err = null;

        CheckArguments:
        do
        {
            /* run one iteration */

            if (args.length == 0)
            {
                /* print usage help */
                printUsage();
                System.exit(0);
                break CheckArguments;
            }
            else if (args.length < 1)
            {
                err = "Insufficient arguments.";
                break CheckArguments;
            }

            /* Nap duration */
            String duration = args[args.length - 1].trim();
            Nap.duration = parseStringToHmss(duration);
           
            if (Nap.duration == null)
            {
                err = "Unable to parse the specified duration \"" + duration + "\".";
                break CheckArguments;
            }
               
            /* process switches */
            for (int i = 0; i < args.length - 1; i++)
            {
                final String sw = args[i].trim();

                if ("-0".equals(sw) || "-s".equals(sw))
                {
                    Nap.verbosity = 0;
                }
                else if ("-1".equals(sw))
                {
                    Nap.verbosity = 1;
                }
                else if ("-2".equals(sw))
                {
                    Nap.verbosity = 2;
                }
                else if ("-3".equals(sw))
                {
                    Nap.verbosity = 3;
                }
                else if ("-4".equals(sw) || "-v".equals(sw))
                {
                    Nap.verbosity = 4;
                }
                else if ("-u".equals(sw))
                {
                    Nap.checkUserAbort = true;
                }
                else if ("-p".equals(sw))
                {
                    Nap.pauseBeforeExit = true;
                }
                else
                {
                    err ="Invalid switch " + sw + ".";
                    break CheckArguments;
                }
            }
        }
        while (false);

        /* Invalid command-line arguments encountered */
        if (err != null)
        {
            System.err.print("\n" + Nap.programTitle + "\n\nERROR: " + err +
                    "\nTo display help, run Nap without any command-line arguments.\n");
            System.exit(1);
        }
    }

       
    /**
    * Class representing the HH, mm, ss, and SSS corresponding to a time duration.
    */
    static class Hmss
    {
        long HH;   // hours
        long mm;   // minutes
        long ss;   // seconds
        long SSS;  // fractions of a second, i.e. "0.SSS second"
           
        /**
        * Return string representation of "HH:mm:ss.SSS".
        *
        * @return string representation in the form HH:mm:ss.SSS
        */
        public String toString()
        {
            return String.format("%02d:%02d:%02d.%03d", HH, mm, ss, SSS);
        }
    }   
   
   
    /**
    * Prints out usage syntax, notes, and comments.
    */
    private static void printUsage()
    {
        /* RULER   00000000011111111112222222222333333333344444444445555555555666666666677777777778 */
        /*         12345678901234567890123456789012345678901234567890123456789012345678901234567890 */
        System.out.print(
                "\n" + Nap.programTitle + "\n" +
                "\nNap pauses for a specified duration, and displays a countdown." +
                "\n" +
                "\nUSAGE:   java -jar Nap.jar  <switches>  [duration]" +
                "\n" +
                "\n <Switches>:" +
                "\n   -[0|1|2|3|4]  verbosity level: -0 is least verbose; -4 is most verbose" +
                "\n                  (default verbosity level is 2)" +
                "\n   -s            silent mode;  equivalent to -0" +
                "\n   -v            verbose mode; equivalent to -4" +
                "\n   -u            allow user to abort countdown" +
                "\n   -p            pause before exiting" +
                "\n" +
                "\n [duration]:" +
                "\n   The nap duration, in the form HH:mm:ss.SSS" +
                "\n" +
                "\nNOTES:" +
                "\n" +
                "\n 1. Nap is very flexible when parsing the specified duration. For example, two" +
                "\n     minutes can be given as \"00:02:00.000\" or \"2:00\" or \"2:\" or even \"120\"." +
                "\n" +
                "\nEXAMPLES:" +
                "\n" +
                "\n 1. Nap for 24 hours, at verbosity level 3:" +
                "\n       java -jar Nap.jar -3 24:00:00" +
                "\n 2. Nap for 2.5 seconds, in silent mode:" +
                "\n       java -jar Nap.jar -s 2.5" +
                "\n 3. Nap for 1 minute, at verbosity level 4, and allow user to abort countdown:" +
                "\n       java -jar Nap.jar -4 -u 1:00" +
                "\n 4. Nap for 10 seconds, and pause before exiting:" +
                "\n       java -jar Nap.jar -p 10" +
                "\n\n");
    }
}

/**
 * BUILD NOTES:
 *
 * Compile:
 * javac -Xlint Nap.java
 *
 * Package into JAR executable:
 * jar cvfm Nap.jar manifest.txt Nap*.class
 */