/**
 *    JfxBrowser.java
 *    Copyright (C) 2010 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;

import java.util.ArrayList;
import java.util.List;

import javafx.animation.FadeTransition;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker.State;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.Tooltip;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.web.WebEvent;
import javafx.scene.web.WebView;
import javafx.util.Duration;
import netscape.javascript.JSObject;

import org.expeditee.gio.gesture.StandardGestureActions;
import org.expeditee.gui.DisplayController;
import org.expeditee.gui.Frame;
import org.expeditee.gui.FreeItems;
import org.expeditee.gui.MessageBay;
import org.expeditee.io.WebParser;
import org.expeditee.items.Picture;
import org.expeditee.items.Text;
import org.w3c.dom.NodeList;

import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;

import dev.langchain4j.model.googleai.GoogleAiGeminiStreamingChatModel;
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
import dev.langchain4j.model.chat.StreamingChatModel;

import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;

import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.parser.Parser;
import com.vladsch.flexmark.util.data.MutableDataSet;


/**
 * Display Markdown ChatLLM response returned via LangChain4j using a JavaFX WebView.
 * Borrows strongly from JfxBrowser
 * 
 * @author davidb
 */

public class JfxChatLLMBrowser extends JfxWebView {
	
	public static boolean JFXBROWSER_IN_USE = false;
	
	//private static final String BACK = "back";
	//private static final String FORWARD = "forward";
	//private static final String REFRESH = "refresh";
	private static final String CONVERT = "convert";
	//private static final String VIDEO = "video";
	
	/*
	private Button _forwardButton;
	private Button _backButton;*/
	
	private Button _stopButton;
	//private Button _goButton;
	private Button _convertButton;
	//private ToggleButton _readableModeButton;
	private Label _statusLabel;
	private FadeTransition _statusFadeIn;
	private FadeTransition _statusFadeOut;

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

	
    private final StringBuilder _fullResponse = new StringBuilder();
    private Parser _parser;
    private HtmlRenderer _renderer;
    
    //protected static GoogleAiGeminiStreamingChatModel _model;
    protected static StreamingChatModel _geminiStreamingModel;
    protected static StreamingChatModel _openaiStreamingModel;
    
    protected static final List<ChatMessage> _history = new ArrayList<>();  // Conversation state
    
	static {
		
		_geminiStreamingModel = GoogleAiGeminiStreamingChatModel.builder()
				.apiKey(System.getenv("GEMINI_API_KEY")) 
				.modelName("gemini-3-flash-preview")	    
				.build();
		
	
		_openaiStreamingModel = OpenAiStreamingChatModel.builder()
                .apiKey(System.getenv("OPENAI_API_KEY"))
                .modelName("gpt-4o-mini")
                .build();
	}

	public JfxChatLLMBrowser(Text source, final String[] args) {
		super(source, args);
		
		
	    _history.add(SystemMessage.from(
                "You are a helpful assistant. " 
                //+ "When you provide code, format it in Markdown fenced blocks. "
                //+ "Keep answers concise unless asked otherwise."
        ));
	    
		Platform.runLater(new Runnable() {
			@Override
			public void run() {
				String prompt = null;
				if (args != null && args.length > 0) {
					prompt = String.join(" ", args);
				}
				else {
					prompt = "Tell me about Waikato University";
				}
				initFx(prompt);
			}
		});
	}
	
	/**
	 * Sets up the browser frame. JFX requires this to be run on a new thread.
	 * 
	 * @param prompt
	 *            The URL to be loaded when the browser is created
	 */
	@Override
	protected void initFx(String prompt) {
		try {
			StackPane mainLayout = new StackPane();
			mainLayout.setId("jfxchatllmbrowser");
			
			VBox vertical = new VBox();
			HBox horizontal = new HBox();
			horizontal.getStyleClass().add("custom-toolbar");
			
			/*
			this._backButton = new Button("\uf060");
			this._backButton.setTooltip(new Tooltip("Back"));
			this._backButton.setMinWidth(Button.USE_PREF_SIZE);
			this._backButton.setMaxHeight(Double.MAX_VALUE);
			this._backButton.setFocusTraversable(false);
			this._backButton.getStyleClass().addAll("first", "fa");

			this._backButton.setDisable(true);
			*/
			
			/*
			this._forwardButton = new Button("\uf061");
			this._forwardButton.setTooltip(new Tooltip("Forward"));
			this._forwardButton.setMinWidth(Button.USE_PREF_SIZE);
			this._forwardButton.setMaxHeight(Double.MAX_VALUE);
			this._forwardButton.setFocusTraversable(false);
			this._forwardButton.getStyleClass().addAll("last", "fa");
*/
			/*
			this._urlField = new TextField(url);
			this._urlField.getStyleClass().add("url-field");
			this._urlField.setMaxWidth(Double.MAX_VALUE);
			this._urlField.setFocusTraversable(false);
			*/
			
			this._stopButton = new Button("\uF00D");
			this._stopButton.setTooltip(new Tooltip("Stop loading the page"));
			this._stopButton.getStyleClass().addAll("url-button", "url-cancel-button", "fa");
			this._stopButton.setMinWidth(Button.USE_PREF_SIZE);
			this._stopButton.setMaxHeight(Double.MAX_VALUE);
			StackPane.setAlignment(this._stopButton, Pos.CENTER_RIGHT);
			this._stopButton.setFocusTraversable(false);

			/*
			this._goButton = new Button("\uf061");
			this._goButton.setTooltip(new Tooltip("Load the entered address"));
			this._goButton.getStyleClass().addAll("url-button", "url-go-button", "fa");
			this._goButton.setMinWidth(Button.USE_PREF_SIZE);
			this._goButton.setMaxHeight(Double.MAX_VALUE);
			StackPane.setAlignment(this._goButton, Pos.CENTER_RIGHT);
			this._goButton.setFocusTraversable(false);
			*/
			/*
			this._readableModeButton = new ToggleButton();
			this._readableModeButton.setMinWidth(Button.USE_PREF_SIZE);
			this._readableModeButton.setFocusTraversable(false);
			this._readableModeButton.setTooltip(new Tooltip("Switch to an easy-to-read view of the page"));
			
			Image readableModeIcon = new Image(ClassLoader.getSystemResourceAsStream("org/expeditee/assets/images/readableModeIcon.png"));
			this._readableModeButton.setGraphic(new ImageView(readableModeIcon));
			*/
			
			
			this._convertButton = new Button("Convert");
			this._convertButton.setMinWidth(Button.USE_PREF_SIZE);
			this._convertButton.setFocusTraversable(false);
				
			
			/*
			this._urlProgressBar = new ProgressBar();		
			this._urlProgressBar.getStyleClass().add("url-progress-bar");
			this._urlProgressBar.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
			*/
			
			// Status label that displays the URL when a link is hovered over
			this._statusLabel = new Label();
			this._statusLabel.getStyleClass().addAll("browser-status-label");
			this._statusLabel.setVisible(false);
			
			this._statusFadeIn = new FadeTransition();
			this._statusFadeIn.setDuration(Duration.millis(200));
			this._statusFadeIn.setNode(this._statusLabel);
			this._statusFadeIn.setFromValue(0);
			this._statusFadeIn.setToValue(1);
			this._statusFadeIn.setCycleCount(1);
			this._statusFadeIn.setAutoReverse(false);
			
			this._statusFadeOut = new FadeTransition();
			this._statusFadeOut.setDuration(Duration.millis(400));
			this._statusFadeOut.setNode(this._statusLabel);
			this._statusFadeOut.setFromValue(1);
			this._statusFadeOut.setToValue(0);
			this._statusFadeOut.setCycleCount(1);
			this._statusFadeOut.setAutoReverse(false);
			
			this._statusFadeOut.setOnFinished(new EventHandler<ActionEvent>() {

				@Override
				public void handle(ActionEvent arg0) {
					JfxChatLLMBrowser.this._statusLabel.setVisible(false);
				}
			});
			

			StackPane urlbar = new StackPane();
			//urlbar.getChildren().addAll(_urlProgressBar, this._urlField, this._stopButton, this._goButton);
			//urlbar.getChildren().addAll(_urlProgressBar, this._urlField, this._stopButton);
			urlbar.getChildren().addAll(this._stopButton);
			
			//horizontal.getChildren().addAll(this._backButton, this._forwardButton, urlbar, this._readableModeButton, this._convertButton);
			horizontal.getChildren().addAll(urlbar,this._convertButton);

			
			//HBox.setHgrow(this._backButton, Priority.NEVER);
			//HBox.setHgrow(this._forwardButton, Priority.NEVER);
			HBox.setHgrow(this._convertButton, Priority.NEVER);
			//HBox.setHgrow(this._readableModeButton, Priority.NEVER);
			HBox.setHgrow(urlbar, Priority.ALWAYS);
			
			//HBox.setMargin(this._readableModeButton, new Insets(0, 5, 0, 5));
			//HBox.setMargin(this._forwardButton, new Insets(0, 5, 0, 0));

			this._webView = new WebView();
			this._webView.setMaxWidth(Double.MAX_VALUE);
			this._webView.setMaxHeight(Double.MAX_VALUE);
			this._webEngine = this._webView.getEngine();
			
			//this._urlProgressBar.progressProperty().bind(_webEngine.getLoadWorker().progressProperty());


			// Pane to hold just the webview. This seems to be the only way to allow the webview to be resized to greater than its parent's
			// size. This also means that the webview's prefSize must be manually set when the Pane resizes, using the event handlers below
			Pane browserPane = new Pane();
			browserPane.getChildren().addAll(_webView, this._statusLabel);
			
			HBox.setHgrow(browserPane, Priority.ALWAYS);
			VBox.setVgrow(browserPane, Priority.ALWAYS);

			browserPane.widthProperty().addListener(new ChangeListener<Object>() {

				@Override
				public void changed(ObservableValue<?> observable, Object oldValue, Object newValue) {
					JfxChatLLMBrowser.this._webView.setPrefWidth((Double) newValue);
				}
			});

			browserPane.heightProperty().addListener(new ChangeListener<Object>() {

				@Override
				public void changed(ObservableValue<?> observable, Object oldValue, Object newValue) {
					JfxChatLLMBrowser.this._webView.setPrefHeight((Double) newValue);
					JfxChatLLMBrowser.this._statusLabel.setTranslateY((Double) newValue - JfxChatLLMBrowser.this._statusLabel.heightProperty().doubleValue());
				}
			});

			vertical.getChildren().addAll(horizontal, browserPane);

			this._overlay = new StackPane();
			this._overlay.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);

			// Class for CSS styling
			this._overlay.getStyleClass().add("browser-overlay");

			// Don't show the overlay until processing the page
			this._overlay.setVisible(false);

			Label overlayLabel = new Label("Importing page to Expeditee...");

			ProgressIndicator prog = new ProgressIndicator();
			prog.setMaxSize(25, 25);

			this._overlay.getChildren().addAll(overlayLabel, prog);

			this._overlay.setAlignment(Pos.CENTER);
			StackPane.setMargin(overlayLabel, new Insets(-50, 0, 0, 0));
			StackPane.setMargin(prog, new Insets(50, 0, 0, 0));

			mainLayout.getChildren().addAll(vertical, this._overlay);
			
			final Scene scene = new Scene(mainLayout);
			
			final String cssPath = ClassLoader.getSystemResource("org/expeditee/assets/style/jfx.css").toString();
			
			scene.getStylesheets().add(cssPath);
		
			this._panel.setScene(scene);
			
			// Disable right click menu
			this._webView.setContextMenuEnabled(false);
			
			// Showing the status label when a link is hovered over
			this._webEngine.setOnStatusChanged(new EventHandler<WebEvent<String>>() {
				
				@Override
				public void handle(WebEvent<String> arg0) {
					if (arg0.getData() != null && hasValidProtocol(arg0.getData())) {
						JfxChatLLMBrowser.this._statusLabel.setText(arg0.getData());
						
						JfxChatLLMBrowser.this._statusFadeOut.stop();
						
						if(JfxChatLLMBrowser.this._statusLabel.isVisible()) {
							// Don't play the fade in if the label is already partially visible
							JfxChatLLMBrowser.this._statusLabel.setOpacity(1);
						} else {
							JfxChatLLMBrowser.this._statusLabel.setVisible(true);
							JfxChatLLMBrowser.this._statusFadeIn.play();
						}
					} else {
						JfxChatLLMBrowser.this._statusFadeIn.stop();

						JfxChatLLMBrowser.this._statusFadeOut.play();
					}
				}
			});
			
			
			//final EventDispatcher initial = this._urlField.getEventDispatcher();
			
			/*
			this._urlField.setEventDispatcher(new EventDispatcher() {
				@Override
				public Event dispatchEvent(Event e, EventDispatchChain tail) {
					if (e instanceof MouseEvent) {
						MouseEvent m = (MouseEvent) e;
                        if (m.getButton() == MouseButton.SECONDARY && m.getEventType() == MouseEvent.MOUSE_RELEASED) {
                            e.consume();
                            JfxChatLLMBrowser.this._urlField.getOnMouseReleased().handle(m);
                        }
                    }
                    return initial.dispatchEvent(e, tail);
				}
			});
			*/
			
			/*
			this._backButton.setOnAction(new EventHandler<ActionEvent>() {
				@Override
                public void handle(ActionEvent e) {
                    navigateBack();
                }
			});

			_forwardButton.setOnAction(new EventHandler<ActionEvent>() {
				@Override
                public void handle(ActionEvent e) {
                    navigateForward();
                }
			});
			*/
			
			this._stopButton.setOnAction(new EventHandler<ActionEvent>() {
				
				@Override
				public void handle(ActionEvent arg0) {
					JfxChatLLMBrowser.this._webEngine.getLoadWorker().cancel();
				}
			});
			
			/*
			this._goButton.setOnAction(new EventHandler<ActionEvent>() {
				
				@Override
				public void handle(ActionEvent arg0) {
                    navigate(JfxChatLLMBrowser.this._urlField.getText());
				}
			});
			
			this._readableModeButton.setOnAction(new EventHandler<ActionEvent>() {

				@Override
				public void handle(ActionEvent arg0) {
					if (arg0.getSource() instanceof ToggleButton) {
						ToggleButton source = (ToggleButton) arg0.getSource();
						
						// This seems backwards, but because the button's just been clicked, its state has already changed
						if(!source.isSelected()) {
							// Disable readable mode by refreshing the page
							JfxChatLLMBrowser.this._webEngine.reload();
						} else {
							JfxChatLLMBrowser.this.enableReadableMode();
						}
					}
				}
			});
			*/
			
			
			this._convertButton.setOnAction(new EventHandler<ActionEvent>() {
				@Override
                public void handle(ActionEvent e) {
					getFrameNew();
                }
			});

			/*
			this._urlField.setOnAction(new EventHandler<ActionEvent>() {
				@Override
                public void handle(ActionEvent e) {
                    navigate(JfxChatLLMBrowser.this._urlField.getText());
                }
			});

			this._urlField.setOnKeyTyped(new EventHandler<KeyEvent>() {
				@Override
				public void handle(KeyEvent e) {
					// Hiding the cursor when typing, to be more Expeditee-like
					DisplayController.setCursor(org.expeditee.items.Item.HIDDEN_CURSOR);
				}
			});

			this._urlField.setOnMouseMoved(new EventHandler<MouseEvent>() {
				@Override
				public void handle(MouseEvent e) {
					JfxChatLLMBrowser.this._backupEvent = e;
					// make sure we have focus if the mouse is moving over us
					if(!JfxChatLLMBrowser.this._urlField.isFocused()) {
						JfxChatLLMBrowser.this._urlField.requestFocus();
					}
					// Checking if the user has been typing - if so, move the cursor to the caret position
					if (DisplayController.getCursor() == Item.HIDDEN_CURSOR) {
						DisplayController.setCursor(org.expeditee.items.Item.TEXT_CURSOR);
						DisplayController.setCursorPosition(SwingConversions.fromSwingPoint(getCoordFromCaret(JfxChatLLMBrowser.this._urlField)));
					} else {
						// Otherwise, move the caret to the cursor location
						// int x = FrameMouseActions.getX() - JfxBrowser.this.getX(), y = FrameMouseActions.getY() - JfxBrowser.this.getY();
						JfxChatLLMBrowser.this._urlField.positionCaret(getCaretFromCoord(JfxChatLLMBrowser.this._urlField, e));
					}
				}
			});
			
			this._urlField.setOnMouseEntered(new EventHandler<MouseEvent>() {
				@Override
                public void handle(MouseEvent arg0) {
					JfxChatLLMBrowser.this._urlField.requestFocus();
				}
			});
			
			this._urlField.setOnMouseExited(new EventHandler<MouseEvent>() {
				@Override
                public void handle(MouseEvent arg0) {
					JfxChatLLMBrowser.this._webView.requestFocus();
                }
			});
			
			this._urlField.setOnMouseDragged(new EventHandler<MouseEvent>() {
				@Override
				public void handle(MouseEvent e) {
					if (!JfxChatLLMBrowser.this._urlField.isDisabled()) {
                        if (JfxChatLLMBrowser.this._buttonDownId == MouseButton.MIDDLE || JfxChatLLMBrowser.this._buttonDownId == MouseButton.SECONDARY) {
                            if (!(e.isControlDown() || e.isAltDown() || e.isShiftDown() || e.isMetaDown())) {
                                setCaretFromCoord(JfxChatLLMBrowser.this._urlField, e);
                            }
                        }
                    }
				}
			});
			
			this._urlField.focusedProperty().addListener(new ChangeListener<Boolean>() {
				@Override
                public void changed(ObservableValue<? extends Boolean> property, Boolean oldValue, Boolean newValue) {
					if(newValue.booleanValue()) {
						DisplayController.setCursor(org.expeditee.items.Item.TEXT_CURSOR);
					} else {
						// Restoring the standard cursor, since it is changed to a text cursor when focus is gained
						DisplayController.setCursor(org.expeditee.items.Item.DEFAULT_CURSOR);
					}
                }
			});

			this._urlField.setOnMouseReleased(new EventHandler<MouseEvent>() {
				@Override
                public void handle(MouseEvent e) {
					JfxChatLLMBrowser.this._buttonDownId = MouseButton.NONE;

					Text item;

					// If nothing is selected, then select all the text so that it will be copied/moved
					if (JfxChatLLMBrowser.this._urlField.getSelectedText() == null || JfxChatLLMBrowser.this._urlField.getSelectedText().length() == 0) {
						JfxChatLLMBrowser.this._urlField.selectAll();
					}

					if (e.getButton() == MouseButton.SECONDARY) {
						// Right mouse button released, so copy the selection (i.e. don't remove the original)
						item = DisplayController.getCurrentFrame().createNewText(JfxChatLLMBrowser.this._urlField.getSelectedText());
						StandardGestureActions.pickup(item);
					} else if (e.getButton() == MouseButton.MIDDLE) {
						// Middle mouse button released, so copy the selection then remove it from the URL field
						item = DisplayController.getCurrentFrame().createNewText(JfxChatLLMBrowser.this._urlField.getSelectedText());
						JfxChatLLMBrowser.this._urlField.setText(
								JfxChatLLMBrowser.this._urlField.getText().substring(0, JfxChatLLMBrowser.this._urlField.getSelection().getStart())
								+ JfxChatLLMBrowser.this._urlField.getText().substring(JfxChatLLMBrowser.this._urlField.getSelection().getEnd(),
										JfxChatLLMBrowser.this._urlField.getText().length()));

						StandardGestureActions.pickup(item);
					}
                }
			});
			
			this._urlField.setOnMousePressed(new EventHandler<MouseEvent>() {
				@Override
                public void handle(MouseEvent e) {
					JfxChatLLMBrowser.this._buttonDownId = e.getButton();
				}
			});
*/
			
			this._webEngine.getLoadWorker().stateProperty().addListener(new ChangeListener<State>() {

				@Override
				public void changed(ObservableValue<? extends State> ov, State oldState, State newState) {

					switch (newState) {
					case READY: // READY
						// MessageBay.displayMessage("WebEngine ready");
						break;
					case SCHEDULED: // SCHEDULED
						// MessageBay.displayMessage("Scheduled page load");
						break;
					case RUNNING: // RUNNING
						System.out.println("Loading page!");

						/*
						// Updating the URL bar to display the URL of the page being loaded
						JfxChatLLMBrowser.this._urlField.setText(JfxChatLLMBrowser.this._webEngine.getLocation());
						
						// Removing the style from the progress bar that causes it to hide
						JfxChatLLMBrowser.this._urlProgressBar.getStyleClass().remove("completed");
*/
						
						JfxChatLLMBrowser.this._stopButton.setVisible(true);
						//JfxChatLLMBrowser.this._goButton.setVisible(false);
						
						/*
						if (JfxChatLLMBrowser.this._webEngine.getHistory().getCurrentIndex() + 1 >= JfxChatLLMBrowser.this._webEngine.getHistory().getEntries().size()) {
							JfxChatLLMBrowser.this._forwardButton.setDisable(true);
						} else {
							JfxChatLLMBrowser.this._forwardButton.setDisable(false);
						}
*/
						
						// Unless the history is empty (i.e. this is the first page being loaded), enable the back button.
						// The only time the back button should be disbaled is on the first page load (which this statement deals with)
						// and if the user has just hit the back button taking them to the first page in the history (dealt with in the
						// navigateBack method)
						/*
						if (JfxChatLLMBrowser.this._webEngine.getHistory().getEntries().size() > 0) {
							JfxChatLLMBrowser.this._backButton.setDisable(false);
						}
*/
						
						JfxChatLLMBrowser.this._convertButton.setDisable(true);
						//JfxChatLLMBrowser.this._readableModeButton.setDisable(true);

						break;
					case SUCCEEDED: // SUCCEEDED
						MessageBay.displayMessage("Finished loading page");
						//JfxChatLLMBrowser.this._urlProgressBar.getStyleClass().add("completed");
						/*
						if(JfxChatLLMBrowser.this._readableModeButton.isSelected()) {
							JfxChatLLMBrowser.this.enableReadableMode();
						}
*/
						
					case CANCELLED: // CANCELLED
						JfxChatLLMBrowser.this._convertButton.setDisable(false);
						//JfxChatLLMBrowser.this._readableModeButton.setDisable(false);
						JfxChatLLMBrowser.this._stopButton.setVisible(false);
						//JfxChatLLMBrowser.this._goButton.setVisible(true);
						break;
					case FAILED: // FAILED
						MessageBay.displayMessage("Failed to load page");
						JfxChatLLMBrowser.this._stopButton.setVisible(false);
						//JfxChatLLMBrowser.this._goButton.setVisible(true);
						break;
					}
				}
			});
			
			// Captures mouse click events on webview to enable expeditee like behavior for JavaFX browser.
			this._webView.setOnMouseClicked(new EventHandler<javafx.scene.input.MouseEvent>() {
				@Override
				public void handle(javafx.scene.input.MouseEvent e) {
					if(e.getButton() == MouseButton.SECONDARY) {
						// Gets text currently selected in webview
						String selection = (String) JfxChatLLMBrowser.this._webEngine.executeScript("window.getSelection().toString()");

						// If no text is selected, see if an image is under the cursor
						if (selection.length() == 0) {
							JSObject window = (JSObject) JfxChatLLMBrowser.this._webEngine.executeScript("window");
							Object o = JfxChatLLMBrowser.this._webEngine.executeScript("document.elementFromPoint(" + e.getX() + "," + e.getY() + ");");
							
							if(o instanceof org.w3c.dom.Node) {
								org.w3c.dom.Node node = (org.w3c.dom.Node) o;
								JSObject style = (JSObject) window.call("getComputedStyle", node);
								
								if(node.getNodeName().toLowerCase().equals("img") ||
										((String) style.call("getPropertyValue", "background-image")).startsWith("url")) {
    								
									try {
    									JSObject bounds = (JSObject) ((JSObject) node).call("getBoundingClientRect", new Object[] {});
    									float width = Float.valueOf(bounds.getMember("width").toString());
    									float height = Float.valueOf(bounds.getMember("height").toString());
    									
    									Picture pic;
    									
    									if (((String) style.call("getPropertyValue", new Object[] { "background-image" })).startsWith("url(")) {
    										pic = WebParser.getBackgroundImageFromNode(node, style, DisplayController.getCurrentFrame(), null,
    											(float) DisplayController.getMouseX(), (float) DisplayController.getMouseY(), width, height);
    										
    									} else {
    										String imgSrc;
    										if(node.getNodeName().toLowerCase().equals("img") && 
    												(imgSrc = ((JSObject) node).getMember("src").toString()) != null) {
    											pic = WebParser.getImageFromUrl(imgSrc, null, DisplayController.getCurrentFrame(),
    													(float) DisplayController.getMouseX(), (float) DisplayController.getMouseY(), (int) width, null, null, null, null, null, 0, 0);
    										} else {
    											return;
    										}
    									}
    									
    									String linkUrl;
    									
    									// Check the image and its immediate parent for links
    									if ((node.getNodeName().toLowerCase().equals("a") && (linkUrl = (String) ((JSObject)node).getMember("href")) != null)
    											|| (node.getParentNode().getNodeName().toLowerCase().equals("a") && (linkUrl = (String)((JSObject)node.getParentNode()).getMember("href")) != null)) {
	    									
    										if(hasValidProtocol(linkUrl)) {
	    										pic.getSource().setAction("createFrameWithBrowser "  + linkUrl);
	    									}
    									}
    									
    									pic.setXY(DisplayController.getMouseX(), DisplayController.getMouseY());
                                		StandardGestureActions.pickup(pic);
                            		} catch (Exception e1) {
    	                                // TODO Auto-generated catch block
    	                                e1.printStackTrace();
                                    }
									
								} else if(node.getNodeName().toLowerCase().equals("video")) {
									String src = ((JSObject)node).getMember("src").toString();
									if(src == null || src.trim().length() == 0) {
										NodeList children = node.getChildNodes();
										for(int i = 0; i < children.getLength(); i++) {
											org.w3c.dom.Node child = children.item(i);
											if(child.getNodeName().toLowerCase().equals("source")) {
												src = ((JSObject)child).getMember("src").toString();
												if(src != null && src.trim().length() > 0) {
													break;
												}
											}
											
										}
										if(src == null || src.trim().length() == 0) {
											return;
										}
									}
									Text t = new Text("@iw: org.expeditee.items.widgets.jfxmedia "
											+ ((JSObject)node).getMember("width")
											+ ((JSObject)node).getMember("height")
											+ ":" + src);
									t.setParent(DisplayController.getCurrentFrame());
									t.setXY(DisplayController.getMouseX(), DisplayController.getMouseY());
									JfxMedia media = new JfxMedia(t, new String[] { src });
									StandardGestureActions.pickup(media.getItems());
									
								} else if(node.getNodeName().toLowerCase().equals("a") && ((JSObject)node).getMember("href") != null) {
									// If a link is right clicked, copy the text content and give it an action to create 
									// a new frame containing a browser pointing to the linked page
									Text t = DisplayController.getCurrentFrame().createNewText(((String) ((JSObject)node).getMember("textContent")).trim());
									t.addAction("createFrameWithBrowser " + (String) ((JSObject)node).getMember("href"));
									t.setXY(DisplayController.getMouseX(), DisplayController.getMouseY());
									StandardGestureActions.pickup(t);
								}
							}
						} else {
							// Copy text and attach to cursor
							Text t = DisplayController.getCurrentFrame().createNewText(selection);
							t.setXY(DisplayController.getMouseX(), DisplayController.getMouseY());
							StandardGestureActions.pickup(t);
						}
					}
				}
			});

		
			llmStart(prompt);
			
		} 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(JfxChatLLMBrowser.this, JfxChatLLMBrowser.this._webEngine, JfxChatLLMBrowser.this._webView, DisplayController.getCurrentFrame());
				}
			}).start();
		} catch (Exception e) {
			e.printStackTrace();
			this._parserRunning = false;
		}
	}
*/
	
	/**
	 * Used to drop text items onto JfxBrowser widget. Does nothing if a text item is not attached to cursor. <br>
	 * "back" -> navigates back a page in browser's session history <br>
	 * "forward" -> navigates forward a page in browser's session history <br>
	 * "refresh" -> reloads current page <br>
	 * "getFrame" -> attempts to parse page into an expeditee frame <br>
	 * url -> all other text is assumed to be a url which browser attempts to navigate to
	 * 
	 * @return Whether a JfxBrowser specific event is run.
	 * 
	 */
	@Override
	public boolean ItemsLeftClickDropped() {
		Text carried = null;
		if ((carried = FreeItems.getTextAttachedToCursor()) == null) { // fails if no text is attached to cursor.
			return false;
		}
		/*
		if (carried.getText().toLowerCase().equals(BACK)) {
			navigateBack();
		} 
		else if (carried.getText().toLowerCase().equals(FORWARD)) {
			navigateForward();
		} 
		else if (carried.getText().toLowerCase().equals(REFRESH)) {
			refresh();
		} 
		else*/ 
		if (carried.getText().toLowerCase().equals(CONVERT)) {
			//System.err.println("Need to implement 'convert' for JfxChatLLMBrowser");
			getFrame();
		} /*else {
			String text = carried.getText().trim();
			this.navigate(text);
			FreeItems.getInstance().clear();
		}*/

		return true;
	}

	/**
	 * Used to enable expeditee like text-widget interaction for middle mouse clicks. Does nothing if a text item is not attached to cursor.
	 * @return false if a text-widget interaction did not occur, true if a text-widget interaction did occur.
	 */
	@Override
	public boolean ItemsMiddleClickDropped() {
		if(ItemsRightClickDropped()) {
			FreeItems.getInstance().clear();						// removed held text item - like normal expeditee middle click behaviour.
			return true;
		}
		return false;
	}
	
	/**
	 * Used to enable expeditee like text-widget interaction for right mouse clicks. Does nothing if a text item is not attached to cursor.
	 * @return false if a text-widget interaction did not occur, true if a text-widget interaction did occur.
	 */
	/*
	@Override
	public boolean ItemsRightClickDropped() {
		Text t = null;
		if((t = FreeItems.getTextAttachedToCursor()) == null) {	// fails if no text item is attached to the cursor.
			return false;
		}

		final int x = DisplayController.getMouseX() - this.getX(), y = DisplayController.getMouseY() - this.getY();
		if(!this._urlField.getBoundsInParent().contains(x, y)) {
			// fails if not clicking on urlField
			return false;
		}

		final String insert = t.getText();
		Platform.runLater(new Runnable() {
			@Override
			public void run() {
				// Inserts text in text item into urlField at the position of the mouse.
        		String s = JfxChatLLMBrowser.this._urlField.getText();
        		int index = getCaretFromCoord(JfxChatLLMBrowser.this._urlField, getMouseEventForPosition(JfxChatLLMBrowser.this._backupEvent, JfxChatLLMBrowser.this._urlField, x, y));
        		if(index < s.length()) {
        			s = s.substring(0, index) + insert + s.substring(index);
        		} else {
        			s = s + insert;
        		}
        		JfxChatLLMBrowser.this._urlField.setText(s);
			}
		});

		return true;
	}
	*/
	
	

	@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;
	}
	

	
	    //public void start(Stage stage) {
	    	public void llmStart(String prompt) {
	        // Initialize Markdown Parser
	        MutableDataSet options = new MutableDataSet();
	        _parser = Parser.builder(options).build();
	        _renderer = HtmlRenderer.builder(options).build();

	        /*
	        // Setup UI
	        _webView = new WebView();
	        VBox root = new VBox(_webView);
	        stage.setScene(new Scene(root, 900, 700));
	        stage.setTitle("Professional Gemini Streaming Client");
	        stage.show();
*/
	        
	        // Initialize Skeleton and Bridge
	        setupWebViewBridge();
	        loadSkeleton();

	        // Hard-wired example prompt
	        startStreaming(prompt);
	    }

	    private void loadSkeletonDarkTheme()
	    {
	        String skeleton = "<html><head>" +
	                "<link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css'>" +
	                "<script src='https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js'></script>" +
	                "<style>" +
	                "  body { font-family: 'Inter', sans-serif; background: #0d1117; color: #c9d1d9; line-height: 1.6; padding: 25px; }" +
	                "  pre { background: #161b22; border-radius: 8px; padding: 16px; position: relative; border: 1px solid #30363d; overflow: auto; }" +
	                "  .copy-btn { position: absolute; top: 8px; right: 8px; background: #21262d; color: #8b949e; border: 1px solid #30363d; padding: 4px 8px; cursor: pointer; border-radius: 6px; font-size: 11px; opacity: 0; transition: 0.2s; }" +
	                "  pre:hover .copy-btn { opacity: 1; }" +
	                "  .copy-btn:hover { background: #30363d; color: #fff; }" +
	                "  code { font-family: 'Fira Code', monospace; }" +
	                "</style>" +
	                "<script>" +
	                "  function updateView(html) {" +
	                "    document.getElementById('content').innerHTML = html;" +
	                "    document.querySelectorAll('pre').forEach(block => {" +
	                "       if (!block.querySelector('.copy-btn')) {" +
	                "           const btn = document.createElement('button'); btn.className='copy-btn'; btn.innerText='Copy';" +
	                "           btn.onclick = () => { javaApp.copy(block.querySelector('code').innerText); btn.innerText='Copied!'; setTimeout(()=>btn.innerText='Copy', 2000); };" +
	                "           block.appendChild(btn);" +
	                "       }" +
	                "    });" +
	                "    hljs.highlightAll();" +
	                "    window.scrollTo(0, document.body.scrollHeight);" +
	                "  }" +
	                "</script></head>" +
	                "<body><div id='content'><i>Waiting for response...</i></div></body></html>";
	        
	        _webView.getEngine().loadContent(skeleton);
	    }

	    
	    
	    private void loadSkeleton()
	    {
	        String skeleton =
	            "<html><head>" +
	            // Light highlight.js theme (GitHub light)
	            "<link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css'>" +
	            "<script src='https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js'></script>" +

	            "<style>" +
	            "  :root {" +
	            "    --bg: #ffffff;" +
	            "    --text: #0f172a;" +          // slate-900-ish
	            "    --muted: #475569;" +         // slate-600-ish
	            "    --border: #e2e8f0;" +        // slate-200-ish
	            "    --code-bg: #f6f8fa;" +       // GitHub-ish code bg
	            "    --pre-border: #d0d7de;" +    // GitHub-ish border
	            "    --btn-bg: #ffffff;" +
	            "    --btn-text: #334155;" +
	            "    --btn-border: #cbd5e1;" +
	            "    --btn-hover-bg: #f1f5f9;" +
	            "  }" +

	            "  body {" +
	            "    font-family: 'Inter', system-ui, -apple-system, Segoe UI, Roboto, sans-serif;" +
	            "    background: var(--bg);" +
	            "    color: var(--text);" +
	            "    line-height: 1.65;" +
	            "    padding: 24px;" +
	            "  }" +

	            "  #content { max-width: 900px; }" +

	            "  p { margin: 0 0 12px 0; }" +
	            "  i { color: var(--muted); }" +

	            "  pre {" +
	            "    background: var(--code-bg);" +
	            "    border-radius: 10px;" +
	            "    padding: 14px 14px;" +
	            "    position: relative;" +
	            "    border: 1px solid var(--pre-border);" +
	            "    overflow: auto;" +
	            "    margin: 14px 0;" +
	            "  }" +

	            "  code {" +
	            "    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;" +
	            "    font-size: 13px;" +
	            "  }" +

	            "  /* Inline code */" +
	            "  p code, li code {" +
	            "    background: var(--code-bg);" +
	            "    border: 1px solid var(--border);" +
	            "    border-radius: 6px;" +
	            "    padding: 2px 6px;" +
	            "  }" +

	            "  .copy-btn {" +
	            "    position: absolute;" +
	            "    top: 10px;" +
	            "    right: 10px;" +
	            "    background: var(--btn-bg);" +
	            "    color: var(--btn-text);" +
	            "    border: 1px solid var(--btn-border);" +
	            "    padding: 4px 8px;" +
	            "    cursor: pointer;" +
	            "    border-radius: 8px;" +
	            "    font-size: 11px;" +
	            "    opacity: 0;" +
	            "    transition: opacity 0.15s ease, background 0.15s ease;" +
	            "  }" +
	            "  pre:hover .copy-btn { opacity: 1; }" +
	            "  .copy-btn:hover { background: var(--btn-hover-bg); }" +

	            "  /* Optional: headings / lists if your HTML includes them */" +
	            "  h1,h2,h3 { margin: 18px 0 10px 0; line-height: 1.25; }" +
	            "  ul,ol { margin: 0 0 12px 22px; }" +
	            "  li { margin: 6px 0; }" +
	            "  a { color: #2563eb; text-decoration: none; }" +
	            "  a:hover { text-decoration: underline; }" +
	            "</style>" +

	            "<script>" +
	            "  function updateView(html) {" +
	            "    document.getElementById('content').innerHTML = html;" +
	            "    document.querySelectorAll('pre').forEach(block => {" +
	            "       if (!block.querySelector('.copy-btn')) {" +
	            "           const btn = document.createElement('button');" +
	            "           btn.className='copy-btn';" +
	            "           btn.innerText='Copy';" +
	            "           btn.onclick = () => {" +
	            "             const code = block.querySelector('code');" +
	            "             const txt = code ? code.innerText : block.innerText;" +
	            "             javaApp.copy(txt);" +
	            "             btn.innerText='Copied!';" +
	            "             setTimeout(()=>btn.innerText='Copy', 2000);" +
	            "           };" +
	            "           block.appendChild(btn);" +
	            "       }" +
	            "    });" +
	            "    hljs.highlightAll();" +
	            "    window.scrollTo(0, document.body.scrollHeight);" +
	            "  }" +
	            "</script></head>" +
	            "<body><div id='content'><i>Waiting for response...</i></div></body></html>";

	        _webView.getEngine().loadContent(skeleton);
	    }
	    
	    private void setupWebViewBridge()
	    {
	        _webView.getEngine().getLoadWorker().stateProperty().addListener((obs, old, state) -> {
	            if (state == javafx.concurrent.Worker.State.SUCCEEDED) {
	                JSObject window = (JSObject) _webView.getEngine().executeScript("window");
	                window.setMember("javaApp", new Object() {
	                    public void copy(String text) {
	                        ClipboardContent content = new ClipboardContent();
	                        content.putString(text);
	                        Clipboard.getSystemClipboard().setContent(content);
	                    }
	                });
	            }
	        });
	    }

	    private void startStreaming(String prompt)
	    {
	    	 	// Add user message to history
	        _history.add(UserMessage.from(prompt));

	        // Reset per-turn buffer
	        _fullResponse.setLength(0);
	        
	        StreamingChatModel model = _geminiStreamingModel;
	        
	        Frame frame = DisplayController.getCurrentFrame();
	        if (frame.hasAnnotation("llmModel")) {
	        		String llm_model_value = frame.getAnnotationValue("llmModel");
	        		if (llm_model_value.toLowerCase().equals("openai")) {
	        			model = this._openaiStreamingModel;
	        		}
	        }
	        
	        model.chat(_history, new StreamingChatResponseHandler() {
	            @Override
	            public void onPartialResponse(String token) {
	                _fullResponse.append(token);
	                updateUI();
	            }

	            @Override 
	            public void onCompleteResponse(ChatResponse response) {
	                String final_assistant_response = _fullResponse.toString();
	                _history.add(AiMessage.from(final_assistant_response));
	            }
	            
	            @Override 
	            public void onError(Throwable error) { 
	            	error.printStackTrace(); 
	            }
	        });
	    }

	    private void updateUI()
	    {
	        Platform.runLater(() -> {
	            String html = _renderer.render(_parser.parse(_fullResponse.toString()));
	            // Escape for JavaScript transmission
	            String escapedHtml = html.replace("\\", "\\\\").replace("'", "\\'").replace("\n", "\\n").replace("\r", "");
	            _webView.getEngine().executeScript("updateView('" + escapedHtml + "')");
	        });
	    }

	
	
}
