package speiger.src.coreengine.math.direction;

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.direction.Direction.Axis;
import speiger.src.coreengine.math.vector.ints.Vec2i;

public final class DirectionList implements Iterable<Direction>, Predicate<Direction> {
	static final DirectionList[] FACINGS = createArray();
	public static final DirectionList EMPTY = ofNumber(0);
	public static final DirectionList NORTH = ofFacings(Direction.NORTH);
	public static final DirectionList EAST = ofFacings(Direction.EAST);
	public static final DirectionList SOUTH = ofFacings(Direction.SOUTH);
	public static final DirectionList WEST = ofFacings(Direction.WEST);
	public static final DirectionList NORTH_EAST = ofFacings(Direction.NORTH, Direction.EAST);
	public static final DirectionList SOUTH_EAST = ofFacings(Direction.EAST, Direction.SOUTH);
	public static final DirectionList SOUTH_WEST = ofFacings(Direction.SOUTH, Direction.WEST);
	public static final DirectionList NORTH_WEST = ofFacings(Direction.WEST, Direction.NORTH);
	public static final DirectionList VERTICAL = ofFacings(Direction.NORTH, Direction.SOUTH);
	public static final DirectionList HORIZONTAL = ofFacings(Direction.EAST, Direction.WEST);
	public static final DirectionList ALL = ofFacings(Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST);
	final byte code;
	final byte next;
	final byte opposite;
	final byte prev;
	final byte count;
	final Vec2i offset;
	final Direction[] array;
	
	private DirectionList(int initCode) {
		code = (byte)MathUtils.clamp(0, 15, initCode);
		Vec2i pos = Vec2i.mutable();
		ObjectList<Direction> facings = new ObjectArrayList<>();
		for(int i = 0;i < 4;i++) {
			if((code & 1 << i) != 0) {
				pos.add(Direction.byIndex(i).getOffset());
				facings.add(Direction.byIndex(i));
			}
		}
		int[] data = new int[3];
		for(int i = 0,m = facings.size();i < m;i++) {
			Direction 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 Direction[facings.size()]);
	}
	
	public static DirectionList ofFacing(Direction facing) { return FACINGS[1 << facing.getIndex()]; }
	public static DirectionList ofFacings(Direction... facings) { return FACINGS[toNumber(facings)]; }
	public static DirectionList ofFlags(boolean[] facings) { return FACINGS[toNumber(facings)]; }
	public static DirectionList ofNumber(int value) { return FACINGS[value & 15]; }
	public static DirectionList ofAxis(Axis axis) { return FACINGS[axis.getCode()]; }
	
	public static DirectionList 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<Direction> toFacings() { return isEmpty() ? EnumSet.noneOf(Direction.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 DirectionList 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 DirectionList invert() { return FACINGS[15 - code]; }
	public DirectionList opposite() { return FACINGS[opposite]; }
	public DirectionList add(Direction facing) { return FACINGS[code | (1 << facing.getIndex())]; }
	public DirectionList add(DirectionList facings) { return FACINGS[code | facings.code]; }
	public DirectionList remove(Direction facing) { return FACINGS[code & ~(1 << facing.getIndex())]; }
	public DirectionList remove(DirectionList facings) { return FACINGS[code & ~facings.code]; }
	public boolean contains(Direction direction) { return (code & 1 << direction.getIndex()) != 0; }
	public boolean contains(DirectionList facings) { return (code & facings.code) == facings.code; }
	public boolean containsAny(DirectionList facings) { return (code & facings.code) != 0; }
	public boolean notContains(Direction direction) { return (code & 1 << direction.getIndex()) == 0; }
	public boolean notContains(DirectionList facings) { return (code & facings.code) != facings.code; }
	public DirectionList flipFacing(Direction facing) { return contains(facing) ? remove(facing).add(facing.opposite()) : this; }
	public DirectionList flipAxis(Axis axis) {
		DirectionList 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 Direction 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(Direction t) { return (code & 1 << t.getIndex()) != 0; }
	
	@Override
	public Iterator<Direction> iterator() {
		return new Iterator<Direction>() {
			int index = 0;
			
			@Override
			public boolean hasNext() { return index < size(); }
			@Override
			public Direction next() { return array[index++]; }
		};
	}
	
	public static int toNumber(Direction... 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(Direction... 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 DirectionList[] createArray() {
		DirectionList[] facings = new DirectionList[16];
		for(int i = 0;i < 16;i++) { facings[i] = new DirectionList(i); }
		return facings;
	}
}
