package speiger.src.coreengine.math.misc;

import java.util.EnumSet;
import java.util.Iterator;
import java.util.Set;
import java.util.StringJoiner;
import java.util.function.Predicate;

import speiger.src.collections.objects.lists.ObjectArrayList;
import speiger.src.collections.objects.lists.ObjectList;
import speiger.src.coreengine.math.MathUtils;
import speiger.src.coreengine.math.misc.Facing.Axis;
import speiger.src.coreengine.math.vector.ints.Vec2i;

public final class FacingList implements Iterable<Facing>, Predicate<Facing> {
	static final FacingList[] FACINGS = createArray();
	public static final FacingList EMPTY = ofNumber(0);
	public static final FacingList NORTH = ofFacings(Facing.NORTH);
	public static final FacingList EAST = ofFacings(Facing.EAST);
	public static final FacingList SOUTH = ofFacings(Facing.SOUTH);
	public static final FacingList WEST = ofFacings(Facing.WEST);
	public static final FacingList NORTH_EAST = ofFacings(Facing.NORTH, Facing.EAST);
	public static final FacingList SOUTH_EAST = ofFacings(Facing.EAST, Facing.SOUTH);
	public static final FacingList SOUTH_WEST = ofFacings(Facing.SOUTH, Facing.WEST);
	public static final FacingList NORTH_WEST = ofFacings(Facing.WEST, Facing.NORTH);
	public static final FacingList VERTICAL = ofFacings(Facing.NORTH, Facing.SOUTH);
	public static final FacingList HORIZONTAL = ofFacings(Facing.EAST, Facing.WEST);
	public static final FacingList ALL = ofFacings(Facing.NORTH, Facing.SOUTH, Facing.EAST, Facing.WEST);
	final byte code;
	final byte next;
	final byte opposite;
	final byte prev;
	final byte count;
	final Vec2i offset;
	final Facing[] array;
	
	private FacingList(int initCode) {
		code = (byte)MathUtils.clamp(0, 15, initCode);
		Vec2i pos = Vec2i.mutable();
		ObjectList<Facing> facings = new ObjectArrayList<>();
		for(int i = 0;i < 4;i++) {
			if((code & 1 << i) != 0) {
				pos.add(Facing.byIndex(i).getOffset());
				facings.add(Facing.byIndex(i));
			}
		}
		int[] data = new int[3];
		for(int i = 0,m = facings.size();i < m;i++) {
			Facing face = facings.get(i);
			data[0] |= 1 << face.forward().getIndex();
			data[1] |= 1 << face.opposite().getIndex();
			data[2] |= 1 << face.backwards().getIndex();
		}
		next = (byte)MathUtils.clamp(0, 15, data[0]);
		opposite = (byte)MathUtils.clamp(0, 15, data[1]);
		prev = (byte)MathUtils.clamp(0, 15, data[2]);
		offset = pos.asImmutable();
		count = (byte)facings.size();
		array = facings.toArray(new Facing[facings.size()]);
	}
	
	public static FacingList ofFacing(Facing facing) { return FACINGS[1 << facing.getIndex()]; }
	public static FacingList ofFacings(Facing... facings) { return FACINGS[toNumber(facings)]; }
	public static FacingList ofFlags(boolean[] facings) { return FACINGS[toNumber(facings)]; }
	public static FacingList ofNumber(int value) { return FACINGS[value & 15]; }
	public static FacingList ofAxis(Axis axis) { return FACINGS[axis.getCode()]; }
	
	public static FacingList fromVec(Vec2i value) {
		value = value.clamp(-1, 1);
		for(int i = 0;i < 16;i++) { if(FACINGS[i].getOffset().equals(value)) { return FACINGS[i]; } }
		return FACINGS[0];
	}
	
	public Set<Facing> toFacings() { return isEmpty() ? EnumSet.noneOf(Facing.class) : EnumSet.copyOf(ObjectArrayList.wrap(array)); }
	public boolean[] toFlags() { return toFlags(array); }
	
	public int getRotation() {
		switch(count) {
			case 1: return array[0].getRotation();
			case 2: return array[0].getRotation(array[1]);
			default: return 0;
		}
	}
	
	public FacingList rotate(int amount) {
		switch(amount & 3) {
			case 1: return FACINGS[next];
			case 2: return FACINGS[opposite];
			case 3: return FACINGS[prev];
			default: return this;
		}
	}
	
	public FacingList invert() { return FACINGS[15 - code]; }
	public FacingList opposite() { return FACINGS[opposite]; }
	public FacingList add(Facing facing) { return FACINGS[code | (1 << facing.getIndex())]; }
	public FacingList add(FacingList facings) { return FACINGS[code | facings.code]; }
	public FacingList remove(Facing facing) { return FACINGS[code & ~(1 << facing.getIndex())]; }
	public FacingList remove(FacingList facings) { return FACINGS[code & ~facings.code]; }
	public boolean contains(Facing direction) { return (code & 1 << direction.getIndex()) != 0; }
	public boolean contains(FacingList facings) { return (code & facings.code) == facings.code; }
	public boolean containsAny(FacingList facings) { return (code & facings.code) != 0; }
	public boolean notContains(Facing direction) { return (code & 1 << direction.getIndex()) == 0; }
	public boolean notContains(FacingList facings) { return (code & facings.code) != facings.code; }
	public FacingList flipFacing(Facing facing) { return contains(facing) ? remove(facing).add(facing.opposite()) : this; }
	public FacingList flipAxis(Axis axis) {
		FacingList result = this;
		for(int i = 0,m = array.length;i < m;i++) { if(array[i].getAxis() == axis) { result = result.remove(array[i]).add(array[i].opposite()); } }
		return result;
	}
	
	public Vec2i getOffset() { return offset; }
	public Facing getFacing(int index) { return array[index]; }
	public int getCode() { return code; }
	public int size() { return count; }
	public boolean isEmpty() { return code == 0; }
	public boolean isFull() { return code == 15; }
	
	@Override
	public String toString() {
		StringJoiner joiner = new StringJoiner(",", "[", "]");
		for(int i = 0,m = array.length;i < m;i++) { joiner.add(array[i].getName()); }
		return joiner.toString();
	}
	
	@Override
	public boolean test(Facing t) { return (code & 1 << t.getIndex()) != 0; }
	
	@Override
	public Iterator<Facing> iterator() {
		return new Iterator<Facing>() {
			int index = 0;
			
			@Override
			public boolean hasNext() { return index < size(); }
			@Override
			public Facing next() { return array[index++]; }
		};
	}
	
	public static int toNumber(Facing... facings) {
		int value = 0;
		for(int i = 0,m = facings.length;i < m;i++) { value |= (1 << facings[i].getIndex()); }
		return value & 15;
	}
	
	public static int toNumber(boolean[] facings) { return (facings[0] ? 1 : 0) << 0 | (facings[1] ? 1 : 0) << 1 | (facings[2] ? 1 : 0) << 2 | (facings[3] ? 1 : 0) << 3; }
	
	public static boolean[] toFlags(Facing... facings) {
		boolean[] array = new boolean[4];
		for(int i = 0,m = facings.length;i < m;i++) { array[facings[i].getIndex()] = true; }
		return array;
	}
	
	private static FacingList[] createArray() {
		FacingList[] facings = new FacingList[16];
		for(int i = 0;i < 16;i++) { facings[i] = new FacingList(i); }
		return facings;
	}
}
