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

import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.function.BiConsumer;

import speiger.src.collections.objects.lists.ObjectArrayList;
import speiger.src.collections.objects.maps.impl.hash.Object2ObjectLinkedOpenHashMap;
import speiger.src.collections.objects.maps.interfaces.Object2ObjectMap;
import speiger.src.collections.objects.misc.pairs.ObjectObjectPair;
import speiger.src.collections.objects.sets.ObjectOpenHashSet;
import speiger.src.collections.objects.utils.ObjectIterators;
import speiger.src.coreengine.assets.AssetLocation;
import speiger.src.coreengine.math.vector.ints.Vec2i;

/**
 * Inspired by: <a href=https://github.com/lukaszdk/texture-atlas-generator/blob/master/AtlasGenerator.java>AtlasGenerator</a>
 */
public class TextureAtlas {
	int width;
	int height;
	Object2ObjectMap<AssetLocation, AtlasEntry> textures;
	
	protected TextureAtlas(Vec2i bounds, Object2ObjectMap<AssetLocation, AtlasEntry> textures) {
		width = bounds.x();
		height = bounds.y();
		this.textures = textures;
	}
	
	public int getWidth() { return width; }
	
	public int getHeight() { return height; }
	
	public AtlasEntry getTexture(AssetLocation texture) {
		return textures.getObject(texture);
	}
	
	public Iterator<AtlasEntry> getContents() { return ObjectIterators.unmodifiable(textures.values().iterator()); }
	
	public static Builder create() {
		return new Builder();
	}
	
	public static class Builder {
		Set<AssetLocation> names = new ObjectOpenHashSet<>();
		List<Record> records = new ObjectArrayList<>();
		int pixelsUsed = 0;
		
		public boolean add(AssetLocation location, int width, int height) {
			return add(location, width, height, 0);
		}
		
		public boolean add(AssetLocation location, int width, int height, int padding) {
			if(location == null || width <= 0 || height <= 0 || padding < 0 || !names.add(location)) return false;
			records.add(new Record(location, width, height, padding));
			pixelsUsed += (width + padding) * (height + padding);
			return true;
		}
		
		private ObjectObjectPair<Vec2i, Object2ObjectMap<AssetLocation, AtlasEntry>> stitch() {
			int textureWidth = 2;
			int textureHeight = 2;
			boolean height = false;
			for(;textureHeight * textureWidth <= pixelsUsed;height = !height) {
				if(height) textureHeight *= 2;
				else textureWidth *= 2;
			}
			records.sort(null);
			int attempts = 0;
			while(attempts < 50) {
				Slot slot = new Slot(0, 0, textureWidth, textureHeight);
				boolean failed = false;
				for(int i = 0,m = records.size();i < m;i++) {
					if(slot.addRecord(records.get(i))) continue;
					failed = true;
					break;
				}
				if(failed) {
					if(height) textureHeight *= 2;
					else textureWidth *= 2;
					attempts++;
					height = !height;
					continue;
				}
				Object2ObjectMap<AssetLocation, AtlasEntry> entries = new Object2ObjectLinkedOpenHashMap<>();
				slot.build(entries::put);
				return ObjectObjectPair.of(Vec2i.of(textureWidth, textureHeight), entries);
			}
			throw new IllegalStateException("Couldn't fit Texture Atlas after growing it 50 Times");
		}
		
		public void buildHollow(BiConsumer<Vec2i, Iterable<AtlasEntry>> builder) {
			ObjectObjectPair<Vec2i, Object2ObjectMap<AssetLocation, AtlasEntry>> pairs = stitch();
			builder.accept(pairs.getKey(), pairs.getValue().values());
		}
		
		public TextureAtlas build(BiConsumer<Vec2i, Iterable<AtlasEntry>> builder) {
			ObjectObjectPair<Vec2i, Object2ObjectMap<AssetLocation, AtlasEntry>> pairs = stitch();
			builder.accept(pairs.getKey(), pairs.getValue().values());
			return new TextureAtlas(pairs.getKey(), pairs.getValue());
		}
		
		public TextureAtlas build() {
			ObjectObjectPair<Vec2i, Object2ObjectMap<AssetLocation, AtlasEntry>> pairs = stitch();
			return new TextureAtlas(pairs.getKey(), pairs.getValue());
		}
	}
	
	public static class AtlasEntry {
		AssetLocation location;
		int x;
		int y;
		int width;
		int height;
		
		private AtlasEntry(Slot slot) {
			Record record = slot.record;
			location = record == null ? null : record.location();
			x = slot.x;
			y = slot.y;
			width = record == null ? slot.width : record.width;
			height = record == null ? slot.height : record.height;
		}
		
		public AssetLocation getLocation() { return location; }
		
		public int getX() { return x; }
		
		public int getY() { return y; }
		
		public int getWidth() { return width; }
		
		public int getHeight() { return height; }
	}
	
	private static class Slot {
		int x;
		int y;
		int width;
		int height;
		Record record;
		Slot[] children = null;
		
		public Slot(int x, int y, int width, int height) {
			this.x = x;
			this.y = y;
			this.width = width;
			this.height = height;
		}
		
		public boolean isLeaf() { return children == null; }
		
		public void build(BiConsumer<AssetLocation, AtlasEntry> builder) {
			if(!isLeaf()) {
				for(int i = 0,m = children.length;i < m;i++) {
					children[i].build(builder);
				}
				return;
			}
			if(record == null) return;
			builder.accept(record.location(), new AtlasEntry(this));
		}
		
		public boolean addRecord(Record 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 p = record.padding();
				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);
				if(dw > 0 && dh > 0) {
					if(dw > dh) {
						children[1] = new Slot(x + rw + p, y, dw - p, rh);
						children[2] = new Slot(x, y + rh + p, width, dh - p);
					}
					else {
						children[1] = new Slot(x, y + rh + p, rw, dh - p);
						children[2] = new Slot(x + rw + p, y, dw - p, height);
					}
				}
				else if(dw == 0) children[1] = new Slot(x, y + rh + p, rw, dh - p);
				else if(dh == 0) children[1] = new Slot(x + rw + p, y, dw - p, rh);
				return children[0].addRecord(record);
			}
			for(int i = 0,m = children.length;i < m;i++) {
				if(children[i].addRecord(record)) return true;
			}
			return false;
		}
	}
	
	private static record Record(AssetLocation location, int width, int height, int padding) implements Comparable<Record> {
		private int getPixelCount() { return (width + padding) * (height + padding); }
		
		@Override
		public int compareTo(Record o) {
			int result = Integer.compare(o.getPixelCount(), getPixelCount());
			return result == 0 ? location().compareTo(o.location()) : result;
		}
	}
}