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

import org.lwjgl.glfw.GLFW;

import speiger.src.collections.ints.sets.IntOpenHashSet;
import speiger.src.collections.ints.sets.IntSet;
import speiger.src.coreengine.math.vector.ints.Vec2i;
import speiger.src.coreengine.rendering.input.devices.Mouse.MouseData;
import speiger.src.coreengine.rendering.input.devices.Mouse.MouseTask;
import speiger.src.coreengine.rendering.input.events.MouseEvent;
import speiger.src.coreengine.rendering.input.window.Window;

public class Mouse extends AbstractDevice<MouseData, MouseTask> {
	public static final Mouse INSTANCE = new Mouse();
	
	@Override
	protected MouseData createData(long windowId) { return new MouseData(); }
	@Override
	public void reset(long windowId) { get(windowId).reset(); }
	protected void process(MouseTask task) { task.process(this); }
	
	@Override
	protected void registerCallbacks(Window window) {
		super.registerCallbacks(window);
		window.addCallback(this::move, GLFW::glfwSetCursorPosCallback);
		window.addCallback(this::click, GLFW::glfwSetMouseButtonCallback);
		window.addCallback(this::enter, GLFW::glfwSetCursorEnterCallback);
		window.addCallback(this::scroll, GLFW::glfwSetScrollCallback);
	}
	
	//@formatter:off
	private void move(long window, double x, double y) { push(window, new Move(window, x, y)); }
	private void click(long window, int button, int action, int mods) { push(window, new Click(window, button, action, mods)); }
	private void enter(long window, boolean enter) { push(window, new Enter(window, enter)); }
	private void scroll(long window, double xoffset, double yoffset) { push(window, new Scroll(window, xoffset, yoffset)); }
	//@formatter:on
	
	public static class MouseData {
		IntSet buttons = new IntOpenHashSet();
		Vec2i position = Vec2i.mutable();
		Vec2i motion = Vec2i.mutable();
		Vec2i scroll = Vec2i.mutable();
		boolean active = true;
		
		void reset() {
			motion.negate();
			position.negate();
			scroll.negate();
		}
		
		//@formatter:off
		public int x() { return position.x(); }
		public int y() { return position.y(); }
		public boolean pressed(int button) { return buttons.contains(button); }
		public Vec2i motion() { return motion; }
		public Vec2i scroll() { return scroll; }
		//@formatter:on
	}
	
	public int x(long windowId) {
		MouseData data = get(windowId);
		return data == null ? 0 : data.x();
	}
	
	public int y(long windowId) {
		MouseData data = get(windowId);
		return data == null ? 0 : data.y();
	}
	
	public boolean pressed(long windowId, int button) {
		MouseData data = get(windowId);
		return data != null && data.pressed(button);
	}
	
	public Vec2i motion(long windowId) {
		MouseData data = get(windowId);
		return data == null ? Vec2i.ZERO : data.motion();
	}
	
	public Vec2i scroll(long windowId) {
		MouseData data = get(windowId);
		return data == null ? Vec2i.ZERO : data.scroll();
	}
	
	public void setPosition(long windowId, int x, int y) {
		MouseData data = get(windowId);
		if(data == null || (data.position.x() == x && data.position.y() == y)) return;
		data.position.set(x, y);
		GLFW.glfwSetCursorPos(windowId, x, y);
	}
	
	public void pressButton(long window, int button, boolean press) { click(window, button, press ? GLFW.GLFW_PRESS : GLFW.GLFW_RELEASE, 0); }
	public void scrollMouse(long window, int xScroll, int yScroll) { scroll(window, xScroll, yScroll); }
	
	public interface MouseTask {
		public void process(Mouse mouse);
	}
	
	private record Move(long window, double x, double y) implements MouseTask {
		@Override
		public void process(Mouse mouse) {
			MouseData data = mouse.get(window);
			int xOff = (int)(x - data.position.x());
			int yOff = (int)(y - data.position.y());
			data.position.set((int)x, (int)y);
			if(!mouse.pushEvent(new MouseEvent.Move(window, (int)x, (int)y, xOff, yOff))) {
				data.motion.add(xOff, yOff);
			}
		}
	}
	
	private record Click(long window, int button, int action, int mods) implements MouseTask {
		@Override
		public void process(Mouse mouse) {
			MouseData data = mouse.get(window);
			if(action == GLFW.GLFW_PRESS) data.buttons.add(action);
			else if(action == GLFW.GLFW_RELEASE) data.buttons.remove(action);
			mouse.pushEvent(new MouseEvent.Click(window, data.position.x(), data.position.y(), button, action == GLFW.GLFW_PRESS));
		}
	}
	
	private record Scroll(long window, double x, double y) implements MouseTask {
		@Override
		public void process(Mouse mouse) {
			MouseData data = mouse.get(window);
			if(!mouse.pushEvent(new MouseEvent.Scroll(window, data.position.x(), data.position.y(), (int)x, (int)y))) {
				data.scroll.add((int)x, (int)y);
			}
		}
	}
	
	private record Enter(long window, boolean enter) implements MouseTask {
		@Override
		public void process(Mouse mouse) {
			MouseData data = mouse.get(window);
			data.active = enter;
			if(!enter) {
				//TODO decide if this should also push phantom events clearing the pressed keys?
				data.buttons.clear();
				data.position.negate();
				data.reset();
			}
		}
	}
}