/*************************************************************************
	> File Name: TalkClient.java
	> Author: Netcan
	> Blog: http://www.netcan666.com
	> Mail: [email protected]
	> Created Time: 2016-12-14 16:49:54 CST
 ************************************************************************/

package pers.netcan.talk.client;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import pers.netcan.talk.common.TalkEmoji;
import pers.netcan.talk.server.TalkServerMaster;

public class TalkClient extends Application  {
	private static Socket client;
	private static BufferedReader in;
	private static PrintWriter out;
	private static Stage pStage; // 主窗口
	private static Stage emojiStage; // 表情窗口
	private static String VERSION = "0.45";
	private static String talkRecordDir = "TalkRecords";
	private ObservableList<String> usrsList;
	private Map<String, String> usrsMsg; // 保存信息
	private Map<String, Boolean> usrsMsgNotify; // 消息提示
	private TextArea message, sendMsg; // 消息框
	private ListView<String> usrsListView;
	private boolean messageGotoEndLine; // 切换消息滚到最后一行
	String ip = "", usrName = "";

	private void loginScene() throws IOException {
		File confFile = new File("Talk.conf");

		if(!confFile.exists())
			confFile.createNewFile();
		else {
			FileReader fr = new FileReader(confFile);
			BufferedReader br = new BufferedReader(fr);
			String line;
			String ipRegex = "IP = (.*)";
			String usrNameRegex = "USERNAME = (.*)";
			while((line = br.readLine()) != null) {
				if(Pattern.matches(ipRegex, line)) {
					Matcher m = Pattern.compile(ipRegex).matcher(line);
					m.find();
					ip = m.group(1);
				} else if(Pattern.matches(usrNameRegex, line)) {
					Matcher m = Pattern.compile(usrNameRegex).matcher(line);
					m.find();
					usrName = m.group(1);
				}
			}
			br.close();
			fr.close();
		}
        GridPane grid = new GridPane();
        grid.setAlignment(Pos.CENTER);
        grid.setHgap(10);
        grid.setVgap(10);
        grid.setPadding(new Insets(25, 25, 25, 25));

        Text scenetitle = new Text("登录");
        scenetitle.setFont(Font.font("Tahoma", FontWeight.NORMAL, 20));

        Label ipAddr = new Label("ip地址:");
        grid.add(ipAddr, 0, 1);

        TextField ipAddrField = new TextField();
        ipAddrField.setText(ip);
        grid.add(ipAddrField, 1, 1);

        Label userName = new Label("用户名:");
        grid.add(userName, 0, 2);

        TextField userNameTextField = new TextField();
        userNameTextField.setText(usrName);
        grid.add(userNameTextField, 1, 2);


        Button btn = new Button("登录");
        HBox hbBtn = new HBox(0);
        hbBtn.setAlignment(Pos.BOTTOM_RIGHT);
        hbBtn.getChildren().add(btn);
        grid.add(hbBtn, 1, 4);

        Scene scene = new Scene(grid, 320, 240);

		pStage.setScene(scene);
		pStage.show();

		btn.setOnAction(new EventHandler<ActionEvent>() {
			@Override
			public void handle(ActionEvent e) {
				Alert alert = new Alert(AlertType.ERROR);
				if(!Pattern.matches("\\w+", userNameTextField.getText())) {
					alert.setContentText("请检查名字!");
					alert.showAndWait();
					return;
				}
				if(!Pattern.matches(
						"([1-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])(\\.(\\d|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])){3}",
						ipAddrField.getText()))  {
					alert.setContentText("请检查ip地址!");
					alert.showAndWait();
					return;
				}

				// write to config file
				FileWriter fw;
				BufferedWriter bw;
				try {
					fw = new FileWriter(confFile);
					bw = new BufferedWriter(fw);
					bw.write("IP = " + ipAddrField.getText() + '\n');
					bw.write("USERNAME = " + userNameTextField.getText() + '\n');
					bw.close();
					fw.close();
				} catch (IOException e1) {
					// TODO Auto-generated catch block
					e1.printStackTrace();
				}

				ip = ipAddrField.getText();
				usrName = userNameTextField.getText();
				if(checkLogin()) { // 登录成功
					grid.getChildren().clear();
					talkScene();
				} else {
					alert.setContentText("登录失败!");
					alert.showAndWait();
				}

			}
		});
	}

	public void getUsrs(String usrsList) { // 刷新在线用户列表
		String []usr = usrsList.split(",");
		ObservableList<String> us = FXCollections.observableArrayList(usr);
		
		// 标记离线状态
		for(int i=0; i<this.usrsList.size(); ++i) {
			if(this.usrsList.get(i).equals(TalkServerMaster.Master)) continue;
			String un = getUsrName(this.usrsList.get(i));
			if(un != null) {
				if( !us.contains(un)) this.usrsList.set(i, un + " (Offline)");
				else this.usrsList.set(i, un);
			}
		}

		// 刷新列表
		for(String u: usr) {
			if(u!=null && !this.usrsList.contains(u) && !this.usrsList.contains(u + " (*)") && !this.usrsList.contains(u + " (Offline)"))
				this.usrsList.add(u);
		}

		// 消息提醒
		for(int i=0; i<this.usrsList.size(); ++i) {
			String u = getUsrName(this.usrsList.get(i));
			if(u != null && usrsMsgNotify.get(u) != null && usrsMsgNotify.get(u)) { // 有新消息
				this.usrsList.set(i, u + " (*)");
			}
		}

	}

	public void storeMsg(String fromUsr, String msg, boolean all) {
		String Msg = String.format("[%s <%s>] %s\n",
				new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()),
				fromUsr, new String(Base64.getDecoder().decode(msg), StandardCharsets.UTF_8)
				);
		if(all) fromUsr = TalkServerMaster.Master;
		if(usrsMsg.get(fromUsr) == null)
			usrsMsg.put(fromUsr, Msg);
		else if(!Msg.contains(TalkServerMaster.WELCOME))
			usrsMsg.put(fromUsr, usrsMsg.get(fromUsr) + Msg);
		usrsMsgNotify.put(fromUsr, true);
	}

	public void execute(String cmd) {
		String regex = "\\[([\\w\\s]+)\\](.*)"; // 匹配[action]arg
		if(cmd == null || ! cmd.matches(regex)) return;
		Pattern p = Pattern.compile(regex);
		Matcher m = p.matcher(cmd);
		m.find(); // 必须要find才能group。。。
		String action = m.group(1);
		String arg = m.group(2);
		if(action.equalsIgnoreCase("USERS")) {
			getUsrs(arg);
		} else if(action.contains("ALLFROM")) { // 存取消息
			String fromUser = action.substring("ALLFROM".length() + 1, action.length());
			storeMsg(fromUser, arg, true);
		} else if(action.contains("FROM")) { // 存取消息
//			System.out.println(cmd);
			String fromUser = action.substring("FROM".length() + 1, action.length());
			storeMsg(fromUser, arg, false);
		}
	}
	
	/**
	 * 加载聊天记录
	 */
	private void loadTalkRecord() {
		File dir = new File(talkRecordDir);
		Pattern p = Pattern.compile("(.+)\\.txt$");
		if(! dir.exists()) return;
		for(File file: dir.listFiles()) { // 遍历记录文件
			Matcher m = p.matcher(file.getName());
			if(m.find()) {
				if(m.group(1).equals(usrName)) continue; // 跳过自己的聊天记录

				try {
					FileInputStream fis = new FileInputStream(file);
					byte[] data = new byte[(int)file.length()];
					fis.read(data);
					fis.close();
					usrsMsg.put(m.group(1), new String(data, "UTF-8"));
					if(!m.group(1).equals(TalkServerMaster.Master)) usrsList.add(m.group(1));
				} catch (FileNotFoundException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
		
	}
	
	/** 
	 * 保存聊天记录
	 */
	private void saveTalkRecord() {
		File dir = new File(talkRecordDir);
		if(! dir.exists()) dir.mkdir();
		if(usrsMsg == null) return;
		for(Map.Entry<String, String> usr: usrsMsg.entrySet()) {
			File file = new File(talkRecordDir + "/" + usr.getKey() + ".txt");
			try {
				FileOutputStream fos = new FileOutputStream(file);
				OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8");
				osw.write(usr.getValue());
				osw.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}


	// 获取用户名,例如"xxx (*)",得到xxx
	private String getUsrName(String u) {
		if(u == null) return null;
		/*
		 * 用户有3种状态:
		 * users: 在线状态
		 * users (*): 该用户发来消息
		 * users (offline): 该用户离线了
		 */
		Pattern p = Pattern.compile("([\\w\\d]+)( \\((\\*|Offline)\\))?"); 
		Matcher m = p.matcher(u);
		m.find();
		return m.group(1);
	}

	/**
	 * 判断用户是否离线
	 * @param u
	 * @return 是否离线
	 */
	private boolean usrIsOffline(String u) { 
		if(u == null) return false;
		Pattern p = Pattern.compile("([\\w\\d]+)( \\((\\*|Offline)\\))?"); 
		Matcher m = p.matcher(u);
		m.find();
		return m.group(3) != null && m.group(3).equals("Offline");
	}


	// 发送消息
	private void sendMsg() {
		String curUsr = getUsrName(usrsListView.getSelectionModel().getSelectedItem());
		if(sendMsg.getText() != null && ! Pattern.matches("\\n*", sendMsg.getText())) {
			out.printf("[SENDTO %s]%s\n", curUsr, Base64.getEncoder().encodeToString(sendMsg.getText().getBytes(StandardCharsets.UTF_8)));
			out.flush();
			String Msg = String.format("[%s <%s>] %s\n",
					new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()),
					usrName, sendMsg.getText()
					);
			if(usrsMsg.get(curUsr) != null)
				usrsMsg.put(curUsr, usrsMsg.get(curUsr) + Msg);
			else
				usrsMsg.put(curUsr, Msg);
			message.appendText(Msg);
			sendMsg.setText("");
		} else {
			sendMsg.setText("");
		}
	}

	private void talkScene() { // 聊天界面
		usrsMsg = new HashMap<String, String>();
		usrsMsgNotify = new HashMap<String, Boolean>();
        usrsList = FXCollections.observableArrayList(TalkServerMaster.Master);
		loadTalkRecord();
		pStage.setTitle("Talk by netcan v"+ VERSION +" [当前用户: "+usrName+"]");

        GridPane grid = new GridPane();
        grid.setHgap(10);
        grid.setVgap(10);
        grid.setPadding(new Insets(25, 25, 25, 25));


        // 在线用户
        usrsListView = new ListView<>(usrsList);
        usrsListView.setItems(usrsList);
        usrsListView.getSelectionModel().select(0);

        VBox usrListBox = new VBox();
        VBox.setVgrow(usrsListView, Priority.ALWAYS);
        usrListBox.getChildren().addAll(usrsListView);


        // 消息界面
        VBox sendBox = new VBox();
        message = new TextArea();
        sendMsg = new TextArea();
        message.setEditable(false);
        message.setPrefRowCount(20);
        message.setWrapText(true);
        sendMsg.setPrefRowCount(5);
        sendMsg.setWrapText(true);

        message.setStyle("-fx-font-size: 17; -fx-font-family: \"OpenSansEmoji\";"); 
        sendMsg.setStyle("-fx-font-size: 17; -fx-font-family: \"OpenSansEmoji\";"); 

        sendBox.getChildren().add(message);
        sendBox.getChildren().add(sendMsg);

        Button btn = new Button("发送");
        Button emojiBtn = new Button(TalkEmoji.emoji[0] + "表情");
        emojiBtn.setStyle("-fx-font-family: \"OpenSansEmoji\";"); 
        HBox hbBtn = new HBox(0);
        hbBtn.setAlignment(Pos.BOTTOM_RIGHT);
        hbBtn.getChildren().add(emojiBtn);
        hbBtn.getChildren().add(btn);
        sendBox.getChildren().add(hbBtn);

        grid.add(usrListBox, 0, 0);
        grid.add(sendBox, 1, 0);
		Scene scene = new Scene(grid);
		pStage.setScene(scene);


		// 事件处理
		// 消息切换
        usrsListView.getSelectionModel().selectedItemProperty().addListener(
        		(ObservableValue<? extends String> ov, String old_val,
        				String new_val) -> {
        					String oldV = getUsrName(old_val);
        					String newV = getUsrName(new_val);
//        					System.out.println("old:" + oldV);
//        					System.out.println("new:" + newV);
        					if(! oldV.equals(newV)) { // 切换对话
        						if(usrsMsg.get(newV) != null) {
        							message.setText(usrsMsg.get(newV));
        							usrsMsgNotify.put(newV, false); // 已读
        							messageGotoEndLine = true; // 因为事件处理无法滚到最后,只能通过这种方式让外部滚动了
        						} else {
        							message.setText("");
        						}
        					}
        				}
        		);

        // 发送消息
        btn.setOnAction(new EventHandler<ActionEvent>() {
        	@Override
        	public void handle(ActionEvent event) {
        		sendMsg();
        	}
		});

        sendMsg.setOnKeyPressed(new EventHandler<KeyEvent>() {
        	   @Override
        	    public void handle(KeyEvent keyEvent) {
        	        if (keyEvent.getCode() == KeyCode.ENTER)  {
        	        	sendMsg();
        	        }
        	    }
		});
        
        // 表情处理
        Button []emojis = new Button[TalkEmoji.emoji.length];
        for(int i=0; i<TalkEmoji.emoji.length; ++i) { // 将表情显示到按钮上
        	emojis[i] = new Button(TalkEmoji.emoji[i]);
        	emojis[i].setPrefSize(56, 56);
        	emojis[i].setStyle("-fx-font-size: 20; -fx-focus-color: transparent; -fx-font-family: \"OpenSansEmoji\";"); // 表情大小,清除选择的框框
        	emojis[i].setOnAction((event) -> { 
        		sendMsg.appendText(((Button) event.getSource()).getText()); // 黑科技,获取事件源对象
        	});
        }
        emojiStage = new Stage();
        
        pStage.setOnCloseRequest(event -> {
        	if(emojiStage.isShowing()) emojiStage.close();
        });

        emojiBtn.setOnAction(event1 -> { // 弹出表情选择
//			emojiBtn.setDisable(true);
        	if(emojiStage.isShowing()) 
        		emojiStage.close();

			emojiStage.setTitle("Select Emoji");
			FlowPane pane = new FlowPane();
			for(int i=0; i<TalkEmoji.emoji.length; ++i) 
				pane.getChildren().add(emojis[i]);
			Scene emojiScene = new Scene(pane);
			emojiStage.setScene(emojiScene);
			emojiStage.setResizable(false);
			emojiStage.setX(pStage.getX() + pStage.getWidth() / 3);
			emojiStage.setY(pStage.getY());
			emojiStage.show();

			emojiStage.setOnCloseRequest(event2 -> {
//				emojiBtn.setDisable(false);
			});
		});

		// 线程处理消息接收
		Task<Void> task = new Task<Void>() {
			@Override
			public Void call() throws Exception {
				while(out != null && in != null) {
					try {
						Thread.sleep(300);
						//	这里处理客户端请求
						// 获取用户列表
						out.println("[GETUSRS]");
						out.flush();

						String cmd = in.readLine();
						if(cmd == null) break;
						// 处理响应
						Platform.runLater(new Runnable() { // 刷新UI要放到runLater刷新
							@Override
							public void run() {
								// TODO Auto-generated method stub
								// 刷新在线用户
								execute(cmd);
								if(messageGotoEndLine) { // 滚到最后
									message.setScrollTop(Double.MAX_VALUE);
									messageGotoEndLine = false;
								}
								// 刷新选中用户最新消息
								int curUsrId = usrsListView.getSelectionModel().getSelectedIndex();
								String curUsr = getUsrName(usrsListView.getSelectionModel().getSelectedItem());

								// 用户离线,消息禁用
								if(usrIsOffline(usrsListView.getSelectionModel().getSelectedItem())) {
									sendMsg.setDisable(true);
									btn.setDisable(true);
								}
								else {
									sendMsg.setDisable(false);
									btn.setDisable(false);
								}

								if(usrsMsgNotify.get(curUsr) != null && usrsMsgNotify.get(curUsr)) { // 有新消息了
									usrsMsgNotify.put(curUsr, false); // 已读
									usrsList.set(curUsrId, curUsr); // 删除(*)提醒
//									System.out.println(curUsr);
									// 附加消息
									message.appendText(usrsMsg.get(curUsr).substring(message.getText().length(), usrsMsg.get(curUsr).length()));
								}
							}
						});


					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					} catch (IOException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
				return null;
			}
		};
		Thread th = new Thread(task);
		th.setDaemon(true);
		th.start();

	}

	private boolean checkLogin() {
		// 检测有效性
		Alert alert = new Alert(AlertType.ERROR);

		// connect to master
		try {
			if(client == null || client.isClosed()) {
				client = new Socket(ip, TalkServerMaster.PORT);
				in = new BufferedReader(new InputStreamReader(client.getInputStream()));
				out = new PrintWriter(client.getOutputStream());
			}
			out.println("[REGISTER]"+usrName);
			out.flush();
			String result = in.readLine();
			if(result.equals("[FAILED]"))
				return false;
			else if(result.equals("[OK]"))
				return true;
		} catch (UnknownHostException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
			alert.setContentText(e1.toString());
			alert.showAndWait();
		} catch (IOException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
			alert.setContentText(e1.toString());
			alert.showAndWait();
		}
		return false;
	}


	// login window
	@Override
	public void start(Stage primaryStage) throws IOException {
		Font.loadFont(getClass().getResource("/assets/OpenSansEmoji.ttf").toExternalForm(), 16);

//		Font.getFamilies();
//		for(String s: Font.getFamilies()) {
//			System.out.println(s);
//		}
		pStage = primaryStage;
		pStage.setTitle("Talk by netcan v"+ VERSION);
		pStage.setResizable(false);
		loginScene();
	}

	@Override
	public void stop() {
		try {
			if(in != null && out != null && client != null) {
				out.println("[LOGOUT]");
				saveTalkRecord();
				out.flush();
				in.close();
				out.close();
				in = null;
				out = null;
				client.close();
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}


}