package speiger.src.coreengine.rendering.guiOld.components;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import speiger.src.collections.objects.lists.ObjectArrayList;
import speiger.src.collections.objects.sets.ObjectOpenHashSet;
import speiger.src.collections.objects.sets.ObjectSet;
import speiger.src.coreengine.math.MathUtils;
import speiger.src.coreengine.math.color.ColorUtils;
import speiger.src.coreengine.math.misc.Facing;
import speiger.src.coreengine.math.vector.ints.Vec2i;
import speiger.src.coreengine.rendering.guiOld.GuiComponent;
import speiger.src.coreengine.rendering.guiOld.base.IButtonComponent;
import speiger.src.coreengine.rendering.guiOld.components.tree.ITreeEntry;
import speiger.src.coreengine.rendering.guiOld.helper.UIShapes;
import speiger.src.coreengine.rendering.guiOld.helper.box.IGuiBox;
import speiger.src.coreengine.rendering.guiOld.helper.constrains.Constrains;
import speiger.src.coreengine.rendering.guiOld.renderer.UIRenderer;
import speiger.src.coreengine.rendering.guiOld.renderer.buffer.RenderBuffer;
import speiger.src.coreengine.rendering.inputOld.Keyboard;

public class TreeComponent<T extends ITreeEntry> extends GuiComponent implements IButtonComponent
{
	public static final int SELECTION_MODE_DISABLE = 0;
	public static final int SELECTION_MODE_SINGLE = 1;
	public static final int SELECTION_MODE_MULTI = 2;
	public static final int SELECTION_MODE_INTERACT = 3;
	
	public static final int UPDATE_MODE_DISABLED = 0;
	public static final int UPDATE_MODE_VISIBLE = 1;
	public static final int UPDATE_MODE_OPEN = 2;
	public static final int UPDATE_MODE_ALL = 3;
	
	public static final int FLAG_NO_BACKGROUND = 1 << 20;
	
	ScrollBarComponent verticalBar = new ScrollBarComponent(ColorUtils.LIGHT_GRAY);
	ScrollBarComponent horizontalBar = new ScrollBarComponent(ColorUtils.LIGHT_GRAY).setHorizontal(true);
	
	int color;
	int selectedColor;
	int hoverColor = ColorUtils.LIGHT_GRAY;
	int hoverIndex = -1;
	int dragIndex = -1;
	int movement;
	Vec2i lastMouse = Vec2i.mutable();
	IButtonComponent customButton;
	protected ObjectSet<T> openNodes = new ObjectOpenHashSet<>();
	protected ObjectSet<T> selectedNodes = new ObjectOpenHashSet<>();
	protected int selectionMode = 1;
	protected int updateMode = 1;
	T node;
	float entryHeight;
	
	boolean listChange = true;
	List<T> visibleNodes = new ObjectArrayList<T>();
	RenderBuffer buffer;
	
	public TreeComponent(int color, float entryHeight)
	{
		super(0F, 0F, 0F, 0F);
		this.entryHeight = entryHeight;
		this.color = color;
		this.selectedColor = color;
	}
	
	public TreeComponent(int color, float entryHeight, T entry)
	{
		super(0F, 0F, 0F, 0F);
		this.entryHeight = entryHeight;
		getNodes(entry, openNodes, false);
		this.color = color;
		this.selectedColor = color;
		node = entry;
		if(entry != null)
		{
			entry.calculateDebth();
		}
	}
	
	public TreeComponent(float x, float y, float width, float height, int color, float entryHeight, T entry)
	{
		super(x, y, width, height);
		this.entryHeight = entryHeight;
		getNodes(entry, openNodes, false);
		this.color = color;
		node = entry;
		if(entry != null)
		{
			entry.calculateDebth();
		}
	}

	@Override
	public void init()
	{
		onClose(buffer = getRenderer().createBuffer());
		addChild(horizontalBar, Constrains.scrollBar(verticalBar::isInUse, true, 5F));
		addChild(verticalBar, Constrains.scrollBar(horizontalBar::isInUse, false, 5F));
		List<T> entries = new ObjectArrayList<T>();
		getNodes(node, entries, false);
		for(int i = 0, m = entries.size();i < m;i++)
		{
			entries.get(i).init(this, getGui());
		}
		updateScrollBar();
		createArrow();
	}
	
	public TreeComponent<T> setColor(int color)
	{
		if(this.color != color)
		{
			this.color = color;
			onChanged(true);
		}
		return this;
	}
	
	public TreeComponent<T> setHoverColor(int color)
	{
		hoverColor = color;
		return this;
	}
	
	public TreeComponent<T> setSelectionColor(int color)
	{
		selectedColor = color;
		return this;
	}
	
	public TreeComponent<T> setEntryHeight(float entryHeight)
	{
		if(this.entryHeight != entryHeight)
		{
			this.entryHeight = entryHeight;
			onChanged(true);
		}
		return this;
	}
	
	public TreeComponent<T> setSelectionMode(int mode)
	{
		if(mode < 0 || mode > 3)
		{
			throw new IllegalStateException("Unknown Mode");
		}
		this.selectionMode = mode;
		selectedNodes.clear();
		return this;
	}
	
	public TreeComponent<T> setUpdateMode(int mode)
	{
		if(mode < 0 || mode > 3)
		{
			throw new IllegalStateException("Unknown Mode");
		}
		updateMode = mode;
		return this;
	}
	
	public TreeComponent<T> disableBackground(boolean value)
	{
		setFlag(FLAG_NO_BACKGROUND, value);
		return this;
	}
	
	@Override
	protected void repaint()
	{
		float scale = getBox().getScale();
		List<T> entries = new ObjectArrayList<T>();
		getNodes(node, entries, false);
		for(int i = 0,m=entries.size();i<m;i++)
		{
			entries.get(i).updateState(this, scale);
		}
		updateScrollBar();
		createArrow();
	}
	
	public TreeComponent<T> setTree(T entry)
	{
		if(node == entry)
		{
			return this;
		}
		node = entry;
		if(entry != null)
		{
			entry.calculateDebth();
			if(getGui() != null)
			{
				List<T> entries = new ObjectArrayList<T>();
				getNodes(entry, entries, false);
				for(int i = 0,m=entries.size();i<m;i++)
				{
					entries.get(i).init(this, getGui());
				}
			}
		}
		listChange = true;
		openNodes.clear();
		selectedNodes.clear();
		onChanged(true);
		return this;
	}
	
	public T getTree()
	{
		return node;
	}
	
	@SuppressWarnings("unchecked")
	public void onTreeChanged()
	{
		listChange = true;
		List<T> entries = new ObjectArrayList<T>();
		getNodes(node, entries, false);
		for(int i = 0,m=entries.size();i<m;i++)
		{
			T entry = entries.get(i);
			if(!entry.isInit())
			{
				entry.init(this, getGui());
				if(!entry.isLeaf() && isOpen((T)entry.getParent()))
				{
					openNode(entry, true);
				}
			}
		}
	}
	
	public boolean isNodeSelected(T entry)
	{
		return selectedNodes.contains(entry);
	}
	
	public void addSelectedNode(T entry)
	{
		switch(selectionMode)
		{
			case SELECTION_MODE_DISABLE: return;
			case SELECTION_MODE_SINGLE:
				selectedNodes.clear();
		}
		selectedNodes.add(entry);
	}
	
	public void removeSelectedNode(T entry)
	{
		selectedNodes.remove(entry);
	}
	
	public void clearSelectedNodes()
	{
		selectedNodes.clear();
	}
	
	public boolean hasSelectedNodes()
	{
		return selectedNodes.size() > 0;
	}
	
	public T getSelectedNode()
	{
		return selectedNodes.isEmpty() ? null : selectedNodes.iterator().next();
	}
	
	public List<T> getSelectedNodes()
	{
		return new ObjectArrayList<T>(this.selectedNodes);
	}
	
	public List<T> getOpenNodes()
	{
		return new ObjectArrayList<T>(this.visibleNodes);
	}
	
	public List<T> getAllNodes()
	{
		List<T> list = new ObjectArrayList<T>();
		getNodes(node, list, false);
		return list;
	}
	
	public void openAll()
	{
		if(node != null)
		{
			openNode(node, true);
		}
	}
	
	public void closeAll()
	{
		if(node != null)
		{
			closeNode(node, true);
		}
	}
	
	public void openNode(T entry, boolean childrenIncluded)
	{
		openNode(entry, childrenIncluded, Integer.MAX_VALUE);
	}
	
	public void openNode(T entry, boolean childrenIncluded, int maxLayers)
	{
		openNodes.add(entry);
		if(childrenIncluded)
		{
			getNodes(entry, openNodes, false, maxLayers);
		}
		listChange = true;
		updateScrollBar();
	}
	
	public void toggleNode(T entry, boolean childrenIncluded)
	{
		if(isOpen(entry))
		{
			closeNode(entry, childrenIncluded);
			return;
		}
		openNode(entry, childrenIncluded);
	}
	
	public boolean isOpen(T entry)
	{
		return openNodes.contains(entry);
	}
	
	public void closeNode(T entry, boolean childrenIncluded)
	{
		openNodes.remove(entry);
		if(childrenIncluded)
		{
			Set<T> entries = new ObjectOpenHashSet<T>();
			getNodes(entry, entries, false);
			openNodes.removeAll(entries);
		}
		listChange = true;
		updateScrollBar();
	}
	
	protected void createArrow()
	{
		float pixelSize = (entryHeight * 0.5F);
		UIShapes.createArrow(buffer, pixelSize, pixelSize, color, Facing.EAST);
		UIShapes.createArrow(buffer, pixelSize, pixelSize, color, Facing.SOUTH);
	}
	
	protected void updateScrollBar()
	{
		if(listChange)
		{
			listChange = false;
			visibleNodes.clear();
			getNodes(node, visibleNodes, true);
			customButton = null;
		}
		float width = 0F;
		float pixelSize = getBox().getScale() * entryHeight;
		for(int i = 0,m=visibleNodes.size();i<m;i++)
		{
			ITreeEntry entry = visibleNodes.get(i);
			width = Math.max(width, entry.getWidth() + (pixelSize * entry.getDebth()));
		}
		verticalBar.setScrollMax(MathUtils.ceil(visibleNodes.size() * entryHeight));
		horizontalBar.setScrollMax((int)(width / getBox().getScale()));
		horizontalBar.onChanged(true);
		verticalBar.onChanged(true);
	}
	
	@Override
	protected boolean fixedUpdateSelf()
	{
		if(updateMode == UPDATE_MODE_ALL)
		{
			for(int i = 0,m=visibleNodes.size();i<m;i++)
			{
				visibleNodes.get(i).onFixedUpdate();
			}
			updateScrollBar();
		}
		else if(updateMode == UPDATE_MODE_VISIBLE)
		{
			int min = getStartIndex();
			int max = min + getIndexWidth();
			for(int i = min,m=visibleNodes.size();i<m&&i<max;i++)
			{
				visibleNodes.get(i).onFixedUpdate();
			}
			updateScrollBar();
		}
		else if(updateMode == UPDATE_MODE_ALL)
		{
			List<T> entries = new ObjectArrayList<T>();
			getNodes(node, entries, false);
			for(int i = 0,m=entries.size();i<m;i++)
			{
				entries.get(i).onFixedUpdate();
			}
			updateScrollBar();
		}
		return true;
	}
	
	@Override
	protected boolean updateSelf(int mouseX, int mouseY, float particalTicks)
	{
		if(selectionMode == SELECTION_MODE_DISABLE || !isTopHovered(mouseX, mouseY) || verticalBar.isComponentColliding(mouseX, mouseY) || horizontalBar.isComponentColliding(mouseX, mouseY))
		{
			hoverIndex = -1;
			return true;
		}
		float scale = getBox().getScale();
		mouseY -= getBox().getMinY() - (verticalBar.getScroll() * scale);
		int index = (int)(mouseY / (entryHeight * scale));
		hoverIndex = index < 0 || index >= visibleNodes.size() ? -1 : index;
		return true;
	}
	
	@Override
	protected boolean renderSelf(int mouseX, int mouseY, float particalTicks)
	{
		float brightness = getActiveBrightness();
		UIRenderer render = getRenderer();
	    int start = getStartIndex();
	    int end = MathUtils.clamp(0, visibleNodes.size(), start + getIndexWidth());
		IGuiBox box = getBox();
		float scale = box.getScale();
		float minX = horizontalBar.getScroll() * scale;
		float minY = verticalBar.getScroll() * scale;
		float maxX = box.getWidth() - verticalBar.getRequiredSpace() + (horizontalBar.getScroll() * scale);
		float pixelSize = scale * entryHeight;
		float offsetSize = pixelSize * 0.8F;
		if(isFlagNotSet(FLAG_NO_BACKGROUND))
		{
			render.setBrightness(brightness).drawQuad(box, color);
		}
		render.push();
		render.translate(box.getMinX(-horizontalBar.getScroll()), box.getMinY(-verticalBar.getScroll()));
		enableScissorsBox(box.getMinX(), box.getMinY(), box.getMaxX()-verticalBar.getRequiredSpace(), box.getMaxY()-horizontalBar.getRequiredSpace());
		boolean skip = false;
		if(hoverIndex != -1 && hoverIndex >= start && hoverIndex < end)
		{
			T node = visibleNodes.get(hoverIndex);
			float xOffset = (offsetSize + (node.getDebth() * offsetSize * 0.6F)) * 0.8F;
			if(!node.isLeaf() && mouseX - box.getMinX() >= xOffset - (pixelSize * 0.6F) && mouseX - box.getMinX() <= xOffset)
			{
				skip = true;
			}
			if(!skip)
			{
				int extraX = (int)(minX - getBox().getMinX() - xOffset);
				int extraY = (int)(minY - hoverIndex * pixelSize - getBox().getMinY());
				if(node instanceof IButtonComponent && ((IButtonComponent)node).isComponentColliding(mouseX + extraX, mouseY + extraY))
				{
					skip = true;
				}
			}
			float otherMax = node.getHighlightWidth() >= 0F ? xOffset + node.getHighlightWidth() : maxX;
			if(mouseX - box.getMinX() >= otherMax) skip = true;
			if(!skip && selectionMode != SELECTION_MODE_INTERACT)
			{
				float offset = pixelSize * hoverIndex;
				render.drawQuad(minX, offset, Math.min(otherMax, maxX), offset + pixelSize, 0.01F, hoverColor);
			}
		}
		mouseX -= box.getMinX();
		mouseY -= box.getMinY();
		if(selectedNodes.size() > 0)
		{
			render.setBrightness(0.75F * brightness);
			for(int i = start;i<end;i++)
			{
				ITreeEntry entry = visibleNodes.get(i);
				if(selectedNodes.contains(entry))
				{
					float offset = pixelSize * i;
					render.drawQuad(minX, offset, maxX, offset + pixelSize, 0.011F, selectedColor);
				}
			}
		}
		for(int i = start;i<end;i++)
		{
			ITreeEntry entry = visibleNodes.get(i);
			float offsetY = i * pixelSize;
			float offsetX = (offsetSize + (entry.getDebth() * offsetSize * 0.6F)) * 0.8F;
			render.translate(offsetX, offsetY, 0.02F);
			if(!entry.isLeaf())
			{
				boolean extra = skip && i == hoverIndex;
				render.translate(-(pixelSize / 3F), pixelSize * 0.5F).scale(scale);
				render.setBrightness(brightness * (extra ? 0.5F : 1.25F)).drawBuffers(buffer.selectionIterator(openNodes.contains(entry) ? 1 : 0), 0F, 0F);
				render.unscale(scale).translate((pixelSize / 3F), -(pixelSize * 0.5F));
			}
			render.setBrightness(brightness);
			entry.onRender(this, isEnabled(), (int)(mouseX + minX - offsetX), (int)(mouseY + minY - offsetY), particalTicks);
			render.translate(-offsetX, -offsetY, -0.02F);
		}
		render.pop();
		disableScissors();
		return true;
	}
	
	@Override
	public void collectTooltips(int mouseX, int mouseY, float particalTicks, Map<UUID, GuiComponent> collector)
	{
		super.collectTooltips(mouseX, mouseY, particalTicks, collector);
	    int start = getStartIndex();
	    int end = MathUtils.clamp(0, visibleNodes.size(), start + getIndexWidth());
		mouseX -= getBox().getMinX();
		mouseY -= getBox().getMinY();
		float scale = getBox().getScale();
		float minX = horizontalBar.getScroll() * scale;
		float minY = verticalBar.getScroll() * scale;
		float pixelSize = scale * entryHeight;
		float offsetSize = pixelSize * 0.8F;
		for(int i = start;i<end;i++)
		{
			ITreeEntry entry = visibleNodes.get(i);
			float offsetY = i * pixelSize;
			float offsetX = (offsetSize + (entry.getDebth() * offsetSize * 0.6F)) * 0.8F;
			entry.collectTooltips(this, (int)(mouseX + minX - offsetX), (int)(mouseY + minY - offsetY), particalTicks, collector);
		}
	}
	
	@Override
	public boolean onClick(int button, int mouseX, int mouseY)
	{
		if(horizontalBar.isInUse() && horizontalBar.isComponentColliding(mouseX, mouseY))
		{
			return horizontalBar.onClick(button, mouseX, mouseY);
		}
		if(verticalBar.isInUse() && verticalBar.isComponentColliding(mouseX, mouseY))
		{
			return verticalBar.onClick(button, mouseX, mouseY);
		}
		if(hoverIndex != -1)
		{
			ITreeEntry entry = visibleNodes.get(hoverIndex);
			float scale = getBox().getScale();
			float offsetSize = entryHeight * scale * 0.8F;
			float offsetX = (offsetSize + (entry.getDebth() * offsetSize * 0.6F)) * 0.8F;
			if(mouseX - getBox().getMinX() >= offsetX - (entryHeight * scale * 0.6F) && mouseX - getBox().getMinX() <= offsetX)
			{
				toggleNode(visibleNodes.get(hoverIndex), Keyboard.isShiftDown());
				return true;
			}
			if(visibleNodes.get(hoverIndex) instanceof IButtonComponent)
			{
				IButtonComponent comp = (IButtonComponent)visibleNodes.get(hoverIndex);
				int extraX = (int)((horizontalBar.getScroll() * scale - getBox().getMinX()) - offsetX);
				int extraY = (int)(verticalBar.getScroll() * scale - (hoverIndex * scale * entryHeight) - getBox().getMinY());
				if(comp.isComponentColliding(mouseX + extraX, mouseY + extraY) && comp.onClick(button, mouseX + extraX, mouseY + extraY))
				{
					customButton = comp;
					return true;
				}
			}
		}
		dragIndex = hoverIndex;
		if(selectionMode == SELECTION_MODE_INTERACT)
		{
			dragIndex = -1;
		}
		lastMouse.set(mouseX, mouseY);
		return true;
	}

	@Override
	public boolean onDrag(int mouseX, int mouseY)
	{
		if(horizontalBar.onDrag(mouseX, mouseY) || verticalBar.onDrag(mouseX, mouseY))
		{
			return true;
		}
		if(customButton != null)
		{
			float scale = getBox().getScale();
			float offsetSize = entryHeight * scale * 0.8F;
			int extraX = (int)((horizontalBar.getScroll() * scale - getBox().getMinX()) - ((offsetSize + (visibleNodes.get(hoverIndex).getDebth() * offsetSize * 0.6F)) * 0.8F));
			int extraY = (int)(verticalBar.getScroll() * scale - (hoverIndex * scale * entryHeight) - getBox().getMinY());
			return customButton.onDrag(mouseX + extraX, mouseY + extraY);
		}
		horizontalBar.addScroll(lastMouse.x() - mouseX);
		verticalBar.addScroll(lastMouse.y() - mouseY);
		movement += Math.abs(lastMouse.x() - mouseX) + Math.abs(lastMouse.y() - mouseY);
		lastMouse.set(mouseX, mouseY);
		return true;
	}

	@Override
	public void onRelease(int button, int mouseX, int mouseY)
	{
		horizontalBar.onRelease(button, mouseX, mouseY);
		verticalBar.onRelease(button, mouseX, mouseY);
		if(customButton != null && (hoverIndex >= 0 && hoverIndex < visibleNodes.size() && customButton == visibleNodes.get(hoverIndex)))
		{
			float scale = getBox().getScale();
			float offsetSize = entryHeight * scale * 0.8F;
			int extraX = (int)((horizontalBar.getScroll() * scale - getBox().getMinX()) - ((offsetSize + (visibleNodes.get(hoverIndex).getDebth() * offsetSize * 0.6F)) * 0.8F));
			int extraY = (int)(verticalBar.getScroll() * scale - (hoverIndex * scale * entryHeight) - getBox().getMinY());
			customButton.onRelease(button, mouseX + extraX, mouseY + extraY);
			customButton = null;
		}
		else if(dragIndex != -1 && dragIndex == hoverIndex && movement < 2)
		{
			if(isNodeSelected(visibleNodes.get(hoverIndex)))
			{
				removeSelectedNode(visibleNodes.get(hoverIndex));
			}
			else
			{
				addSelectedNode(visibleNodes.get(hoverIndex));	
			}
			notifyListeners(LISTENER_USER_ACTION);
			dragIndex = -1;
		}
		movement = 0;
	}

	@Override
	public boolean onScroll(int scroll, int mouseX, int mouseY)
	{
		if((horizontalBar.isComponentColliding(mouseX, mouseY) && horizontalBar.onScroll(scroll, mouseX, mouseY)) || (verticalBar.isComponentColliding(mouseX, mouseY) && verticalBar.onScroll(scroll, mouseX, mouseY)))
		{
			movement = 100;
			return true;
		}
		if(hoverIndex != -1 && visibleNodes.get(hoverIndex) instanceof IButtonComponent)
		{
			IButtonComponent comp = (IButtonComponent)visibleNodes.get(hoverIndex);
			float scale = getBox().getScale();
			float offsetSize = entryHeight * scale * 0.8F;
			int extraX = (int)((horizontalBar.getScroll() * scale - getBox().getMinX()) - ((offsetSize + (visibleNodes.get(hoverIndex).getDebth() * offsetSize * 0.6F)) * 0.8F));
			int extraY = (int)(verticalBar.getScroll() - (hoverIndex * getBox().getScale() * entryHeight) - getBox().getMinY());
			if(comp.isComponentColliding(mouseX + extraX, mouseY + extraY) && comp.onScroll(scroll, mouseX + extraX, mouseY + extraY))
			{
				return true;
			}
		}
		if(verticalBar.isInUse())
		{
			verticalBar.addScroll(-(int)(scroll * 5F * getBox().getScale()));
			return true;
		}
		return false;
	}
	
	public int getStartIndex()
	{
		return MathUtils.clamp(0, visibleNodes.size(), MathUtils.floor(verticalBar.getScroll() / entryHeight));
	}
	
	public int getIndexWidth()
	{
		return MathUtils.clamp(0, visibleNodes.size(), MathUtils.ceil((getBox().getBaseHeight() - (horizontalBar.getRequiredSpace() / getBox().getScale())) / entryHeight) + 1);
	}
	
	@SuppressWarnings("unchecked")
	protected void getNodes(T entry, Collection<T> collection, boolean openOnly, int layers)
	{
		if(entry != null && layers >= 0)
		{
			collection.add(entry);
			if(!entry.isLeaf() && (!openOnly || openNodes.contains(entry)))
			{
				for(int i = 0,m=entry.getChildCount();i<m;i++)
				{
					getNodes((T)entry.getChild(i), collection, openOnly, layers-1);
				}
			}
		}
	}

	
	@SuppressWarnings("unchecked")
	protected void getNodes(T entry, Collection<T> collection, boolean openOnly)
	{
		if(entry != null)
		{
			collection.add(entry);
			if(!entry.isLeaf() && (!openOnly || openNodes.contains(entry)))
			{
				for(int i = 0,m=entry.getChildCount();i<m;i++)
				{
					getNodes((T)entry.getChild(i), collection, openOnly);
				}
			}
		}
	}
}
