/*
 * Copyright (c) 2013-2021 Chris Newland.
 * Licensed under https://github.com/AdoptOpenJDK/jitwatch/blob/master/LICENSE-BSD
 * Instructions: https://github.com/AdoptOpenJDK/jitwatch/wiki
 */
package org.adoptopenjdk.jitwatch.ui.main;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.adoptopenjdk.jitwatch.core.JITWatchConfig;
import org.adoptopenjdk.jitwatch.model.MetaClass;
import org.adoptopenjdk.jitwatch.model.MetaPackage;
import org.adoptopenjdk.jitwatch.util.UserInterfaceUtil;

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.scene.control.CheckBox;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.TREE_PACKAGE_ROOT;

public class ClassTree extends VBox
{
	private TreeView<Object> treeView;
	private TreeItem<Object> rootItem;

	private JITWatchUI parent;
	private JITWatchConfig config;

	private boolean sameVmCommand;
	private Set<String> openPackageNodes = new HashSet<>();

	private CheckBox cbHideInterfaces;
	private CheckBox cbHideUncompiled;

	private boolean selectedProgrammatically = false;

	public ClassTree(final JITWatchUI parent, final JITWatchConfig config)
	{
		this.parent = parent;
		this.config = config;

		final String cbStyle = "-fx-background-color:#dddddd; -fx-padding:4px";

		cbHideInterfaces = UserInterfaceUtil.createCheckBox("HIDE_INTERFACES");
		cbHideInterfaces.setMaxWidth(280);
		cbHideInterfaces.setSelected(config.isHideInterfaces());
		cbHideInterfaces.selectedProperty().addListener(new ChangeListener<Boolean>()
		{
			@Override
			public void changed(ObservableValue<? extends Boolean> ov, Boolean oldVal, Boolean newVal)
			{
				config.setHideInterfaces(newVal);
				config.saveConfig();

				parent.clearAndRefreshTreeView(true);
			}
		});

		cbHideInterfaces.setStyle(cbStyle);
		cbHideInterfaces.prefWidthProperty().bind(widthProperty());

		cbHideUncompiled = UserInterfaceUtil.createCheckBox("HIDE_UNCOMPILED_CLASSES");
		cbHideUncompiled.setSelected(config.isShowOnlyCompiledClasses());
		cbHideUncompiled.selectedProperty().addListener(new ChangeListener<Boolean>()
		{
			@Override
			public void changed(ObservableValue<? extends Boolean> ov, Boolean oldVal, Boolean newVal)
			{
				config.setShowOnlyCompiledClasses(newVal);
				config.saveConfig();

				parent.clearAndRefreshTreeView(true);
			}
		});

		cbHideUncompiled.setStyle(cbStyle);
		cbHideUncompiled.prefWidthProperty().bind(widthProperty());

		rootItem = new TreeItem<>(TREE_PACKAGE_ROOT);

		rootItem.setExpanded(true);

		treeView = new TreeView<>(rootItem);

		treeView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);

		treeView.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<TreeItem<Object>>()
		{
			@Override
			public void changed(ObservableValue<? extends TreeItem<Object>> observableValue, TreeItem<Object> oldItem,
					TreeItem<Object> newItem)
			{
				if (!selectedProgrammatically)
				{
					if (newItem != null)
					{
						Object value = newItem.getValue();

						if (value instanceof MetaClass)
						{
							parent.metaClassSelectedFromClassTree((MetaClass) value);
						}
					}
				}
			}
		});

		HBox hboxOptions = new HBox();

		hboxOptions.getChildren().add(cbHideInterfaces);
		hboxOptions.getChildren().add(cbHideUncompiled);

		getChildren().add(hboxOptions);
		getChildren().add(treeView);

		treeView.prefHeightProperty().bind(heightProperty());
	}

	public void handleConfigUpdate(JITWatchConfig config)
	{
		cbHideInterfaces.setSelected(config.isHideInterfaces());
		cbHideUncompiled.setSelected(config.isShowOnlyCompiledClasses());
	}
	
	public boolean isHidingClassesWithNoCompiledMethods()
	{
		return cbHideInterfaces.isSelected();
	}

	private TreeItem<Object> findOrCreateTreeItem(final TreeItem<Object> parent, final Object value)
	{
		ObservableList<TreeItem<Object>> children = parent.getChildren();

		TreeItem<Object> found = null;

		int placeToInsert = 0;
		boolean foundInsertPos = false;

		for (TreeItem<Object> child : children)
		{
			int stringCompare = child.getValue().toString().compareTo(value.toString());

			if (stringCompare == 0)
			{
				found = child;
				break;
			}
			else if (!foundInsertPos && stringCompare < 0)
			{
				// make sure sub packages listed before classes in this package

				if (not(child.getValue() instanceof MetaPackage && value instanceof MetaClass))
				{
					placeToInsert++;
				}
			}
			else
			{
				if (child.getValue() instanceof MetaPackage && value instanceof MetaClass)
				{
					placeToInsert++;
				}
				else
				{
					foundInsertPos = true;
				}
			}
		}

		if (found == null)
		{
			found = new TreeItem<>(value);
			children.add(placeToInsert, found);

			if (value instanceof MetaPackage)
			{
				final String packageName = value.toString();

				found.expandedProperty().addListener(new ChangeListener<Boolean>()
				{
					@Override
					public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue)
					{
						if (newValue != null)
						{
							if (newValue == true)
							{
								openPackageNodes.add(packageName);
							}
							else
							{
								openPackageNodes.remove(packageName);
							}
						}
					}
				});

				if (sameVmCommand && openPackageNodes.contains(packageName))
				{
					found.setExpanded(true);
				}
			}
		}

		boolean hasCompiledChildren = false;

		if (value instanceof MetaPackage && ((MetaPackage) value).hasCompiledClasses())
		{
			hasCompiledChildren = true;
		}
		else if (value instanceof MetaClass && ((MetaClass) value).hasCompiledMethods())
		{
			hasCompiledChildren = true;
		}

		if (UserInterfaceUtil.IMAGE_TICK != null && hasCompiledChildren)
		{
			found.setGraphic(new ImageView(UserInterfaceUtil.IMAGE_TICK));
		}

		return found;
	}

	public void clearOpenPackageHistory()
	{
		openPackageNodes.clear();
	}

	private boolean not(boolean someCondition)
	{
		return !someCondition;
	}

	/*
	 * @param boolean sameVmCommand. Same class file executed so maintain
	 * expanded nodes.
	 */
	public void showTree(boolean sameVmCommand)
	{
		this.sameVmCommand = sameVmCommand;

		List<MetaPackage> roots = parent.getPackageManager().getRootPackages();

		for (MetaPackage mp : roots)
		{
			boolean allowed = true;

			if (!mp.hasCompiledClasses() && config.isShowOnlyCompiledClasses())
			{
				allowed = false;
			}

			if (allowed)
			{
				showTree(rootItem, mp);
			}
		}
	}

	private void showTree(TreeItem<Object> currentNode, MetaPackage mp)
	{
		TreeItem<Object> packageItem = findOrCreateTreeItem(currentNode, mp);

		List<MetaPackage> childPackages = mp.getChildPackages();

		for (MetaPackage childPackage : childPackages)
		{
			boolean allowed = true;

			if (!childPackage.hasCompiledClasses() && config.isShowOnlyCompiledClasses())
			{
				allowed = false;
			}

			if (allowed)
			{
				showTree(packageItem, childPackage);
			}
		}

		List<MetaClass> packageClasses = mp.getPackageClasses();

		for (MetaClass packageClass : packageClasses)
		{
			boolean allowed = true;

			if (packageClass.isInterface() && config.isHideInterfaces())
			{
				allowed = false;
			}

			if (allowed)
			{
				if (!packageClass.hasCompiledMethods() && config.isShowOnlyCompiledClasses())
				{
					allowed = false;
				}
			}

			if (allowed)
			{
				findOrCreateTreeItem(packageItem, packageClass);
			}
		}
	}

	public void select(TreeItem<Object> node)
	{
		selectedProgrammatically = true;
		treeView.getSelectionModel().select(node);
		selectedProgrammatically = false;
	}

	public void scrollTo(int rowsAbove)
	{
		treeView.scrollTo(rowsAbove);
	}

	public TreeItem<Object> getRootItem()
	{
		return rootItem;
	}

	public void clear()
	{
		rootItem.getChildren().clear();
	}
}