package kst4contest.controller;

import java.io.*;
import java.net.*;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import kst4contest.ApplicationConstants;
import kst4contest.model.ThreadStateMessage;
import kst4contest.view.GuiUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import kst4contest.model.ChatMember;
import kst4contest.model.Band;

import javafx.application.Platform;

/**
 * This thread is responsible for reading server's input and printing it to the
 * console. It runs in an infinite loop until the client disconnects from the
 * server.
 *
 * @author www.codejava.net
 */
public class ReadUDPbyUCXMessageThread extends Thread {
	private BufferedReader reader;
	private Socket socket;
	private ChatController client;
    private int udpPortNr = 12060;
    private ThreadStatusCallback callBackToController;
    private String ThreadNickName = "UDP-Log msg";

//	public ReadUDPbyUCXMessageThread(int localPort , ThreadStatusCallback callback) {
//
////        this.callBackToController = callback;
//	}

	public ReadUDPbyUCXMessageThread(int localPort, ChatController client, ThreadStatusCallback callback) {
		this.udpPortNr = localPort;
        this.client = client;
        this.callBackToController = callback;
	}

	@Override
	public void interrupt() {
		super.interrupt();
		try {
			if (this.socket != null) {
				System.out.println(">>>>>>>>>>>>>>ReadUdpbyUCS: closing socket");
				terminateConnection();
//                callBackToController.onThreadStatus("UDPReceiver", new String[]);
			}
		} catch (Exception e) {
			// TODO Auto-generated catch block
			System.out.println("UCXUDPRDR: catched error " + e.getMessage());
		}
	}

	/**
	 * Strips binary logger framing bytes before the XML payload. Some UCXLog packets
	 * contain transport bytes before the XML declaration.
	 *
	 * @param rawPacket raw UDP payload
	 * @return cleaned XML string or trimmed original text
	 */
	private String helper_extractXmlPayload(String rawPacket) {
		if (rawPacket == null) {
			return "";
		}

		int xmlStart = rawPacket.indexOf("<?xml");
		if (xmlStart < 0) {
			xmlStart = rawPacket.indexOf("<contactinfo");
		}
		if (xmlStart < 0) {
			xmlStart = rawPacket.indexOf("<contactreplace");
		}
		if (xmlStart < 0) {
			xmlStart = rawPacket.indexOf("<RadioInfo");
		}

		return xmlStart >= 0
				? rawPacket.substring(xmlStart).trim()
				: rawPacket.trim();
	}

	/**
	 * Reads an optional XML child node.
	 *
	 * @param element parent element
	 * @param tagName tag to read
	 * @return trimmed value or empty string
	 */
	private String helper_getOptionalElementText(Element element, String tagName) {
		if (element == null || tagName == null) {
			return "";
		}

		NodeList nodeList = element.getElementsByTagName(tagName);
		if (nodeList == null || nodeList.getLength() == 0 || nodeList.item(0) == null) {
			return "";
		}

		String textContent = nodeList.item(0).getTextContent();
		return textContent == null ? "" : textContent.trim();
	}

	/**
	 * Resolves the QSO locator from UCXLog contactinfo. gridsquare is preferred,
	 * rcvnr is used as fallback for exchanges such as 001JO41HK.
	 *
	 * @param element contactinfo XML element
	 * @return normalized six-character locator or null
	 */
	private String helper_resolveLocatorFromContactInfo(Element element) {
		String gridSquare = WorkedGrossFieldCache.extractLocator6(helper_getOptionalElementText(element, "gridsquare"));
		if (gridSquare != null) {
			return gridSquare;
		}

		return WorkedGrossFieldCache.extractLocator6(helper_getOptionalElementText(element, "rcvnr"));
	}

	/**
	 * Resolves the project Band enum from logger band values.
	 *
	 * @param band logger band text
	 * @return matching Band or null
	 */
	private Band helper_resolveBandFromLoggerBand(String band) {
		if (band == null) {
			return null;
		}

		switch (band.trim()) {
			case "50":
			case "6m":
				return Band.B_50;
			case "70":
			case "4m":
				return Band.B_70;
			case "144":
			case "2m":
				return Band.B_144;
			case "432":
			case "70cm":
				return Band.B_432;
			case "1240":
			case "1296":
			case "23cm":
				return Band.B_1296;
			case "2300":
			case "2320":
			case "13cm":
				return Band.B_2320;
			case "3400":
			case "9cm":
				return Band.B_3400;
			case "5600":
			case "5760":
			case "6cm":
				return Band.B_5760;
			case "10G":
			case "10368":
			case "3cm":
				return Band.B_10G;
			default:
				return null;
		}
	}

	public void run() {

        System.out.println("ReadUDPByUCXLogThread: started Thread for UCXLog getUDP");
		Thread.currentThread().setName("ReadUDPByUCXLogThread");


        ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
        callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);

		DatagramSocket socket = null;

        boolean running;
		byte[] buf = new byte[1777];
		DatagramPacket packet = new DatagramPacket(buf, buf.length);

		try {
//			socket = new DatagramSocket(12060);
            socket = new DatagramSocket(udpPortNr);
			socket.setSoTimeout(2000); //TODO try for end properly
		}
		
		catch (SocketException e) {
			//this will catch the repeating Sockettimeoutexception...nothing to do
//			e.printStackTrace();
		}

		while (true) {
			
			boolean timeOutIndicator = false;
			
//			packet = new DatagramPacket(buf, buf.length); //TODO: Changed that due to memory leak, check if all works (seems like that)
//    		 DatagramPacket packet  = new DatagramPacket(SRPDefinitions.BYTE_BUFFER_MAX_LENGTH); //TODO: Changed that due to memory leak, check if all works (seems like that)
			try {
				socket.receive(packet);

			} catch (SocketTimeoutException e2) {

				timeOutIndicator = true;
				// this will catch the repeating Sockettimeoutexception...nothing to do
//				e2.printStackTrace();
			}
			catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (NullPointerException nE) {
				// TODO Auto-generated catch block
				nE.printStackTrace();
				System.out.println("ReadUdpByUCXTH: Socket not ready");

				try {
					socket = new DatagramSocket(client.getChatPreferences().getLogsynch_ucxUDPWkdCallListenerPort());
					socket.setSoTimeout(2000);
				} catch (SocketException e) {
					System.out.println("[ReadUDPByUCSMsgTH, Error]: socket in use or something:");
					e.printStackTrace();

					try {
						socket = new DatagramSocket(null);
						socket.setReuseAddress(true);
						socket.bind(new InetSocketAddress(client.getChatPreferences().getLogsynch_ucxUDPWkdCallListenerPort()));
						socket.receive(packet);
						socket.setSoTimeout(3000);
					} catch (Exception ex) {
						System.out.println("ReadUDPByUCXMsgTh: Could not solve that. Program Restart needed.");
						throw new RuntimeException(ex);
					}

				}

			}

			InetAddress address = packet.getAddress();
			int port = packet.getPort();
//			packet = new DatagramPacket(buf, buf.length, address, port);
			String received = new String(packet.getData(), packet.getOffset(), packet.getLength());
			received = received.trim();


//			System.out.println("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<recv " + received);

			if (received.contains(ApplicationConstants.DISCONNECT_RDR_POISONPILL)) {
				System.out.println("ReadUdpByUCX, Info: got poison, now dieing....");
				socket.close();
				timeOutIndicator = true;

//                threadStatusMessage = new String[2];
//                threadStatusMessage[0] = "stopped";
//                threadStatusMessage[1] = "by poisonpill message (disconnect on purpose)";
                threadStateMessage = new ThreadStateMessage(this.ThreadNickName, false, "stopped by Poisonpill", false);
                callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
				break;
			}

			if (this.client.isDisconnectionPerformedByUser()) {
				break;//TODO: what if it´s not the finally closage but a band channel change?
			}

			if (!timeOutIndicator) {
				processUCXUDPMessage(received);
			} else {
				//dont process the empty message
			}

			buf = new byte[1777]; // reset buffer for future smaller packets

		}

	}

	public String processUCXUDPMessage(String udpPacketToProcess) {

		File logUDPMessageToThisFile;

		String udpMsg = helper_extractXmlPayload(udpPacketToProcess);

		ChatMember modifyThat = null;

        ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "received Message\n" + udpMsg, false);

        callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);

		DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
		try {
			dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
		} catch (ParserConfigurationException e1) {

			e1.printStackTrace();
		}

		try {
			DocumentBuilder db = dbf.newDocumentBuilder();
			Document doc = db.parse(new InputSource(new StringReader(udpMsg)));

			/**
			 * case Log-QSO-Packet in ucxlog / DXLog and compatible
			 * 
			 */
			NodeList list = doc.getElementsByTagName("contactinfo");

			if (list.getLength() == 0) {
				list = doc.getElementsByTagName("contactreplace");
				//DXlog will send contactreplace instead of contactinfo on clicking "broadcast whole logbook"
			}

			if (list.getLength() != 0) {

				/*
				 * QSO and TRX information share the same UDP receiver. The log-sync
				 * preference therefore controls processing, not ownership of the socket.
				 */
				if (!client.getChatPreferences().isLogsynch_ucxUDPWkdCallListenerEnabled()) {
					return "";
				}

				for (int temp = 0; temp < list.getLength(); temp++) {

					Node node = list.item(temp);

					if (node.getNodeType() == Node.ELEMENT_NODE) {

						Element element = (Element) node;

						String call = element.getElementsByTagName("call").item(0).getTextContent();
						String band = helper_getOptionalElementText(element, "band");
						String gridSquare = helper_resolveLocatorFromContactInfo(element);
						String points = helper_getOptionalElementText(element, "points");

						System.out.println("[Readudp, info ]: received Current Element :" + node.getNodeName()
								+ "call: " + call + " / " + band + " ----> " + points + " POINTS");

//						client.getChatPreferences().setBcn_contestScoreSum(Long.parseLong(points));

						ChatMember workedCall = new ChatMember();
						workedCall.setCallSign(call);
						workedCall.setWorked(true);

						if (gridSquare != null) {
							workedCall.setQra(gridSquare);
						}

						Band workedBand = helper_resolveBandFromLoggerBand(band);

						switch (band) {
						case "50":
						case "6m":
						{
							workedCall.setWorked50(true);
							break;
						}

						case "70":
						case "4m":
						{
							workedCall.setWorked70(true);
							break;
						}

						case "144":
						case "2m": //minos contest logger
						{
							workedCall.setWorked144(true);
							break;
						}

						case "432":
						case "70cm":
							{
								workedCall.setWorked432(true);
								break;
							}

						case "1240": //ucxlog style
						case "1296": //used for n1mm / Dxlog
						case "23cm": //minos contest logger
						{
							workedCall.setWorked1240(true);
							break;
						}

						case "2300":
						case "13cm":
							{
							workedCall.setWorked2300(true);
							break;
						}

						case "3400":
						case "9cm":
						{
							workedCall.setWorked3400(true);
							break;
						}

						case "5600":
						case "6cm":
						{
							workedCall.setWorked5600(true);
							break;
						}

						case "10G":
						case "3cm":
						{
							workedCall.setWorked10G(true);
                            break;
						}


							default:
								System.out.println("[ReadUDPFromUCX, Error:] unexpected band value: \"" + band + "\"");
								break;
						}

						{
							/**
							 * That means, the station is worked already but maybe at another band. So we
							 * have to get the worked ChatMember out of the list and to modify the worked
							 * options.
							 */

//							modifyThat = (ChatMember) client.getMap_ucxLogInfoWorkedCalls().get(call);

//							asd //TODO: Check if callsign and callsignraw is similar, then mark first and further via new checklistforchatmembermultiplemethod with array of indize

							ArrayList<Integer> markTheseChattersAsWorked = client.checkListForChatMemberIndexesByCallSign(workedCall);

							if (markTheseChattersAsWorked.isEmpty()) {
								//Worked call is not part of the chatmember list
							} else {
								for (int index : markTheseChattersAsWorked) {
									modifyThat = client.getLst_chatMemberList().get(index);

									modifyThat.setWorked(true);

									if (workedCall.isWorked50()) {
										modifyThat.setWorked50(true);

									} else if (workedCall.isWorked70()) {
										modifyThat.setWorked70(true);

									} else if (workedCall.isWorked144()) {
										modifyThat.setWorked144(true);

									} else if (workedCall.isWorked432()) {
										modifyThat.setWorked432(true);

									} else if (workedCall.isWorked1240()) {
										modifyThat.setWorked1240(true);

									} else if (workedCall.isWorked2300()) {
										modifyThat.setWorked2300(true);

									} else if (workedCall.isWorked3400()) {
										modifyThat.setWorked3400(true);

									} else if (workedCall.isWorked5600()) {
										modifyThat.setWorked5600(true);

									} else if (workedCall.isWorked10G()) {
										modifyThat.setWorked10G(true);
									}
								}

								try {

									GuiUtils.triggerGUIFilteredChatMemberListChange(this.client);
									// BEGIN PATCH: trigger band-upgrade hint after log entry (UCXLog)
									try {
										client.onExternalLogEntryReceived(workedCall.getCallSignRaw());
									} catch (Exception e) {
										System.out.println("[UCXUDPRcvr, warning]: band-upgrade hint failed: " + e.getMessage());
									}


								} catch (Exception IllegalStateException) {
									//do nothing, as it works...
								}
							}

							/**
							 * old mechanic to markup worked stations in the chatmember table
							 */
//							int indexOfChatMemberInTable = -1; //chatmember not in table
//							indexOfChatMemberInTable = client.checkListForChatMemberIndexByCallSign(workedCall);
//
//							if (indexOfChatMemberInTable == -1) {
//								// do nothing
//							} else {
//								modifyThat = client.getLst_chatMemberList().get(indexOfChatMemberInTable);
//
//								client.getLst_chatMemberList()
//										.get(client.checkListForChatMemberIndexByCallSign(modifyThat)).setWorked(true);
//
//								if (workedCall.isWorked144()) {
//									modifyThat.setWorked144(true);
//									client.getLst_chatMemberList()
//											.get(client.checkListForChatMemberIndexByCallSign(modifyThat))
//											.setWorked144(true);
//
//								} else if (workedCall.isWorked432()) {
//									modifyThat.setWorked432(true);
//									client.getLst_chatMemberList()
//											.get(client.checkListForChatMemberIndexByCallSign(modifyThat))
//											.setWorked432(true);
//
//								} else if (workedCall.isWorked1240()) {
//									modifyThat.setWorked1240(true);
//									client.getLst_chatMemberList()
//											.get(client.checkListForChatMemberIndexByCallSign(modifyThat))
//											.setWorked1240(true);
//
//								} else if (workedCall.isWorked2300()) {
//									modifyThat.setWorked2300(true);
//									client.getLst_chatMemberList()
//											.get(client.checkListForChatMemberIndexByCallSign(modifyThat))
//											.setWorked2300(true);
//
//								} else if (workedCall.isWorked3400()) {
//									modifyThat.setWorked3400(true);
//									client.getLst_chatMemberList()
//											.get(client.checkListForChatMemberIndexByCallSign(modifyThat))
//											.setWorked3400(true);
//
//								} else if (workedCall.isWorked5600()) {
//									modifyThat.setWorked5600(true);
//									client.getLst_chatMemberList()
//											.get(client.checkListForChatMemberIndexByCallSign(modifyThat))
//											.setWorked5600(true);
//
//								} else if (workedCall.isWorked10G()) {
//									modifyThat.setWorked10G(true);
//									client.getLst_chatMemberList()
//											.get(client.checkListForChatMemberIndexByCallSign(modifyThat))
//											.setWorked10G(true);
//								}
							/**
							 * //TODO: following line is a quick fix to making disappear worked chatmembers of the list
							 * Thats uncomfortable due to this also causes selection changes,
							 * Better way is to change all worked and qrv values to observables and then trigger the underlying
							 * list to fire an invalidationevent. Really Todo!
							 */
//								try{
//
//									GuiUtils.triggerGUIFilteredChatMemberListChange(client); //not clean at all
//								} catch (Exception IllegalStateException) {
//									//do nothing, as it works...
//								}
//						}
							/**
							 * end -> old mechanic to markup worked stations in the chatmember table
							 */
						}

						if (workedBand != null && gridSquare != null) {
							this.client.registerWorkedGrossField(workedBand, gridSquare, workedCall, "UCXLOG");
						}

						boolean isInChat = this.client.getDbHandler().updateWkdInfoOnChatMember(workedCall);
						// This will update the worked info on a worked chatmember. DBHandler will
						// check, if an entry at the db had been modified. If not, then the worked
						// station had not been stored. DBHandler will store the information then.
						if (!isInChat) {

							workedCall.setName("unknown");

							if (workedCall.getQra() == null || workedCall.getQra().isBlank()) {
								workedCall.setQra("unknown");
							}

							workedCall.setLastActivity(new Utils4KST().time_generateActualTimeInDateFormat());
							this.client.getDbHandler().storeChatMember(workedCall);
						}
						

						logUDPMessageToThisFile = new File(this.client.getChatPreferences()
								.getLogSynch_storeWorkedCallSignsFileNameUDPMessageBackup());

						FileWriter fileWriterPersistUDPToFile = null;
						BufferedWriter bufwrtrRawMSGOut;

						try {
							fileWriterPersistUDPToFile = new FileWriter(logUDPMessageToThisFile, true);

						} catch (IOException e1) {
							e1.printStackTrace();
						}

						bufwrtrRawMSGOut = new BufferedWriter(fileWriterPersistUDPToFile);

						if (modifyThat != null) {
							bufwrtrRawMSGOut.write("\n" + modifyThat.toString());
							bufwrtrRawMSGOut.flush();
							bufwrtrRawMSGOut.close();

						} else {
							bufwrtrRawMSGOut.write("\n" + workedCall.toString());
							bufwrtrRawMSGOut.flush();
							bufwrtrRawMSGOut.close();

						}

					}
				}
			} else {
				list = doc.getElementsByTagName("RadioInfo");

				/*
				 * RadioInfo packets may arrive on the shared UDP port even when automatic
				 * QRG synchronization is disabled. Ignore them unless TRX sync is enabled.
				 */
				if (list.getLength() != 0
						&& !client.getChatPreferences().isTrxSynch_ucxLogUDPListenerEnabled()) {
					return "";
				}

				for (int temp = 0; temp < list.getLength(); temp++) {

					Node node = list.item(temp);

					if (node.getNodeType() == Node.ELEMENT_NODE) {

						String formattedQRG;

						Element element = (Element) node;

						String qrg = element.getElementsByTagName("Freq").item(0).getTextContent();
						String mode = element.getElementsByTagName("Mode").item(0).getTextContent();

//						System.out.println("QRG Length: " + qrg.length() + " // " + qrg);

						/**
						 * The following if statement is only for formatting the frequency input for
						 * good readability to avoid values like 129601000 and set it to something
						 * readable like 1296.010.00
						 * 
						 */
						if (qrg.length() == 6) {
							// 701000 KHz
							formattedQRG = qrg.format("%s.%s.%s", qrg.substring(0, 1), qrg.substring(2, 5),
									qrg.substring(5, 6));

						} else if (qrg.length() == 7) {
							// 700000 KHz
							formattedQRG = qrg.format("%s.%s.%s", qrg.substring(0, 2), qrg.substring(2, 5),
									qrg.substring(5, 7));
						} else if (qrg.length() == 8) {
							// 144.123.22 KHz
							formattedQRG = qrg.format("%s.%s.%s", qrg.substring(0, 3), qrg.substring(3, 6),
									qrg.substring(6, 8));
						} else if (qrg.length() == 9) {
							// 1296.010.00
							formattedQRG = qrg.format("%s.%s.%s", qrg.substring(0, 4), qrg.substring(4, 7),
									qrg.substring(7, 9));
						} else if (qrg.length() == 10) {
							// 10000.010.00
							formattedQRG = qrg.format("%s.%s.%s", qrg.substring(0, 5), qrg.substring(5, 8),
									qrg.substring(8, 10));
						}

						else {
							formattedQRG = qrg;
						}

//						System.out.println("Current Element :" + node.getNodeName());
//						System.out.println("Radio QRG : " + qrg);
//						System.out.println("Radio Mode: " + mode);
//						System.out.println("[ReadUDPFromUCX, Info:] Setted QRG pref to: \"" + qrg + "\"" );

//						this.client.getChatPreferences().getMYQRGFirstCat().set(formattedQRG);

						final String finalFormattedQRG = formattedQRG;
						helper_runOnFxThread(() ->
								this.client.getChatPreferences().getMYQRGFirstCat().set(finalFormattedQRG)
						);

//						System.out.println("[ReadUDPbyUCXTh: ] Radioinfo processed: " + formattedQRG);
					}
				}

			}

		} catch (ParserConfigurationException | SAXException | IOException e) {
			e.printStackTrace();
			System.out.println(e.getCause());
			System.out.println(e.getMessage());

//            threadStatusMessage = new String[2];
//            threadStatusMessage[0] = "STOPPED";
//            threadStatusMessage[1] = Arrays.toString(e.getStackTrace());
            threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "CRASHED" + udpMsg, true);
            threadStateMessage.setCriticalStateFurtherInfo(Arrays.toString(e.getStackTrace()));

            callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);

		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
            threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "CRASHED" + udpMsg, true);
            threadStateMessage.setCriticalStateFurtherInfo(Arrays.toString(e.getStackTrace()));

            callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);

//            threadStatusMessage = new String[2];
//            threadStatusMessage[0] = "STOPPED";
//            threadStatusMessage[1] = Arrays.toString(e.getStackTrace());
//            callBackToController.onThreadStatus(ThreadNickName,threadStatusMessage);
		}

//		System.out.println("[ReadUDPbyUCXTh: ] worked size = " + this.client.getMap_ucxLogInfoWorkedCalls().size());
//		System.out.println("[ReadUDPbyUCXTh: ] worked size = removeThisActions" );

		return "";
	}

	public boolean terminateConnection() throws IOException {
//        String[] threadStatusMessage = new String[2];
//        threadStatusMessage = new String[2];
//        threadStatusMessage[0] = "STOPPED";
//        threadStatusMessage[1] = "Connection terminated for purpose.";
//        callBackToController.onThreadStatus(ThreadNickName,threadStatusMessage);

        ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, false, "terminated", false);
        callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);

		this.socket.close();

		return true;

	}

	/**
	 * Runs UI-bound changes on the JavaFX application thread.
	 *
	 * <p>UCXLog UDP packets are processed in a background thread. Some preference
	 * properties are bound to JavaFX controls, so setting them directly from this
	 * thread can crash JavaFX with "Not on FX application thread".</p>
	 *
	 * @param runnable UI-bound update
	 */
	private void helper_runOnFxThread(Runnable runnable) {
		if (runnable == null) {
			return;
		}

		if (Platform.isFxApplicationThread()) {
			runnable.run();
		} else {
			Platform.runLater(runnable);
		}
	}

}