package com.knutejohnson.pi.motiondetection;

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.io.*;
import java.nio.charset.*;
import java.security.*;
import java.security.spec.*;
import java.time.*;
import java.time.format.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.*;
import static java.util.stream.Collectors.*;
import javax.activation.*;
import javax.crypto.*; 
import javax.crypto.spec.*;
import javax.imageio.*;
import javax.mail.*;
import javax.mail.internet.*;
import javax.net.ssl.*;
import javax.mail.util.*;
import javax.swing.*;
import java.util.*;

/**
 * <div>
 * MotionDetection is a program for the RaspberryPi computer and camera to take
 * a series of photos and detect motion in the images.  It does this by
 * dividing the image up into a series of boxes of about 1% of the image
 * dimensions.  These boxes are then compared for an average brightness and if
 * the brightness difference between the same area on the two images exceeds
 * the threshold in a minimum number of boxes the image is marked as having
 * motion.  The marked images may be saved to storage or emailed.  Either the
 * last image taken or a composite image made up of the last two images and a
 * set of blue boxes marking the areas that exceeded the brightness threshold
 * may be displayed.  The dimensions of the captured images and the size of the
 * image displayed on the program may be selected from program menus.  All
 * program parameters are saved to a file stored in the user's home directory.
 * </div>
 * <div style="margin-top: 30px;">
 * </div>
 * <table class="striped" style="border: 1px solid black; border-collapse: collapse; padding: 4px;">
 * <caption>Program Revisions</caption>
 * <tr><th>Version<th>Date<th>Modification
 * <tr><td>0.10.0beta<td style="white-space: nowrap;">08 Jan 2017 <td>incept
 * <tr><td>0.12.0beta<td>09 Jan 2017<td>fix bug in directory file chooser
 * <tr><td>0.13.0beta<td>10 Jan 2017<td>rewrite directory selection code and make minor changes to comments and directions
 * <tr><td>0.20.0<td>12 Jan 2017<td>move the image processing code off to a separate thread
 * <tr><td>0.21.0<td>13 Jan 2017<td>correct bad index in image processing code and get rid of one thread
 * <tr><td>0.21.1<td>15 Jan 2017<td>clean up javadocs and comments
 * <tr><td>0.22.0<td>08 Feb 2017<td>set the raspistill timeout to 1000ms to get the exposure correct on the images
 * <tr><td>0.23.0<td>09 Feb 2017<td>make sure to close the InputStream from the process running raspistill
 * <tr><td>0.23.1<td>16 Feb 2017<td>minor changes to the options passed to raspistill
 * <tr><td>0.24.0<td>23 Mar 2017<td>add alligator to main thread to destroy process if it hangs
 * <tr><td>0.25.0<td>05 Apr 2017<td>change the command line options to raspistill to get the image slightly faster
 * <tr><td>0.30.0<td style="white-space: nowrap;">05 May 2017<td>change the way we are using raspistill to run it in timelapse mode and collect the image files to a ramdisk then read those files off the disk and put them in the processing queue - this allows us to collect images from raspistill every 500ms even with large image sizes
 * <tr><td>0.31.0<td>06 May 2017<td>clean up and simplify the ProcessBuilder code
 * <tr><td>0.32.0<td>07 May 2017<td>more code cleanup
 * <tr><td>0.33.0<td>08 May 2017<td>simplify ProcessManager code, organize code in CaptureSizeAction and stop(), and increase the image processing threads to four
 * <tr><td>0.40.0<td>09 May 2017<td>get rid of the transfer queue and associated code and clean up the processImage method
 * <tr><td>0.41.0<td>10 May 2017<td>clean up the code that keeps track of the display aspect ratio and add some 3x2 aspect ratio capture sizes
 * <tr><td>0.42.0<td>12 May 2017<td>store properties whenever data changes rather than just when the program stops
 * <tr><td>0.50.0<td>14 May 2017<td>add option to email captured photos
 * <tr><td>0.51.0<td>15 May 2017<td>add code to default the SSL email protocols
 * <tr><td>0.51.1<td>15 May 2017<td>clean up javadocs and code comments
 * <tr><td>0.52.0<td>15 May 2017<td>fixed a bug where the image about to be saved or emailed was overwritten by the composite image and clean up the status messages to the console
 * <tr><td>0.60.0<td>18 May 2017<td>change storeProperties method to run in new thread and do a lot of cleanup in processImage
 * <tr><td>0.60.1<td>19 May 2017<td>remove some redundant variables
 * <tr><td>0.70.0<td>09 Jun 2017<td>rewrite of the image scan code provided by Federico Pedemonte
 * <tr><td>0.71.0<td>10 Jun 2017<td>make the dot colors static variables and clean up the ramdisk file deletion code
 * <tr><td>0.72.0<td>19 Feb 2018<td>rewrite sendImage() to use a multipart message and replace several for loops with streams
 * <tr><td>0.73.0<td>07 Apr 2018<td>make some minor speed improvements in the processImage method
 * <tr><td>0.74.0<td>08 Apr 2018<td>improve timing code for testing
 * <tr><td>0.75.0<td>17 Apr 2018<td>add option to time stamp the images
 * <tr><td>0.76.0<td>18 Apr 2018<td>queue images for emailing rather than running multiple emailling threads simultaneously and add the milliseconds to image file names to prevent images being overwritten when more than one was taken in a given second
 * <tr><td>0.77.0<td>19 Apr 2018<td>modify time stamp code to draw in white or black depending on the brightness of the image
 * <tr><td>0.77.1<td>19 Apr 2018<td>update directions for the time stamp option
 * <tr><td>0.78.0<td>19 Apr 2018<td>change the time stamp code to adjust the font for the width of the image instead of the height
 * <tr><td>0.78.1<td>20 Apr 2018<td>rearrange the capture image sizes by aspect ratio and make minor comment and variable changes
 * <tr><td>0.78.2<td>21 Apr 2018<td>update program description in the javadocs and in the directions
 * <tr><td>0.78.3<td>21 Apr 2018<td>make a small optimization in the processImage method when testing for points
 * <tr><td>0.78.4<td>21 Apr 2018<td>minor changes to the stop method
 * <tr><td>0.80.0<td>23 Apr 2018<td>raspistill now writes to one file on the ramdisk, the processImage method has been changed to read the one file and then delete it, this simplifies the image handling and reduces the storage requirements on the ramdisk, in the createComposite method use the old image as the drawing surface instead of creating a new BufferedImage
 * <tr><td>0.81.0<td>24 Apr 2018<td>fix a bug I introduced into the createComposite method with version 0.80
 * <tr><td>0.82.0<td>18 Oct 2018<td>add --thumb none option to raspistill to reduce file size
 * <tr><td>0.83.0<td>09 Dec 2018<td>move the image time stamp to earlier in the processImage method so that saved and emailed files would be stamped correctly
 * <tr><td>0.84.0<td>10 Dec 2018<td>make changes to compile scripts, manifest file and the emailImage method to prepare for JDK11
 * <tr><td>0.85.0<td>03 Mar 2019<td>run through spotbugs and correct bug in RGB bit shift operations 
 * <tr><td>0.85.1<td>26 Mar 2019<td>minor housekeeping
 * <tr><td>0.86.0<td>14 Jul 2019<td>turn off raspistill white balance to hopefully fix a problem that showed up in Buster
 * <tr><td>0.87.0<td>21 Jul 2019<td>remove the changes from 0.86.0, not required
 * <tr><td>0.87.1<td>24 Jul 2019<td>minor doc change
 * <tr><td>0.87.2<td>14 Oct 2019<td>test some new code and minor doc fixes
 * <tr><td>0.88.0<td>06 Aug 2020<td>add short delay to emailImage() allow time for multiple images to be put in one email
 * <tr><td>0.90.0<td>30 Aug 2020<td>eliminate ram disk usage and the ProcessManager class
 * <tr><td>0.91.0<td>07 Sep 2020<td>change timestamp to white letters with black outline to make it more readable
 * <tr><td>0.91.1<td>07 Sep 2020<td>remove unused variable from the processImage method and clean up its javadoc
 * <tr><td>0.92.0<td>12 Sep 2020<td>clean up comments and fix minor bug in processImage()
 * <tr><td>0.93.0<td>12 Sep 2020<td>modify EmailSettings to use AES encryption for the email password instead of the rotate obfuscation
 * <tr><td>0.94.0<td>13 Sep 2020<td>fix exception not caught bug in AES#decrypt
 * <tr><td>0.95.0<td>14 Nov 2020<td>improve status reporting on camera thread
 * <tr><td>0.95.1<td>17 Nov 2020<td>edit Directions
 * </table>
 *
 * @author  Knute Johnson
 * @version 0.95.1 17 November 2020
 */
public class MotionDetection extends JFrame implements Runnable {
    /** Serial version UID */
    private static final long serialVersionUID = 1L;

    /** Program version */
    public static final String VERSION = "0.95.1";

    /** Program date */
    public static final String DATE = "17 November 2020";

    /** Capture size labels */
    private static final String[] CAPTURE_SIZE_LABELS = {
     "640x480     (4x3)",
     "720x540     (4x3)",
     "800x600     (4x3)",
     "1024x768   (4x3)",
     "1366x1024 (4x3)",
     "1600x1200 (4x3)",
     "1920x1440 (4x3)",
     "2592x1944 (4x3)",
     "3280x2464 (4x3)",
     "600x400     (3x2)",
     "720x480     (3x2)",
     "900x600     (3x2)",
     "1200x800   (3x2)",
     "1350x900   (3x2)",
     "1500x1000 (3x2)",
     "1800x1200 (3x2)",
     "2592x1728 (3x2)",
     "3210x2140 (3x2)",
     "640x360     (16x9)",
     "720x405     (16x9)",
     "800x450     (16x9)",
     "1024x576   (16x9)",
     "1366x768   (16x9)",
     "1600x900   (16x9)",
     "1920x1080 (16x9)",
     "2592x1458 (16x9)",
     "3280x1845 (16x9)",
    };

    /** Display size labels */
    private static final String[] DISPLAY_SIZE_LABELS = {
     "640","800","1024","1200" };

    /** INI file name */
    private static final String INI_FILE_NAME = ".motiondetection";

    /** Number of processors */
    private static final int PROCESSORS =
     Runtime.getRuntime().availableProcessors();

    /** Transparent black color for dot */
    private static final Color TRANSPARENT_BLACK = new Color(0,0,0,0);

    /** Translucent red color for dot */
    private static final Color TRANSLUCENT_RED = new Color(255,0,0,160);

    /** Translucent green color for dot */
    private static final Color TRANSLUCENT_GREEN = new Color(0,255,0,160);

    /** Flag to print processing times on console */
    private static boolean timesFlag;

    /** Date pattern for saved image files */
    private static final String DATE_PATTERN = "yyyyMMddHHmmssSSS";

    /** Date format for saved image files */
    private static final DateTimeFormatter IMAGE_FILE_FORMATTER =
     DateTimeFormatter.ofPattern(DATE_PATTERN);

    /** Date patter for image time stamps */
    private static final String TIME_STAMP_PATTERN = "yyyy/MM/dd HH:mm:ss zzz";

    /** Date format for image time stamps */
    private static final DateTimeFormatter TIME_STAMP_FORMATTER =
     DateTimeFormatter.ofPattern(TIME_STAMP_PATTERN);

    /** Encryption key for email password */
    private static final String KEY = "*enkryptionKII*!";

    /** Main program thread */
    private final Thread cameraThread;

    /** Image processor thread */
    private final Thread processorThread;

    /** Email thread */
    private final Thread emailThread;

    /** AES encrytor/decryptor */
    private final AES aes = new AES();

    /** Main thread run flag */
    private volatile boolean runFlag;

    /** Capture image width */
    private volatile int captureWidth = 640;

    /** Capture image height */
    private volatile int captureHeight = 480;

    /** Display aspect ratio */
    private double aspectRatio = (double)captureWidth / captureHeight;

    /** Display image width */
    private int displayWidth = 640;

    /** Display image height */
    private int displayHeight = 480;

    /** Flag if images are to be saved */
    private volatile boolean saveFile;

    /** Flag if images are to be emailed */
    private volatile boolean emailImage;

    /** Flag if images are to be time stamped */
    private volatile boolean timeStampImage;

    /** Default time stamp font */
    private static final Font timeStampFont =
     new Font(Font.SANS_SERIF,Font.PLAIN,14);

    /** Alpha composite used to create composite image for the display */
    private static final AlphaComposite ALPHA_COMP =
         AlphaComposite.getInstance(AlphaComposite.SRC_OVER,0.60f);

    /** Flag if composite images are to be displayed */
    private volatile boolean showComposite;

    /** File chooser for saved images directory */
    private final JFileChooser fileChooser;

    /** Directory where captured images are written */
    private volatile File directory = new File(System.getProperty("user.dir"));

    /** Display panel for images */
    private final ImageJPanel imagePanel;

    /** RenderingHints for all image drawing */
    private final RenderingHints hints;

    /** Threshold value */
    private volatile double threshold = 10;

    /** Minimum boxes value */
    private volatile double minBoxes = 2;
    
    /** Maximum boxes value */
    private volatile double maxBoxes = 40;

    /** Properties to store values set from menus */
    private final Properties properties = new Properties();

    /** Queue to hold images from camera thread to process thread */
    private final LinkedTransferQueue<BufferedImage> queue =
     new LinkedTransferQueue<>();

    /** Previous image */
    private BufferedImage imageOld;

    /** Current image */
    private BufferedImage imageNew;

    /** Previous image box values */
    private int [][] brightOld;

    /** Current image box values */
    private int [][] brightNew;

    /** Queue for images to be emailed */
    private final BlockingQueue<BufferedImage> imageQueue =
     new LinkedBlockingQueue<>();

    /** Image timing data map keys, used for testing */
    private enum Keys {
     /** processing */
     PROC,
     /** reading */
     READ,
     /** scanning */
     SCAN,
     /** time stamping */
     STMP,
     /** saving */
     SAVE,
     /** sending */
     SEND,
     /** creating composite */
     COMP };


    /** Timing data map, used for testing */
    private final Map<Keys,java.util.List<Long>> timingMap = new HashMap<>();

    /**
     * Creates a new MotionDetection program
     */
    public MotionDetection() {
        super("MotionDetection - " + VERSION + " - " + DATE);

        System.out.printf("available processors: %d%n",PROCESSORS);

        // create the list used to hold the timing values and store them with
        // the appropriate key in the timing hash map
        if (timesFlag) {
            for (Keys key : Keys.values())
                timingMap.put(key,new ArrayList<Long>());
        }

        // read the properties file
        try (FileReader reader = new FileReader(new File(
         System.getProperty("user.home"),INI_FILE_NAME))) {
            properties.load(reader);
        } catch (IOException ioe) {
            JOptionPane.showMessageDialog(null,
             "Error reading properties file!\n" + ioe + "\nUsing defaults.\n" +
             "NOTE: This normal the first time the program is run.",
             "MotionDetection",JOptionPane.WARNING_MESSAGE);
        }
        properties.setProperty("mail.smtp.ssl.trust","*");

        // set values from the properties
        directory = new File(properties.getProperty("captureDirectory",
         directory.getPath()));
        saveFile = Boolean.parseBoolean(properties.getProperty("saveFile",
         Boolean.toString(saveFile)));
        emailImage = Boolean.parseBoolean(properties.getProperty("emailImage",
         Boolean.toString(emailImage)));
        timeStampImage = Boolean.parseBoolean(properties.getProperty(
         "timeStampImage",Boolean.toString(timeStampImage)));
        showComposite = Boolean.parseBoolean(properties.getProperty(
         "showComposite",Boolean.toString(showComposite)));
        threshold = Double.parseDouble(properties.getProperty("threshold",
         Double.toString(threshold)));
        minBoxes = Double.parseDouble(properties.getProperty("minBoxes",
         Double.toString(minBoxes)));
        maxBoxes = Double.parseDouble(properties.getProperty("maxBoxes",
         Double.toString(maxBoxes)));
        captureWidth = Integer.parseInt(properties.getProperty("captureWidth",
         Integer.toString(captureWidth)));
        captureHeight = Integer.parseInt(properties.getProperty("captureHeight",
         Integer.toString(captureHeight)));
        displayWidth = Integer.parseInt(properties.getProperty("displayWidth",
         Integer.toString(displayWidth)));
        displayHeight = Integer.parseInt(properties.getProperty("displayHeight",
         Integer.toString(displayHeight)));
        aspectRatio = (double)captureWidth / captureHeight;

        // set up the file chooser to display only directories
        fileChooser = new JFileChooser(directory);
        fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
        fileChooser.setAcceptAllFileFilterUsed(false);
        fileChooser.addChoosableFileFilter(
         new javax.swing.filechooser.FileFilter() {
             @Override public boolean accept(File f) {
                 return f.isDirectory();
             }
             @Override public String getDescription() {
                return "All Directories";
             }
        });

        // set up rendering hints for image drawing
        hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING,
         RenderingHints.VALUE_ANTIALIAS_ON);
        hints.add(new RenderingHints(RenderingHints.KEY_RENDERING,
         RenderingHints.VALUE_RENDER_QUALITY));
        hints.add(new RenderingHints(RenderingHints.KEY_INTERPOLATION,
         RenderingHints.VALUE_INTERPOLATION_BICUBIC));
        hints.add(new RenderingHints(RenderingHints.KEY_COLOR_RENDERING,
         RenderingHints.VALUE_RENDER_QUALITY));

        cameraThread = new Thread(this,"Camera Thread");
        processorThread = new Thread(() -> processImage(),"Processor Thread");
        emailThread = new Thread(() -> emailImage(),"Email Thread");

        addWindowListener(new WindowAdapter() {
            // start the program threads on window open
            @Override public void windowOpened(WindowEvent we) { start(); };
            // window is diposed if X is clicked
            @Override public void windowClosing(WindowEvent we) { dispose(); }
            // main thread is stopped when window is closed
            @Override public void windowClosed(WindowEvent we) { stop();
                if (timesFlag) {
                    timingMap.forEach((key,value) -> {
                        LongSummaryStatistics lss = value.stream().
                         collect(summarizingLong(l -> l));

                        if (lss.getCount() > 0)
                            System.out.printf(
                             "%s : count=%d min=%d avg=%.1f max=%d%n",key,
                             lss.getCount(),lss.getMin(),lss.getAverage(),
                             lss.getMax());
                    });
                }
            }
        });

        // prevent the window from being maximized
        addWindowStateListener(new WindowStateListener() {
            @Override public void windowStateChanged(WindowEvent we) {
                if ((we.getNewState() & Frame.MAXIMIZED_BOTH) != 0)
                    ((Frame)we.getSource()).setExtendedState(Frame.NORMAL);
            }
        });

        // prevent the window from being resized
        addComponentListener(new ComponentAdapter() {
            @Override public void componentResized(ComponentEvent ce) {
                pack();
            }
        });

        JMenuBar menuBar = new JMenuBar();
        setJMenuBar(menuBar);

        JMenu file = menuBar.add(new JMenu("File"));
        JMenu settings = menuBar.add(new JMenu("Settings"));
        JMenu view = menuBar.add(new JMenu("View"));
        JMenu help = menuBar.add(new JMenu("Help"));

        JMenuItem mi;

        mi = file.add(new JCheckBoxMenuItem("Save Images",saveFile));
        mi.addActionListener(ae -> {
            saveFile = ((JCheckBoxMenuItem)ae.getSource()).isSelected();
            properties.setProperty("saveFile",Boolean.toString(saveFile));
            storeProperties();
        });

        mi = file.add(new JCheckBoxMenuItem("Email Images",emailImage));
        mi.addActionListener(ae -> {
            emailImage = ((JCheckBoxMenuItem)ae.getSource()).isSelected();
            properties.setProperty("emailImage",Boolean.toString(emailImage));
            storeProperties();
        });

        mi = file.add("Directory");
        mi.addActionListener(ae -> {
            int state = fileChooser.showDialog(this,"Select Directory");
            if (state == JFileChooser.APPROVE_OPTION) {
                File dir = fileChooser.getSelectedFile();
                if (dir.exists()) {
                    directory = dir;
                } else {
                    int option = JOptionPane.showConfirmDialog(this,
                     dir.getPath().concat("\nDoesn't exist, create it?")
                     ,"Directory",JOptionPane.OK_CANCEL_OPTION,
                     JOptionPane.QUESTION_MESSAGE);
                    if (option == JOptionPane.OK_OPTION) {
                        if (dir.mkdir()) {
                            directory = dir;
                        } else {
                            JOptionPane.showMessageDialog(this,
                             "Can't Create Directory!","Directory",
                             JOptionPane.WARNING_MESSAGE);
                        }
                    }
                }
                fileChooser.setCurrentDirectory(directory);
                properties.setProperty("captureDirectory",directory.getPath());
                storeProperties();
            }
        });

        file.addSeparator();

        mi = file.add("Exit");
        mi.addActionListener(ae -> {
            dispose();
        });

        mi = settings.add("Capture Levels");
        mi.addActionListener(ae -> {
            JPanel p = new JPanel(new GridBagLayout());
            GridBagConstraints c = new GridBagConstraints();
            c.insets = new Insets(2,2,2,2);

            c.gridy = 0;
            JLabel l = new JLabel("Threshold");
            p.add(l,c);

            JSpinner thresholdSpinner = new JSpinner(new SpinnerNumberModel(
             threshold,0.0,100.0,1.0));
            p.add(thresholdSpinner,c);

            ++c.gridy;
            l = new JLabel("Minimum");
            p.add(l,c);

            JSpinner minSpinner = new JSpinner(new SpinnerNumberModel(
             minBoxes,0.0,100.0,0.1));
            p.add(minSpinner,c);

            ++c.gridy;
            l = new JLabel("Maximum");
            p.add(l,c);

            JSpinner maxSpinner = new JSpinner(new SpinnerNumberModel(
             maxBoxes,0.0,100.0,1.0));
            p.add(maxSpinner,c);

            int option = JOptionPane.showConfirmDialog(this,p,"CaptureLevels",
             JOptionPane.OK_CANCEL_OPTION,JOptionPane.QUESTION_MESSAGE);

            if (option == JOptionPane.OK_OPTION) {
                threshold = (Double)thresholdSpinner.getValue();
                properties.setProperty("threshold",Double.toString(threshold));
                minBoxes = (Double)minSpinner.getValue();
                properties.setProperty("minBoxes",Double.toString(minBoxes));
                maxBoxes = (Double)maxSpinner.getValue();
                properties.setProperty("maxBoxes",Double.toString(maxBoxes));
                storeProperties();
            }
        });

        JMenu captureSize = new JMenu("Capture Image Size");
        settings.add(captureSize);

        ButtonGroup group = new ButtonGroup();
        for (String label : CAPTURE_SIZE_LABELS) {
            JRadioButtonMenuItem ri =
             new JRadioButtonMenuItem(new CaptureSizeAction(label));
            group.add((JRadioButtonMenuItem)captureSize.add(ri));
            String dim = String.format("%dx%d",captureWidth,captureHeight);
            if (label.startsWith(dim))
                ri.setSelected(true);
        }

        mi = settings.add("Email Settings");
        mi.addActionListener(ae -> {
            EmailSettings es = new EmailSettings(properties);
            int option = JOptionPane.showConfirmDialog(this,es,
             "Email Settings",JOptionPane.OK_CANCEL_OPTION,
             JOptionPane.INFORMATION_MESSAGE);

            if (option == JOptionPane.OK_OPTION) {
                es.updateProperties();
                storeProperties();
            }
        });

        mi = settings.add(new JCheckBoxMenuItem("Time Stamp Image",
         timeStampImage));
        mi.addActionListener(ae -> {
            timeStampImage = ((JCheckBoxMenuItem)ae.getSource()).isSelected();
            properties.setProperty("timeStampImage",
             Boolean.toString(timeStampImage));
            storeProperties();
        });

        mi = view.add(new JCheckBoxMenuItem("Show Composite",showComposite));
        mi.addActionListener(ae -> {
            showComposite = ((JCheckBoxMenuItem)ae.getSource()).isSelected();
            properties.setProperty("showComposite",
             Boolean.toString(showComposite));
            storeProperties();
        });

        JMenu displaySize = new JMenu("Display Width");
        view.add(displaySize);

        group = new ButtonGroup();
        for (String label : DISPLAY_SIZE_LABELS) {
            JRadioButtonMenuItem ri = new JRadioButtonMenuItem(
             new DisplaySizeAction(label));
            group.add((JRadioButtonMenuItem)displaySize.add(ri));
            ri.setSelected(displayWidth == Integer.parseInt(label));
        }

        try (InputStream is =
         getClass().getResourceAsStream("mdDirections.html")) {
            if (is != null) {
                try (BufferedReader br = new BufferedReader(
                 new InputStreamReader(is))) {
                    String html = br.lines().collect(joining("\n"));
                    JScrollPane pane = new JScrollPane(new JLabel(html),
                     JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
                     JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
                    pane.setPreferredSize(new Dimension(
                     pane.getPreferredSize().width,300));
                    pane.getVerticalScrollBar().setUnitIncrement(10);
                    mi = help.add("Directions");
                    mi.addActionListener(ae ->
                    JOptionPane.showMessageDialog(this,pane,
                     "MotionDetection",JOptionPane.PLAIN_MESSAGE));
                    help.addSeparator();
                }
            }
        } catch (IOException ioe) {
            JOptionPane.showMessageDialog(null,
             "Error occured loading directions file\n" + ioe,
             "MotionDetection",JOptionPane.WARNING_MESSAGE);
        }

        mi = help.add("About");
        mi.addActionListener(ae -> JOptionPane.showMessageDialog(this,
         "MotionDetection\n" + "Version: " + VERSION + "\nDate: " + DATE +
         "\nWritten by: Knute Johnson" + "\nContributers: Federico Pedemonte",
         "About",
         JOptionPane.INFORMATION_MESSAGE));

        imagePanel = new ImageJPanel();
        imagePanel.setPreferredSize(new Dimension(displayWidth,displayHeight));
        add(imagePanel,BorderLayout.CENTER);

        pack();
        setVisible(true);
    }

    /**
     * Starts the camera thread and the image processing thread
     */
    private void start() {
        runFlag = true;
        cameraThread.start();
        processorThread.start();
        emailThread.start();
    }

    /**
     * The camera thread controls the process that runs the raspistill program.
     */
    @Override public void run() {
        System.out.println("camera thread started");
        while (runFlag) {
            try {
                ProcessBuilder pb = new ProcessBuilder("raspistill",
                 "--nopreview",
                 "--thumb","none",
                 "--timeout","0",
                 "--timelapse","0",
                 "--width",Integer.toString(captureWidth),
                 "--height",Integer.toString(captureHeight),
                 "--output","-");
                Process process = pb.start();
                InputStream is = process.getInputStream();
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                new Thread(() -> {
                    System.out.println("camera read thread started");
                    try {
                        while (runFlag) {
                            long readStartTime = System.currentTimeMillis();
                            int last = 0;
                            int nextToLast = 0;
                            while (!(nextToLast == 0xff && last == 0xd9)) {
                                int n = is.read();
                                if (n == -1)
                                    throw new IOException("end of stream");
                                baos.write(n);
                                nextToLast = last;
                                last = n;
                            }
                            nextToLast = 0;
                            ByteArrayInputStream bais =
                             new ByteArrayInputStream(baos.toByteArray());
                            BufferedImage image = ImageIO.read(bais);
                            if (image != null)
                                queue.add(image);
                            baos.reset();

                            long readStopTime = System.currentTimeMillis();
                            if (timesFlag) {
                                System.out.printf("image read time: %dms%n",
                                 readStopTime - readStartTime);
                                timingMap.get(Keys.READ).
                                 add(readStopTime - readStartTime);
                            }
                        }
                    } catch (IOException ioe) {
                        System.out.println(ioe);
                    }
                    System.out.println("camera read thread stopped");
                }).start();

                process.waitFor();

                System.out.println("camera process stopped");

                // this delay is to give CaptureSizeAction time to get
                //  everything done before we start taking pictures again
                // on a call to stop() this thread is interrupted so this sleep
                //  will never happen
                Thread.sleep(1000);
            } catch (IOException|InterruptedException ex) {
                if (runFlag)
                    ex.printStackTrace();
            }
        }
        System.out.println("camera thread stopped");
    }

    /**
     * Get the latest image file from the queue and process it for motion
     * detection.  The processing is split into 4 threads to speed things up.
     */
    private void processImage() {
        System.out.println("processor thread started");
        while (runFlag) {
            try {
                long procStartTime = System.currentTimeMillis();

                // new is now old
                imageOld = imageNew;

                // in case we are taking photos faster than processing 
                while (queue.size() > 1) {
                    queue.take();
                    System.out.println("excess image removed from queue");
                }

                // get the image file
                imageNew = queue.take();

                int boxWidth = imageNew.getWidth() / 100;
                int boxHeight = imageNew.getHeight() / 100;
                int width = imageNew.getWidth() / boxWidth;
                int height = imageNew.getHeight() / boxHeight;
                int boxes = width * height;

                // swap pointer for old brightness array
                brightOld = brightNew;
                // create new brightness array
                brightNew = new int[width][height];

                long scanStartTime = System.currentTimeMillis();
                // total up the brightness of all the pixels by box and take
                // the average - this very elegant piece of code contributed by
                // Federico Pedemonte
                IntStream.range(0,boxes).parallel().forEach(box -> {
                    int row = box / width;
                    // faster than box % width
                    int col = box - (row * width);
    
                    int[] array = imageNew.getRGB(col*boxWidth,
                     row*boxHeight,boxWidth,boxHeight,
                     new int[boxWidth*boxHeight],0,boxWidth);

                    int totalNew = IntStream.of(array).map(rgb -> 
                     (rgb >> 24 & 0xff)+(rgb >> 16 & 0xff)+(rgb & 0xff)).
                     sum();
    
                    brightNew[col][row] = totalNew / (boxWidth * boxHeight);
                });
                long scanStopTime = System.currentTimeMillis();
                if (timesFlag) {
                    System.out.printf("image scan: %dms%n",
                     scanStopTime - scanStartTime);
                    timingMap.get(Keys.SCAN).add(scanStopTime - scanStartTime);
                }

                if (Thread.interrupted())
                    throw new InterruptedException("after scan");

                // if there isn't an old brightness array or the new and old
                // arrays have different dimensions, go back and get another
                // image
                if (brightOld == null ||
                 brightOld.length != brightNew.length ||
                 brightOld[0].length != brightNew[0].length)
                    continue;

                // search for boxes with average brightness greater than the
                // threshold and store them in the points list
                java.util.List<Point> points = new ArrayList<>();
                for (int i=0; i<width; i++)
                    for (int j=0; j<height; j++)
                        if (Math.abs(brightOld[i][j] - brightNew[i][j]) >
                          // threshold is a percentage
                          // this is faster than threshold / 100.0 * 765.0
                          threshold * 7.65)
                            points.add(new Point(i,j));

                // if the Time Stamp check box is checked draw the current
                // date and time on the image
                if (timeStampImage) {
                    long timeStampStartTime = System.currentTimeMillis();
                    // font multiplier for images wider than 640 pixels
                    float multiplier = imageNew.getWidth() / 640.0f;
                    Font font = timeStampFont.deriveFont(
                     timeStampFont.getSize2D() * multiplier);
                    Graphics2D g = imageNew.createGraphics();
                    g.setRenderingHints(hints);
                    g.setFont(font);

                    FontMetrics fm = g.getFontMetrics();
                    int w = fm.stringWidth(TIME_STAMP_PATTERN);
                    int y = fm.getHeight();
                    int x = (int)(10 * multiplier);
                    double transX[] = { -0.5,0.5,0.5,-0.5,0.0 };
                    double transY[] = { 0.0,0.5,-0.5,-0.5,0.5 };
                    for (int i=0; i<transX.length; i++) {
                        if (i < transX.length - 1)
                            g.setColor(Color.BLACK);
                        else
                            g.setColor(Color.WHITE);
                        g.translate(transX[i],transY[i]);
                        g.drawString(ZonedDateTime.now().
                         format(TIME_STAMP_FORMATTER),x,y);
                    }
                    g.dispose();

                    long timeStampStopTime = System.currentTimeMillis();
                    if (timesFlag) {
                        System.out.printf("image time stamp time: %dms%n",
                         timeStampStopTime - timeStampStartTime);
                        timingMap.get(Keys.STMP).add(
                         timeStampStopTime - timeStampStartTime);
                    }
                } 

                if (Thread.interrupted())
                    throw new InterruptedException("after time stamp");

                // create a transparent color for the dot
                Color dotColor;
                // if the number of boxes over the threshold is > than minBoxes
                // set the dot color to a transparent green
                if (points.size() >= boxes * (minBoxes / 100.0) &&
                 points.size() <= boxes * (maxBoxes / 100.0)) {
                    dotColor = TRANSLUCENT_GREEN;
                    // if the Save Image check box is checked, save the image
                    if (saveFile)
                        saveImage(imageNew,directory);
                    // if the Email Image check box is checked, email the image
                    if (emailImage)
                        imageQueue.put(imageNew);
                // else if the number of boxes over the threshold is > than
                // maxBoxes set the dot color to a transparent red
                } else if (points.size() > boxes * (maxBoxes / 100.0)) {
                    dotColor = TRANSLUCENT_RED;
                } else
                    dotColor = TRANSPARENT_BLACK;

                // if the Show Composite check box is checked display the
                // composite image
                if (showComposite) {
                    BufferedImage compositeImage = createCompositeImage(
                     imageOld,imageNew,points,boxWidth,boxHeight);
                    imagePanel.setImage(compositeImage,dotColor);
                // otherwise display the latest image
                } else
                     imagePanel.setImage(imageNew,dotColor);

                long procStopTime = System.currentTimeMillis();
                if (timesFlag) {
                    System.out.printf("image process time: %dms%n",
                     procStopTime - procStartTime);
                    timingMap.get(Keys.PROC).add(procStopTime - procStartTime);
                }

                if (Thread.interrupted())
                    throw new InterruptedException("after image set");

            } catch (InterruptedException ex) {
                System.out.printf("processor thread: %s%n",ex);
            }
        }
        System.out.println("processor thread stopped");
    }

    /**
     * Kill the raspistill process, stop the camera thread, and the image
     * processing thread
     */
    private void stop() {
        runFlag = false;
        try {
            cameraThread.interrupt();
            emailThread.interrupt();

            System.out.println("stopping raspistill process");
            ProcessBuilder pb = new ProcessBuilder(
             "sudo","pkill","-HUP","raspistill");
            Process p = pb.start();
            p.waitFor();

            processorThread.interrupt();
            processorThread.join();
        } catch (IOException|InterruptedException ex) {
            ex.printStackTrace();
        }
    }

    /**
     * Store the program properties to the INI file
     */
    private void storeProperties() {
        new Thread(() -> {
            System.out.println("storing properties");

            try (FileWriter writer = new FileWriter(new File(
             System.getProperty("user.home"),INI_FILE_NAME))) {
                properties.store(writer,"MotionDetection");
            } catch (IOException ioe) {
                ioe.printStackTrace();
            }
        }).start();
    }

    /**
     * Writes an image to the specified directory with the file name created
     * from the local date and time in a background thread.
     *
     * @param   image   the image to save
     * @param   directory   the directory to save that image file in
     */
    private void saveImage(BufferedImage image, File directory) {
        new Thread(() -> {
            long saveStartTime = System.currentTimeMillis();
            System.out.println("saving file");

            String fname = LocalDateTime.now().format(IMAGE_FILE_FORMATTER).
             concat(".jpg");
            try {
                ImageIO.write(image,"JPEG",new File(directory,fname));
            } catch (IOException ioe) {
                ioe.printStackTrace();
            }

            long saveStopTime = System.currentTimeMillis();
            if (timesFlag) {
                System.out.printf("save time: %dms%n",
                 saveStopTime - saveStartTime);
                timingMap.get(Keys.SAVE).add(saveStopTime - saveStartTime);
            }
        }).start();
    }

    /**
     * Sends the captured image(s) via email using the entered settings.
     */
    private void emailImage() {
        while (runFlag) {
            try {
                // get an image from the queue
                BufferedImage image = imageQueue.take();
                Thread.sleep(5000);  // pause 5 secs for more images
                long sendStartTime = System.currentTimeMillis();
                System.out.println("emailling image(s)");

                int count = 0;
                try {
                    // TLSv1.3 is only available in JDK 11 or later
                    // It is possible if the program was run the first time
                    // using JDK 11 and then run with JDK 8 that there could
                    // be a TLSv1.3 protocol in the protocol list.  This code
                    // strips out the TLSv1.3 protocol from the list.
                    // *** remove this code when JDK 8 is no longer used ***
                    try {
                        double version = Double.parseDouble(System.getProperty(
                         "java.specification.version"));
                        if (version < 11.0) {
                            String protocols =
                             properties.getProperty("mail.smtp.ssl.protocols");
                            properties.setProperty("mail.smtp.ssl.protocols",
                             protocols.replaceAll("TLSv1.3",""));
                        }
                    } catch (NumberFormatException nfe) {
                        nfe.printStackTrace();
                    }
                    Session session = Session.getInstance(
                     properties,new SMTPAuthenticator());
                    MimeMessage mime = new MimeMessage(session);
                    mime.setFrom(properties.getProperty("mail.from"));
                    mime.setSubject(properties.getProperty("mail.subject"));
                    mime.setSentDate(new Date());
                    mime.setRecipients(Message.RecipientType.TO,
                     properties.getProperty("mail.to"));

                    MimeMultipart multi = new MimeMultipart();

                    MimeBodyPart textPart = new MimeBodyPart();
                    textPart.setContent(ZonedDateTime.now().
                     format(TIME_STAMP_FORMATTER),"text/plain");
                    textPart.setDisposition(Part.INLINE);
                    multi.addBodyPart(textPart);

                    // attach the image to the email
                    do {
                        MimeBodyPart imagePart = new MimeBodyPart();
                        imagePart.setDataHandler(new DataHandler(
                         new BufferedImageSource(image)));
                        imagePart.setFileName(LocalDateTime.now().
                         format(IMAGE_FILE_FORMATTER).concat(".jpg"));
                        imagePart.setDisposition(Part.ATTACHMENT);
                        multi.addBodyPart(imagePart);
                        ++count;
                    // send up to 5 images if they are waiting in the queue
                    } while (count < 5 && (image = imageQueue.poll()) != null) ;

                    mime.setContent(multi);

                    Transport.send(mime);
                } catch (SendFailedException sfe) {
                    System.out.printf(
                     "message could not be sent to some/all recipients: %s%n",
                     sfe);
                } catch (MessagingException|IOException ex) {
                    ex.printStackTrace();
                }
                System.out.printf("%d image(s) emailed%n",count);

                long sendStopTime = System.currentTimeMillis();
                if (timesFlag) {
                    System.out.printf("email time: %dms%n",
                     sendStopTime - sendStartTime);
                    timingMap.get(Keys.SEND).add(sendStopTime - sendStartTime);
                }
            } catch (InterruptedException ie) {
                if (runFlag)
                    ie.printStackTrace();
            }
        }
        System.out.println("email thread stopped");
    }

    /**
     * Action for the Capture Image Size JRadioMenuItems to set the capture
     * size and adjust the display aspect ratio appropriately for the image.
     */
    private class CaptureSizeAction extends AbstractAction {
        /** Serial version UID */
        private static final long serialVersionUID = 1L;

        /**
         * Create a new CaptureSizeAction
         *
         * @param   label   the capture size label (eg. 640x480 (4x3))
         */
        public CaptureSizeAction(String label) {
            putValue(NAME,label);
        }

        /**
         * Called when one of the Capture Image Size radio buttons is clicked.
         * Stops the raspistill program process, deletes all the files on the
         * ram disk, interrupts the image processing thread and updates the GUI
         * presentation.
         *
         * @param   ae  the ActionEvent passed in from the JRadioMenuItem
         */
        @Override public void actionPerformed(ActionEvent ae) {
            String ac = ae.getActionCommand();

            // stop the raspistill program and delete any left over files
            try {
                ProcessBuilder pb = new ProcessBuilder(
                 "sudo","pkill","-TERM","raspistill");
                Process p = pb.start();
                p.waitFor();

                processorThread.interrupt();
            } catch (IOException|InterruptedException ex) {
                ex.printStackTrace();
            }

            // eg. 640x480   (4x3)
            // split it on the spaces
            String[] split = ac.split("\\s+");
            System.out.println(split[0]);
            // split the size on the x
            String[] dims = split[0].split("x");
            captureWidth = Integer.parseInt(dims[0]);
            properties.setProperty("captureWidth",
             Integer.toString(captureWidth));
            captureHeight = Integer.parseInt(dims[1]);
            properties.setProperty("captureHeight",
             Integer.toString(captureHeight));
            aspectRatio = (double)captureWidth / captureHeight;
            displayHeight = (int)(displayWidth / aspectRatio);
            properties.setProperty("displayHeight",
             Integer.toString(displayHeight));
            storeProperties();
            imagePanel.setPreferredSize(new Dimension(
             displayWidth,displayHeight));
            imagePanel.setSize(imagePanel.getPreferredSize());
            pack();
        }
    }

    /**
     * Called when one of the Display Width radio buttons is clicked.  Sets the
     * dimensions of the image displayed in the program window to the selected
     * size.
     */
    private class DisplaySizeAction extends AbstractAction {
        /** Serial version UID */
        private static final long serialVersionUID = 1L;

        /**
         * Creates a new DisplaySizeAction
         *
         * @param   label   the width label (eg. 800 or 1024)
         */
        public DisplaySizeAction(String label) {
            putValue(NAME,label);
        }

        /**
         * Called when the JRadioMenuItem is clicked.  Changes the program
         * display dimensions setting the height from the width and aspect
         * ratio of the capture image.
         *
         * @param   ae  the ActionEvent passed in from the JRadioMenuItem
         */
        @Override public void actionPerformed(ActionEvent ae) {
            String ac = ae.getActionCommand();

            displayWidth = Integer.parseInt(ac);
            properties.setProperty("displayWidth",
             Integer.toString(displayWidth));
            displayHeight = (int)(displayWidth / aspectRatio + 0.5);
            properties.setProperty("displayHeight",
             Integer.toString(displayHeight));
            imagePanel.setPreferredSize(new Dimension(
             displayWidth,displayHeight));
            imagePanel.setSize(imagePanel.getPreferredSize());
            pack();
            storeProperties();

            System.out.printf("display: %dx%d%n",displayWidth,displayHeight);
        }
    }

    /**
     * A specilized JPanel used to display the latest captured image and draw
     * a dot in the upper right corner to signal a capture or an overload
     */
    private class ImageJPanel extends JPanel {
        /** Serial version UID */
        private static final long serialVersionUID = 1L;

        /** Image to display */
        private BufferedImage image;

        /** Color of the dot to display in the upper right corner */
        private Color dotColor;

        /**
         * Paints the ImageJPanel with the specified image and dot color
         *
         * @param   image   image to display
         * @param   dotColor    color of the dot to draw in the upper right
         *                      corner
         */
        public synchronized void setImage(BufferedImage image, Color dotColor) {
            this.image = image;
            this.dotColor = dotColor;
            repaint();
        }

        /**
         * Performs the actual drawing of the image on the panel
         *
         * @param   g2D graphics context
         */
        @Override public void paintComponent(Graphics g2D) {
            if (image != null) {
                Graphics2D g = (Graphics2D)g2D;
                g.setRenderingHints(hints);

                g.drawImage(image,0,0,getWidth(),getHeight(),null);
                // draw the dot in the upper right corner
                g.setColor(dotColor);
                g.fillOval(getWidth()-50,20,30,30);
            }
        }
    }

    /**
     * Draws img2 and blue boxes specified by the List points onto img1
     *
     * @param   img1    the older image
     * @param   img2    the new image
     * @param   points  list of upper left corner points
     * @param   boxWidth    width of the boxes in pixels
     * @param   boxHeight   height of the boxes in pixels
     *
     * @return  the older image with the newer image and boxes drawn over it
     */
    private BufferedImage createCompositeImage(BufferedImage img1,
     BufferedImage img2, java.util.List<Point> points,int boxWidth,
     int boxHeight) {
        long compositeStartTime = System.currentTimeMillis();
        BufferedImage composite = new BufferedImage(img1.getWidth(),
         img1.getHeight(),img1.getType());
        Graphics2D g = composite.createGraphics();
        g.setRenderingHints(hints);
        g.drawImage(img1,0,0,null);
        Composite comp = g.getComposite();
        g.setComposite(ALPHA_COMP);
        g.drawImage(img2,0,0,null);
        g.setComposite(comp);
        g.setColor(Color.BLUE);
        for (Point p : points)
            g.drawRect(p.x*boxWidth,p.y*boxHeight,boxWidth,boxHeight);
        g.dispose();
        long compositeStopTime = System.currentTimeMillis();
        if (timesFlag) {
            System.out.printf("composite time: %dms%n",
             compositeStopTime - compositeStartTime);
            timingMap.get(Keys.COMP).add(compositeStopTime-compositeStartTime);
        }
        return composite;
    }

    /**
     * A JPanel containing the GUI components to input email settings.
     */
    private class EmailSettings extends JPanel {
        /** Serial version UID */
        private static final long serialVersionUID = 1L;

        /** Properties object that holds the email settings */
        private final Properties properties;

        /** JTextField to input the email server host address */
        private final JTextField hostField;

        /** JTextfield to input the email server port */
        private final JTextField portField;

        /** JCheckbox to enable authentication with the email server */
        private final JCheckBox authBox;

        /** JTextField to input the email user name */
        private final JTextField userField;

        /** JPasswordField to input the email user's password */
        private JPasswordField passwdField;

        /** JCheckBox to enable StartTLS communication with the email server */
        private final JCheckBox starttlsBox;

        /** JTextField to input the enabled SSL protocols */
        private final JTextField protocolsField;

        /** JCheckbox to enable JavaMail debug messages to be displayed */
        private final JCheckBox debugBox;

        /** JTextField for the sender's email address */
        private final JTextField fromField;

        /** JTextField for the receiver's email address */
        private final JTextField toField;

        /** JTextField for the subject of the email */
        private final JTextField subjectField;

        /**
         * Create a new EmailSettings JPanel with the specified Properties.
         *
         * @param   properties the Properties containing the email settings
         */
        public EmailSettings(Properties properties) {
            super(new GridBagLayout());

            this.properties = properties;

            GridBagConstraints c = new GridBagConstraints();
            c.gridy = 0;  c.insets = new Insets(1,2,2,1);
            c.anchor = GridBagConstraints.WEST;
            c.fill = GridBagConstraints.HORIZONTAL;

            JLabel l = new JLabel("From:");
            add(l,c);

            fromField = new JTextField(
             properties.getProperty("mail.from",""),16);
            add(fromField,c);

            ++c.gridy;
            l = new JLabel("To:");
            add(l,c);

            toField = new JTextField(properties.getProperty("mail.to",""),16);
            add(toField,c);

            ++c.gridy;
            l = new JLabel("Subject:");
            add(l,c);

            subjectField = new JTextField(
             properties.getProperty("mail.subject","MotionDetection"),16);
            add(subjectField,c);

            ++c.gridy;
            l = new JLabel("Host:");
            add(l,c);

            hostField = new JTextField(
             properties.getProperty("mail.smtp.host",""),16);
            add(hostField,c);

            ++c.gridy;
            l = new JLabel("Port:");
            add(l,c);

            c.fill = GridBagConstraints.NONE;
            portField = new JTextField(
             properties.getProperty("mail.smtp.port","25"),4);
            add(portField,c);

            ++c.gridy;  c.fill = GridBagConstraints.HORIZONTAL;
            l = new JLabel("Use Auth:");
            add(l,c);

            authBox = new JCheckBox("",Boolean.parseBoolean(
             properties.getProperty("mail.smtp.auth","false")));
            authBox.setBorder(BorderFactory.createEmptyBorder());
            add(authBox,c);

            ++c.gridy;
            l = new JLabel("User:");
            add(l,c);

            userField = new JTextField(
             properties.getProperty("mail.user",""),16);
            add(userField,c);

            ++c.gridy;
            l = new JLabel("Password:");
            add(l,c);

            String passwd = "";
            try {
                passwd = aes.decrypt(
                 properties.getProperty("mail.passwd",""),KEY);
            } catch (GeneralSecurityException|IllegalArgumentException ex) {
                System.out.printf("exception decrypting password: %s%n",ex);
                JOptionPane.showMessageDialog(MotionDetection.this,ex,
                 "Error Decrypting Password",JOptionPane.ERROR_MESSAGE);
            }
            passwdField = new JPasswordField(passwd);
            add(passwdField,c);

            ++c.gridy;
            l = new JLabel("StartTTLS:");
            add(l,c);

            starttlsBox = new JCheckBox("",Boolean.parseBoolean(
             properties.getProperty("mail.smtp.starttls.enable","false")));
            starttlsBox.setBorder(BorderFactory.createEmptyBorder());
            add(starttlsBox,c);

            // get the supported SSL protocols
            String protocols;
            SSLServerSocketFactory factory =
             (SSLServerSocketFactory)SSLServerSocketFactory.getDefault();
            try (SSLServerSocket serverSocket =
             (SSLServerSocket)factory.createServerSocket()) {
                protocols = Arrays.
                 stream(serverSocket.getSupportedProtocols()).
                 collect(joining(" "));
            } catch (IOException ioe) {
                protocols = "";
            }

            ++c.gridy;
            l = new JLabel("Protocols:");
            add(l,c);

            protocolsField = new JTextField(properties.getProperty(
             "mail.smtp.ssl.protocols",protocols),16);
            protocolsField.setToolTipText(protocols);
            add(protocolsField,c);

            ++c.gridy;
            l = new JLabel("Debug:");
            add(l,c);

            debugBox = new JCheckBox("",Boolean.parseBoolean(
             properties.getProperty("mail.debug","false")));
            debugBox.setBorder(BorderFactory.createEmptyBorder());
            add(debugBox,c);
        }

        /**
         * Method to extract the new email settings from the GUI and store them
         * in the passed in Properties.
         */
        public void updateProperties() {
            properties.setProperty("mail.from",fromField.getText());
            properties.setProperty("mail.to",toField.getText());
            properties.setProperty("mail.subject",subjectField.getText());
            properties.setProperty("mail.smtp.host",hostField.getText());
            properties.setProperty("mail.smtp.port",portField.getText());
            properties.setProperty("mail.smtp.auth",
             Boolean.toString(authBox.isSelected()));
            properties.setProperty("mail.user",userField.getText());
            try {
                properties.setProperty("mail.passwd",
                 aes.encrypt(new String(passwdField.getPassword()),KEY));
            } catch (GeneralSecurityException gse) {
                properties.setProperty("mail.passwd","passwd");
                System.out.printf("updateProperties: %s%n",gse);
                JOptionPane.showMessageDialog(MotionDetection.this,
                 "Exception in updateProperties()\nPassword Was NOT Saved\n" +
                 gse,"Error Encrypting Password",JOptionPane.ERROR_MESSAGE);
            }
            properties.setProperty("mail.smtp.starttls.enable",
             Boolean.toString(starttlsBox.isSelected()));
            properties.setProperty("mail.smtp.ssl.protocols",
             protocolsField.getText());
            properties.setProperty("mail.debug",
             Boolean.toString(debugBox.isSelected()));
        }
    }

    /**
     * AES is a class with static methods to encrypt or decrypt a string
     * using the AES algorithm
     */
    public class AES {
        /** Algorithm used during encryption/decryption */
        private static final String ALGORITHM = "AES";

        /**
         * Encrypts a {@code String} using the supplied password
         *
         * @param input     the {@code String} to encrypt
         * @param password  the password {@code String}
         *
         * @return          the encrypted {@code String}
         *
         * @throws GeneralSecurityException if there is an encryption error
         */
        public String encrypt(String input, String password) throws
         GeneralSecurityException {
            Key key = generateKey(password);
            Cipher c = Cipher.getInstance(ALGORITHM);
            c.init(Cipher.ENCRYPT_MODE,key);
            byte[] encryptedValue =
             c.doFinal(input.getBytes(StandardCharsets.UTF_8));
            String encodedValue =
             Base64.getEncoder().encodeToString(encryptedValue);

            return encodedValue;
        }

        /**
         * Decrypts a {@code String} using the supplied password
         *
         * @param input     the encrypted {@code String}
         * @param password  the password {@code String}
         *
         * @return          the decrypted {@code String}
         *
         * @throws GeneralSecurityException if there is a decryption error
         */
        public String decrypt(String input, String password) throws
         GeneralSecurityException, IllegalArgumentException {
            Key key = generateKey(password);
            Cipher c = Cipher.getInstance(ALGORITHM);
            c.init(Cipher.DECRYPT_MODE,key);
            byte[] decodedValue = Base64.getDecoder().decode(input);
            String decryptedValue =
             new String(c.doFinal(decodedValue),StandardCharsets.UTF_8);

            return decryptedValue;
        }

        /**
         * Generates a {@code SecretKey} from a password string
         *
         * @param passwd    password {@code String}
         *
         * @return          the {@code SecretKey}
         */
        private Key generateKey(String passwd) {
            byte[] key = Arrays.copyOf(
             passwd.getBytes(StandardCharsets.UTF_8),16);
            return new SecretKeySpec(key,ALGORITHM);
        }
    }

    /**
     * An authenticator class used to obtain user name a password for
     * sending email.
     */
    private class SMTPAuthenticator extends javax.mail.Authenticator {
        /**
         * Gets a PasswordAuthentication object.
         *
         * @return  a PasswordAuthentication object that returns the user
         *          name and the decrypted password
         */
        @Override protected javax.mail.PasswordAuthentication
         getPasswordAuthentication() {
            try {
                return new javax.mail.PasswordAuthentication(
                 properties.getProperty("mail.user"),
                 aes.decrypt(properties.getProperty("mail.passwd"),KEY));
            } catch (GeneralSecurityException|IllegalArgumentException ex) {
                System.out.printf(
                 "exception decrypting password in SMTPAuthenticator: %s%n",ex);
                return new javax.mail.PasswordAuthentication(
                 "user","passwd");
            }
        }
    }

    /**
     * A DataSource to prepare a BufferedImage for emailing.
     */
    private class BufferedImageSource implements DataSource {
        /** A temporary place to hold the data from the BufferedImage */
        private final ByteArrayOutputStream baos;

        /**
         * Creates a new BufferedImageSource with the specified
         * BufferedImage
         *
         * @param   image the BufferedImage to source
         *
         * @throws  IOException if an error occurs writing the image to the
         *          temporary ByteArrayOutputStream
         */
        public BufferedImageSource(BufferedImage image) throws IOException {
            baos = new ByteArrayOutputStream();
            ImageIO.write(image,"JPEG",baos);
        }

        /**
         * Gets the content type of the converted image, in this case
         * image/jpeg
         *
         * @return  a String with the mime type of the data source
         */
        @Override public String getContentType() {
            return "image/jpeg";
        }

        /**
         * Gets an InputStream from which to read the image data
         *
         * @return  the InputStream containing the image data
         */
        @Override public InputStream getInputStream() {
            return new ByteArrayInputStream(baos.toByteArray());
        }

        /**
         * Gets the name of the data, usually a file name but in this case
         * a generic file name of "image.jpg".
         *
         * @return  the String containing the name of the data
         */
        @Override public String getName() {
            return "image.jpg";
        }

        /**
         * Get the OutputStream associated with this data source, in this
         * case there is no OutputStream and calling this method just
         * throws an IOException.
         *
         * @return  the OutputStream for the data source
         *
         * @throws  IOException if this method is called
         */
        @Override public OutputStream getOutputStream() throws IOException {
            throw new IOException("no output stream available");
        }
    }

    /**
     * Main program entry point
     *
     * @param   args    command line arguments, used for testing
     */
    public static void main(String... args) {
        timesFlag = Stream.of(args).anyMatch(a -> a.equalsIgnoreCase("times"));

        // create the program GUI
        EventQueue.invokeLater(() -> new MotionDetection());
    }
}