package com.imyeyu.fx.ui.components;

import com.imyeyu.fx.icon.TimiFXIcon;
import com.imyeyu.fx.icon.TimiIcon;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.fx.ui.components.popup.PopupTipsService;
import com.imyeyu.fx.utils.grid.Column;
import com.imyeyu.fx.utils.layout.Layout;
import com.imyeyu.fx.utils.scroll.SmoothScroll;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.Callback;
import com.imyeyu.java.ref.Ref;
import com.sun.javafx.scene.control.skin.Utils;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableMap;
import javafx.event.Event;
import javafx.geometry.Insets;
import javafx.geometry.NodeOrientation;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.control.Button;
import javafx.scene.control.IndexRange;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Skin;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.control.TextInputControl;
import javafx.scene.control.skin.TextAreaSkin;
import javafx.scene.control.skin.TextFieldSkin;
import javafx.scene.control.skin.TextInputControlSkin;
import javafx.scene.input.ContextMenuEvent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Path;
import javafx.scene.text.Text;
import lombok.Getter;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * 复杂文本域编辑器，显示按钮操作文本域，文本域显示行号，可自定义操作功能
 *
 * @author 夜雨
 * @since 2022-07-11 15:47
 */
public class TextAreaEditor extends TextArea implements TimiFXUI {

	private static final String STYLE_CLASS = "text-area-editor";

	/** 控制区面板，此面板在 */
	@Getter
	protected final HBox ctrl;

	/** 顶部控制区面板 */
	@Getter
	protected final BorderPane header;

	/** 撤销按钮 */
	@Getter
	protected final IconButton undo;

	/** 重做按钮 */
	@Getter
	protected final IconButton redo;

	/** 复制按钮 */
	@Getter
	protected final IconButton copy;

	/** 剪切按钮 */
	@Getter
	protected final IconButton cut;

	/** 粘贴按钮 */
	@Getter
	protected final IconButton paste;

	/** 换行按钮 */
	@Getter
	protected final ToggleIcon wrap;

	/** 显示行号 */
	protected final BooleanProperty showLineNumber;

	/** 显示查找面板 */
	protected BooleanProperty visibleFindPaneProperty;

	/** 发生换行的文本节点 Map&lt;行号, 换行次数&gt; */
	private final Map<Integer, Integer> wraps;

	/** 行号组件 */
	private LineNumber lineNumber;

	/** 根布局 */
	private final BorderPane root;

	/** 默认构造 */
	public TextAreaEditor() {
		wraps = new HashMap<>();
		showLineNumber = new SimpleBooleanProperty(true);

		// 撤销
		undo = new IconButton(TimiFXIcon.get(TimiIcon.ARROW_0_W)).withBackground();
		undo.setBorder(Stroke.RIGHT);
		undo.disableProperty().bind(undoableProperty().not());
		PopupTipsService.installText(undo, TimiFXUI.MULTILINGUAL.text("undo"));

		// 重做
		redo = new IconButton(TimiFXIcon.get(TimiIcon.ARROW_0_E)).withBackground();
		redo.setBorder(Stroke.RIGHT);
		redo.disableProperty().bind(redoableProperty().not());
		PopupTipsService.installText(redo, TimiFXUI.MULTILINGUAL.text("redo"));

		// 复制
		copy = new IconButton(TimiFXIcon.get(TimiIcon.COPY)).withBackground();
		copy.setBorder(Stroke.RIGHT);
		PopupTipsService.installText(copy, TimiFXUI.MULTILINGUAL.text("copy"));

		// 剪切
		cut = new IconButton(TimiFXIcon.get(TimiIcon.CUT)).withBackground();
		cut.setBorder(Stroke.RIGHT);
		cut.disableProperty().bind(editableProperty().not());
		PopupTipsService.installText(cut, TimiFXUI.MULTILINGUAL.text("cut"));

		// 粘贴
		paste = new IconButton(TimiFXIcon.get(TimiIcon.PASTE)).withBackground();
		paste.setBorder(Stroke.RIGHT);
		paste.disableProperty().bind(editableProperty().not());
		PopupTipsService.installText(paste, TimiFXUI.MULTILINGUAL.text("paste"));

		// 换行
		wrap = new ToggleIcon(TimiFXIcon.get(TimiIcon.WRAP));
		wrap.setBorder(Stroke.RIGHT);
		PopupTipsService.installText(wrap, TimiFXUI.MULTILINGUAL.text("wrap"));

		ctrl = new HBox();
		ctrl.setPickOnBounds(false);
		ctrl.getChildren().addAll(undo, redo, copy, cut, paste, wrap);

		header = new BorderPane();
		header.setCenter(ctrl);
		header.setPickOnBounds(false);

		// 顶部背景（阻止触发文本域选择）
		Button headerBackground = new Button(" ");
		headerBackground.getStyleClass().add(CSS.BG_TP);
		headerBackground.setBorder(Border.EMPTY);
		headerBackground.setBackground(BG.DEFAULT);

		AnchorPane headerPane = new AnchorPane();
		Layout.anchor.fill(header);
		Layout.anchor.fill(headerBackground);
		headerPane.getChildren().setAll(headerBackground, header);
		headerPane.setBorder(Stroke.BOTTOM);

		// 搜索
		FindPane findPane = new FindPane(this);
		visibleFindPaneProperty = findPane.visibleProperty();
		findPane.managedProperty().bind(visibleFindPaneProperty);
		findPane.setVisible(false);

		root = new BorderPane();
		root.setTop(new VBox() {{
			getChildren().addAll(headerPane, findPane);
		}});

		getStyleClass().add(STYLE_CLASS);

		// ---------- 事件 ----------

		undo.setOnAction(e -> undo());
		redo.setOnAction(e -> redo());
		copy.setOnAction(e -> copy());
		cut.setOnAction(e -> cut());
		paste.setOnAction(e -> paste());
		wrapTextProperty().bindBidirectional(wrap.selectedProperty());

		// 键盘事件
		addEventFilter(KeyEvent.KEY_RELEASED, e -> {
			boolean control = e.isControlDown();
			boolean shift = e.isShiftDown();
			boolean alt = e.isAltDown();
			KeyCode code = e.getCode();

			if (control && shift && !alt) {
				switch (code) {
					case ENTER -> {
						// 向上开新行
						int start = getText().lastIndexOf("\n", getCaretPosition() - 1);
						if (start == -1) {
							insertText(0, "\n");
							positionCaret(0);
						} else {
							insertText(start, "\n");
							positionCaret(start + 1);
						}
					}
					case U -> {
						// 切换大小写
						selectPreviousWord();
						positionCaret(getSelection().getStart());
						selectEndOfNextWord();

						IndexRange range = getSelection();
						String text = getSelectedText();
						if (65 <= text.charAt(0) && text.charAt(0) <= 90) {
							// 当前大小
							replaceSelection(text.toLowerCase());
						} else {
							// 当前小写
							replaceSelection(text.toUpperCase());
						}
						selectRange(range.getStart(), range.getEnd());
					}
				}
				return;
			}
			if (control && !shift && !alt) {
				switch (code) {
					case F -> {
						// 打开查找面板
						findPane.setVisible(!findPane.isVisible());
						findPane.keyword.setText(getSelectedText());
					}
					case D -> {
						// 删除聚焦行
						int start = getText().lastIndexOf("\n", getCaretPosition() - 1);
						start = Math.max(start, 0);
						int end = getText().indexOf("\n", getCaretPosition());
						end = end < 0 ? getText().length() : end;

						deleteText(start, end);
					}
				}
				return;
			}
			if (!control && shift && !alt && code == KeyCode.ENTER) {
				// 向下开新行
				int end = getText().indexOf("\n", getCaretPosition());
				end = end < 0 ? getText().length() : end;

				insertText(end, "\n");
				return;
			}
			if (!control && !shift && !alt && code == KeyCode.ESCAPE) {
				// 隐藏查找面板
				findPane.setVisible(false);
			}
		});
	}

	@Override
	public String getUserAgentStylesheet() {
		return Objects.requireNonNull(TextAreaEditor.class.getResource(RESOURCE + "style/components/text-area-editor.css")).toExternalForm();
	}

	@Override
	protected Skin<?> createDefaultSkin() {
		TextAreaEditSkin skin = new TextAreaEditSkin(this);
		try {
			// 嵌入面板
			ScrollPane scrollPane = Ref.getClassFieldValue(skin, TextAreaSkin.class, "scrollPane", ScrollPane.class);
			scrollPane.getStyleClass().add(CSS.SP_BORDER);
			// 锐化光标
			Path caret = Ref.getClassFieldValue(skin, TextInputControlSkin.class, "caretPath", Path.class);
			caret.setSmooth(false);
			// 插入行号
			double fontSize = skin.getSkinnable().getFont().getSize() + 2;
			lineNumber = new LineNumber(scrollPane, fontSize);
			lineNumber.visibleProperty().bind(showLineNumber);
			lineNumber.managedProperty().bind(showLineNumber);

			// 内容监听，计算行号
			Group paragraphNodes = Ref.getClassFieldValue(skin, TextAreaSkin.class, "paragraphNodes", Group.class);
			Callback lineNumberParser = () -> {
				wraps.clear();
				if (isWrapText() && paragraphNodes.getChildren().getFirst() instanceof Text text) {
					// 计算段落被渲染换行数
					double wrappingWidth = scrollPane.getWidth() - 12;
					for (int i = 0, l = getParagraphs().size(); i < l; i++) {
						int wrap = (int) (Utils.computeTextHeight(getFont(), getParagraphs().get(i).toString(), wrappingWidth, text.getBoundsType()) / fontSize);
						if (wrap != 1) {
							wraps.put(i, wrap - 1);
						}
					}
				}
				lineNumber.render(getParagraphs().size());
			};
			getParagraphs().addListener((ListChangeListener<CharSequence>) c -> {
				if (c.next()) {
					lineNumberParser.handler();
				}
			});
			wrapTextProperty().addListener((obs, o, n) -> lineNumberParser.handler());
			widthProperty().addListener((obs, o, n) -> lineNumberParser.handler());
			// 适应底部滚动条
			scrollPane.skinProperty().addListener((obs, o, spSkin) -> {
				try {
					ScrollBar hsb = Ref.getFieldValue(spSkin, "hsb", ScrollBar.class);
					lineNumber.scrollPane.paddingProperty().bind(Bindings.createObjectBinding(() -> {
						if (hsb.isVisible()) {
							double height = Ref.getFieldValue(spSkin, "hsbHeight", Double.class);
							return new Insets(0, 0, height, 0);
						} else {
							return Insets.EMPTY;
						}
					}, hsb.visibleProperty()));
				} catch (IllegalAccessException e) {
					throw new RuntimeException(e);
				}
			});

			// 平滑滚动
			SmoothScroll.pane.install(scrollPane);

			root.setLeft(lineNumber);
			root.setCenter(scrollPane);
			skin.getChildren().setAll(root);

			lineNumber.render(getParagraphs().size());
		} catch (NoSuchFieldException | IllegalAccessException e) {
			throw new RuntimeException(e);
		}
		return skin;
	}

	/**
	 * 获取是否显示行号
	 *
	 * @return true 为显示行号
	 */
	public boolean isShowLineNumber() {
		return showLineNumber.get();
	}

	/**
	 * 设置是否显示行号
	 *
	 * @param showLineNumber true 为显示行号
	 */
	public void setShowLineNumber(boolean showLineNumber) {
		this.showLineNumber.set(showLineNumber);
	}

	/**
	 * 获取是否显示行号监听
	 *
	 * @return 显示行号监听
	 */
	public BooleanProperty showLineNumberProperty() {
		return showLineNumber;
	}

	/**
	 * 获取是否显示查找面板
	 *
	 * @return true 为显示查找面板
	 */
	public boolean isVisibleFindPane() {
		return visibleFindPaneProperty.get();
	}

	/**
	 * 设置是否显示查找面板
	 *
	 * @param visibleFindPane true 为显示查找面板
	 */
	public void setVisibleFindPane(boolean visibleFindPane) {
		this.visibleFindPaneProperty.set(visibleFindPane);
	}

	/**
	 * 获取是否显示查找面板监听
	 *
	 * @return 显示查找面板监听
	 */
	public BooleanProperty visibleFindPaneProperty() {
		return visibleFindPaneProperty;
	}

	/**
	 * 字符串查找面板
	 *
	 * @author 夜雨
	 * @since 2022-08-26 18:01
	 */
	private class FindPane extends GridPane {

		final Callback fetch;
		final Label result;
		final TextField keyword;

		// Map<下标, 起始选择>
		private final ObservableMap<Integer, Integer> selects;

		// 当前查找结果选中下标
		private int nearestI;

		private ToggleIcon toggleCase;

		public FindPane(TextInputControl inputControl) {
			selects = FXCollections.observableHashMap();

			// 查找
			Text icon = TimiFXIcon.get(TimiIcon.MAGNIFIER);
			keyword = new TextField() {

				@Override
				protected Skin<?> createDefaultSkin() {
					Skin<?> skin = super.createDefaultSkin();
					if (skin instanceof TextFieldSkin textFieldSkin) {
						try {

							// 输入
							Pane textGroup = Ref.getFieldValue(textFieldSkin, "textGroup", Pane.class);
							textGroup.setTranslateY(-.5);

							// 匹配大小写
							toggleCase = new ToggleIcon(TimiFXIcon.get(TimiIcon.FONT_SIZE));
							toggleCase.setBorder(Stroke.LEFT);
							toggleCase.setCursor(Cursor.DEFAULT);
							toggleCase.setFocusTraversable(false);
							toggleCase.selectedProperty().addListener((obs, o, n) -> fetch.handler());

							BorderPane root = new BorderPane();
							root.setCenter(textGroup);
							root.setRight(new HBox(toggleCase));

							textFieldSkin.getChildren().setAll(root);

						} catch (IllegalAccessException e) {
							throw new RuntimeException(e);
						}
					}
					return skin;
				}
			};
			keyword.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
			keyword.getStyleClass().addAll("find-field", CSS.PADDING_N);
			keyword.setBorder(Stroke.RIGHT);

			// 查找结果
			result = TimiFXUI.label(" ");
			result.setPadding(new Insets(0, 4, 0, 4));
			IconButton prev = new IconButton(TimiFXIcon.get(TimiIcon.ARROW_0_N)).withBackground();
			prev.setBorder(Stroke.RIGHT);
			prev.setFocusTraversable(false);
			IconButton next = new IconButton(TimiFXIcon.get(TimiIcon.ARROW_0_S)).withBackground();
			next.setBorder(Stroke.RIGHT);
			next.setFocusTraversable(false);

			// 关闭
			IconButton close = new IconButton(TimiFXIcon.get(TimiIcon.FAIL)).withBackground();
			close.setBorder(Stroke.LEFT);
			close.setFocusTraversable(false);

			// 替换值
			TextField replaceValue = new TextField();
			replaceValue.getStyleClass().add("replace-field");
			replaceValue.setBorder(Stroke.TR);
			replaceValue.disableProperty().bind(editableProperty().not());
			Button replace = new Button(TimiFXUI.MULTILINGUAL.text("replace"));
			replace.setBorder(Stroke.RIGHT);
			replace.setFocusTraversable(false);
			replace.disableProperty().bind(editableProperty().not().or(replaceValue.textProperty().isEmpty().or(Bindings.isEmpty(selects))));
			replace.addEventFilter(MouseEvent.MOUSE_DRAGGED, e -> {
				if (replaceValue.isDisabled()) {
					e.consume();
				}
			});
			Button replaceAll = new Button(TimiFXUI.MULTILINGUAL.text("replace_all"));
			replaceAll.setBorder(Stroke.RIGHT);
			replaceAll.setFocusTraversable(false);
			replaceAll.disableProperty().bind(replace.disabledProperty());

			getStyleClass().add("find-pane");
			getColumnConstraints().addAll(Column.builder().build(), Column.builder().width(260).build(), Column.valueFill());
			setBorder(Stroke.BOTTOM);
			setBackground(BG.DEFAULT);

			addRow(0, new StackPane() {{
				setPadding(new Insets(0, 2, 0, 2));
				setBackground(BG.WHITE);
				getChildren().add(icon);
			}}, keyword, new BorderPane() {{
				setCenter(new HBox() {{
					setAlignment(Pos.CENTER_LEFT);
					getChildren().addAll(prev, next, result);
				}});
				setRight(close);
			}});

			add(replaceValue, 0, 1, 2, 1);
			addRow(1, new HBox() {{
				setBorder(Stroke.TOP);
				getChildren().addAll(replace, replaceAll);
			}});

			// ---------- 事件 ----------

			// 忽略拖拽
			addEventFilter(MouseEvent.MOUSE_DRAGGED, e -> {
				if (!keyword.isFocused() && !replaceValue.isFocused()) {
					e.consume();
				}
			});

			// 刷新搜索结果
			Callback updateResult = () -> {
				if (TimiJava.isEmpty(selects) || nearestI == -1) {
					result.setText(" ");
				} else {
					inputControl.selectRange(selects.get(nearestI), selects.get(nearestI) + keyword.getLength());
					result.setText("%s / %s".formatted(nearestI + 1, selects.size()));
				}
			};

			// 搜索
			fetch = () -> {
				inputControl.deselect();
				selects.clear();
				if (TimiJava.isEmpty(keyword.getText())) {
					result.setText(" ");
				} else {
					String text, keywordValue;
					if (toggleCase.isSelected()) {
						text = inputControl.getText();
						keywordValue = keyword.getText();
					} else {
						text = inputControl.getText().toLowerCase();
						keywordValue = keyword.getText().toLowerCase();
					}
					int keywordLength = keywordValue.length();

					nearestI = -1;
					boolean isFoundNearestI = false;
					for (int i = 0, l = text.length(); i < l; i += keywordLength) {
						i = text.indexOf(keywordValue, i);
						if (i == -1) {
							break;
						} else {
							selects.put(selects.size(), i);
							if (!isFoundNearestI && inputControl.getCaretPosition() < i) {
								nearestI = selects.size() - 1;
								isFoundNearestI = true;
							}
						}
					}
					if (!isFoundNearestI && TimiJava.isNotEmpty(selects)) {
						nearestI = 0;
					}
					updateResult.handler();
				}
			};

			// 查找
			keyword.textProperty().addListener((obs, o, newKeyword) -> fetch.handler());

			// 更新查找
			inputControl.textProperty().addListener((obs, o, n) -> {
				if (TimiJava.isNotEmpty(keyword.getText()) && isVisible()) {
					fetch.handler();
				}
			});

			// 选中上一个
			prev.setOnAction(e -> {
				if (TimiJava.isNotEmpty(selects)) {
					nearestI--;
					if (nearestI < 0) {
						nearestI = selects.size() - 1;
					}
					updateResult.handler();
				}
			});

			// 选中下一个
			next.setOnAction(e -> {
				if (TimiJava.isNotEmpty(selects)) {
					nearestI++;
					if (selects.size() - 1 < nearestI) {
						nearestI = 0;
					}
					updateResult.handler();
				}
			});

			// 替换
			replace.setOnAction(e -> {
				if (inputControl.getSelectedText().equals(keyword.getText())) {
					inputControl.replaceSelection(replaceValue.getText());
				} else {
					int i = inputControl.getText().indexOf(keyword.getText(), inputControl.getCaretPosition());
					if (i == -1) {
						i = inputControl.getText().indexOf(keyword.getText());
					}
					if (i != -1) {
						inputControl.replaceText(i, i + keyword.getLength(), replaceValue.getText());
					}
					inputControl.positionCaret(i + replaceValue.getLength());
				}
				fetch.handler();
			});

			// 替换全部
			replaceAll.setOnAction(e -> {
				inputControl.setText(inputControl.getText().replace(keyword.getText(), replaceValue.getText()));
				fetch.handler();
			});

			// 关闭
			close.setOnAction(e -> setVisible(false));
		}
	}

	/**
	 * 行号文本域
	 *
	 * @author 夜雨
	 * @since 2022-07-12 13:05
	 */
	private class LineNumber extends TextArea {

		/** 行号滚动面板 */
		private ScrollPane scrollPane;

		/** 字号 */
		private final double fontSize;

		/** 所属文本域滚动面板 */
		private final ScrollPane ownerScrollPane;

		public LineNumber(ScrollPane ownerScrollPane, double fontSize) {
			this.fontSize = fontSize;
			this.ownerScrollPane = ownerScrollPane;
			this.ownerScrollPane.vvalueProperty().addListener((obs, o, newV) -> {
				// 不可单向绑定，TextArea 内部需要调度 setVvalue，也不可双向绑定，会造成滚动错位
				scrollPane.setVvalue(newV.doubleValue());
			});
			setBorder(Stroke.LEFT);
			setEditable(false);
			setNodeOrientation(NodeOrientation.RIGHT_TO_LEFT);
			addEventFilter(MouseEvent.ANY, Event::consume);
			addEventFilter(ContextMenuEvent.CONTEXT_MENU_REQUESTED, Event::consume);
		}

		/**
		 * 行号渲染
		 *
		 * @param size 行数
		 */
		private void render(int size) {
			clear();
			for (int i = 0; i < size; i++) {
				appendText(String.valueOf(i + 1));
				if (wraps.containsKey(i)) {
					// 发生换行
					appendText("\n".repeat(Math.max(0, wraps.get(i))));
				}
				if (i < size - 1) {
					appendText("\n");
				}
			}
			// 行号宽度
			int lineL = 0;
			for (long i = size; i != 0; i *= .01) {
				lineL++;
			}
			setPrefWidth(lineL * fontSize + 12);
		}

		@Override
		protected Skin<?> createDefaultSkin() {
			Skin<?> skin = super.createDefaultSkin();
			try {
				// 同步滚动
				scrollPane = Ref.getClassFieldValue(skin, TextAreaSkin.class, "scrollPane", ScrollPane.class);
				scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
				scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
				scrollPane.vvalueProperty().addListener((obs, o, n) -> scrollPane.setVvalue(ownerScrollPane.getVvalue()));
				scrollPane.getContent().setCursor(Cursor.DEFAULT);

				SmoothScroll.pane.install(scrollPane);
			} catch (NoSuchFieldException | IllegalAccessException e) {
				throw new RuntimeException(e);
			}
			return skin;
		}
	}

	/**
	 * 重写样式 layoutChildren，让滚动面板适应本组件的功能组件注入
	 *
	 * @author 夜雨
	 * @since 2022-07-11 16:56
	 */
	private static class TextAreaEditSkin extends TextAreaSkin {

		public TextAreaEditSkin(TextAreaEditor control) {
			super(control);
		}

		@Override
		protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
			getChildren().getFirst().resizeRelocate(contentX, contentY, contentWidth, contentHeight);
		}
	}
}
