/**
 * LoggerConsole.java
 * Copyright 2008 Zach Scrivena
 * zachscrivena@gmail.com
 * http://zs.freeshell.org/
 *
 * 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 org.freeshell.zs.common;

import java.awt.Font;
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.Arrays;
import java.util.Date;
import java.util.Locale;
import javax.swing.AbstractAction;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JRadioButton;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;


/**
 * Simple console for logging messages.
 * Messages are stored in a circular array of fixed capacity.
 * All the methods of this class are thread-safe.
 */
public class LoggerConsole
        extends JFrame
{
    /** time format string (yyyy-MM-dd HH:mm:ss.SSS) */
    private static final String TIME_FORMAT_STRING = "%1$tF %1$tT.%1$tL";

    /** default capacity of the logger (1000) */
    private static final int DEFAULT_CAPACITY = 1000;

    /** capacity of the logger, i.e. maximum number of messages to store */
    private final int capacity;

    /** circular array of logged messages */
    private final String[] messages;

    /** index in <code>messages</code> where the next message should be stored */
    private int messagesIndex;

    /** is the logger currently on? */
    private volatile boolean isLoggerOn = true;


    /**
    * Construct a logger console with the specified title and capacity of 1000.
    *
    * @param title
    *     title of the logger console
    */
    public LoggerConsole(
            final String title)
    {
        this(title, DEFAULT_CAPACITY);
    }


    /**
    * Construct a logger console with the specified title and capacity.
    *
    * @param title
    *     title of the logger console
    * @param capacity
    *     capacity of the logger, i.e. maximum number of messages to store
    */
    public LoggerConsole(
            final String title,
            final int capacity)
    {
        this.capacity = capacity;
        messages = new String[capacity];
        Arrays.fill(messages, null);
        messagesIndex = 0;

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

        initComponents();

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

        setTitle(title);

        addWindowListener(new WindowAdapter()
        {
            @Override
            public void windowIconified(WindowEvent e)
            {
                hideForm();
            }

            @Override
            public void windowDeiconified(WindowEvent e)
            {
                showForm();
            }

            @Override
            public void windowClosing(WindowEvent e)
            {
                hideForm();
            }
        });

        /* console text area */
        consoleText.setFont(new Font(
                Font.DIALOG,
                Font.PLAIN,
                consoleText.getFont().getSize() - 2));

        /* radio buttons: Logger "On", "Off" */
        onButton.setSelected(isLoggerOn);
        offButton.setSelected(!isLoggerOn);

        for (JRadioButton b : new JRadioButton[] {onButton, offButton})
        {
            b.addActionListener(new ActionListener()
            {
                public void actionPerformed(ActionEvent e)
                {
                    isLoggerOn = onButton.isSelected();
                }
            });
        }

        /* button: "Reset" */
        resetButton.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                reset();
            }
        });

        /* button: "Refresh" */
        refreshButton.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                refresh();
            }
        });

        /* button: "Close" */
        closeButton.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                hideForm();
            }
        });

        /* key binding: ESCAPE key */
        consolePane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
                .put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "ESCAPE_CLOSE_BUTTON");

        consolePane.getActionMap().put("ESCAPE_CLOSE_BUTTON", new AbstractAction()
        {
            public void actionPerformed(ActionEvent e)
            {
                closeButton.doClick();
            }
        });

        /* position in the center of the screen */
        setLocationRelativeTo(null);
    }


    /**
    * Log the specified message.
    * The message will be automatically timestamped.
    * This method can be called on any thread.
    *
    * @param format
    *     format string
    * @param args
    *     arguments referenced by the format specifiers in the format string
    */
    public void log(
            final String format,
            final Object... args)
    {
        if (isLoggerOn)
        {
            synchronized (messages)
            {
                final String message = String.format(format, args);

                messages[messagesIndex++] = String.format(
                        Locale.ENGLISH,
                        "[" + TIME_FORMAT_STRING + "] %2$s\n",
                        new Date(), message);

                if (messagesIndex >= capacity)
                {
                    messagesIndex = 0;
                }
            }
        }
    }


    /**
    * Reset the stored messages.
    * This method can be called on any thread.
    */
    public void reset()
    {
        synchronized (messages)
        {
            Arrays.fill(messages, null);
        }

        refresh();
    }


    /**
    * Refresh the console.
    * This method can be called on any thread.
    */
    public void refresh()
    {
        consoleText.setText("");

        synchronized (messages)
        {
            for (int i = 0; i < capacity; i++)
            {
                final String s = messages[(messagesIndex + i) % capacity];

                if (s != null)
                {
                    consoleText.append(s);
                }
            }
        }

        /* scroll to the bottom of the console */
        SwingUtilities.invokeLater(new Runnable()
        {
            public void run()
            {
                try
                {
                    consoleText.setCaretPosition(consoleText.getDocument().getLength());
                }
                catch (Exception e)
                {
                    /* ignore */
                }
            }
        });
    }


    /**
    * Show the console.
    * This method can be called on any thread.
    */
    public void showForm()
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            public void run()
            {
                setVisible(true);
                setExtendedState(JFrame.NORMAL);
                toFront();
                closeButton.requestFocus();
            }
        });

        refresh();
    }


    /**
    * Hide the console.
    * This method can be called on any thread.
    */
    public void hideForm()
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            public void run()
            {
                setVisible(false);
            }
        });
    }

    /***************************
    * 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.
    */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        onOffGroup = new javax.swing.ButtonGroup();
        buttonsPanel = new javax.swing.JPanel();
        onOffPanel = new javax.swing.JPanel();
        onButton = new javax.swing.JRadioButton();
        offButton = new javax.swing.JRadioButton();
        resetButton = new javax.swing.JButton();
        refreshButton = new javax.swing.JButton();
        closeButton = new javax.swing.JButton();
        consolePanel = new javax.swing.JPanel();
        consolePane = new javax.swing.JScrollPane();
        consoleText = new javax.swing.JTextArea();

        setDefaultCloseOperation(javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE);
        setMinimumSize(new java.awt.Dimension(600, 100));

        onOffPanel.setBorder(javax.swing.BorderFactory.createTitledBorder("Logger"));

        onOffGroup.add(onButton);
        onButton.setMnemonic('o');
        onButton.setSelected(true);
        onButton.setText("On");

        onOffGroup.add(offButton);
        offButton.setMnemonic('f');
        offButton.setText("Off");

        javax.swing.GroupLayout onOffPanelLayout = new javax.swing.GroupLayout(onOffPanel);
        onOffPanel.setLayout(onOffPanelLayout);
        onOffPanelLayout.setHorizontalGroup(
            onOffPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(onOffPanelLayout.createSequentialGroup()
                .addContainerGap()
                .addGroup(onOffPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(onButton)
                    .addComponent(offButton))
                .addContainerGap(14, Short.MAX_VALUE))
        );
        onOffPanelLayout.setVerticalGroup(
            onOffPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(onOffPanelLayout.createSequentialGroup()
                .addContainerGap()
                .addComponent(onButton)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                .addComponent(offButton)
                .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
        );

        resetButton.setMnemonic('t');
        resetButton.setText("Reset");

        refreshButton.setMnemonic('r');
        refreshButton.setText("Refresh");

        closeButton.setMnemonic('c');
        closeButton.setText("Close");

        javax.swing.GroupLayout buttonsPanelLayout = new javax.swing.GroupLayout(buttonsPanel);
        buttonsPanel.setLayout(buttonsPanelLayout);
        buttonsPanelLayout.setHorizontalGroup(
            buttonsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, buttonsPanelLayout.createSequentialGroup()
                .addContainerGap()
                .addGroup(buttonsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING, false)
                    .addComponent(refreshButton, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                    .addComponent(resetButton, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                    .addComponent(closeButton, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                    .addComponent(onOffPanel, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
                .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
        );
        buttonsPanelLayout.setVerticalGroup(
            buttonsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, buttonsPanelLayout.createSequentialGroup()
                .addContainerGap()
                .addComponent(onOffPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 13, Short.MAX_VALUE)
                .addComponent(resetButton)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(refreshButton)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(closeButton, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addContainerGap())
        );

        getContentPane().add(buttonsPanel, java.awt.BorderLayout.LINE_END);

        consoleText.setColumns(20);
        consoleText.setEditable(false);
        consoleText.setRows(5);
        consolePane.setViewportView(consoleText);

        javax.swing.GroupLayout consolePanelLayout = new javax.swing.GroupLayout(consolePanel);
        consolePanel.setLayout(consolePanelLayout);
        consolePanelLayout.setHorizontalGroup(
            consolePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGap(0, 481, Short.MAX_VALUE)
            .addGroup(consolePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                .addGroup(consolePanelLayout.createSequentialGroup()
                    .addContainerGap()
                    .addComponent(consolePane, javax.swing.GroupLayout.DEFAULT_SIZE, 471, Short.MAX_VALUE)))
        );
        consolePanelLayout.setVerticalGroup(
            consolePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGap(0, 205, Short.MAX_VALUE)
            .addGroup(consolePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                .addGroup(consolePanelLayout.createSequentialGroup()
                    .addContainerGap()
                    .addComponent(consolePane, javax.swing.GroupLayout.DEFAULT_SIZE, 183, Short.MAX_VALUE)
                    .addContainerGap()))
        );

        getContentPane().add(consolePanel, java.awt.BorderLayout.CENTER);

        pack();
    }// </editor-fold>//GEN-END:initComponents
    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JPanel buttonsPanel;
    private javax.swing.JButton closeButton;
    private javax.swing.JScrollPane consolePane;
    private javax.swing.JPanel consolePanel;
    private javax.swing.JTextArea consoleText;
    private javax.swing.JRadioButton offButton;
    private javax.swing.JRadioButton onButton;
    private javax.swing.ButtonGroup onOffGroup;
    private javax.swing.JPanel onOffPanel;
    private javax.swing.JButton refreshButton;
    private javax.swing.JButton resetButton;
    // End of variables declaration//GEN-END:variables
}