package com.knutejohnson.pi.chat;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;
import java.text.*;
import java.util.*;
import static java.util.stream.Collectors.*;
import javax.swing.*;
import javax.swing.text.*;
import javax.swing.plaf.*;

/**
 * <div>
 * The ChatClient routes messages through the ChatServer to other logged in
 * clients.  The message transmitted contains the handle of the sender, the
 * sender's password, the handle of the intended recipient and the message.
 * The user types the recipient's handle followed by a space and the text of
 * the message.  To send the message the user presses the [ENTER] key.
 * </div>
 * <div style="margin-top: 30px;">
 * TODO: javadocs
 * </div>
 * <table style="margin-top: 30px; width: 100%; border-collapse: collapse;" border cellspacing="2" summary="Version info">
 * <tr><th>Version<th>Date<th>Modification
 * <tr><td>0.10.0<td style="white-space: nowrap;">28 May 2017<td>incept
 * </table>
 * 
 * @author  Knute Johnson
 * @version 0.10.0 - 28 May 2017
 */
public class ChatClient extends JFrame implements Runnable {
    public static final String VERSION = "0.10.0";
    public static final String DATE = "28 May 2017";
    private static final int SERVER_PORT = 28888;
    private static final int CLIENT_PORT = 28889;
    private static final File USER_HOME =
     new File(System.getProperty("user.home"));
    private static final File PROPERTIES_FILE =
     new File(USER_HOME,".chatclient");
    private static final int CHANNELS = 5;
    private static final String[] chanLabels = {
     "CHAN1","CHAN2","CHAN3","CHAN4","CHAN5" };
    private static final String DIRECTIONS_FILE_NAME = "client.html";
    private static final Color RASPBERRY = new Color(227,11,92);
    private final Properties properties = new Properties();
    private final Thread thread = new Thread(this);
    private volatile boolean runFlag;
    private final ReceiverArea area;
    private final JTextField field;
    private volatile DatagramSocket socket;
    private final JCheckBoxMenuItem[] chanBoxes =
     new JCheckBoxMenuItem[CHANNELS];

    public ChatClient() {
        super("ChatClient - " + VERSION + " - " + DATE);

        addWindowListener(new WindowAdapter() {
            // login automatically on program start
            public void windowOpened(WindowEvent we) {
                try {
                    Message msg = new Message(
                     properties.getProperty("handle"),
                     properties.getProperty("password"),"login","",
                     InetAddress.getByName(
                     properties.getProperty("server")));
                    DatagramPacket packet = msg.toPacket();
                    packet.setPort(SERVER_PORT);
                    socket.send(packet);
                } catch (IOException ioe) {
                    ioe.printStackTrace();
                    JOptionPane.showMessageDialog(ChatClient.this,
                     "Error Sending Login Message\n" + ioe);
                }
            }
            // logout automatically on program end
            public void windowClosing(WindowEvent we) {
                try {
                    Message msg = new Message(
                     properties.getProperty("handle"),
                     properties.getProperty("password"),"logout","",
                     InetAddress.getByName(
                     properties.getProperty("server")));
                    DatagramPacket packet = msg.toPacket();
                    packet.setPort(SERVER_PORT);
                    socket.send(packet);
                } catch (IOException ioe) {
                    ioe.printStackTrace();
                }

                // stop the main thread and dispose the frame
                stop();
                dispose();
            }
        });

        try {
            loadProperties(properties);
        } catch (IOException ioe) {
            ioe.printStackTrace();
            if (ioe instanceof FileNotFoundException) {
                JOptionPane.showMessageDialog(this,
                 "Properties File Not Found.\n" +
                 "This Is Normal The First Time The Program Is Run.");
            } else {
                JOptionPane.showMessageDialog(this,
                 "Load Properties Failed!\n" + ioe);
            }
        }

        JMenuBar menuBar = new JMenuBar();
        setJMenuBar(menuBar);
        JMenu file = menuBar.add(new JMenu("File"));
        JMenu settings = menuBar.add(new JMenu("Settings"));
        JMenu view = menuBar.add(new JMenu("View"));
        JMenu help = menuBar.add(new JMenu("Help"));

        JMenuItem mi;
        mi = file.add("Quit");
        mi.addActionListener(ae -> { stop();  dispose(); });

        mi = settings.add("Handle");
        mi.addActionListener(ae -> {
            String newHandle = JOptionPane.showInputDialog(this,"Handle:",
             properties.getProperty("handle"));
            // handle cannont be null, shorter than 2 chars, longer than 16
            //  chars, matching chan[1-5], login or logout
            if (newHandle != null &&
             newHandle.length() >= 2 &&
             newHandle.length() <= 16 &&
             !newHandle.matches("(?i)chan[1-5]") &&
             !newHandle.equalsIgnoreCase("login") &&
             !newHandle.equalsIgnoreCase("logout")) {
                properties.setProperty("handle",newHandle);
                try {
                    storeProperties(properties);
                } catch (IOException ioe) {
                    ioe.printStackTrace();
                    JOptionPane.showMessageDialog(this,
                     "Store Properties Failed!\n" + ioe);
                }
            } else {
                JOptionPane.showMessageDialog(this,"Error Handle Not Saved!");
            }
        });

        mi = settings.add("Password");
        mi.addActionListener(ae -> {
            JPanel panel = new JPanel(new BorderLayout());
            JLabel label = new JLabel("Password:");
            panel.add(label,BorderLayout.CENTER);
            JPasswordField pwField = new JPasswordField(properties.getProperty(
             "password"));
            panel.add(pwField,BorderLayout.SOUTH);
            int option = JOptionPane.showOptionDialog(this,panel,"Input",
             JOptionPane.OK_CANCEL_OPTION,JOptionPane.QUESTION_MESSAGE,
             (Icon)null,(Object[])null,(Object)null);
            if (option == JOptionPane.OK_OPTION) {
                String password = new String(pwField.getPassword());
                properties.setProperty("password",password);
                try {
                    storeProperties(properties);
                } catch (IOException ioe) {
                    ioe.printStackTrace();
                    JOptionPane.showMessageDialog(this,
                     "Store Properties Failed!\n" + ioe);
                }
            }
        });

        mi = settings.add("Server Address");
        mi.addActionListener(ae -> {
            String address = JOptionPane.showInputDialog(this,
             "Server Address:",properties.getProperty("server"));
            if (address != null) {
                try {
                    InetAddress.getByName(address);
                    properties.setProperty("server",address);
                    storeProperties(properties);
                } catch (IOException ioe) {
                    ioe.printStackTrace();
                    if (ioe instanceof UnknownHostException)
                        JOptionPane.showMessageDialog(this,
                         "Not a Valid IP Address!\n" + ioe);
                    else
                        JOptionPane.showMessageDialog(this,
                         "Store Properties Failed!\n" + ioe);
                }
            }
        });

        mi = settings.add("Bind Address");
        mi.addActionListener(ae -> {
            String address = JOptionPane.showInputDialog(this,
             "Bind Address:",properties.getProperty("address","0.0.0.0"));
            if (address != null) {
                try {
                   InetAddress.getByName(address);
                   properties.setProperty("address",address);
                   storeProperties(properties);
                   // restart the packet receiver with the new address
                   socket.close();
                } catch (IOException ioe) {
                    ioe.printStackTrace();
                    if (ioe instanceof UnknownHostException)
                        JOptionPane.showMessageDialog(this,
                         "Not a Valid IP Address!\n" + ioe);
                    else
                        JOptionPane.showMessageDialog(this,
                         "Store Properties Failed!\n" + ioe);
                }
            }
        });

        JMenu channelsMenu = new JMenu("Channels");
        view.add(channelsMenu);
        for (int i=0; i<chanLabels.length; i++) {
            chanBoxes[i] = new JCheckBoxMenuItem(chanLabels[i],
             Boolean.valueOf(properties.getProperty(chanLabels[i])));
            final int x = i;
            chanBoxes[i].addActionListener(ae -> {
                JCheckBoxMenuItem cb = (JCheckBoxMenuItem)ae.getSource();
                properties.setProperty(chanLabels[x],
                 cb.isSelected() ? "true" : "false");
                try {
                    storeProperties(properties);
                } catch (IOException ioe) {
                    ioe.printStackTrace();
                    JOptionPane.showMessageDialog(this,
                     "Store Properties Failed!\n" + ioe);
                }
            });
            channelsMenu.add(chanBoxes[i]);
        }

        // read directions file from the program jar and create a menu item for
        //  it if it is found
        try (InputStream is =
         getClass().getResourceAsStream(DIRECTIONS_FILE_NAME)) {
            if (is != null) {
                try (BufferedReader reader = new BufferedReader(
                 new InputStreamReader(is))) {
                    String html = reader.lines().collect(joining("\n"));
                    mi = help.add("Directions");
                    help.addSeparator();
                    mi.addActionListener(ae -> {
                        // create a JLabel since it displays html
                        JLabel label = new JLabel(html);
                        // create a scroll pane to hold the label
                        JScrollPane pane = new JScrollPane(label,
                         JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
                         JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
                        // get the scroll pane's vertical scrollbar
                        JScrollBar vBar = pane.getVerticalScrollBar();
                        // get the dimensions of the label
                        Dimension labelDim = label.getPreferredSize();
                        // get the scroll bar's dimensions
                        Dimension barDim = vBar.getPreferredSize();
                        // set the preferred size of the scroll bar to be the
                        //  width of the label and scroll bar by 200 pixels
                        pane.setPreferredSize(new Dimension(
                         labelDim.width + barDim.width,200));
                        JOptionPane.showMessageDialog(this,pane);
                    });
                }
            }
        } catch (IOException ioe) {
            ioe.printStackTrace();
            JOptionPane.showMessageDialog(this,
             "Error Reading Directions!\n" + ioe);
        }

        mi = help.add("About");
        mi.addActionListener(ae -> JOptionPane.showMessageDialog(this,
         "ChatClient\nVersion: " + VERSION + "\nDate: " + DATE +
         "\nWritten by: Knute Johnson"));

        area = new ReceiverArea(
         new SelfLimitingDocument(1048576),"",24,80,"HH:mm:ss ");
        area.setFont(new Font("Monospaced",Font.PLAIN,12));
        area.setEditable(false);
        area.setLineWrap(true);
        area.setWrapStyleWord(true);
        area.addFocusListener(new FocusAdapter() {
            public void focusGained(FocusEvent fe) {
                field.requestFocus();
            }
        });
        area.setForeground(RASPBERRY);
        JScrollPane pane = new JScrollPane(area);
        add(pane,BorderLayout.CENTER);

        field = new JTextField(60);
        // pressing F1 through F5 types the chan[1-5] address into the text
        //  field
        field.addKeyListener(new KeyAdapter() {
            public void keyPressed(KeyEvent ke) {
                String chan;
                switch (ke.getKeyCode()) {
                    case KeyEvent.VK_F1:  chan = "chan1 ";  break;
                    case KeyEvent.VK_F2:  chan = "chan2 ";  break;
                    case KeyEvent.VK_F3:  chan = "chan3 ";  break;
                    case KeyEvent.VK_F4:  chan = "chan4 ";  break;
                    case KeyEvent.VK_F5:  chan = "chan5 ";  break;
                    default:  chan = "";  break;
                }
                if (!chan.equals(""))
                    field.setText(chan.concat(field.getText()));
            }
        });
        field.addActionListener(ae -> {
            // strip the whitespace from the message string
            String str = field.getText().trim();
            // split the string on a space
            String[] arr = str.split(" ");
            // dest is the first array element fi there is more than one
            //  otherwise it is the string itself
            String dest = arr.length > 1 ? arr[0] : str;
            // the text is everything after the first space
            String text = str.substring(dest.length()).trim();

            // if message is being sent to chan[1-5] set the channel
            int channel = -1;
            for (int i=0; i<CHANNELS; i++)
                // use the chanLabels from the Channels menu selection to
                //  compare to the entered dest
                if (dest.equalsIgnoreCase(chanLabels[i]))
                    channel = i;

            try {
                Message msg = new Message(properties.getProperty("handle"),
                 properties.getProperty("password"),dest,text,
                 InetAddress.getByName(properties.getProperty("server")));

                // if the message is being sent to a channel and the chanBox is
                //  not selected show error message and return
                if (channel > -1 && !chanBoxes[channel].isSelected()) {
                    Message error = new Message(msg.getHandle(),"",
                     msg.getDest(),"THIS CHANNEL IS OFF!",msg.getAddress());
                    area.append(error.toClientString());
                    return;
                }

                // send the message
                DatagramPacket packet = msg.toPacket();
                packet.setPort(SERVER_PORT);
                socket.send(packet);
                // clear the message entry field
                field.setText("");
            } catch (IOException ioe) {
                ioe.printStackTrace();
                JOptionPane.showMessageDialog(this,
                 "Error Occured Sending Message\n" + ioe);
            }
        });
        field.setForeground(RASPBERRY);
        add(field,BorderLayout.SOUTH);

        pack();
        setVisible(true);
    }

    public void start() {
        // if the thread has never been started
        if (thread.getState() == Thread.State.NEW) {
            runFlag = true;
            thread.start();
        }
    }

    public void run() {
        while (runFlag) {
            try {
                socket = new DatagramSocket(new InetSocketAddress(
                 properties.getProperty("address","0.0.0.0"),CLIENT_PORT));
                while (true) {
                    DatagramPacket packet =
                     new DatagramPacket(new byte[1500],1500);
                    socket.receive(packet);
                    Message msg = new Message(packet);

                    // if the message is to us, show it
                    if (msg.getDest().equals(
                     properties.getProperty("handle"))) {
                        area.append(msg.toClientString());
                        continue;
                    }

                    // if the message is to a selected channel show it
                    if (msg.getDest().matches("(?i)chan[1-5]")) {
                        for (int i=0; i<chanBoxes.length; i++) {
                            if (msg.getDest().equalsIgnoreCase(
                             chanBoxes[i].getText()) &&
                             chanBoxes[i].isSelected()) {
                                area.append(msg.toClientString());
                                break;
                            }
                        }
                        continue;
                    }

                    // ignore any messages sent to our address that don't meet
                    //  the critera from above (that could be unselected
                    //  channels or messages not addressed to us but sent to
                    //  our IP address
                }
            } catch (IOException ioe) {
                if (runFlag)
                    System.out.println(ioe);
            } finally {
                socket.close();
            }
        }
    }

    public void stop() {
        runFlag = false;
        socket.close();
    }

    private void loadProperties(Properties props) throws IOException {
        try (FileReader reader = new FileReader(PROPERTIES_FILE)) {
            props.load(reader);
        }
    }

    private void storeProperties(Properties props) throws IOException {
        try (FileWriter writer = new FileWriter(PROPERTIES_FILE)) {
            props.store(writer,"ChatClient");
        }
    }

    public static void main(String... args) {
        EventQueue.invokeLater(() -> {
            ChatClient client = new ChatClient();
            client.start();
        });
    }
}