/*
 * [TZ.java]
 *
 * Summary: TZ display TimeZones supported on your computer with their offsets from GMT.
 *
 * Copyright: (c) 2004-2017 Roedy Green, Canadian Mind Products, http://mindprod.com
 *
 * Licence: This software may be copied and used freely for any purpose but military.
 *          http://mindprod.com/contact/nonmil.html
 *
 * Requires: JDK 1.8+
 *
 * Created with: JetBrains IntelliJ IDEA IDE http://www.jetbrains.com/idea/
 *
 * Version History:
 *  1.1 2005-06-30 add stomp bat files
 *  1.2 2005-07-27 add main method
 *  1.3 2006-03-05 reformat with IntelliJ and and Javadoc
 *  1.4 2007-08-08 add default TZ display, convert to JDK 1.5, add time display.
 *                 add live local time display.
 *  1.5 2007-08-08 add utc time display, sort by offset and id.
 *  1.6 2007-10-09 utc time display, in 24-hour time.
 *  1.7 2008-04-05 add build to title.
 *  1.8 2008-04-27 make columns sortable.
 *  1.9 2008-06-13 flip to JDK 1.6 to allow access to TableRowSorter
 */
package com.mindprod.tz;

import com.mindprod.common18.Build;
import com.mindprod.common18.CMPAboutJBox;
import com.mindprod.common18.Common18;
import com.mindprod.common18.FontFactory;
import com.mindprod.common18.HybridJ;
import com.mindprod.common18.JEButton;
import com.mindprod.common18.VersionCheck;

import javax.swing.BorderFactory;
import javax.swing.JApplet;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.Timer;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableModel;
import javax.swing.table.TableRowSorter;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;

/**
 * TZ display TimeZones supported on your computer with their offsets from GMT.
 *
 * @author Roedy Green, Canadian Mind Products
 * @version 1.9 2008-06-13 flip to JDK 1.6 to allow access to TableRowSorter
 * @since 2004
 */
@SuppressWarnings( { "FieldCanBeLocal" } )
public final class TZ extends JApplet
    {
    /**
     * height of Applet box in pixels. Does not include surrounding frame.
     */
    private static final int APPLET_HEIGHT = 645;

    /**
     * Width of Applet box in pixels.
     */
    private static final int APPLET_WIDTH = 822;

    private static final int FIRST_COPYRIGHT_YEAR = 2004;

    /**
     * undisplayed copyright notice
     */
    @SuppressWarnings( { "UnusedDeclaration" } )
    private static final String EMBEDDED_COPYRIGHT =
            "Copyright: (c) 2004-2017 Roedy Green, Canadian Mind Products, http://mindprod.com";

    private static final String RELEASE_DATE = "2008-06-13";

    private static final String TITLE_STRING = "TimeZones";

    private static final String VERSION_STRING = "1.9";

    /**
     * pale violet blue
     */
    private static final Color BACKGROUND_FOR_APPLET = new Color( 0xd4d4ff );

    private static final Color FOREGROUND_FOR_INSTRUCTIONS = new Color( 0x008000 );

    private static final Color FOREGROUND_FOR_LABEL = new Color( 0x0000b0 );

    /**
     * for titles
     */
    private static final Color FOREGROUND_FOR_TITLE = new Color( 0xdc143c );

    /**
     * pick noon 2007-06-21 0:00 as a typical summer day that likely will be using DST.
     */
    private static final Date aSummerDay =
            new GregorianCalendar( 2007, Calendar.JUNE, 21 ).getTime();

    /**
     * pick 2007-06-21 0::00 as a typical winter day that will not be using DST.
     */
    private static final Date aWinterDay =
            new GregorianCalendar( 2007, Calendar.DECEMBER, 21 ).getTime();

    /**
     * for for titles and About buttons
     */
    private static final Font FONT_FOR_TITLE = FontFactory.build( "Dialog", Font.BOLD, 16 );

    /**
     * for for title second line
     */
    private static final Font FONT_FOR_TITLE2 = FontFactory.build( "Dialog", Font.PLAIN, 14 );

    /**
     * zz=PDT zzzz="pacific Daylight Time" EEEE=dow  hh=12 hour
     */
    private static final SimpleDateFormat localTimeDisplay =
            new SimpleDateFormat( "yyyy-MM-dd hh:mm:ss aa zz : zzzzzz EEEE" );

    /**
     * zz=PDT zzzz="pacific Daylight Time" EEEE=dow   HH=24 hour
     */
    private static final SimpleDateFormat utcTimeDisplay =
            new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss zz : zzzzzz EEEE" );

    private static final int[] PREFERRED_COLUMN_WIDTH = { 45, 45, 215, 330 };

    static
        {
        utcTimeDisplay.setTimeZone( TimeZone.getTimeZone( "UTC" ) );
        }

    private JButton about;

    private JLabel instructions;

    private JLabel localTimeLabel;

    private JLabel localTZLabel;

    private JLabel supportedLabel;

    private JLabel title;

    /**
     * title, second line
     */
    private JLabel title2;

    private JLabel utcTimeLabel;

    /**
     * scroll the JTable of TimeZones
     */
    private JScrollPane scroller;

    /**
     * grid of TimeZone data
     */
    private JTable timeZoneTable;

    private JTextField localTimeField;

    private JTextField localTZField;

    private JTextField utcTimeField;

    /**
     * timer to help us keep clock up to date.
     */
    private Timer timer;

    /**
     * allocate all components
     */
    private void buildComponents()
        {
        title = new JLabel( TITLE_STRING + " " + VERSION_STRING );
        title.setFont( FONT_FOR_TITLE );
        title.setForeground( FOREGROUND_FOR_TITLE );
        title2 = new JLabel(
                "released:" +
                RELEASE_DATE +
                " build:" +
                Build.BUILD_NUMBER
        );
        title2.setFont( FONT_FOR_TITLE2 );
        title2.setForeground( FOREGROUND_FOR_TITLE );
        about = new JEButton( "About" );
        about.setToolTipText( "About " + TITLE_STRING + " " + VERSION_STRING );
        about.addActionListener( new ActionListener()
            {
            /**
             * Method actionPerformed ...
             * @param e of type ActionEvent
             */
            public void actionPerformed( ActionEvent e )
                {
                // open aboutbox frame
                new CMPAboutJBox( TITLE_STRING,
                        VERSION_STRING,
                        "Display all Timezones that Java supports on this machine.",
                        "",
                        "freeware",
                        RELEASE_DATE,
                        FIRST_COPYRIGHT_YEAR,
                        "Roedy Green",
                        "TZ",
                        "1.8" );
                }
            } );
        utcTimeLabel = new JLabel( "UTC" );
        utcTimeLabel.setForeground( FOREGROUND_FOR_LABEL );
        utcTimeField = new JTextField( localTimeDisplay.format( new Date() ) );
        utcTimeField.setEditable( false );
        localTimeLabel = new JLabel( "local time" );
        localTimeLabel.setForeground( FOREGROUND_FOR_LABEL );
        localTimeField =
                new JTextField( localTimeDisplay.format( new Date() ) );
        localTimeField.setEditable( false );
        localTZLabel = new JLabel( "local TZ" );
        localTZLabel.setForeground( FOREGROUND_FOR_LABEL );
        localTZField = new JTextField( TimeZone.getDefault().getDisplayName() );
        localTZField.setEditable( false );
        supportedLabel = new JLabel( "Supported TimeZones", JLabel.CENTER );
        supportedLabel.setForeground( FOREGROUND_FOR_LABEL );
        final String[] allTimeZoneIDs = TimeZone.getAvailableIDs();
        Arrays.sort( allTimeZoneIDs, new ByOffset() );
        final TZTableModel model = new TZTableModel( allTimeZoneIDs );
        timeZoneTable = new JTable( model );
        // set some initial column widths
        final TableColumnModel tableColumnModel =
                timeZoneTable.getColumnModel();
        // give a bit more space between columns
        tableColumnModel.setColumnMargin( 5 );
        // hook up our HeaderRenderer to all four columns
        // and set preferred width in pixels for each column.
        final TableCellRenderer headerRenderer = new HeaderRenderer();
        for ( int i = 0; i < 4; i++ )
            {
            final TableColumn tableColumn = tableColumnModel.getColumn( i );
            tableColumn.setHeaderRenderer( headerRenderer );
            tableColumn.setPreferredWidth( PREFERRED_COLUMN_WIDTH[ i ] );
            }
        scroller =
                new JScrollPane( timeZoneTable,
                        JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
                        JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED );
        // this is all you need to make every column sortable ascending/descending by clicking on the heading.
        timeZoneTable.setRowSorter( new TableRowSorter<>( model ) );
        instructions = new JLabel( "Click column heading to sort.", JLabel.CENTER );
        instructions.setForeground( FOREGROUND_FOR_INSTRUCTIONS );
        }

    private void layoutComponents( Container contentPane )
        {
        // ---0-----1-----2--------3--
        // 0 title----- title2-- about
        // ~time  time----------------
        // ~default default-----------
        // ~ supported----------------
        // jtable --------------------
        // instructions---------------
        // x y w h wtx wty anchor fill T L B R padx pady
        contentPane.add( title,
                new GridBagConstraints( 0,
                        0,
                        2,
                        1,
                        0.0,
                        0.0,
                        GridBagConstraints.WEST,
                        GridBagConstraints.NONE,
                        new Insets( 10, 10, 5, 5 ),
                        0,
                        0 )
        );
        contentPane.add( title2,
                new GridBagConstraints( 2,
                        0,
                        1,
                        1,
                        0.0,
                        0.0,
                        GridBagConstraints.WEST,
                        GridBagConstraints.NONE,
                        new Insets( 10, 5, 5, 5 ),
                        0,
                        0 )
        );
        contentPane.add( about,
                new GridBagConstraints( 3,
                        0,
                        1,
                        1,
                        0.0,
                        0.0,
                        GridBagConstraints.EAST,
                        GridBagConstraints.NONE,
                        new Insets( 10, 5, 5, 10 ),
                        0,
                        0 )
        );
        contentPane.add( utcTimeLabel,
                new GridBagConstraints( 0,
                        1,
                        1,
                        1,
                        0.0,
                        0.0,
                        GridBagConstraints.EAST,
                        GridBagConstraints.NONE,
                        new Insets( 10, 10, 0, 5 ),
                        0,
                        0 )
        );
        contentPane.add( utcTimeField,
                new GridBagConstraints( 1,
                        1,
                        3,
                        1,
                        0.0,
                        0.0,
                        GridBagConstraints.WEST,
                        GridBagConstraints.HORIZONTAL,
                        new Insets( 10, 5, 0, 10 ),
                        0,
                        0 )
        );
        contentPane.add( localTimeLabel,
                new GridBagConstraints( 0,
                        2,
                        1,
                        1,
                        0.0,
                        0.0,
                        GridBagConstraints.EAST,
                        GridBagConstraints.NONE,
                        new Insets( 10, 10, 0, 5 ),
                        0,
                        0 )
        );
        contentPane.add( localTimeField,
                new GridBagConstraints( 1,
                        2,
                        3,
                        1,
                        0.0,
                        0.0,
                        GridBagConstraints.WEST,
                        GridBagConstraints.HORIZONTAL,
                        new Insets( 10, 5, 0, 10 ),
                        0,
                        0 )
        );
        contentPane.add( localTZLabel,
                new GridBagConstraints( 0,
                        3,
                        1,
                        1,
                        0.0,
                        0.0,
                        GridBagConstraints.EAST,
                        GridBagConstraints.NONE,
                        new Insets( 10, 10, 0, 5 ),
                        0,
                        0 )
        );
        contentPane.add( localTZField,
                new GridBagConstraints( 1,
                        3,
                        3,
                        1,
                        0.0,
                        0.0,
                        GridBagConstraints.WEST,
                        GridBagConstraints.HORIZONTAL,
                        new Insets( 10, 5, 0, 10 ),
                        0,
                        0 )
        );
        contentPane.add( supportedLabel,
                new GridBagConstraints( 0,
                        4,
                        3,
                        1,
                        0.0,
                        0.0,
                        GridBagConstraints.CENTER,
                        GridBagConstraints.BOTH,
                        new Insets( 10, 10, 0, 10 ),
                        0,
                        0 )
        );
        contentPane.add( scroller,
                new GridBagConstraints( 0,
                        5,
                        4,
                        1,
                        1.0,
                        1.0,
                        GridBagConstraints.CENTER,
                        GridBagConstraints.BOTH,
                        new Insets( 10, 10, 10, 10 ),
                        0,
                        0 )
        );
        contentPane.add( instructions,
                new GridBagConstraints( 0,
                        6,
                        4,
                        1,
                        0.0,
                        0.0,
                        GridBagConstraints.CENTER,
                        GridBagConstraints.BOTH,
                        new Insets( 10, 10, 10, 10 ),
                        0,
                        0 )
        );
        }

    /**
     * Allow this Applet to run as as application as well.
     *
     * @param args command line arguments ignored.
     */
    public static void main( String args[] )
        {
        HybridJ.fireup( new TZ(),
                TITLE_STRING + " " + VERSION_STRING,
                APPLET_WIDTH,
                APPLET_HEIGHT );
        } // end main

    /**
     * Called by the browser or Applet viewer to inform
     * this Applet that it is being reclaimed and that it should destroy
     * any resources that it has allocated.
     */
    public void destroy()
        {
        about = null;
        instructions = null;
        localTimeField = null;
        localTimeLabel = null;
        localTZField = null;
        localTZLabel = null;
        scroller = null;
        supportedLabel = null;
        timer = null;
        timeZoneTable = null;
        title2 = null;
        title = null;
        utcTimeField = null;
        utcTimeLabel = null;
        }

    /**
     * Called by the browser or Applet viewer to inform
     * this Applet that it has been loaded into the system.
     */
    @Override
    public void init()
        {
        if ( !VersionCheck.isJavaVersionOK( 1, 8, 0, this ) )
            {
            return;
            }
        Common18.setLaf();
        Container contentPane = this.getContentPane();
        contentPane.setBackground( BACKGROUND_FOR_APPLET );
        contentPane.setLayout( new GridBagLayout() );
        buildComponents();
        layoutComponents( contentPane );
        this.validate();
        }

    /**
     * usual Applet start, we start up Timer to update clock
     */
    public void start()
        {
        timer = new Timer( 500/* every half second */, new ActionListener()
            {
            public void actionPerformed( ActionEvent e )
                {
                // executed on Swing Thread.
                localTimeField.setText( localTimeDisplay.format( new Date() ) );
                utcTimeField.setText( utcTimeDisplay.format( new Date() ) );
                }
            } );
        timer.setRepeats( true );
        // start it ticking,
        timer.start();
        }

    /**
     * usual Applet stop, we  kill Timer to update clock
     */
    public void stop()
        {
        timer.stop();
        timer = null;
        }

    /**
     * Sort by TimeZone offset.
     * <p/>
     * Defines an alternate sort order for String.
     */
    private static class ByOffset implements Comparator<String>
        {
        /**
         * Sort by TimeZone offset.
         * Defines an alternate sort order for String.
         * Compare two String Objects.
         * Compares getRawOffset then String
         * Informally, returns (a-b), or +ve if a is more positive than b.
         *
         * @param a first String to compare
         * @param b second String to compare
         *
         * @return +ve if a&gt;b, 0 if a==b, -ve if a&lt;b
         */
        public final int compare( String a, String b )
            {
            final int diff = TimeZone.getTimeZone( a ).getRawOffset() - TimeZone.getTimeZone( b ).getRawOffset();
            if ( diff != 0 )
                {
                return diff;
                }
            return a.compareTo( b );
            }
        }

    /**
     * nested inner class to render a column heading
     */
    static final class HeaderRenderer implements TableCellRenderer
        {
        /**
         * template for HeaderRender, reuses same component for all headers.
         */
        private static final JLabel headerTemplate;

        static
            {
            headerTemplate = new JLabel( "", JLabel.CENTER );
            headerTemplate.setForeground( FOREGROUND_FOR_LABEL );
            headerTemplate.setBackground( BACKGROUND_FOR_APPLET );
            headerTemplate.setBorder( BorderFactory.createLineBorder( Color.GRAY ) );
            headerTemplate.setOpaque( true );
            }

        public Component getTableCellRendererComponent( JTable table,
                                                        Object value,
                                                        boolean isSelected,
                                                        boolean hasFocus,
                                                        int row,
                                                        int column )
            {
            headerTemplate.setText( value.toString() );
            return headerTemplate;
            }
        }

    /**
     * nested inner class
     */
    static final class TZTableModel extends AbstractTableModel implements TableModel
        {
        /**
         * names of the columns
         */
        static final String[] columnNames = {
                "Offset", "DST", "ID", "Display Name" };

        /**
         * timeZoneIDs array of all possible TimeZone IDs
         */
        private final String[] timeZoneIDs;

        /**
         * constructor.
         *
         * @param timeZoneIDs array of all possible TimeZone IDs, sorted in some reasonable order.
         */
        TZTableModel( String[] timeZoneIDs )
            {
            this.timeZoneIDs = timeZoneIDs;
            }

        /**
         * Returns the most specific superclass for all the cell values in the column.  This is used by the
         * <code>JTable</code> to set up a default renderer and editor for the column.
         *
         * @param columnIndex the index of the column
         *
         * @return the common ancestor class of the object values in the model.
         */
        public Class<?> getColumnClass( int columnIndex )
            {
            switch ( columnIndex )
                {
                case 0:
                case 1:
                    return Double.class;
                case 2:
                case 3:
                    return String.class;
                default:
                    throw new ArrayIndexOutOfBoundsException(
                            "column index out of range in get ColumnClass" );
                }
            }

        /**
         * Returns the number of columns in the model. A <code>JTable</code> uses this method to determine how many
         * columns it should create and display by default.
         *
         * @return the number of columns in the model
         * @see #getRowCount
         */
        public int getColumnCount()
            {
            return 4;
            }

        /**
         * Returns the name of the column at <code>columnIndex</code>.  This is used to initialize the table's column
         * header name.  Note: this name does not need to be unique; two columns in a table can have the same name.
         *
         * @param column the index of the column
         *
         * @return the name of the column
         */
        public String getColumnName( int column )
            {
            return columnNames[ column ];
            }

        /**
         * Returns the number of rows in the model. A <code>JTable</code> uses this method to determine how many rows it
         * should display.  This method should be quick, as it is called frequently during rendering.
         *
         * @return the number of rows in the model
         * @see #getColumnCount
         */
        public int getRowCount()
            {
            return timeZoneIDs.length;
            }

        /* get value at given row and column
         * @param row    zero-based row number
         * @param col    zero-based column number
         * @return Object, will be String, Integer or Float.
         */
        public Object getValueAt( int rowIndex, int columnIndex )
            {
            final String timeZoneID = timeZoneIDs[ rowIndex ];
            final TimeZone timeZone = TimeZone.getTimeZone( timeZoneID );
            switch ( columnIndex )
                {
                case 0:
                    // offset in hours.
                    // want floating point arithmetic. Don't use TimeUnit.
                    return timeZone.getRawOffset() / ( 60 * 60 * 1000.0D );
                case 1:
                    final double raw =
                            timeZone.getRawOffset() / ( 60
                                                        * 60
                                                        * 1000.0D );// hrs winter
                    final double summer =
                            timeZone.getOffset( aSummerDay.getTime() ) / ( 60
                                                                           * 60
                                                                           * 1000.0D );// millis -> hrs summer
                    final double winter =
                            timeZone.getOffset( aWinterDay.getTime() ) / ( 60
                                                                           * 60
                                                                           * 1000.0D );// millis -> hrs summer
                    // dst in hours.
                    return ( raw == summer ) ? winter : summer;
                case 2:
                    return timeZoneID;
                case 3:
                    return timeZone.getDisplayName();
                default:
                    throw new ArrayIndexOutOfBoundsException(
                            "col index out of range in get getValueAt" );
                }
            }
        }
    }