import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;
import javax.swing.*;
import javax.swing.event.*;
import java.util.ArrayList;

// TO DO
// Add functionality for sending files

public class myServer
{
	ArrayList<myServerThread> myThreads = new ArrayList<myServerThread>();
	ServerSocket serverSocket;
	JFrame frame;
	JList list, bList;
	DefaultListModel listModel, bListModel;
	JButton kickButton, banButton, addIPButton, removeIPButton;
	JTextArea logArea;

	public myServer(int port)
	{
		// Create a server socket
		try {
			serverSocket = new ServerSocket(port);
		} catch (IOException e) {
			System.out.println("Could not listen on port: " + port);
			System.exit(0);
		}
		
		// Create the window
		JFrame.setDefaultLookAndFeelDecorated(true);
		frame = new JFrame("My Server");
		frame.setResizable(false);
		frame.setLayout(new GridBagLayout());
		GridBagConstraints c = new GridBagConstraints();
		
		JLabel label = new JLabel("Connected Users");
		c.insets = new Insets(5, 5, 5, 5);
		c.gridwidth = 2;
		c.gridx = 0;
		c.gridy = 0;
		c.anchor = GridBagConstraints.LINE_START;
		frame.add(label, c);
		
		// User List
		listModel = new DefaultListModel();
		list = new JList(listModel);
		list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
		list.addListSelectionListener(new userListener());
		list.setVisibleRowCount(5);
		list.setFixedCellWidth(160);
		JScrollPane listScrollPane = new JScrollPane(list, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
		c.insets = new Insets(0, 5, 0, 0);
		c.gridwidth = 1;
		c.gridheight = 2;
		c.gridx = 0;
		c.gridy = 1;
		frame.add(listScrollPane, c);

		MyListener myListener = new MyListener(); // use this listener for all buttons
		kickButton = new JButton("Kick User");
		kickButton.setActionCommand("kick");
		kickButton.addActionListener(myListener);
		kickButton.setEnabled(false);
		c.insets = new Insets(0, 0, 0, 5);
		c.fill = GridBagConstraints.HORIZONTAL;
		c.gridheight = 1;
		c.gridx = 1;
		c.gridy = 1;
		c.anchor = GridBagConstraints.PAGE_START;
		frame.add(kickButton, c);
		
		banButton = new JButton("Ban User");
		banButton.setActionCommand("ban");
		banButton.addActionListener(myListener);
		banButton.setEnabled(false);
		c.gridx = 1;
		c.gridy = 2;
		frame.add(banButton, c);
		
		label = new JLabel("Banned IPs");
		c.insets = new Insets(5, 5, 5, 0);
		c.gridx = 0;
		c.gridy = 3;
		c.anchor = GridBagConstraints.LINE_START;
		frame.add(label, c);
		
		// Ban List
		bListModel = new DefaultListModel();
		bList = new JList(bListModel);
		bList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
		bList.addListSelectionListener(new banListener());
		bList.setVisibleRowCount(5);
		bList.setFixedCellWidth(160);
		JScrollPane listScrollPane2 = new JScrollPane(bList, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
		c.insets = new Insets(0, 5, 5, 0);
		c.fill = GridBagConstraints.NONE;
		c.gridwidth = 1;
		c.gridheight = 2;
		c.gridx = 0;
		c.gridy = 4;
		c.anchor = GridBagConstraints.CENTER;
		frame.add(listScrollPane2, c);

		addIPButton = new JButton("Add IP");
		addIPButton.setActionCommand("addip");
		addIPButton.addActionListener(myListener);
		addIPButton.setEnabled(true);
		c.fill = GridBagConstraints.HORIZONTAL;
		c.insets = new Insets(0, 0, 0, 5);
		c.gridx = 1;
		c.gridy = 4;
		c.gridheight = 1;
		c.anchor = GridBagConstraints.PAGE_START;
		frame.add(addIPButton, c);
		
		removeIPButton = new JButton("Remove IP");
		removeIPButton.setActionCommand("removeip");
		removeIPButton.addActionListener(myListener);
		removeIPButton.setEnabled(false);
		c.gridy = 5;
		frame.add(removeIPButton, c);
		
		label = new JLabel("Server Log");
		c.gridx = 2;
		c.gridy = 0;
		c.gridwidth = 1;
		c.gridheight = 1;
		c.fill = GridBagConstraints.NONE;
		c.insets = new Insets(5, 0, 5, 0);
		c.anchor = GridBagConstraints.LINE_START;
		frame.add(label, c);

		label = new JLabel("Listening on Port " + port);
		c.gridx = 3;
		c.gridy = 0;
		c.insets = new Insets(5, 0, 5, 5);
		c.anchor = GridBagConstraints.LINE_END;
		frame.add(label, c);

		logArea = new JTextArea();
		logArea.setEditable(false);
		logArea.setLineWrap(true);
		logArea.setWrapStyleWord(true);
		logArea.setColumns(30);
		JScrollPane logPane = new JScrollPane(logArea, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
		c.gridx = 2;
		c.gridy = 1;
		c.gridwidth = 2;
		c.gridheight = 5;
		c.fill = GridBagConstraints.BOTH;
		c.insets = new Insets(0, 0, 5, 5);
		c.anchor = GridBagConstraints.CENTER;
		frame.add(logPane, c);

		// Adds a window listener for certain events
		frame.addWindowListener(new WindowAdapter() {
			public void windowClosing(WindowEvent e) {
				cleanUp();
				System.exit(0);
			}
		});
		
		// Show Window
		frame.pack();
		frame.setVisible(true);
		
		// Start accepting connections
		try {
			while(true)
				new myServerThread(serverSocket.accept()).start();
		} catch (IOException e) {
			System.out.println(e.getMessage());
		}
	}
	public myServerThread getThreadByUsername(String user)
	{
		// Returns the thread that has a matching username
		for(int i = 0; i < myThreads.size(); i++)
		{
			if(myThreads.get(i).getUsername().equals(user))
				return myThreads.get(i);
		}
		return null; // A thread with that username was not found
	}
	public void SendUsernameToClients(String user)
	{
		// Since this method is called before adding the newly connected
		// user thread to the arraylist, we can assume to send this data
		// to all clients
		for(int i = 0; i < myThreads.size(); i++)
		{
			myThreads.get(i).SendUsername(user);
		}
	}
	public void SendMsgToClients(String textToSend)
	{
		// Sends the message to all users (including the user who sent it
		// himself/herself)
		for(int i = 0; i < myThreads.size(); i++)
		{
			myThreads.get(i).SendMsg(textToSend);
		}
	}
	public void SendUserListToClient(myServerThread mThread)
	{
		for(int i = 0; i < myThreads.size(); i++)
		{
			mThread.SendUsername(myThreads.get(i).getUsername());
		}
	}
	public boolean AlreadyInUse(String user)
	{
		// Self-Explanatory...
		for(int i = 0; i < myThreads.size(); i++)
		{
			if(myThreads.get(i).getUsername().equals(user))
				return true;
		}
		return false;
	}
	public boolean IsUserBanned(String clientHostname)
	{
		for(int i = 0; i < bListModel.getSize(); i++)
		{
			if(((String) bListModel.getElementAt(i)).equals(clientHostname))
				return true;
		}
		return false;
	}
	public void RemoveUserAndNotifyClients(String user)
	{
		// Removes the thread from the Threads ArrayList and the username from
		// the JList, then notifies all the users still in the ArrayList
		myThreads.remove(getThreadByUsername(user));
		listModel.removeElement(user);
		for(int i = 0; i < myThreads.size(); i++)
		{
			myThreads.get(i).SendDisconnectedUser(user);
		}
	}
	public void ForwardRequestToClient(String user, String tFile, String tPath, String fUser)
	{
		getThreadByUsername(user).RequestToSendFile(tFile, tPath, fUser);
	}
	public void ForwardSendFileCmd(String sPort, String sPath, String sUser, String sHost)
	{
		getThreadByUsername(sUser).SendFile(sHost, sPort, sPath);
	}
	public void cleanUp()
	{
		// Since the server is being closed, there might be users still connected...
		// send each one of them the "disconnected" message to let them clean up and close streams
		for(int i = 0; i < myThreads.size(); i++)
		{
			myThreads.get(i).SendServerDisconnect();
			myThreads.get(i).closeStreams(); // Close streams on server side
		}
		try {
			serverSocket.close();
		} catch (IOException e) {
			System.out.println("Could not close server socket");
		}
	}
	class userListener implements ListSelectionListener
	{
		public void valueChanged(ListSelectionEvent e)
		{
			if(list.getSelectedIndex() == -1)
			{
				kickButton.setEnabled(false);
				banButton.setEnabled(false);
			}
			else
			{
				kickButton.setEnabled(true);
				banButton.setEnabled(true);
			}
		}
	}
	class banListener implements ListSelectionListener
	{
		public void valueChanged(ListSelectionEvent e)
		{
			if(bList.getSelectedIndex() == -1)
			{
				removeIPButton.setEnabled(false);
			}
			else
			{
				removeIPButton.setEnabled(true);
			}
		}
	}
	class MyListener implements ActionListener
	{
		public void actionPerformed(ActionEvent e)
		{
			if(e.getActionCommand().equals("kick"))
			{
				String kickedUser = (String) listModel.getElementAt(list.getSelectedIndex());
				getThreadByUsername(kickedUser).SendKickedMsg();
				getThreadByUsername(kickedUser).closeStreams();
				RemoveUserAndNotifyClients(kickedUser);
				logArea.append(kickedUser + " has been kicked.\n");
				logArea.setCaretPosition(logArea.getDocument().getLength()-1);
			}
			else if(e.getActionCommand().equals("ban"))
			{
				String bannedUser = (String) listModel.getElementAt(list.getSelectedIndex());
				String bannedHostname = getThreadByUsername(bannedUser).getHName();
				getThreadByUsername(bannedUser).SendBannedMsg();
				getThreadByUsername(bannedUser).closeStreams();
				RemoveUserAndNotifyClients(bannedUser);
				bListModel.addElement(bannedHostname);
				logArea.append(bannedUser + " has been kicked and banned.\n");
				logArea.setCaretPosition(logArea.getDocument().getLength()-1);
			}
			else if(e.getActionCommand().equals("addip"))
			{
				String inputIP = JOptionPane.showInputDialog(frame, "Enter IP to add:", "Add IP", JOptionPane.QUESTION_MESSAGE);
				if(inputIP != null)
				{
					bListModel.addElement(inputIP);
					logArea.append(inputIP + " added to ban list.\n");
					logArea.setCaretPosition(logArea.getDocument().getLength()-1);
				}
			}
			else if(e.getActionCommand().equals("removeip"))
			{
				String removedIP = (String) bListModel.remove(bList.getSelectedIndex());
				logArea.append(removedIP + " removed from ban list.\n");
				logArea.setCaretPosition(logArea.getDocument().getLength()-1);
			}
			else
			{
				System.out.println("Button with action command \"" + e.getActionCommand() + "\" has no actions defined");
			}
		}
	}
	class myServerThread extends Thread
	{
		private Socket socket;
		private PrintWriter out;
		private BufferedReader in;
		private String username;
		
		public myServerThread(Socket socket)
		{
			this.socket = socket;
		}
		public void run()
		{
			try {
				// Open streams for sending and receiving data
				out = new PrintWriter(socket.getOutputStream(), true);
				in = new BufferedReader(new InputStreamReader(socket.getInputStream()));

				// Version checking
				if(!(in.readLine().equals("v001")))
				{
					out.println("versiondifferent");
					closeStreams();
					return;
				}
				// Send some random junk to client because he is expecting something
				// to do with version checking
				out.println("versionissame");
				
				// Is this user banned?
				if(IsUserBanned(socket.getInetAddress().getHostName()))
				{
					out.println("userbanned");
					closeStreams();
					return;
				}
				// Again, send random junk
				out.println("notbanned");

				// Get username
				username = in.readLine();
				
				// Check to see if someone else already has this username
				if(AlreadyInUse(username))
				{
					out.println("usernameinuse");
					closeStreams();
					return;
				}
				// Send a message to the client stating he is successfully connected
				out.println("connected");
				// Send username to any other users connected
				SendUsernameToClients(username);
				// Add this thread to the threads arraylist
				myThreads.add(this);
				// Add User to the List
				listModel.addElement(username);
				// Get a list of usernames for user
				SendUserListToClient(this);

				// Add info to server log
				logArea.append(username + " (" + socket.getInetAddress().getHostName() + ") has connected.\n");
				logArea.setCaretPosition(logArea.getDocument().getLength()-1);

				// The Loop
				// Listen for client data and do stuff with it
				String input;
				while((input = in.readLine()) != null)
				{
					if(input.equals("disconnected"))
					{
						// Add disconnected info to log
						logArea.append(username + " has disconnected.\n");
						logArea.setCaretPosition(logArea.getDocument().getLength()-1);
						break;
					}
					if(input.equals("text"))
					{
						SendMsgToClients(in.readLine());
					}
					if(input.equals("requestToSendFile"))
					{
						ForwardRequestToClient(in.readLine(), in.readLine(), in.readLine(), in.readLine());
					}
					if(input.equals("sendfile"))
					{
						ForwardSendFileCmd(in.readLine(), in.readLine(), in.readLine(), socket.getInetAddress().getHostName());
					}
				}

				// Close streams and socket
				closeStreams();
				
				// Remove user from list
				RemoveUserAndNotifyClients(username);
				
			} catch (IOException e) {
				System.out.println(e.getMessage());
			}
		}
		public String getUsername()
		{
			return username;
		}
		public String getHName()
		{
			return socket.getInetAddress().getHostName();
		}
		// These methods are synchronized because I don't want the threads to do
		// something like
		// Thread 1: out.println("new user");
		// Thread 2: out.println("text");
		// Thread 1: out.println(username);
		// Thread 2: out.println(textToSend);
		// This would totally throw off the client
		public synchronized void SendUsername(String user)
		{
			out.println("new user");
			out.println(user);
		}
		public synchronized void SendDisconnectedUser(String user)
		{
			out.println("user disconnected");
			out.println(user);
		}
		public synchronized void SendServerDisconnect()
		{
			out.println("disconnected");
		}
		public synchronized void SendMsg(String textToSend)
		{
			out.println("text");
			out.println(textToSend);
		}
		public synchronized void RequestToSendFile(String tFile, String tPath, String fUser)
		{
			out.println("requestToSendFile");
			out.println(tFile);
			out.println(tPath);
			out.println(fUser);
		}
		public synchronized void SendFile(String sHost, String sPort, String sPath)
		{
			out.println("sendfile");
			out.println(sHost);
			out.println(sPort);
			out.println(sPath);
		}
		public synchronized void SendKickedMsg()
		{
			out.println("kicked");
		}
		public synchronized void SendBannedMsg()
		{
			out.println("banned");
		}
		public void closeStreams()
		{
			try {
				out.close();
				in.close();
				socket.close();
			} catch (IOException e) {
				System.out.println(e.getMessage());
			}
		}
	}
	public static void main(String[] args)
	{
		// Get the port number specified by the user through command-line
		int p = 0;
		try {
			p = Integer.parseInt(args[0]);
		} catch (NumberFormatException e) {
			System.out.println("Usage: java myServer [PORT]");
			System.out.println("");
			System.out.println("[PORT]\t\tThe port to host the server on");
			System.exit(0);
		} catch (ArrayIndexOutOfBoundsException e) {
			System.out.println("Usage: java myServer [PORT]");
			System.out.println("");
			System.out.println("[PORT]\t\tThe port to host the server on");
			System.exit(0);
		}
		new myServer(p);
	}
}


