/*
 * Copyright (C) 2013 Yoshinori Hayakawa <hayakawa@cite.tohoku.ac.jp>
 *
 * 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 turtlemessenger;

import java.awt.Color;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
import javax.imageio.ImageIO;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.WindowConstants;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import sun.misc.HexDumpEncoder;

/**
 *
 * @author Yoshinori Hayakawa <hayakawa@cite.tohoku.ac.jp>
 */
public class TMController extends javax.swing.JFrame {

    private static final int MAXCHARS = 140 ; // muest be less than 255
    private static final int PORT_BROADCAST = 3100;
    private static final int PORT_SUBMISSION = 3100;
    
    private static final int RSA_KEY_LENGTH = 512 ;
    
    private int serverChannel ; /* offset for communication ports A:+0, B:+1, C:+2 */
    
    private static final int BROADCAST_MESSAGE_LENGTH = 1024 ; // must be larger than MAXCHARS * 3 (utf-8)
    private static final int BORADCAST_INTERVAL_MS = 5000;
    private boolean receiverIsRunning;
    private TMReceiver receiver;
    private BroadcastThread broadcaster;
    private boolean broadcasterIsTerminating=false ;
    
    private TMHttpServer httpserver=null ;

    public HashMap<String, Integer> choices;
    public int nchoices[];
    
    private TMMessages messageDialog ;
    private TMVotingSummary votingSummaryDialog ;

    
    private KeyPair rsaKeyPair ;
    public RSAPrivateKey privateKey ;
    public RSAPublicKey publicKey ;
    
    public byte[] broadcastAddress ;
    
    private Preferences prefs;
    
    List<String> commentList ;
    /**
     * Creates new form TMController
     */
    public TMController() {
        initComponents();

        if (!isFaculty()) {
            JOptionPane.showMessageDialog(
                    this,
                    java.util.ResourceBundle.getBundle("turtlemessenger/TMBundle").getString("STUDENTS ARE NOT ELIGIBLE TO USE THIS SOFTWARE"),
                    "WARNING",
                    JOptionPane.WARNING_MESSAGE);
            System.exit(0);
        }

        choices = new HashMap<String, Integer>(100);
        resetChoices();
        nchoices = new int[10];
        
        broadcastAddress = new byte[4] ;
        broadcastAddress[0] = (byte) 0xff ;
        broadcastAddress[1] = (byte) 0xff ;
        broadcastAddress[2] = (byte) 0xff ;
        broadcastAddress[3] = (byte) 0xff ;
        
        serverChannel = 0 ; /* channel A */

        prefs = Preferences.userNodeForPackage(this.getClass());
        restorePreferences();

        receiver = new TMReceiver(this, PORT_SUBMISSION + serverChannel);
        receiverIsRunning = false;

        broadcaster = null;
        
        messageDialog = null ;
        votingSummaryDialog = null ;
        
        commentList = new ArrayList<String>() ;
         
        // commentsTextArea.setText(java.util.ResourceBundle.getBundle("turtlemessenger/TMBundle").getString("WRITE INSTRUCTION HERE")) ;
        // commentsTextArea.requestFocusInWindow() ;
        // commentsTextArea.selectAll();

        generateKeyPair() ;
        
        Image img;
        try {
            img = ImageIO.read(getClass().getResource("images/tmsgr-icon.png"));
            setIconImage(img);
        } catch (IOException ex) { 
            System.err.println("icon not found"); 
        }
        
        setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
        addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                exitMenuItemActionPerformed(null);
            }
        });
    }

    /**
     * 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() {

        channelButtonGroup = new javax.swing.ButtonGroup();
        jPanel1 = new javax.swing.JPanel();
        resetVoteButton = new javax.swing.JButton();
        startToggleButton = new javax.swing.JToggleButton();
        passcodeTextField = new javax.swing.JTextField();
        displayMessagesButton = new javax.swing.JButton();
        displayVotingResutsButton = new javax.swing.JButton();
        jPanel2 = new javax.swing.JPanel();
        channelARadioButton = new javax.swing.JRadioButton();
        channelBRadioButton = new javax.swing.JRadioButton();
        channelCRadioButton = new javax.swing.JRadioButton();
        jLabel2 = new javax.swing.JLabel();
        jLabel1 = new javax.swing.JLabel();
        commentsTextArea = new javax.swing.JTextArea();
        readFileButton = new javax.swing.JButton();
        lineNumberSpinner = new javax.swing.JSpinner();
        enableHttpCheckBox = new javax.swing.JCheckBox();
        jScrollPane1 = new javax.swing.JScrollPane();
        logTextPane = new javax.swing.JTextPane();
        jMenuBar1 = new javax.swing.JMenuBar();
        jMenu1 = new javax.swing.JMenu();
        saveLogMenuItem = new javax.swing.JMenuItem();
        exitMenuItem = new javax.swing.JMenuItem();
        copyMenu = new javax.swing.JMenu();
        cutMenuItem = new javax.swing.JMenuItem();
        copyMenuItem = new javax.swing.JMenuItem();
        pasteMenuItem = new javax.swing.JMenuItem();
        copyLogMenuItem = new javax.swing.JMenuItem();
        settingsMenu = new javax.swing.JMenu();
        broadcastMenuItem = new javax.swing.JMenuItem();
        helpMenu = new javax.swing.JMenu();
        helpMenuItem = new javax.swing.JMenuItem();
        aboutMenuItem = new javax.swing.JMenuItem();

        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
        setTitle("TurtleMessenger");
        setMinimumSize(new java.awt.Dimension(520, 330));

        java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("turtlemessenger/TMBundle"); // NOI18N
        resetVoteButton.setText(bundle.getString("RESET VOTE COUNT")); // NOI18N
        resetVoteButton.setToolTipText("投票数をリセットします");
        resetVoteButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                resetVoteButtonActionPerformed(evt);
            }
        });

        startToggleButton.setText(bundle.getString("START")); // NOI18N
        startToggleButton.setToolTipText("<html>このボタンを押すと、受講者からのフィードバックの受付が始まります。<br>\nもう一度おすと、受付を停止します。\n</html>");
        startToggleButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                startToggleButtonActionPerformed(evt);
            }
        });

        passcodeTextField.setText("any");
        passcodeTextField.setToolTipText("<html>\nこのパスコード（英数字列）を設定している学生のみからの投稿を受付ます<br>\n誰でも投稿可能にするには any を設定します\n</html>");
        passcodeTextField.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                passcodeTextFieldActionPerformed(evt);
            }
        });

        displayMessagesButton.setText(bundle.getString("DISPLAY MESSAGES")); // NOI18N
        displayMessagesButton.setToolTipText("受講者からのメッセージ投稿の一覧を別ウィンドウに表示します");
        displayMessagesButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                displayMessagesButtonActionPerformed(evt);
            }
        });

        displayVotingResutsButton.setText(bundle.getString("DISPLAY VOTING RESULTS")); // NOI18N
        displayVotingResutsButton.setToolTipText("投票結果の一覧を別ウィンドウに表示します");
        displayVotingResutsButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                displayVotingResutsButtonActionPerformed(evt);
            }
        });

        jPanel2.setBorder(javax.swing.BorderFactory.createEtchedBorder());
        jPanel2.setToolTipText("<html>\n通信に使う「チャネル」設定です<br>\n通常はAのままでお使いください\n</html>");

        channelButtonGroup.add(channelARadioButton);
        channelARadioButton.setSelected(true);
        channelARadioButton.setText("A");
        channelARadioButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                channelARadioButtonActionPerformed(evt);
            }
        });

        channelButtonGroup.add(channelBRadioButton);
        channelBRadioButton.setText("B");
        channelBRadioButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                channelBRadioButtonActionPerformed(evt);
            }
        });

        channelButtonGroup.add(channelCRadioButton);
        channelCRadioButton.setText("C");
        channelCRadioButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                channelCRadioButtonActionPerformed(evt);
            }
        });

        org.jdesktop.layout.GroupLayout jPanel2Layout = new org.jdesktop.layout.GroupLayout(jPanel2);
        jPanel2.setLayout(jPanel2Layout);
        jPanel2Layout.setHorizontalGroup(
            jPanel2Layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING)
            .add(jPanel2Layout.createSequentialGroup()
                .addContainerGap()
                .add(channelARadioButton)
                .add(12, 12, 12)
                .add(channelBRadioButton)
                .addPreferredGap(org.jdesktop.layout.LayoutStyle.UNRELATED)
                .add(channelCRadioButton)
                .add(0, 8, Short.MAX_VALUE))
        );
        jPanel2Layout.setVerticalGroup(
            jPanel2Layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING)
            .add(jPanel2Layout.createSequentialGroup()
                .addContainerGap()
                .add(jPanel2Layout.createParallelGroup(org.jdesktop.layout.GroupLayout.TRAILING)
                    .add(channelARadioButton)
                    .add(jPanel2Layout.createParallelGroup(org.jdesktop.layout.GroupLayout.BASELINE)
                        .add(channelCRadioButton)
                        .add(channelBRadioButton)))
                .addContainerGap(8, Short.MAX_VALUE))
        );

        jLabel2.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING);
        jLabel2.setText(bundle.getString("CHANNEL")); // NOI18N

        jLabel1.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING);
        jLabel1.setText(bundle.getString("PASSCODE")); // NOI18N
        jLabel1.setToolTipText("<html>\nこのパスコード（英数字列）を設定している学生のみからの投稿を受付ます<br>\n誰でも投稿可能にするには any を設定します\n</html>");

        commentsTextArea.setColumns(20);
        commentsTextArea.setLineWrap(true);
        commentsTextArea.setRows(5);
        commentsTextArea.setToolTipText("<html>この欄に記入した指示や質問は、学生側のウィンドウに表示されます。<br>\n文字数は140まで。\n</html>\n");
        commentsTextArea.setBorder(javax.swing.BorderFactory.createLineBorder(new java.awt.Color(0, 0, 0)));

        readFileButton.setText("...");
        readFileButton.setToolTipText("<html>\nこのボタンをクリックして、テキストファイル開くと<br>\nファイルの各行が左欄に入力されます\n</html>");
        readFileButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                readFileButtonActionPerformed(evt);
            }
        });

        lineNumberSpinner.setModel(new javax.swing.SpinnerNumberModel(Integer.valueOf(0), Integer.valueOf(0), null, Integer.valueOf(1)));
        lineNumberSpinner.setToolTipText("ファイルの何行目を左欄に入力するかを指定します");
        lineNumberSpinner.addChangeListener(new javax.swing.event.ChangeListener() {
            public void stateChanged(javax.swing.event.ChangeEvent evt) {
                lineNumberSpinnerStateChanged(evt);
            }
        });

        enableHttpCheckBox.setText(bundle.getString("ENABLE_WEB_ACCESS")); // NOI18N
        enableHttpCheckBox.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                enableHttpCheckBoxActionPerformed(evt);
            }
        });

        org.jdesktop.layout.GroupLayout jPanel1Layout = new org.jdesktop.layout.GroupLayout(jPanel1);
        jPanel1.setLayout(jPanel1Layout);
        jPanel1Layout.setHorizontalGroup(
            jPanel1Layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING)
            .add(jPanel1Layout.createSequentialGroup()
                .add(12, 12, 12)
                .add(jPanel1Layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING)
                    .add(jPanel1Layout.createSequentialGroup()
                        .add(jPanel1Layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING)
                            .add(jPanel1Layout.createSequentialGroup()
                                .add(jPanel1Layout.createParallelGroup(org.jdesktop.layout.GroupLayout.TRAILING, false)
                                    .add(org.jdesktop.layout.GroupLayout.LEADING, commentsTextArea)
                                    .add(jPanel1Layout.createSequentialGroup()
                                        .add(startToggleButton, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 143, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
                                        .addPreferredGap(org.jdesktop.layout.LayoutStyle.UNRELATED)
                                        .add(jLabel2, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 80, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
                                        .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED)
                                        .add(jPanel2, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
                                        .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED)
                                        .add(jLabel1, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 78, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)))
                                .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED)
                                .add(jPanel1Layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING)
                                    .add(lineNumberSpinner, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 87, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
                                    .add(readFileButton)
                                    .add(passcodeTextField, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 72, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)))
                            .add(jPanel1Layout.createSequentialGroup()
                                .add(displayVotingResutsButton, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 258, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
                                .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED)
                                .add(resetVoteButton)))
                        .addContainerGap(org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
                    .add(jPanel1Layout.createSequentialGroup()
                        .add(displayMessagesButton, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 258, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
                        .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                        .add(enableHttpCheckBox)
                        .add(30, 30, 30))))
        );
        jPanel1Layout.setVerticalGroup(
            jPanel1Layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING)
            .add(jPanel1Layout.createSequentialGroup()
                .add(jPanel1Layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING)
                    .add(jPanel1Layout.createSequentialGroup()
                        .addContainerGap()
                        .add(jPanel1Layout.createParallelGroup(org.jdesktop.layout.GroupLayout.BASELINE)
                            .add(startToggleButton, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 60, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
                            .add(jLabel2)))
                    .add(jPanel1Layout.createSequentialGroup()
                        .add(21, 21, 21)
                        .add(jPanel1Layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING)
                            .add(jPanel2, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
                            .add(jPanel1Layout.createParallelGroup(org.jdesktop.layout.GroupLayout.BASELINE)
                                .add(jLabel1)
                                .add(passcodeTextField, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)))))
                .addPreferredGap(org.jdesktop.layout.LayoutStyle.UNRELATED)
                .add(jPanel1Layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING)
                    .add(jPanel1Layout.createSequentialGroup()
                        .add(readFileButton)
                        .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                        .add(lineNumberSpinner, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 25, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE))
                    .add(commentsTextArea, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE))
                .add(18, 18, 18)
                .add(jPanel1Layout.createParallelGroup(org.jdesktop.layout.GroupLayout.BASELINE)
                    .add(displayMessagesButton, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 39, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
                    .add(enableHttpCheckBox))
                .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED)
                .add(jPanel1Layout.createParallelGroup(org.jdesktop.layout.GroupLayout.BASELINE)
                    .add(displayVotingResutsButton, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 40, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
                    .add(resetVoteButton, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 41, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE))
                .addContainerGap())
        );

        jScrollPane1.setVerticalScrollBarPolicy(javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);

        logTextPane.setEditable(false);
        logTextPane.setToolTipText("<html>投稿の状況の記録が表示されます<br>\n内容を保存するには File メニューから Save log を選んでください\n</html>");
        jScrollPane1.setViewportView(logTextPane);

        jMenu1.setText(bundle.getString("FILE")); // NOI18N

        saveLogMenuItem.setText(bundle.getString("SAVE LOG")); // NOI18N
        saveLogMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                saveLogMenuItemActionPerformed(evt);
            }
        });
        jMenu1.add(saveLogMenuItem);

        exitMenuItem.setText(bundle.getString("EXIT")); // NOI18N
        exitMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                exitMenuItemActionPerformed(evt);
            }
        });
        jMenu1.add(exitMenuItem);

        jMenuBar1.add(jMenu1);

        copyMenu.setText(bundle.getString("EDIT")); // NOI18N

        cutMenuItem.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_X, java.awt.event.InputEvent.CTRL_MASK));
        cutMenuItem.setText(bundle.getString("CUT")); // NOI18N
        cutMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                cutMenuItemActionPerformed(evt);
            }
        });
        copyMenu.add(cutMenuItem);

        copyMenuItem.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_C, java.awt.event.InputEvent.CTRL_MASK));
        copyMenuItem.setText(bundle.getString("COPY")); // NOI18N
        copyMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                copyMenuItemActionPerformed(evt);
            }
        });
        copyMenu.add(copyMenuItem);

        pasteMenuItem.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_V, java.awt.event.InputEvent.CTRL_MASK));
        pasteMenuItem.setText(bundle.getString("PASTE")); // NOI18N
        pasteMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                pasteMenuItemActionPerformed(evt);
            }
        });
        copyMenu.add(pasteMenuItem);

        copyLogMenuItem.setText(bundle.getString("COPY LOG")); // NOI18N
        copyLogMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                copyLogMenuItemActionPerformed(evt);
            }
        });
        copyMenu.add(copyLogMenuItem);

        jMenuBar1.add(copyMenu);

        settingsMenu.setText(bundle.getString("SETTINGS")); // NOI18N

        broadcastMenuItem.setText(bundle.getString("BROADCAST")); // NOI18N
        broadcastMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                broadcastMenuItemActionPerformed(evt);
            }
        });
        settingsMenu.add(broadcastMenuItem);

        jMenuBar1.add(settingsMenu);

        helpMenu.setText(bundle.getString("HELP")); // NOI18N

        helpMenuItem.setText(bundle.getString("HELP")); // NOI18N
        helpMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                helpMenuItemActionPerformed(evt);
            }
        });
        helpMenu.add(helpMenuItem);

        aboutMenuItem.setText(bundle.getString("ABOUT")); // NOI18N
        aboutMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                aboutMenuItemActionPerformed(evt);
            }
        });
        helpMenu.add(aboutMenuItem);

        jMenuBar1.add(helpMenu);

        setJMenuBar(jMenuBar1);

        org.jdesktop.layout.GroupLayout layout = new org.jdesktop.layout.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING)
            .add(jScrollPane1)
            .add(jPanel1, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING)
            .add(org.jdesktop.layout.GroupLayout.TRAILING, layout.createSequentialGroup()
                .add(jPanel1, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED)
                .add(jScrollPane1, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, 173, Short.MAX_VALUE))
        );

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

    private void resetVoteButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_resetVoteButtonActionPerformed
        appendLogWithTimestamp("#\tRESET VOTE COUNT\n") ;
        resetChoices();
    }//GEN-LAST:event_resetVoteButtonActionPerformed

    private void startToggleButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_startToggleButtonActionPerformed
        if (broadcasterIsTerminating) {
            startToggleButton.setSelected(false) ;
            startToggleButton.setForeground(Color.BLACK) ;
            return ;
        }
        
        if (!receiverIsRunning) {          
            startBroadcast();
            receiver.setMyPasscode(passcodeTextField.getText().trim());
            receiver.setReceiverPort(PORT_SUBMISSION+serverChannel) ;
            receiver.startReceiver();
            receiverIsRunning = true;
            startToggleButton.setForeground(Color.RED) ;
            startToggleButton.setText(java.util.ResourceBundle.getBundle("turtlemessenger/TMBundle").getString("RUNNING"));
            appendLogWithTimestamp("#\tSTART LISTENING" + "\n") ;
        } else {
            stopBroadcast();
            receiver.terminateReceiver();
            receiverIsRunning = false;
            startToggleButton.setForeground(Color.BLACK) ;
            startToggleButton.setText(java.util.ResourceBundle.getBundle("turtlemessenger/TMBundle").getString("START"));
            appendLogWithTimestamp("#\tSTOP LISTENING" + "\n") ;
        }
    }//GEN-LAST:event_startToggleButtonActionPerformed

    private void exitMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exitMenuItemActionPerformed
        if (confirmToSaveDialogue() < 0) {
            return;
        }
        stopBroadcast();
        receiver.terminateReceiver();
        if (httpserver != null) {
            httpserver.stopServer();
        }
        savePreferences();
        System.exit(0);
    }//GEN-LAST:event_exitMenuItemActionPerformed

    private void copyLogMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_copyLogMenuItemActionPerformed
        logTextPane.copy();
    }//GEN-LAST:event_copyLogMenuItemActionPerformed

    private void saveLogMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_saveLogMenuItemActionPerformed
        saveLogFile() ;
    }//GEN-LAST:event_saveLogMenuItemActionPerformed

    private void displayVotingResutsButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_displayVotingResutsButtonActionPerformed
        if (votingSummaryDialog==null) votingSummaryDialog = new TMVotingSummary(this,false) ;
        votingSummaryDialog.setVisible(true);
        
    }//GEN-LAST:event_displayVotingResutsButtonActionPerformed

    private void displayMessagesButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_displayMessagesButtonActionPerformed
        if (messageDialog == null) messageDialog = new TMMessages(this,false) ;
        messageDialog.updateTable() ;
        messageDialog.setVisible(true);
    }//GEN-LAST:event_displayMessagesButtonActionPerformed

    private void passcodeTextFieldActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_passcodeTextFieldActionPerformed
        appendLogWithTimestamp("#\tSET PASSCODE " + passcodeTextField.getText().trim() + "\n") ;
        if (receiver != null) {
            receiver.setMyPasscode(passcodeTextField.getText().trim());
        }
    }//GEN-LAST:event_passcodeTextFieldActionPerformed

    private void channelARadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_channelARadioButtonActionPerformed
        appendLogWithTimestamp("#\tSELECT CHANNEL A\n") ;
        serverChannel = 0 ;
    }//GEN-LAST:event_channelARadioButtonActionPerformed

    private void channelBRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_channelBRadioButtonActionPerformed
        appendLogWithTimestamp("#\tSELECT CHANNEL B\n") ;
        serverChannel = 1 ;
    }//GEN-LAST:event_channelBRadioButtonActionPerformed

    private void channelCRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_channelCRadioButtonActionPerformed
        appendLogWithTimestamp("#\tSELECT CHANNEL C\n") ;
        serverChannel = 2 ;
    }//GEN-LAST:event_channelCRadioButtonActionPerformed

    private void aboutMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_aboutMenuItemActionPerformed
        TMAboutDialog about = new TMAboutDialog(this,false) ;
        about.setVisible(true);
    }//GEN-LAST:event_aboutMenuItemActionPerformed

    private void helpMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_helpMenuItemActionPerformed
        TMHelpDialog help = new TMHelpDialog(this,false) ;
        help.setVisible(true) ;
    }//GEN-LAST:event_helpMenuItemActionPerformed

    private void cutMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cutMenuItemActionPerformed
        commentsTextArea.cut() ;
    }//GEN-LAST:event_cutMenuItemActionPerformed

    private void copyMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_copyMenuItemActionPerformed
        commentsTextArea.copy() ;
    }//GEN-LAST:event_copyMenuItemActionPerformed

    private void pasteMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_pasteMenuItemActionPerformed
        commentsTextArea.paste() ;
    }//GEN-LAST:event_pasteMenuItemActionPerformed

    private void lineNumberSpinnerStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_lineNumberSpinnerStateChanged
        setCommentsTextArea() ;
    }//GEN-LAST:event_lineNumberSpinnerStateChanged

    private void readFileButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_readFileButtonActionPerformed
        openCommentFile() ;
    }//GEN-LAST:event_readFileButtonActionPerformed

    private void broadcastMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_broadcastMenuItemActionPerformed
        TMBcasAddrSettingDialog bcas = new TMBcasAddrSettingDialog(this,false) ;
        bcas.setBroadcastAddressToTextField(broadcastAddress) ;
        bcas.setVisible(true) ;
    }//GEN-LAST:event_broadcastMenuItemActionPerformed

    private void enableHttpCheckBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_enableHttpCheckBoxActionPerformed
        if (enableHttpCheckBox.isSelected()) {
            if (httpserver==null) {
                httpserver = new TMHttpServer(this, 3080) ;
                appendLogWithTimestamp("#\tSTART HTTPD" + "\n") ;
            }
        } else { 
            if (httpserver!=null) {
                httpserver.stopServer() ;
                httpserver=null ;
                appendLogWithTimestamp("#\tSTOP HTTPD" + "\n") ;
            }
        }
    }//GEN-LAST:event_enableHttpCheckBoxActionPerformed

    private int saveLogFile() {
        JFileChooser fileChooser = new JFileChooser();
        if (fileChooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
            File outputFile = fileChooser.getSelectedFile();
            if (outputFile.exists() && !canOverwriteFile(outputFile.getName())) {
                return -1;
            }
            BufferedWriter writer;
            try {
                writer = new BufferedWriter(
                        new OutputStreamWriter(new FileOutputStream(outputFile), "utf-8"));
                writer.write(logTextPane.getText());
                writer.close();
            } catch (IOException ioe) {
                return -1 ;
            }
            return 0 ;
        }
        return -1 ;
    }
    
     private int confirmToSaveDialogue() {
        if (receiver.hasMessage()) {
            int rc = JOptionPane.showConfirmDialog(
                    new javax.swing.JPanel(),
                    java.util.ResourceBundle.getBundle("turtlemessenger/TMBundle").getString("DO YOU WANT TO SAVE FEEDBACK FROM STUDENTS?"));

            if (rc == JOptionPane.YES_OPTION) {
                return saveLogFile() ;
            } else if (rc == JOptionPane.CANCEL_OPTION) {
                return -1;
            }
        }
        return 0;
    }
         
    public void savePreferences() {
        prefs.put("PASSCODE", passcodeTextField.getText());
        prefs.putInt("CHANNEL", serverChannel);
        prefs.putByteArray("BROADCAST",broadcastAddress) ;
        
        Rectangle fbound = this.getBounds();
        prefs.putInt("WIN_LOC_X", fbound.x);
        prefs.putInt("WIN_LOC_Y", fbound.y);
    
    }
    
    public void restorePreferences() {
        serverChannel = prefs.getInt("CHANNEL", 0);
        switch (serverChannel) {
            case 0:
                channelARadioButton.setSelected(true);
                break;
            case 1:
                channelBRadioButton.setSelected(true);
                break;
            case 2:
                channelCRadioButton.setSelected(true);
                break;
            default:
                serverChannel = 0;
                channelARadioButton.setSelected(true);
                break;
        }
        broadcastAddress = prefs.getByteArray("BROADCAST", broadcastAddress) ; 
        passcodeTextField.setText(prefs.get("PASSCODE","any")) ;
        
        int x = prefs.getInt("WIN_LOC_X", 10);
        int y = prefs.getInt("WIN_LOC_Y", 10);
        this.setLocation(x,y) ;
    }
   
    public void resetChoices() {
        synchronized(choices) {
            choices.clear();
        }
    }

    public void votedTo(String userid, int choice) {
        synchronized (choices) {
            if (choice >= 0 && choice <= 9) {
                choices.remove(userid);
                choices.put(userid, choice);
            }
        }
    }

    public void countVotes() {
        for (int c = 0; c < 10; c++) {
            nchoices[c] = 0;
        }
        Set set = choices.keySet();
        Iterator iter = set.iterator();
        while (iter.hasNext()) {
            Object userid = iter.next();
            int c = choices.get(userid);
            if (c >= 0 && c <= 9) {
                nchoices[c]++;
            }
        }
    }

    public String getVoteCountInJSON() {
        boolean firstElement=true ;
        countVotes() ;
        StringBuilder strBuf = new StringBuilder("") ;
        strBuf.append("[");
        for (int i = 0; i <= 9; i++) {
            if (firstElement) {
                firstElement=false ;
            } else {
                strBuf.append(",");
            }
            strBuf.append("{");
            strBuf.append("\"item\":" + i + ",");
            strBuf.append("\"count\":" + nchoices[i]);
            strBuf.append("}");
        }
        strBuf.append("]");
        return strBuf.toString();
    }
    
    public boolean isFaculty() {
        // add codes to check if instructors are using this software..
        return true ;
    }

    public void appendLog(String str) {
        Document doc = logTextPane.getDocument();
        try {
            doc.insertString(doc.getLength(), str, null);
        } catch (BadLocationException ex) {
            ;
        }
    }

    public void appendLogWithTimestamp(String msg) {
        Date date = new Date(); 
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy'/'MM'/'dd HH:mm:ss");
        appendLog(dateFormat.format(date) + "\t" + msg) ;
    }

    
    public boolean canOverwriteFile(String fn) {
        int rc = JOptionPane.showConfirmDialog(
                this,
                java.util.ResourceBundle.getBundle("turtlemessenger/TMBundle").getString("FILE") + " \"" + fn + "\" " + java.util.ResourceBundle.getBundle("turtlemessenger/TMBundle").getString("EXIST. DO YOU WANT TO OVERWRITE IT?"),
                "WARNING",
                JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE);
        if (rc == JOptionPane.OK_OPTION) {
            return true;
        } else {
            return false;
        }
    }

    public void changeBroadcastAddress(byte[] bcas) {
        broadcastAddress[0] = bcas[0] ;
        broadcastAddress[1] = bcas[1] ;
        broadcastAddress[2] = bcas[2] ;
        broadcastAddress[3] = bcas[3] ;
    }
    
    public void receiverTerminated() {
        receiverIsRunning = false;
        startToggleButton.setSelected(false);
        startToggleButton.setText("Start");
    }

    public void startBroadcast() {
        if (broadcaster != null && broadcaster.isAlive()) {
            return;
        }
        broadcaster = new BroadcastThread();
        broadcaster.start();
    }

    public void stopBroadcast() {
        if (broadcaster != null && broadcaster.isAlive()) {
            broadcasterIsTerminating=true ;
            // broadcaster.stop();
            try {
                Thread.sleep(100);
            } catch (InterruptedException ex) {;
            }
            broadcaster = null;
        }
    }
    
    public String getCommentsInJSON() {
        String comments = commentsTextArea.getText() ;
        comments = comments.replaceAll("\"", "&quot;");
        comments = comments.replaceAll("[\r\n]", "<br>");
        StringBuilder strBuf = new StringBuilder("") ;
        strBuf.append("{") ;
        strBuf.append("\"comment\":\"" + comments + "\"" ) ;
        strBuf.append("}") ;
        return strBuf.toString() ;
    }
   
    public String getMessageInJSON_Id(int id) {
        return receiver.getMessageInJSON_Id(id);
    }
     
    public String getMessagesInJSON(int nmsg) {
        return receiver.getMessagesInJSON(nmsg) ;
    }
   
    public List<List<String>> getMessages(int nmsg) {
        return receiver.getMessages(nmsg) ;
    }
    
    public List<List<String>> getNewMessages() {
        return receiver.getNewMessages() ;
    }
     
    public void generateKeyPair() {
        try {
            KeyPairGenerator keygen = KeyPairGenerator.getInstance("RSA");
            keygen.initialize(RSA_KEY_LENGTH);
            rsaKeyPair = keygen.generateKeyPair();
            publicKey = (RSAPublicKey) rsaKeyPair.getPublic() ;
            privateKey = (RSAPrivateKey) rsaKeyPair.getPrivate() ;
        } catch (NoSuchAlgorithmException e) {
            rsaKeyPair = null ;
            publicKey = null ;
            privateKey = null ;
        }
    }
    
    private static void hexDump(byte[] dump) {
        HexDumpEncoder hexDump = new HexDumpEncoder();
        System.out.println(hexDump.encode(dump));
    }
    
    
    private void openCommentFile() {
        File commentFile ;
        JFileChooser fileChooser = new JFileChooser();
        FileNameExtensionFilter filter = new FileNameExtensionFilter(
                "Plain text",
                "txt");
        fileChooser.addChoosableFileFilter(filter);
        if (fileChooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
            commentFile = fileChooser.getSelectedFile();
            BufferedReader reader;
            commentList.clear();
            try {
                reader = new BufferedReader(
                        new InputStreamReader(new FileInputStream(commentFile), "UTF-8"));
                while (reader.ready()) {
                    String line = reader.readLine() ;
                    if (line.length()>140) line = line.substring(0,140) ; // max 140 chars
                    commentList.add(line) ; 
                }
                reader.close();
            } catch (IOException ioe) {
                commentsTextArea.setText("cannot open file: " + commentFile.getName()) ;
            }
        }
        lineNumberSpinner.setValue(0) ;
        setCommentsTextArea() ;
    }
    
    private void setCommentsTextArea() {
        int n = (Integer) lineNumberSpinner.getValue() ;
        if (commentList!=null && commentList.size()>0) {
            if (n<1) { 
                n = 1 ; 
                lineNumberSpinner.setValue(n) ;
            } else if (n>commentList.size()) {
                n = commentList.size() ;
                lineNumberSpinner.setValue(n) ;
            }
            String comment = commentList.get(n-1) ;
            commentsTextArea.setText(comment) ;
        } else {
            lineNumberSpinner.setValue(0) ;
        }
    }
    
    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JMenuItem aboutMenuItem;
    private javax.swing.JMenuItem broadcastMenuItem;
    private javax.swing.JRadioButton channelARadioButton;
    private javax.swing.JRadioButton channelBRadioButton;
    private javax.swing.ButtonGroup channelButtonGroup;
    private javax.swing.JRadioButton channelCRadioButton;
    private javax.swing.JTextArea commentsTextArea;
    private javax.swing.JMenuItem copyLogMenuItem;
    private javax.swing.JMenu copyMenu;
    private javax.swing.JMenuItem copyMenuItem;
    private javax.swing.JMenuItem cutMenuItem;
    private javax.swing.JButton displayMessagesButton;
    private javax.swing.JButton displayVotingResutsButton;
    private javax.swing.JCheckBox enableHttpCheckBox;
    private javax.swing.JMenuItem exitMenuItem;
    private javax.swing.JMenu helpMenu;
    private javax.swing.JMenuItem helpMenuItem;
    private javax.swing.JLabel jLabel1;
    private javax.swing.JLabel jLabel2;
    private javax.swing.JMenu jMenu1;
    private javax.swing.JMenuBar jMenuBar1;
    private javax.swing.JPanel jPanel1;
    private javax.swing.JPanel jPanel2;
    private javax.swing.JScrollPane jScrollPane1;
    private javax.swing.JSpinner lineNumberSpinner;
    private javax.swing.JTextPane logTextPane;
    private javax.swing.JTextField passcodeTextField;
    private javax.swing.JMenuItem pasteMenuItem;
    private javax.swing.JButton readFileButton;
    private javax.swing.JButton resetVoteButton;
    private javax.swing.JMenuItem saveLogMenuItem;
    private javax.swing.JMenu settingsMenu;
    private javax.swing.JToggleButton startToggleButton;
    // End of variables declaration//GEN-END:variables

    
    // Format of broadcast packet:
    // 
    //   byte: length of message from instructor
    //   array of byte: ByteArrayed string in utf-8
    //
    //   byte: length of modulus data of RSA for public key
    //   array of byte: modukus data
    //
    //   byte: length of exponent data of RSA for public key
    //   array of byte: exponent data
    //
 
    class BroadcastThread extends Thread {

        private DatagramSocket dsock;

        public BroadcastThread() {
        }

        @Override
        public void run() {
            try {
                dsock = new DatagramSocket();
                dsock.setBroadcast(true);
            } catch (SocketException ex) {
                broadcaster=null ;
                return;
            }
            
            byte[] sendData = new byte[BROADCAST_MESSAGE_LENGTH];
            
            DatagramPacket pkt = new DatagramPacket(sendData, sendData.length);
            try {
                pkt.setAddress(InetAddress.getByAddress( broadcastAddress ) ) ;
            } catch (UnknownHostException ex) {
                dsock.close() ;
                broadcaster=null ;
                return;
            }
            pkt.setPort(PORT_BROADCAST + serverChannel);
            
            byte[] pubkeyMod=null, pubkeyExp=null ;
            if (publicKey!=null) {
                pubkeyMod = publicKey.getModulus().toByteArray() ;
                pubkeyExp  = publicKey.getPublicExponent().toByteArray() ;
                // hexDump(pubkeyMod) ;
                // hexDump(pubkeyExp) ;
            }

            for (;;) {
                try {                   
                    if (broadcasterIsTerminating) {
                        broadcasterIsTerminating=false ;
                        dsock.close() ;
                        return ;
                    }
                    
                   String comment = commentsTextArea.getText();
                    if (comment.getBytes("UTF-8").length > 255) {
                        int k,ell ;
                        k=0 ;
                        ell=comment.length()-1 ;
                        String sh ;
                        do {
                            sh = comment.substring(0,(k+ell)/2) ;
                            if (sh.getBytes("UTF-8").length < 255) {
                                k = (k+ell)/2 ;
                            } else {
                                ell = (k+ell)/2 ;
                            }
                            // System.out.println(i + " " + j);
                        } while (ell-k>1) ;
                        comment = comment.substring(0,k) ;
                    }

                    byte[] commentData = comment.getBytes("UTF-8");
                    
                    int i,bp = 0 ;
                    sendData[bp] = (byte) commentData.length ;
                    bp++ ;
                    for (i=0 ; i < commentData.length && bp < BROADCAST_MESSAGE_LENGTH; bp++, i++) {
                            sendData[bp] = commentData[i];                          
                    }
                    
                    if (pubkeyMod != null && pubkeyExp != null) {
                        sendData[bp] = (byte) pubkeyMod.length;
                        bp++;
                        for (i = 0; i < pubkeyMod.length && bp < BROADCAST_MESSAGE_LENGTH; bp++, i++) {
                            sendData[bp] = pubkeyMod[i];
                        }

                        sendData[bp] = (byte) pubkeyExp.length;
                        bp++;
                        for (i = 0; i < pubkeyExp.length && bp < BROADCAST_MESSAGE_LENGTH; bp++, i++) {
                            sendData[bp] = pubkeyExp[i];
                        }
                    }
                    
                    for ( ; bp < BROADCAST_MESSAGE_LENGTH; bp++) {
                            sendData[bp] = 0 ; // padding..                      
                    }
                   
                    pkt.setData(sendData);
                    dsock.send(pkt);
                    Thread.sleep(BORADCAST_INTERVAL_MS);
                } catch (IOException ex) {
                    break;
                } catch (InterruptedException ex) {
                    break;
                }
            }

            dsock.close();
            broadcaster=null ;
        }
    }
}
