package speiger.src.coreengine.rendering.input.devices;

import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.util.BitSet;

import org.lwjgl.glfw.GLFW;

import speiger.src.collections.floats.lists.FloatArrayList;
import speiger.src.collections.floats.lists.FloatList;
import speiger.src.collections.ints.collections.IntIterator;
import speiger.src.collections.ints.maps.impl.hash.Int2ObjectOpenHashMap;
import speiger.src.collections.ints.maps.interfaces.Int2ObjectMap;
import speiger.src.collections.ints.sets.IntLinkedOpenHashSet;
import speiger.src.collections.ints.sets.IntSet;
import speiger.src.collections.longs.collections.LongIterator;
import speiger.src.collections.longs.sets.LongOpenHashSet;
import speiger.src.collections.longs.sets.LongSet;
import speiger.src.coreengine.rendering.input.devices.Joystick.JoyStickData;
import speiger.src.coreengine.rendering.input.devices.Joystick.JoyStickTask;
import speiger.src.coreengine.rendering.input.events.JoystickEvent;
import speiger.src.coreengine.rendering.input.window.WindowManager;
import speiger.src.coreengine.utils.eventbus.EventBus;

public class Joystick extends AbstractDevice<JoyStickData, JoyStickTask> {
	public static final Joystick INSTANCE = new Joystick();
	WindowManager manager;
	IntSet presentJoysticks = new IntLinkedOpenHashSet();
	LongSet alwaysProcessInputs = new LongOpenHashSet();
	
	public void init(WindowManager manager, EventBus bus) {
		this.manager = manager;
		super.init(bus);
		manager.addCallback(this::plugin, GLFW::glfwSetJoystickCallback);
		for(int i = 0,m=GLFW.GLFW_JOYSTICK_LAST;i<=m;i++) {
			if(GLFW.glfwJoystickPresent(i)) {
				plugin(i, GLFW.GLFW_CONNECTED);
			}
		}
	}
	
	@Override
	public void init(EventBus bus) { throw new UnsupportedOperationException("Use init(WindowManager, EventBus) instead"); }
	
	public void setWindowAlwaysProcessInputs(long windowId, boolean value) {
		if(value) alwaysProcessInputs.add(windowId);
		else alwaysProcessInputs.remove(windowId);
	}
	
	public boolean isWindowAlwaysProcessingInputs(long windowId) {
		return alwaysProcessInputs.contains(windowId);
	}
	
	private void plugin(int jid, int event) {
		if(event == GLFW.GLFW_CONNECTED) presentJoysticks.add(jid);
		else presentJoysticks.remove(jid);
		for(LongIterator iter = knownWindows().iterator();iter.hasNext();) {
			long window = iter.nextLong();
			push(window, new Plugin(window, jid, event));
		}
	}
	
	@Override
	public void reset(long windowId) {}
	@Override
	protected JoyStickData createData(long windowId) { return new JoyStickData(presentJoysticks); }
	@Override
	protected void process(JoyStickTask task) { task.process(this); }
	
	@Override
	public void processInput(long windowId) {
		super.processInput(windowId);
		if(manager.getActiveWindow() != windowId && !alwaysProcessInputs.contains(windowId)) return;
		JoyStickData data = get(windowId);
		if(data == null) return;
		for(IntIterator iter = presentJoysticks.iterator();iter.hasNext();) {
			int jid = iter.nextInt();
			ButtonData buttons = data.data.get(jid);
			if(buttons == null) continue;
			handleJoystick(windowId, jid, buttons);
		}
	}
	
	public void handleJoystick(long window, int jid, ButtonData data) {
		ByteBuffer buttons = GLFW.glfwGetJoystickButtons(jid);
		for(int i = 0;buttons.hasRemaining();i++) {
			boolean state = buttons.get() == GLFW.GLFW_PRESS;
			if(data.buttons.get(i) != state) {
				data.buttons.set(i, state);
				pushEvent(new JoystickEvent.Button(window, jid, i, state));
			}
		}
		FloatBuffer axis = GLFW.glfwGetJoystickAxes(jid);
		for(int i = 0;axis.hasRemaining();i++) {
			float state = axis.get();
			if(i >= data.axis.size()) data.axis.add(state);
			else data.axis.set(i, state);
			boolean negative = state < 0F;
			boolean max = state < -0.95F || state > 0.95F;
			boolean min = (state < -0.5F || state > 0.5F) && !max;
			if(data.axisState.get(i*3) != negative || data.axisState.get(i*3+1) != max || data.axisState.get(i*3+2) != min) {
				data.axisState.set(i*3, negative);
				data.axisState.set(i*3+1, max);
				data.axisState.set(i*3+2, min);
				pushEvent(new JoystickEvent.Axis(window, jid, i, state, negative, min, max));
			}
		}
	}
	
	public static class JoyStickData {
		Int2ObjectMap<ButtonData> data = new Int2ObjectOpenHashMap<>();
		
		public JoyStickData(IntSet knownJoysticks) {
			for(IntIterator iter = knownJoysticks.iterator();iter.hasNext();) {
				data.put(iter.nextInt(), new ButtonData());
			}
		}
	}
	
	public static class ButtonData {
		BitSet buttons = new BitSet();
		FloatList axis = new FloatArrayList();
		BitSet axisState = new BitSet();
	}
	
	public static interface JoyStickTask {
		public void process(Joystick stick);
	}
	
	public record Plugin(long window, int jid, int event) implements JoyStickTask {
		@Override
		public void process(Joystick stick) {
			JoyStickData data = stick.get(window);
			if(event == GLFW.GLFW_CONNECTED) data.data.put(jid, new ButtonData());
			else if(event == GLFW.GLFW_DISCONNECTED) data.data.remove(jid);
			stick.pushEvent(new JoystickEvent.Connected(window, jid, event == GLFW.GLFW_CONNECTED));
		}
	}
}