/**
 *    JfxWebView.java
 *    Copyright (C) 2026 New Zealand Digital Library, http://expeditee.org
 *
 *    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 org.expeditee.items.widgets;

/*
 * JavaFX is not on the default java classpath until Java 8 (but is still included with Java 7), so your IDE will probably complain that the imports below can't be resolved. 
 * In Eclipse hitting'Proceed' when told 'Errors exist in project' should allow you to run Expeditee without any issues (although the JFX Browser widget will not display),
 * or you can just exclude JfxBrowser, WebParser and JfxbrowserActions from the build path.
 *
 * If you are using Ant to build/run, 'ant build' will try to build with JavaFX jar added to the classpath. 
 * If this fails, 'ant build-nojfx' will build with the JfxBrowser, WebParser and JfxbrowserActions excluded from the build path.
 */
import java.awt.Point;
import java.awt.event.KeyListener;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

import javafx.application.Platform;
import javafx.embed.swing.JFXPanel;
import javafx.event.Event;
import javafx.geometry.Point2D;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.control.TextField;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Font;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import org.expeditee.gui.DisplayController;
import org.expeditee.io.WebParser;
import org.expeditee.items.Text;

// Updating from JDK-FX8 to JDK-FX11 classes
import javafx.scene.text.HitInfo;
import javafx.scene.control.skin.TextFieldSkin;

/**
 * Web browser using a JavaFX WebView.
 * 
 * @author ngw8
 * @author jts21
 * @author davidb
 */

public abstract class JfxWebView extends DataFrameWidget {
	
	public static boolean JFXBROWSER_IN_USE = false;
	

	protected JFXPanel _panel;
	protected WebView _webView;
	protected WebEngine _webEngine;

	protected StackPane _overlay;
	
	protected boolean _parserRunning;
	
	//private MouseButton _buttonDownId = MouseButton.NONE;
	//private MouseEvent _backupEvent = null;
	protected static Field MouseEvent_x, MouseEvent_y;

	static {
		Platform.setImplicitExit(false);

		Font.loadFont(ClassLoader.getSystemResourceAsStream("org/expeditee/assets/resources/fonts/FontAwesome/fontawesome-webfont.ttf"), 12);
		try {
			MouseEvent_x = MouseEvent.class.getDeclaredField("x");
			MouseEvent_x.setAccessible(true);
			MouseEvent_y = MouseEvent.class.getDeclaredField("y");
			MouseEvent_y.setAccessible(true);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public JfxWebView(Text source, final String[] args) {
		super(source, new JFXPanel(), -1, 500, -1, -1, 300, -1);

		_panel = (JFXPanel) _swingComponent;

		// Quick & easy way of having a cancel function for the web parser.
		// Can't just have a JFX button, as the JFX thread is occupied with running JavaScript so it wouldn't receive the click event straight away
		_swingComponent.addKeyListener(new KeyListener() {
			
			@Override
			public void keyReleased(java.awt.event.KeyEvent e) {
				if(e.getKeyCode() == java.awt.event.KeyEvent.VK_ESCAPE) {
					JfxWebView.this.cancel();
				}
			}
			
			@Override
			public void keyPressed(java.awt.event.KeyEvent e) {				
			}
			
			@Override
			public void keyTyped(java.awt.event.KeyEvent e) {				
			}
		});
		
		JFXBROWSER_IN_USE = true;
	}
	
	/**
	 * Sets up the browser frame. JFX requires this to be run on a new thread.
	 * 
	 * @param url
	 *            The URL to be loaded when the browser is created
	 */
	abstract protected void initFx(String url);

	/*
	public void navigate(String url) {
		final String actualURL;

		// check if protocol is missing
		if (!hasValidProtocol(url)) {
			// check if it's a search
			int firstSpace = url.indexOf(" ");
			int firstDot = url.indexOf(".");
			int firstSlash = url.indexOf('/');
			int firstQuestion = url.indexOf('?');
			int firstSQ;
			if(firstSlash == -1) {
				firstSQ = firstQuestion;
			} else if(firstQuestion == -1) {
				firstSQ = firstSlash;
			} else {
				firstSQ = -1;
			}
			if(firstDot <= 0 ||                                        // no '.' or starts with '.' -> search
			        (firstSpace != -1 && firstSpace < firstDot + 1) || // ' ' before '.'            -> search
			        (firstSpace != -1 && firstSpace < firstSQ)) {      // no '/' or '?'             -> search
				// make it a search
				actualURL = NetworkSettings.SearchEngine.get() + url;
			} else {
				// add the missing protocol
				actualURL = "http://" + url;
			}
		} else {
			actualURL = url;
		}
		System.out.println(actualURL);
		try {
			Platform.runLater(new Runnable() {
				@Override
				public void run() {
					try {
						JfxWebView.this._webEngine.load(actualURL);
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			});
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
*/
	
	
	/**
	 * Traverses DOM an turns elements into expeditee items.
	 */
	public void getFrame() {
		try {
			WebParser.parsePageSimple(this, _webEngine, _webView, DisplayController.getCurrentFrame());
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public void getFrameNew() {

		this._parserRunning = true;
		
		try {
			// hack to make sure we don't try parsing the page from within the JavaFX thread,
			// because doing so causes deadlock
			new Thread(new Runnable() {
				public void run() {
					WebParser.parsePageSimple(JfxWebView.this, JfxWebView.this._webEngine, JfxWebView.this._webView, DisplayController.getCurrentFrame());
				}
			}).start();
		} catch (Exception e) {
			e.printStackTrace();
			this._parserRunning = false;
		}
	}

	
	
	/**
	 * Shows/hides a message reading 'Importing page' over the widget
	 * 
	 * @param visible
	 */
	public void setOverlayVisible(boolean visible) {
		this._overlay.setVisible(visible);
	}

	public void setScrollbarsVisible(boolean visible) {
		if (!visible) {
			this._webView.getStyleClass().add("scrollbars-hidden");
		} else {
			this._webView.getStyleClass().remove("scrollbars-hidden");
		}
	}

	/**
	 * Sets the size of the webview element of the widget
	 * 
	 * @param width
	 * @param height
	 */
	public void setWebViewSize(double width, double height) {
		this._webView.setPrefSize(width, height);
	}

	/**
	 * Resizes the webview back to the size of its parent element
	 */
	public void rebindWebViewSize() {
		this._webView.getParent().resize(0, 0);
	}

	/*
	@Override
	protected String[] getArgs() {
		String[] r = null;
		if (this._webView != null) {
			try {
				r = new String[] { this._webEngine.getLocation() };
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		return r;
	}
	*/
	
	protected Point getCoordFromCaret(TextField text) {
		TextFieldSkin skin = (TextFieldSkin) text.getSkin();
		
		Point2D onScene = text.localToScene(0, 0);
		
		double x = onScene.getX() + JfxWebView.this.getX();// - org.expeditee.gui.Browser._theBrowser.getOrigin().x;
		double y = onScene.getY() + JfxWebView.this.getY();// - org.expeditee.gui.Browser._theBrowser.getOrigin().y;
		
		Rectangle2D cp = skin.getCharacterBounds(text.getCaretPosition());
		
		return new Point((int) (cp.getMinX() + x), (int) (cp.getMinY() + y));
	}
	
	/**
	 * Get internal JavaFX methods via reflection at runtime.<br>
	 * These are internal API methods for converting mouse (pixel) position to caret (text) position.
	 * This is used because JavaFX does not appear to support this functionality in any public API.
	 * This class solves two problems:
	 *  - If the system we are compiling on does not have the method, it should still compile
	 *  - If the system we are running on does not have the method, it should still run
	 *    (just without the expeditee-like URL bar text selection)
	 * Unfortunately it will still fail if the internal API ever removes the TextFieldSkin or HitInfo classes,
	 * But that is unavoidable without a compile-time preprocessor or some greater hacks
	 */
	private static final class kludges {
		
		private static Method getGetIndex() {
			try {
				return TextFieldSkin.class.getMethod("getIndex", MouseEvent.class);
			} catch(Exception e) {
				return null;
			}
		}
		private static Method getGetInsertionIndex() {
			try {
				return HitInfo.class.getMethod("getInsertionIndex");
			} catch(Exception e) {
				return null;
			}
		}
		private static Method getPositionCaret() {
			try {
				return TextFieldSkin.class.getMethod("positionCaret", HitInfo.class,  boolean.class);
			} catch(Exception e) {
				return null;
			}
		}
		
		private static final Method getIndex = getGetIndex();
		private static final Method getInsertionIndex = getGetInsertionIndex();
		private static final Method positionCaret = getPositionCaret();
		private static final boolean enabled = (getIndex != null && getInsertionIndex != null && positionCaret != null);
	}
	
	/**
	 * Attempts to get the caret (text) position for a given pixel (MouseEvent) position
	 * 
	 * @param text The textfield to find the caret position for
	 * @param e The MouseEvent containing the coordinates to convert to caret position
	 * 
	 * @return The caret position if successful, otherwise the current caret position of the TextField.
	 */
	protected int getCaretFromCoord(TextField text, MouseEvent e) {
		if (kludges.enabled) {
			try {
				return (int) kludges.getInsertionIndex.invoke(kludges.getIndex.invoke(text.getSkin(), e));
			} catch (Exception ex) {
				ex.printStackTrace();
			}
		}
		return text.getCaretPosition();
	}
	
	/**
	 * Attempts to set the caret (text) position from a given pixel (MouseEvent) position
	 * 
	 * @param text The textfield to set the caret position of
	 * @param e The MouseEvent containing the coordinates to convert to caret position
	 */
	protected void setCaretFromCoord(TextField text, MouseEvent e) {
		if (kludges.enabled) {
			try {
				Object skin = text.getSkin();
				kludges.positionCaret.invoke(skin, kludges.getIndex.invoke(skin, e), true);
			} catch (Exception ex) {
				ex.printStackTrace();
			}
		}
	}
	
	/**
	 * @param src The MouseEvent to clone
	 * @param node The node the position will be relative to
	 * @param x The position in Expeditee space
	 * @param y The position in Expeditee space
	 * @return A fake MouseEvent for a specific position relative to a Node
	 */
	protected MouseEvent getMouseEventForPosition(MouseEvent src, Node node, int x, int y) {
		MouseEvent dst = (MouseEvent) ((Event) src).copyFor(null, null);
		try {
	        MouseEvent_x.set(dst, x - node.localToScene(0, 0).getX());
	        MouseEvent_y.set(dst, y - node.localToScene(0, 0).getY());
        } catch (Exception e) {
	        e.printStackTrace();
        }
		return dst;
	}
	
	
	/**
	 * Reads a resource file into a string
	 * @return The contents of the specified file as a string
	 */
	protected static String readResourceFile(String path) {
		BufferedReader bufferedReader = null;
		StringBuilder stringBuilder = new StringBuilder();
 
		String line;
		
		try {
			bufferedReader = new BufferedReader(new InputStreamReader(ClassLoader.getSystemResourceAsStream(path)));

			while ((line = bufferedReader.readLine()) != null) {
				stringBuilder.append(line + "\n");
			}
 
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if (bufferedReader != null) {
				try {
					bufferedReader.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
 
		return stringBuilder.toString();
	}
	
	
	/**
	 * @return Whether the parser is running. If this is true then the parser is running, 
	 *  however even if it is false, the parser may still be running (but it has been requested to stop)
	 */
	public boolean isParserRunning() {
		return this._parserRunning;
	}
	
	/**
	 * Should be called when the web parser has finished converting a page
	 */
	public void parserFinished() {
		this._parserRunning = false;
	}
	
	/**
	 * Cancels the current action being performed by the browser, such as loading a page or converting a page
	 */
	public void cancel() {
		if(isParserRunning()) {
			this._parserRunning = false;
		} else {
			Platform.runLater(new Runnable() {
				
				@Override
				public void run() {
					JfxWebView.this._webEngine.getLoadWorker().cancel();					
				}
			});
		}
	}
	
	/**
	 * Checks if a URL string starts with a protocol that can be loaded by the webview
	 * @param url URL string to check
	 * @return
	 */
	// **** Needs to be put back in JfxBrowser
	protected static boolean hasValidProtocol(String url) {
		String urlLower = url.toLowerCase();
		
		// check if protocol is present
		return (urlLower.startsWith("http://") || url.startsWith("https://") || urlLower.startsWith("ftp://") || urlLower.startsWith("file://"));
	}
}
