/*
TZ display timezones supported on your computer with their offsets from GMT.
copyright (c) 2004-2008 Roedy Green, Canadian Mind Products
may be copied and used freely for any purpose but military.
Roedy Green
Canadian Mind Products
roedy g at mindprod dotcom
http://mindprod.com
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.common11.Build;
import com.mindprod.common11.FontFactory;
import com.mindprod.common11.VersionCheck;
import com.mindprod.common13.CMPAboutJBox;
import com.mindprod.common13.Common13;
import com.mindprod.common13.HybridJ;
import com.mindprod.common13.JEButton;

import javax.swing.*;
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.*;
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;

/**
 * Display list of available timezones, short name, long name and raw offset.
 *
 * @author Roedy Green, Canadian Mind Products
 * @version 1.9 2008-06-13 flip to JDK 1.6 to allow access to TableRowSorter
 */
@SuppressWarnings( { "FieldCanBeLocal" } )
public final class TZ extends JApplet
    {
    // ------------------------------ FIELDS ------------------------------

    /**
     * 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;

    /**
     * pale violet blue
     */
    private static final Color APPLET_BACKGROUND = new Color( 0xd4d4ff );

    private static final Color INSTRUCTIONS_FOREGROUND = new Color( 0x008000 );

    private static final Color LABEL_FOREGROUND = new Color( 0x0000b0 );

    /**
     * for titles
     */
    private static final Color TITLE_FOREGROUND = 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 title second line
     */
    private static final Font TITLE2_FONT = FontFactory.build( "Dialog", Font.PLAIN, 14 );

    /**
     * for for titles and About buttons
     */
    private static final Font TITLE_FONT = FontFactory.build( "Dialog", Font.BOLD, 16 );

    /**
     * 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" );
    /**
     * undisplayed copyright notice
     */
    @SuppressWarnings( { "UnusedDeclaration" } )
    public static final String EMBEDDED_COPYRIGHT =
            "copyright (c) 2004-2008 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";

    private static final int[] PREFERRED_COLUMN_WIDTH = { 45, 45, 215, 330 };

    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;

    // -------------------------- PUBLIC INSTANCE  METHODS --------------------------
    /**
     * Standard applet init
     */
    public void init()
        {
        if ( !VersionCheck.isJavaVersionOK( 1, 5, 0, this ) )
            {
            return;
            }
        Common13.setLaf();
        Container contentPane = this.getContentPane();
        contentPane.setBackground( APPLET_BACKGROUND );
        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;
        }

    // -------------------------- STATIC METHODS --------------------------

    static
        {
        utcTimeDisplay.setTimeZone( TimeZone.getTimeZone( "UTC" ) );
        }

    // -------------------------- OTHER METHODS --------------------------

    /**
     * allocate all components
     */
    private void buildComponents()
        {
        title = new JLabel( TITLE_STRING + " " + VERSION_STRING );
        title.setFont( TITLE_FONT );
        title.setForeground( TITLE_FOREGROUND );

        title2 = new JLabel(
                "released:" +
                RELEASE_DATE +
                " build:" +
                Build.BUILD_NUMBER );
        title2.setFont( TITLE2_FONT );
        title2.setForeground( TITLE_FOREGROUND );

        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,
                    2004,
                    "Roedy Green",
                    "TZ",
                    "1.6" );
            }
        } );

        utcTimeLabel = new JLabel( "UTC" );
        utcTimeLabel.setForeground( LABEL_FOREGROUND );

        utcTimeField = new JTextField( localTimeDisplay.format( new Date() ) );
        utcTimeField.setEditable( false );

        localTimeLabel = new JLabel( "local time" );
        localTimeLabel.setForeground( LABEL_FOREGROUND );

        localTimeField =
                new JTextField( localTimeDisplay.format( new Date() ) );
        localTimeField.setEditable( false );

        localTZLabel = new JLabel( "local TZ" );
        localTZLabel.setForeground( LABEL_FOREGROUND );

        localTZField = new JTextField( TimeZone.getDefault().getDisplayName() );
        localTZField.setEditable( false );

        supportedLabel = new JLabel( "Supported TimeZones", JLabel.CENTER );
        supportedLabel.setForeground( LABEL_FOREGROUND );
        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<TZTableModel>( model ) );

        instructions = new JLabel( "Click column heading to sort.", JLabel.CENTER );
        instructions.setForeground( INSTRUCTIONS_FOREGROUND );
        }

    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 ) );
        }

    // -------------------------- INNER CLASSES --------------------------

    /**
     * used to sort list of TimeZone ids in order
     */
    private static final class ByOffset implements Comparator<String>
        {
        /**
         * Compare two TimeZone Ids. effectively returns a-b;
         *
         * @param o1 first object a to be compared
         * @param o2 second object b to be compared
         * @return +1 if a>b, 0 if a=b, -1 if a<b
         */
        public final int compare( String o1, String o2 )
            {
            // compare offsets, then id as tie-breaker.

            final int offsetFirst = TimeZone.getTimeZone( o1 ).getRawOffset();
            final int offsetSecond = TimeZone.getTimeZone( o2 ).getRawOffset();
            if ( offsetFirst != offsetSecond )
                {
                return offsetFirst - offsetSecond;
                }
            return o1.compareTo( o2 );
            }// end compare
        }

    /**
     * nested inner class to render a column heading
     */
    static final class HeaderRenderer implements TableCellRenderer
        {
        public Component getTableCellRendererComponent( JTable table,
                                                        Object value,
                                                        boolean isSelected,
                                                        boolean hasFocus,
                                                        int row,
                                                        int column )
            {
            headerTemplate.setText( value.toString() );
            return headerTemplate;
            }

        /**
         * template for HeaderRender, reuses same component for all headers.
         */
        private static final JLabel headerTemplate;

        static
            {
            headerTemplate = new JLabel( "", JLabel.CENTER );
            headerTemplate.setForeground( LABEL_FOREGROUND );
            headerTemplate.setBackground( APPLET_BACKGROUND );
            headerTemplate.setBorder( BorderFactory.createLineBorder( Color.GRAY ) );
            headerTemplate.setOpaque( true );
            }
        }

    /**
     * nested inner class
     */
    static final class TZTableModel extends AbstractTableModel implements TableModel
        {
        /**
         * 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 ];
            }

        /**
         * 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;
            }

        /**
         * names of the columns
         */
        final static String[] columnNames = {
                "Offset", "DST", "ID", "Display Name" };

        /**
         * 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;
            }

        /**
         * timeZoneIDs array of all possible timezone IDs
         */
        private final String[] timeZoneIDs;

        /* 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.
                    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" );
                }
            }
        }

    // --------------------------- main() method ---------------------------

    /**
     * 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
    }
