package me.coley.recaf.ui.controls;

import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.control.*;
import me.coley.recaf.control.Controller;
import me.coley.recaf.control.gui.GuiController;
import me.coley.recaf.search.*;
import me.coley.recaf.ui.controls.tree.*;
import me.coley.recaf.util.Log;
import me.coley.recaf.workspace.Workspace;

import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import static me.coley.recaf.util.LangUtil.translate;

/**
 * Pane for displaying search query inputs & results.
 *
 * @author Matt
 */
@SuppressWarnings("unchecked")
public class SearchPane extends SplitPane {
	private final Map<String, Input> inputMap = new HashMap<>();
	private final TreeView tree = new TreeView();
	private final Runnable searchAction;


	/**
	 * @param controller
	 * 		Controller to act on.
	 * @param type
	 * 		Type of query.
	 */
	public SearchPane(GuiController controller, QueryType type) {
		setOrientation(Orientation.VERTICAL);
		setDividerPositions(0.5);
		tree.setCellFactory(e -> new JavaResourceCell());
		ColumnPane params = new ColumnPane();
		Button btn = new Button("Search");
		btn.getStyleClass().add("search-button");
		switch(type) {
			case MEMBER_DEFINITION:
				addInput(new Input<>(params, "ui.search.declaration.owner", "ui.search.declaration.owner.sub",
						NullableText::new, NullableText::get, NullableText::setText));
				addInput(new Input<>(params, "ui.search.declaration.name", "ui.search.declaration.name.sub",
						NullableText::new, NullableText::get, NullableText::setText));
				addInput(new Input<>(params, "ui.search.declaration.desc", "ui.search.declaration.desc.sub",
						NullableText::new, NullableText::get, NullableText::setText));
				addInput(new Input<>(params, "ui.search.matchmode", "ui.search.matchmode.sub", () -> {
					ComboBox<StringMatchMode> comboMode = new ComboBox<>();
					comboMode.getItems().setAll(StringMatchMode.values());
					comboMode.setValue(StringMatchMode.CONTAINS);
					return comboMode;
				}, ComboBoxBase::getValue, ComboBoxBase::setValue));
				searchAction = () -> search(controller, () -> buildDefinitionSearch(controller.getWorkspace()));
				btn.setOnAction(e -> search());
				break;
			case CLASS_REFERENCE:
				addInput(new Input<>(params, "ui.search.cls_reference.name", "ui.search.cls_reference.name.sub",
						NullableText::new, NullableText::get, NullableText::setText));
				addInput(new Input<>(params, "ui.search.matchmode", "ui.search.matchmode.sub", () -> {
					ComboBox<StringMatchMode> comboMode = new ComboBox<>();
					comboMode.getItems().setAll(StringMatchMode.values());
					comboMode.setValue(StringMatchMode.CONTAINS);
					return comboMode;
				}, ComboBoxBase::getValue, ComboBoxBase::setValue));
				searchAction = () -> search(controller, () -> buildClassReferenceSearch(controller.getWorkspace()));
				btn.setOnAction(e -> search());
				break;
			case MEMBER_REFERENCE:
				addInput(new Input<>(params, "ui.search.mem_reference.owner", "ui.search.mem_reference.owner.sub",
						NullableText::new, NullableText::get, NullableText::setText));
				addInput(new Input<>(params, "ui.search.mem_reference.name", "ui.search.mem_reference.name.sub",
						NullableText::new, NullableText::get, NullableText::setText));
				addInput(new Input<>(params, "ui.search.mem_reference.desc", "ui.search.mem_reference.desc.sub",
						NullableText::new, NullableText::get, NullableText::setText));
				addInput(new Input<>(params, "ui.search.matchmode", "ui.search.matchmode.sub", () -> {
					ComboBox<StringMatchMode> comboMode = new ComboBox<>();
					comboMode.getItems().setAll(StringMatchMode.values());
					comboMode.setValue(StringMatchMode.CONTAINS);
					return comboMode;
				}, ComboBoxBase::getValue, ComboBoxBase::setValue));
				searchAction = () -> search(controller, () -> buildMemberReferenceSearch(controller.getWorkspace()));
				btn.setOnAction(e -> search());
				break;
			case STRING:
				addInput(new Input<>(params, "ui.search.string", "ui.search.string.sub",
						TextField::new, TextField::getText, TextField::setText));
				addInput(new Input<>(params, "ui.search.matchmode", "ui.search.matchmode.sub", () -> {
					ComboBox<StringMatchMode> comboMode = new ComboBox<>();
					comboMode.getItems().setAll(StringMatchMode.values());
					comboMode.setValue(StringMatchMode.CONTAINS);
					return comboMode;
				}, ComboBoxBase::getValue, ComboBoxBase::setValue));
				searchAction = () -> search(controller, () -> buildStringSearch(controller.getWorkspace()));
				btn.setOnAction(e -> search());
				break;
			case VALUE:
				addInput(new Input<>(params, "ui.search.value", "ui.search.value.sub",
						NumericText::new, NumericText::get, (e, t) -> e.setText(t.toString())));
				searchAction = () -> {
					if(input("ui.search.value") == null)
						return;
					search(controller, () -> buildValueSearch(controller.getWorkspace()));
				};
				btn.setOnAction(e -> search());
				break;
			case INSTRUCTION_TEXT:
				addInput(new Input<>(params, "ui.search.insn.lines", "ui.search.insn.lines.sub",
						TextArea::new, t -> Arrays.asList(t.getText().split("[\n\r]")),
						(e, t) -> e.setText(String.join("\n", t))));
				addInput(new Input<>(params, "ui.search.matchmode", "ui.search.matchmode.sub", () -> {
					ComboBox<StringMatchMode> comboMode = new ComboBox<>();
					comboMode.getItems().setAll(StringMatchMode.values());
					comboMode.setValue(StringMatchMode.CONTAINS);
					return comboMode;
				}, ComboBoxBase::getValue, ComboBoxBase::setValue));
				searchAction = () -> search(controller, () -> buildInsnSearch(controller.getWorkspace()));
				btn.setOnAction(e -> search());
				break;
			default:
				searchAction = null;
				break;
		}
		PackageSelector selector = new PackageSelector(controller.windows());
		addInput(new Input<>(params, "ui.search.skippackages", "ui.search.skippackages.sub",
				() -> selector, PackageSelector::get, PackageSelector::set));
		params.add(null, btn);
		getItems().addAll(params, tree);
		SplitPane.setResizableWithParent(params, Boolean.FALSE);
	}

	/**
	 * Run search and display results.
	 */
	public void search() {
		searchAction.run();
		tree.requestFocus();
	}

	/**
	 * Run search and display results.
	 *
	 * @param controller
	 * 		Controller for the workspace.
	 * @param collectorSupplier
	 * 		Search generator.
	 */
	private void search(Controller controller, Supplier<SearchCollector> collectorSupplier) {
		Workspace workspace = controller.getWorkspace();
		List<SearchResult> results = null;
		SearchCollector collector = null;
		try {
			collector = collectorSupplier.get();
			results = collector.getAllResults();
		} catch(IllegalArgumentException ex) {
			// Some search argument requirements were not met
			results = Collections.emptyList();
			// TODO: visual warning
			Log.warn("Failed search due to illegal arguments: {}", ex.getMessage());
		}
		// Create parameter map so the root item can show the parameters of the search
		Map<String, Object> params = new TreeMap<>(inputMap.entrySet().stream()
				.collect(Collectors.toMap(
						e -> e.getKey().substring(e.getKey().lastIndexOf(".") + 1),
						e -> e.getValue().getOr("")
				)));
		tree.setRoot(new SearchRootItem(workspace.getPrimary(), results, params));
		JavaResourceTree.recurseOpen(tree.getRoot());
	}

	private SearchCollector buildDefinitionSearch(Workspace workspace) {
		return SearchBuilder.in(workspace)
				.skipDebug()
				.skipCode()
				.query(new MemberDefinitionQuery(
						input("ui.search.declaration.owner"), input("ui.search.declaration.name"),
						input("ui.search.declaration.desc"), input("ui.search.matchmode")))
				.skipPackages(input("ui.search.skippackages"))
				.build();
	}

	private SearchCollector buildClassReferenceSearch(Workspace workspace) {
		return SearchBuilder.in(workspace)
				.query(new ClassReferenceQuery(
						input("ui.search.cls_reference.name"), input("ui.search.matchmode")))
				.skipPackages(input("ui.search.skippackages"))
				.build();
	}

	private SearchCollector buildMemberReferenceSearch(Workspace workspace) {
		return SearchBuilder.in(workspace)
				.query(new MemberReferenceQuery(
						input("ui.search.mem_reference.owner"), input("ui.search.mem_reference.name"),
						input("ui.search.mem_reference.desc"), input("ui.search.matchmode")))
				.skipPackages(input("ui.search.skippackages"))
				.build();
	}

	private SearchCollector buildStringSearch(Workspace workspace) {
		return SearchBuilder.in(workspace)
				.skipDebug()
				.query(new StringQuery(input("ui.search.string"), input("ui.search.matchmode")))
				.skipPackages(input("ui.search.skippackages"))
				.build();
	}

	private SearchCollector buildValueSearch(Workspace workspace) {
		return SearchBuilder.in(workspace)
				.skipDebug()
				.skipPackages(input("ui.search.skippackages"))
				.query(new ValueQuery(input("ui.search.value"))).build();
	}

	private SearchCollector buildInsnSearch(Workspace workspace) {
		return SearchBuilder.in(workspace)
				.skipPackages(input("ui.search.skippackages"))
				.query(new InsnTextQuery(input("ui.search.insn.lines"), input("ui.search.matchmode"))).build();
	}

	/**
	 * @param input
	 * 		Input instance to register.
	 * @param <E>
	 * 		Editor type.
	 * @param <R>
	 * 		Editor content type.
	 */
	private <E extends Node, R> void addInput(Input<E, R> input) {
		inputMap.put(input.key, input);
	}

	/**
	 * @param key
	 * 		Input key.
	 * @param <R>
	 * 		Input content type.
	 *
	 * @return Input value.
	 */
	private <R> R input(String key) {
		Input<?, R> obj = inputMap.get(key);
		if (obj == null)
			throw new IllegalStateException("No input by key: " + key);
		return obj.get();
	}

	/**
	 * @param key
	 * 		Input key.
	 * @param value
	 * 		Input value to set.
	 * @param <R>
	 * 		Input content type.
	 */
	public <R> void setInput(String key, R value) {
		Input<?, R> obj = inputMap.get(key);
		if (obj == null)
			throw new IllegalStateException("No input by key: " + key);
		obj.set(value);
	}

	/**
	 * Wrapper for inputs.
	 *
	 * @param <E>
	 * 		Editor type.
	 * @param <R>
	 * 		Editor content type.
	 */
	private static class Input<E extends Node, R> {
		private final E editor;
		private final Function<E, R> mapper;
		private final BiConsumer<E, R> setter;
		private final String key;

		private Input(ColumnPane root, String key, String desc, Supplier<E> create,
					  Function<E, R> mapper, BiConsumer<E, R> setter) {
			this.editor = create.get();
			this.mapper = mapper;
			this.setter = setter;
			this.key = key;
			SubLabeled labeled = new SubLabeled(translate(key), translate(desc));
			root.add(labeled, editor);
		}

		/**
		 * @param value
		 * 		Value of editor to set.
		 */
		public void set(R value) {
			setter.accept(editor, value);
		}

		/**
		 * @return Content of editor.
		 */
		public R get() {
			return mapper.apply(editor);
		}

		/**
		 * @param fallback
		 * 		Value to return if the editor's value is {@code null}.
		 *
		 * @return Content of editor.
		 */
		public R getOr(R fallback) {
			R ret = get();
			if(ret == null)
				return fallback;
			return ret;
		}
	}
}