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

import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.Function;
import java.util.function.Predicate;

import speiger.src.collections.ints.collections.IntCollection;
import speiger.src.collections.ints.lists.IntArrayList;
import speiger.src.collections.ints.lists.IntList;
import speiger.src.collections.ints.sets.IntLinkedOpenHashSet;
import speiger.src.collections.objects.lists.ObjectArrayList;
import speiger.src.collections.objects.lists.ObjectList;
import speiger.src.collections.objects.utils.ObjectIterators;
import speiger.src.coreengine.math.MathUtils;
import speiger.src.coreengine.math.color.ColorUtils;
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.list.IListEntry;
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.inputOld.Keyboard;

public class ListComponent<T extends IListEntry> extends GuiComponent
		implements Iterable<T>, 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_ALL = 2;
	
	public static final int FLAG_DISABLE_BACKGROUND = 1 << 20;
	public static final int FLAG_START_AT_BOTTOM = 1 << 21;
	
	protected int color;
	protected int hoverColor = ColorUtils.LIGHT_GRAY;
	protected int hoverIndex = -1;
	protected int dragIndex = -1;
	protected int movement = 0;
	protected IButtonComponent customButton;
	protected IntLinkedOpenHashSet selectedIndexes = new IntLinkedOpenHashSet();
	protected ObjectList<T> entries = new ObjectArrayList<T>();
	protected int selectionMode = 1;
	protected int updateMode = 1;
	protected float entryHeight;
	protected float cachedWidth = 0F;
	protected ScrollBarComponent verticalBar = new ScrollBarComponent(ColorUtils.LIGHT_GRAY);
	protected ScrollBarComponent horizontalBar = new ScrollBarComponent(ColorUtils.LIGHT_GRAY).setHorizontal(true);
	protected Vec2i lastMouse = Vec2i.mutable();
	
	public ListComponent()
	{
		super(0F, 0F, 0F, 0F);
	}
		
	public ListComponent(int color, float entryHeight)
	{
		super(0F, 0F, 0F, 0F);
		this.color = color;
		this.entryHeight = entryHeight;
	}
	
	public ListComponent(float x, float y, float width, float height, int color, float entryHeight)
	{
		super(x, y, width, height);
		this.color = color;
		this.entryHeight = entryHeight;
	}
	
	@Override
	public void init()
	{
		addChild(horizontalBar, Constrains.scrollBar(verticalBar::isInUse, true, 5F));
		addChild(verticalBar, Constrains.scrollBar(horizontalBar::isInUse, false, 5F));
		for(int i = 0, m = entries.size();i < m;i++)
		{
			entries.get(i).init(this, getGui());
		}
		updateScrollBar();
	}
	
	@Override
	public void onClosed()
	{
		for(int i = 0,m=entries.size();i<m;i++)
		{
			entries.get(i).onClosed();
		}
		super.onClosed();
	}
	
	public ScrollBarComponent getHorizontalBar()
	{
		return horizontalBar;
	}
	
	public ScrollBarComponent getVerticalBar()
	{
		return verticalBar;
	}
	
	public ListComponent<T> disableBackground()
	{
		setFlag(FLAG_DISABLE_BACKGROUND);
		return this;
	}
	
	public ListComponent<T> setStartAtBottom(boolean value)
	{
		setFlag(FLAG_START_AT_BOTTOM, value);
		return this;
	}
	
	public boolean isStartAtBottom()
	{
		return isFlagSet(FLAG_START_AT_BOTTOM);
	}
	
	public ListComponent<T> setColor(int color)
	{
		this.color = color;
		return this;
	}
	
	public ListComponent<T> setHoverColor(int color)
	{
		hoverColor = color;
		return this;
	}
	
	public ListComponent<T> setEntryHeight(float entryHeight)
	{
		if(this.entryHeight != entryHeight)
		{
			this.entryHeight = entryHeight;
			onChanged(true);
		}
		return this;
	}
	
	public ListComponent<T> setSelectionMode(int mode)
	{
		if(mode < 0 || mode > 3)
		{
			throw new IllegalStateException("Unknown Mode");
		}
		this.selectionMode = mode;
		selectedIndexes.clear();
		return this;
	}
	
	public ListComponent<T> setUpdateMode(int mode)
	{
		if(mode < 0 || mode > 2)
		{
			throw new IllegalStateException("Unknown Mode");
		}
		updateMode = mode;
		return this;
	}
	
	public ListComponent<T> setSelectedIndex(int index)
	{
		switch(selectionMode)
		{
			case 0:
				throw new IllegalStateException("Selection is not allowed");
			case 1:
				selectedIndexes.clear();
			case 2:
				if(index >= 0 && index < entries.size())
				{
					if(selectionMode == SELECTION_MODE_MULTI && Keyboard.isShiftDown())
					{
						for(int i = selectedIndexes.getLastInt();i<index;i++)
						{
							selectedIndexes.add(i);
						}
						for(int i = selectedIndexes.getLastInt();i>index;i--)
						{
							selectedIndexes.add(i);							
						}
					}
					selectedIndexes.add(index);
				}
				break;
		}
		return this;
	}
	
	public boolean isSelected(int index)
	{
		return selectedIndexes.contains(index);
	}
	
	public T removeSelectedIndex(int index)
	{
		return selectedIndexes.remove(index) ? entries.get(index) : null;
	}
	
	public boolean hasSelected()
	{
		return !selectedIndexes.isEmpty();
	}
	
	public int getSelectedIndex()
	{
		return selectedIndexes.isEmpty() ? -1 : selectedIndexes.iterator().nextInt();
	}
	
	public T getSelectedValue()
	{
		return selectedIndexes.isEmpty() ? null : entries.get(selectedIndexes.iterator().nextInt());
	}
	
	public void clearSelection()
	{
		selectedIndexes.clear();
	}
	
	public IntList getSelection()
	{
		return new IntArrayList(selectedIndexes);
	}
	
	public List<T> getSelectedValues()
	{
		List<T> entries = new ObjectArrayList<T>();
		for(int index : selectedIndexes)
		{
			entries.add(this.entries.get(index));
		}
		return entries;
	}
	
	public ListComponent<T> add(T entry)
	{
		entries.add(entry);
		if(getGui() != null)
		{
			entry.init(this, getGui());
			updateScrollBar();
		}
		return this;
	}
	
	public ListComponent<T> addAll(Collection<T> entries)
	{
		this.entries.addAll(entries);
		if(getGui() != null)
		{
			for(T entry : entries)
			{
				entry.init(this, getGui());
			}
			updateScrollBar();
		}
		return this;
	}
	
	public int size()
	{
		return entries.size();
	}
	
	public T get(int index)
	{
		return entries.get(index);
	}
	
	public boolean removeIf(Predicate<T> filter)
	{
		if(entries.isEmpty()) return false;
		selectedIndexes.clear();
		return entries.removeIf(K -> {
			if(filter.test(K)) {
				K.onClosed();
				return true;
			}
			return false;
		});
	}
	
	public <K> List<K> map(Function<T, K> mapper)
	{
		List<K> list = new ObjectArrayList<>(entries.size());
		for(int i = 0,m=entries.size();i<m;list.add(mapper.apply(entries.get(i++))));
		return list;
	}
	
	public void moveUp(int index)
	{
		if(index > 0)
		{
			entries.add(index - 1, entries.remove(index));
			if(isSelected(index))
			{
				removeSelectedIndex(index);
				setSelectedIndex(index-1);
			}
		}
	}
	
	public void moveDown(int index)
	{
		if(index + 1 < entries.size())
		{
			entries.add(index + 1, entries.remove(index));
			if(isSelected(index))
			{
				removeSelectedIndex(index);
				setSelectedIndex(index+1);
			}
		}
	}
	
	public void move(int fromIndex, int toIndex)
	{
		entries.add(toIndex, entries.remove(fromIndex));
	}
	
	public int indexOf(T value)
	{
		return entries.indexOf(value);
	}
	
	public T remove(int index)
	{
		if(index < 0 || index >= entries.size())
		{
			return null;
		}
		T value = entries.remove(index);
		if(value != null)
		{
			value.onClosed();
			selectedIndexes.remove(index);
		}
		return value;
	}
	
	public boolean removeAll(IntCollection values)
	{
		List<T> result = new ObjectArrayList<T>();
		for(int index : values)
		{
			if(index < 0 || index >= entries.size())
			{
				continue;
			}
			selectedIndexes.remove(index);
			T entry = entries.get(index);
			entry.onClosed();
			result.add(entry);
		}
		if(result.size() > 0)
		{
			entries.removeAll(result);
			return true;
		}
		return false;
	}
	
	public boolean remove(T value)
	{
		return remove(indexOf(value)) != null;
	}
	
	public void clear()
	{
		for(int i = 0,m=entries.size();i<m;i++)
		{
			entries.get(i).onClosed();
		}
		entries.clear();
		clearSelection();
	}
	
	@Override
	public Iterator<T> iterator()
	{
		return ObjectIterators.unmodifiable(entries.iterator());
	}
	
	protected Iterator<T> rangeIterator(int start, int end)
	{
		return new Iterator<T>() {
			int index = start;
			
			@Override
			public boolean hasNext()
			{
				return index < end && entries.size() > index;
			}
			
			@Override
			public T next()
			{
				return entries.get(index++);
			}
		};
	}
	
	public Iterator<T> visibleIterator()
	{
		int start = getStartIndex();
		return rangeIterator(start, MathUtils.clamp(0, entries.size() - 1, start + getIndexWidth()));
	}
	
	public float getCachedWidth()
	{
		return cachedWidth;
	}
	
	@Override
	protected void repaint()
	{
		float scale = getBox().getScale();
		for(int i = 0,m=entries.size();i<m;i++)
		{
			entries.get(i).updateState(this, scale);
		}
		updateScrollBar();
	}
	
	protected void updateScrollBar()
	{
		float width = 0F;
		for(int i = 0,m=entries.size();i<m;i++)
		{
			width = Math.max(width, entries.get(i).getWidth());
		}
		this.cachedWidth = width;
		boolean lastVertical = this.verticalBar.isInUse();
		boolean lastHorizontal = this.horizontalBar.isInUse();
		verticalBar.setScrollMax(MathUtils.ceil(this.entries.size() * entryHeight));
		horizontalBar.setScrollMax((int)(width / getBox().getScale()));
		if(lastHorizontal != horizontalBar.isInUse())
		{
			horizontalBar.onChanged(true);
		}
		if(lastVertical != verticalBar.isInUse())
		{
			verticalBar.onChanged(true);
		}
	}
	
	@Override
	protected boolean fixedUpdateSelf()
	{
		if(updateMode == UPDATE_MODE_ALL)
		{
			for(int i = 0,m=entries.size();i<m;i++)
			{
				entries.get(i).onFixedUpdate();
			}
			updateScrollBar();
		}
		else if(updateMode == UPDATE_MODE_VISIBLE)
		{
			int min = getStartIndex();
			int max = min + getIndexWidth();
			for(int i = min,m=entries.size();i<m&&i<max;i++)
			{
				entries.get(i).onFixedUpdate();
			}
			updateScrollBar();
		}
		return true;
	}
	
	@Override
	public 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();
		boolean bottom = !verticalBar.isInUse() && isStartAtBottom();
		float pixelSize = scale * entryHeight;
		mouseY -= getBox().getMinY() - (verticalBar.getScroll() * scale) + (bottom ? (((getBox().getHeight() / pixelSize) - MathUtils.clamp(0, entries.size(), getIndexWidth())) * pixelSize) : 0);
		int index = (int)(mouseY / pixelSize);
		hoverIndex = index < 0 || index >= entries.size() ? -1 : index;
		return true;
	}
	
	@Override
	public boolean renderSelf(int mouseX, int mouseY, float particalTicks)
	{
		float brightness = getActiveBrightness();
		UIRenderer render = getRenderer();
	    int start = getStartIndex();
	    int end = MathUtils.clamp(0, entries.size(), start + getIndexWidth());
		IGuiBox box = getBox();
		float scale = box.getScale();
		float pixelSize = scale * entryHeight;
		render.setBrightness(brightness);
		if(isFlagNotSet(FLAG_DISABLE_BACKGROUND))
		{
			render.drawQuad(box, color);
		}
		render.push();
		render.translate(box.getMinX(-horizontalBar.getScroll()), box.getMinY(-verticalBar.getScroll()));
		float minX = horizontalBar.getScroll() * scale;
		float maxX = box.getWidth() - verticalBar.getRequiredSpace() + (horizontalBar.getScroll() * scale);
		enableScissorsBox(box.getMinX(), box.getMinY(), box.getMaxX()-verticalBar.getRequiredSpace(), box.getMaxY()-horizontalBar.getRequiredSpace());
		boolean bottom = !verticalBar.isInUse() && isStartAtBottom();
		if(hoverIndex != -1 && selectionMode != SELECTION_MODE_INTERACT && hoverIndex >= start && hoverIndex < end)
		{
			float offset = bottom ? box.getHeight()-(end-hoverIndex) * pixelSize : hoverIndex * pixelSize;
			render.drawQuad(minX, offset, maxX, offset + pixelSize, 0.01F, hoverColor);
		}
		if(selectedIndexes.size() > 0)
		{
			render.setBrightness(0.75F * brightness);
			for(int index : selectedIndexes)
			{
				if(index >= start && index < end)
				{
					float offset = bottom ? box.getHeight()-(end-index) * pixelSize : index * pixelSize;
					render.drawQuad(minX, offset, maxX, offset + pixelSize, 0.011F, color);
				}
			}
		}
		mouseX -= box.getMinX();
		mouseY -= box.getMinY();
		for(int i = start;i<end;i++)
		{
			float offset = bottom ? box.getHeight()-(end-i) * pixelSize : i * pixelSize;
			render.setBrightness(brightness).translate(0F, offset, 0.02F);
			entries.get(i).onRender(this, isEnabled(), (int)(mouseX + horizontalBar.getScroll() * scale), (int)((mouseY + verticalBar.getScroll() * scale) - offset), particalTicks);
			render.translate(0F, -offset, -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, entries.size(), start + getIndexWidth());
		IGuiBox box = getBox();
		float scale = box.getScale();
		float pixelSize = scale * entryHeight;
		mouseX -= box.getMinX();
		mouseY -= box.getMinY();
		boolean bottom = !verticalBar.isInUse() && isStartAtBottom();
		for(int i = start;i<end;i++)
		{
			float offset = bottom ? box.getHeight()-(end-i) * pixelSize : i * pixelSize;
			entries.get(i).collectTooltips(this, (int)(mouseX + horizontalBar.getScroll() * scale), (int)((mouseY + verticalBar.getScroll() * scale) - offset), particalTicks, collector);
		}
	}
	
	public int getStartIndex()
	{
		return MathUtils.clamp(0, entries.size(), MathUtils.floor(verticalBar.getScroll() / entryHeight));
	}
	
	public int getIndexWidth()
	{
		return MathUtils.clamp(0, entries.size(), MathUtils.ceil((getBox().getBaseHeight() - (horizontalBar.getRequiredSpace() / getBox().getScale())) / entryHeight) + 1);
	}
	
	protected int getExtraY()
	{
		boolean bottom = !verticalBar.isInUse() && isStartAtBottom();
		float pixelSize = getBox().getScale() * entryHeight;
		return (int)(verticalBar.getScroll() * getBox().getScale() - (bottom ? getBox().getHeight()-(MathUtils.clamp(0, entries.size(), getStartIndex() + getIndexWidth()) - hoverIndex) * pixelSize : hoverIndex * pixelSize) - getBox().getMinY());
	}
	
	@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 && get(hoverIndex) instanceof IButtonComponent)
		{
			IButtonComponent comp = (IButtonComponent)get(hoverIndex);
			int extraX = (int)(horizontalBar.getScroll() * getBox().getScale() - getBox().getMinX());
			int extraY = getExtraY();
			if(comp.isComponentColliding(mouseX + extraX, mouseY + extraY) && comp.onClick(button, mouseX + extraX, mouseY + extraY))
			{
				customButton = comp;
				return true;
			}
		}
		dragIndex = hoverIndex;
		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)
		{
			int extraX = (int)(horizontalBar.getScroll() * getBox().getScale() - getBox().getMinX());
			int extraY = getExtraY();
			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 false;
	}

	@Override
	public void onRelease(int button, int mouseX, int mouseY)
	{
		horizontalBar.onRelease(button, mouseX, mouseY);
		verticalBar.onRelease(button, mouseX, mouseY);
		if(customButton != null)
		{
			int extraX = (int)(horizontalBar.getScroll() * getBox().getScale() - getBox().getMinX());
			int extraY = getExtraY();
			customButton.onRelease(button, mouseX + extraX, mouseY + extraY);
			customButton = null;
		}
		else if(dragIndex != -1 && dragIndex == hoverIndex && movement < 2)
		{
			if(isSelected(dragIndex))
			{
				removeSelectedIndex(dragIndex);
			}
			else
			{
				setSelectedIndex(dragIndex);	
			}
			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 && get(hoverIndex) instanceof IButtonComponent)
		{
			IButtonComponent comp = (IButtonComponent)get(hoverIndex);
			int extraX = (int)(horizontalBar.getScroll() - getBox().getMinX());
			int extraY = getExtraY();
			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;
	}
}
