/**
 * ThisTime 2.0 (2008-03-30)
 * Copyright 2007 Zach Scrivena
 * zachscrivena@gmail.com
 * http://thistime.sourceforge.net/
 *
 * Simple clock and timer program.
 *
 * TERMS AND CONDITIONS:
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package thistime;

import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.regex.Pattern;
import javax.swing.AbstractAction;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.event.DocumentEvent;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;


/**
 * "Time Zone Selector" form.
 */
class TimeZoneSelector
        extends JFrame
{
    /** map: timezone string description ---> TimeZone object */
    private static final Map<String,TimeZone> TIMEZONE_STRING_TO_OBJECT_MAP = new HashMap<String,TimeZone>();

    /** list of timezone string descriptions */
    private static final String[] TIMEZONE_STRINGS;

    /** parent ClockUnit object */
    private final ClockUnit parent;

    /** "quick find" list model */
    private final QuickFindListModel model;


    /**
    * Static initialization block.
    */
    static
    {
        /* normalized custom ID format for a TimeZone object */
        final Pattern normalizedCustomIdFormat = Pattern.compile("GMT[\\+\\-][0-9]{2}\\:[0-9]{2}");

        final Map<Integer,List<TimeZone>> rawOffsetToDifferentRulesTimeZonesMap = new HashMap<Integer,List<TimeZone>>();
        final Map<TimeZone,List<TimeZone>> sameRulesTimeZonesMap = new HashMap<TimeZone,List<TimeZone>>();

        for (String id : TimeZone.getAvailableIDs())
        {
            final TimeZone tz = TimeZone.getTimeZone(id);

            if (normalizedCustomIdFormat.matcher(tz.getDisplayName()).matches())
            {
                /* this time zone does not have a "meaningful" name */
                continue;
            }

            final int rawOffset = tz.getRawOffset();
            List<TimeZone> rawOffsetTimeZones = rawOffsetToDifferentRulesTimeZonesMap.get(rawOffset);

            if (rawOffsetTimeZones == null)
            {
                rawOffsetTimeZones = new ArrayList<TimeZone>();
                rawOffsetToDifferentRulesTimeZonesMap.put(rawOffset, rawOffsetTimeZones);
            }

            boolean differentRules = true;

            for (TimeZone t : rawOffsetTimeZones)
            {
                if (tz.hasSameRules(t))
                {
                    differentRules = false;
                    sameRulesTimeZonesMap.get(t).add(tz);
                    break;
                }
            }

            if (differentRules)
            {
                rawOffsetTimeZones.add(tz);
                final List<TimeZone> sameRulesTimeZones = new ArrayList<TimeZone>();
                sameRulesTimeZones.add(tz);
                sameRulesTimeZonesMap.put(tz, sameRulesTimeZones);
            }
        }

        /* populate map: timezone string description ---> TimeZone object */
        for (Map.Entry<Integer,List<TimeZone>> me : rawOffsetToDifferentRulesTimeZonesMap.entrySet())
        {
            final int rawOffset = me.getKey();

            /* determine GMT offset string */
            int offsetMinutes = rawOffset / (60 * 1000);
            boolean negative = false;

            if (offsetMinutes < 0)
            {
                negative = true;
                offsetMinutes = -offsetMinutes;
            }

            final int h = offsetMinutes / 60;
            final int m = offsetMinutes - h * 60;

            final String gmtOffset = "GMT" +
                    ((negative) ? "-" : "+") +
                    ((h < 10) ? "0" : "") + h + ":" +
                    ((m < 10) ? "0" : "") + m;

            for (TimeZone tz : me.getValue())
            {
                final List<String> sameRulesIds = new ArrayList<String>();

                for (TimeZone t : sameRulesTimeZonesMap.get(tz))
                {
                    /* extract the city name (if any) from the ID */
                    final String id = t.getID();
                    sameRulesIds.add(id.substring(id.lastIndexOf("/") + 1).replace('_', ' '));
                }

                Collections.sort(sameRulesIds);

                final StringBuilder sb = new StringBuilder("(" + gmtOffset + ") ");
                String prev = null;

                for (String s : sameRulesIds)
                {
                    if (s.equals(prev))
                    {
                        /* skip duplicates */
                        continue;
                    }

                    sb.append(s);
                    sb.append(", ");
                    prev = s;
                }

                sb.delete(sb.length() - 2, sb.length());

                TIMEZONE_STRING_TO_OBJECT_MAP.put(sb.toString(), tz);
            }
        }

        /* populate list of timezone string descriptions, and sort in order of GMT offset */
        final List<Pair<Integer,String>> descriptions = new ArrayList<Pair<Integer,String>>();

        for (Map.Entry<String,TimeZone> me : TIMEZONE_STRING_TO_OBJECT_MAP.entrySet())
        {
            descriptions.add(new Pair<Integer,String>(me.getValue().getRawOffset(), me.getKey()));
        }

        Collections.sort(descriptions);

        TIMEZONE_STRINGS = new String[descriptions.size()];

        for (int i = 0; i < descriptions.size(); i++)
        {
            TIMEZONE_STRINGS[i] = descriptions.get(i).e2;
        }
    }


    /**
    * Constructor.
    *
    * @param parent
    *      parent ClockUnit object
    */
    TimeZoneSelector(
            final ClockUnit parent)
    {
        /*********************
        * INITIALIZE FIELDS *
        *********************/

        this.parent = parent;
        this.model = new QuickFindListModel(TIMEZONE_STRINGS);

        /******************************
        * INITIALIZE FORM COMPONENTS *
        ******************************/

        initComponents();

        /*****************************
        * CONFIGURE FORM COMPONENTS *
        *****************************/

        setTitle("Select Time Zone - " + this.parent.getTitle());

        addWindowListener(new WindowAdapter()
        {
            @Override
            public void windowClosing(WindowEvent e)
            {
                TimeZoneSelector.this.cancelButton.doClick();
            }
        });

        /* inherit "always on top" mode from parent */
        try
        {
            setAlwaysOnTop(this.parent.isAlwaysOnTop());
        }
        catch (Exception e)
        {
            /* ignore */
        }

        /* inherit program icon of parent */
        final List<Image> icons = this.parent.getIconImages();

        if (!icons.isEmpty())
        {
            setIconImage(icons.get(0));
        }

        /* field: "Quick find term" */
        findField.getDocument().addDocumentListener(new DocumentListenerAdapter()
        {
            @Override
            public void insertUpdate(DocumentEvent e)
            {
                TimeZoneSelector.this.model.quickFind(SwingManipulator.getTextJTextField(findField));
            }

            @Override
            public void removeUpdate(DocumentEvent e)
            {
                TimeZoneSelector.this.model.quickFind(SwingManipulator.getTextJTextField(findField));
            }

            @Override
            public void changedUpdate(DocumentEvent e)
            {
                TimeZoneSelector.this.model.quickFind(SwingManipulator.getTextJTextField(findField));
            }
        });
        SwingManipulator.addStandardEditingPopupMenu(new JTextField[] {findField});

        /* list of timezones */
        list.setModel(model);
        list.clearSelection();

        list.addListSelectionListener(new ListSelectionListener()
        {
            public void valueChanged(ListSelectionEvent e)
            {
                okButton.setEnabled(!list.isSelectionEmpty());
            }
        });

        model.addListDataListener(new ListDataListener()
        {
            public void intervalAdded(ListDataEvent e)
            {
                list.clearSelection();
            }

            public void intervalRemoved(ListDataEvent e)
            {
                list.clearSelection();
            }

            public void contentsChanged(ListDataEvent e)
            {
                list.clearSelection();
            }
        });

        /* button: "OK" */
        okButton.setEnabled(!list.isSelectionEmpty());
        okButton.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                final String tz = (String) TimeZoneSelector.this.list.getSelectedValue();

                if (tz != null)
                {
                    TimeZoneSelector.this.parent.setTimeZone(TIMEZONE_STRING_TO_OBJECT_MAP.get(tz));
                }

                TimeZoneSelector.this.setVisible(false);
            }
        });

        /* button: "Cancel" */
        cancelButton.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                TimeZoneSelector.this.setVisible(false);
            }
        });

        /* key binding: ENTER key */
        for (JComponent c : new JComponent[] {findField, list})
        {
            c.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
                    .put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "ENTER_OK_BUTTON");

            c.getActionMap().put("ENTER_OK_BUTTON", new AbstractAction()
            {
                public void actionPerformed(ActionEvent e)
                {
                    okButton.doClick();
                }
            });
        }

        /* key binding: ESCAPE key */
        for (JComponent c : new JComponent[] {findField, list, buttonsPanel})
        {
            c.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
                    .put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "ESCAPE_CANCEL_BUTTON");

            c.getActionMap().put("ESCAPE_CANCEL_BUTTON", new AbstractAction()
            {
                public void actionPerformed(ActionEvent e)
                {
                    cancelButton.doClick();
                }
            });
        }

        /* key binding: arrow up/down keys */
        for (int key : new int[] {KeyEvent.VK_UP, KeyEvent.VK_DOWN})
        {
            findField.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
                    .put(KeyStroke.getKeyStroke(key, 0), "ARROW_" + key + "_TO_LIST");

            findField.getActionMap().put("ARROW_" + key + "_TO_LIST", new AbstractAction()
            {
                public void actionPerformed(ActionEvent e)
                {
                    list.requestFocus();
                }
            });
        }

        /* center form on the screen */
        setLocationRelativeTo(this.parent);
    }

    /***************************
    * NETBEANS-GENERATED CODE *
    ***************************/

    /** This method is called from within the constructor to
    * initialize the form.
    * WARNING: Do NOT modify this code. The content of this method is
    * always regenerated by the Form Editor.
    */
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents()
    {

        findLabel = new javax.swing.JLabel();
        findField = new javax.swing.JTextField();
        scrollPane = new javax.swing.JScrollPane();
        list = new javax.swing.JList();
        buttonsPanel = new javax.swing.JPanel();
        okButton = new javax.swing.JButton();
        cancelButton = new javax.swing.JButton();

        setDefaultCloseOperation(javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE);

        findLabel.setDisplayedMnemonic('f');
        findLabel.setLabelFor(findField);
        findLabel.setText("Find:");

        list.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION);
        scrollPane.setViewportView(list);

        buttonsPanel.setLayout(new java.awt.GridLayout(1, 0));

        okButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/thistime/resources/tick.png"))); // NOI18N
        okButton.setMnemonic('o');
        okButton.setText("OK");
        buttonsPanel.add(okButton);

        cancelButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/thistime/resources/cross.png"))); // NOI18N
        cancelButton.setMnemonic('c');
        cancelButton.setText("Cancel");
        buttonsPanel.add(cancelButton);

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
                    .addComponent(scrollPane, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, 280, Short.MAX_VALUE)
                    .addComponent(buttonsPanel, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, 280, Short.MAX_VALUE)
                    .addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup()
                        .addComponent(findLabel)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(findField, javax.swing.GroupLayout.DEFAULT_SIZE, 252, Short.MAX_VALUE)))
                .addContainerGap())
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                    .addComponent(findLabel)
                    .addComponent(findField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                .addComponent(scrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 200, Short.MAX_VALUE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                .addComponent(buttonsPanel, javax.swing.GroupLayout.PREFERRED_SIZE, 34, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addContainerGap())
        );

        pack();
    }// </editor-fold>//GEN-END:initComponents

    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JPanel buttonsPanel;
    private javax.swing.JButton cancelButton;
    private javax.swing.JTextField findField;
    private javax.swing.JLabel findLabel;
    private javax.swing.JList list;
    private javax.swing.JButton okButton;
    private javax.swing.JScrollPane scrollPane;
    // End of variables declaration//GEN-END:variables
}