package speiger.src.coreengine.rendering.textures.custom;

import java.util.List;

import speiger.src.collections.objects.lists.ObjectArrayList;
import speiger.src.collections.objects.utils.ObjectIterables;
import speiger.src.collections.utils.HashUtil;
import speiger.src.coreengine.assets.AssetLocation;
import speiger.src.coreengine.rendering.textures.custom.AtlasStitcher.Entry;

/**
 * Inspired by: <a href=https://github.com/lukaszdk/texture-atlas-generator/blob/master/AtlasGenerator.java>AtlasGenerator</a>
 */
public class AtlasStitcher<T extends Entry> {
	
	final int maxWidth;
	final int maxHeight;
	int width;
	int height;
	int pixelsUsed;
	boolean valid = true;
	List<Record<T>> toStitch = new ObjectArrayList<>();
	Slot<T> slot = null;
	
	public AtlasStitcher(int bounds) {
		this(bounds, bounds);
	}
	
	public AtlasStitcher(int maxWidth, int maxHeight) {
		this.maxWidth = maxWidth;
		this.maxHeight = maxHeight;
		if(maxWidth < 256 || maxHeight < 256) throw new IndexOutOfBoundsException("Minimum Size has to be 256x256 pixels");
	}
	
	public int width() { return width; }
	public int height() { return height; }
	public boolean isValid() { return valid; }
	
	public void add(T entry) { add(new Record<>(entry)); }
	public void addAll(Iterable<? extends T> iterables) { ObjectIterables.map(iterables, Record<T>::new).forEach(this::add); }
	@SuppressWarnings("unchecked")
	public void addAll(T...entries) {
		for(T entry : entries) {
			add(new Record<>(entry));
		}
	}
	
	private void add(Record<T> entry) {
		toStitch.add(entry);
		pixelsUsed += entry.pixels();
	}
	
	public void stitch() {
		toStitch.sort(null);
		int expected = HashUtil.nextPowerOfTwo((int)Math.sqrt(pixelsUsed)) / 2;
		if(expected > maxWidth && expected > maxHeight) {
			valid = false;
			return;
		}
		for(Record<T> entry : toStitch) {
			if(!addToSlot(entry)) {
				valid = false;
				return;
			}
		}
	}
	
	public void process(IAtlasScanner<T> scanner) {
		if(slot == null) return;
		slot.scan(scanner);
	}
	
	private boolean addToSlot(Record<T> entry) {
		if(slot != null && slot.insert(entry)) return true;
		if(expand(entry)) {
			width = slot.width;
			height = slot.height;
			return true;
		}
		return false;
	}
	
	private boolean expand(Record<T> entry) {
		int width = HashUtil.nextPowerOfTwo(entry.width());
		int height = HashUtil.nextPowerOfTwo(entry.height());
		if(width > maxWidth || height > maxHeight) return false;
		if(slot == null) {
			int min = HashUtil.nextPowerOfTwo((int)Math.sqrt(pixelsUsed)) / 2;
			slot = new Slot<>(0, 0, Math.max(width, min), Math.max(height, min));
			return slot.insert(entry);
		}
		if(width > slot.width) {
			if(slot.height * 2 > maxHeight) return false;
			slot = new Slot<>(slot, width - slot.width, slot.height);
			return slot.insert(entry);
		}
		slot = slot.height >= slot.width ? new Slot<>(slot, slot.width, 0) : new Slot<>(slot, 0, slot.height);
		return slot.insert(entry);		
	}
		
	private static record Record<T extends Entry>(T entry, int width, int height) implements Comparable<Record<T>> {
		public Record(T entry) {
			this(entry, entry.width(), entry.height()); 
		}
		
		public int pixels() { return width() * height(); }
		
		@Override
		public int compareTo(Record<T> o) {
			int comp = Integer.compare(o.height, height);
			if(comp != 0) return comp;
			comp = Integer.compare(o.width, width);
			if(comp != 0) return comp;
			return entry.id().compareTo(o.entry.id());
		}
	}
	
	private static class Slot<T extends Entry> {
		int x;
		int y;
		int width;
		int height;
		Record<T> record;
		Slot<T>[] children = null;
		
		public Slot(int x, int y, int width, int height) {
			this.x = x;
			this.y = y;
			this.width = width;
			this.height = height;
		}
		
		@SuppressWarnings("unchecked")
		public Slot(Slot<T> slot, int eWidth, int eHeight) {
			x = 0;
			y = 0;
			width = slot.width + eWidth;
			height = slot.height + eHeight;
			children = new Slot[eWidth > 0 && eHeight > 0 ? 3 : 2];
			children[0] = slot;
			expandSlot(slot.width, slot.height, eWidth, eHeight);
		}
		
		public boolean isLeaf() { return children == null; }
		
		public void scan(IAtlasScanner<T> scanner) {
			if(record != null) {
				scanner.accept(record.entry(), x, y);
				return;
			}
			if(children != null) {
				for(int i = 0,m = children.length;i < m;i++) {
					children[i].scan(scanner);
				}
			}
		}
		
		@SuppressWarnings("unchecked")
		public boolean insert(Record<T> record) {
			if(isLeaf()) {
				int rw = record.width();
				int rh = record.height();
				if(this.record != null || rw > width || rh > height) return false;
				if(rw == width && rh == height) {
					this.record = record;
					return true;
				}
				int dw = width - rw;
				int dh = height - rh;
				children = new Slot[dw > 0 && dh > 0 ? 3 : 2];
				children[0] = new Slot<>(x, y, rw, rh);
				expandSlot(rw, rh, dw, dh);
				return children[0].insert(record);
			}
			for(int i = 0,m = children.length;i < m;i++) {
				if(children[i].insert(record)) return true;
			}
			return false;
		}
		
		private void expandSlot(int rw, int rh, int dw, int dh) {
			if(dw > 0 && dh > 0) {
				if(dw >= dh) {
					children[1] = new Slot<>(x + rw, y, dw, rh);
					children[2] = new Slot<>(x, y + rh, width, dh);
				}
				else {
					children[1] = new Slot<>(x, y + rh, rw, dh);
					children[2] = new Slot<>(x + rw, y, dw, height);
				}
			}
			else if(dw == 0) children[1] = new Slot<>(x, y + rh, rw, dh);
			else if(dh == 0) children[1] = new Slot<>(x + rw, y, dw, rh);
		}
		
		@Override
		public String toString() {
			return "Slot[x="+x+", y="+y+", w="+width+", h="+height+"]";
		}
	}
	
	public static interface IAtlasScanner<T> {
		public void accept(T entry, int x, int y);
	}
	
	public static interface Entry {
		public AssetLocation id();
		public int width();
		public int height();
	}
}
