#pragma once

#include <SFML/Graphics.hpp>
#include <string>
#include <utility>
#include <vector>
#include <unordered_map>
#include <functional>
#include <fstream>
#include <iostream>
#include <sstream>

// l_ (local), m_ (member)

enum class EventType {
	KeyDown = sf::Event::KeyPressed,
	KeyUp = sf::Event::KeyReleased,
	MButtonDown = sf::Event::MouseButtonPressed,
	MButtonUp = sf::Event::MouseButtonReleased,
	MouseWheel = sf::Event::MouseWheelMoved,
	WindowResized = sf::Event::Resized,
	GainedFocus = sf::Event::GainedFocus,
	LostFocus = sf::Event::LostFocus,
	MouseEntered = sf::Event::MouseEntered,
	MouseLeft = sf::Event::MouseLeft,
	Closed = sf::Event::Closed,
	TextEntered = sf::Event::TextEntered,
	Keyboard = sf::Event::Count + 1,
	Mouse,
	Joystick
};

struct EventInfo {
	EventInfo() : m_code(0) {}
	EventInfo(int l_code) : m_code(l_code) {}
	union {
		int m_code;
	};
};

using Events = std::vector<std::pair<EventType, EventInfo>>;

struct EventDetails {
	EventDetails(const std::string& l_bindName)
		: m_name(l_bindName) {
		Clear();
	}
	std::string m_name;

	sf::Vector2i m_size;
	sf::Uint32 m_textEntered;
	sf::Vector2i m_mouse;
	int m_mouseWheelDelta;
	int m_keyCode;

	void Clear() {
		m_size = sf::Vector2i(0, 0);
		m_textEntered = 0;
		m_mouse = sf::Vector2i(0, 0);
		m_mouseWheelDelta = 0;
		m_keyCode = -1;
	}
};

struct Binding {
	Binding(const std::string& l_name)
		: m_name(l_name), m_details(l_name), c(0) {}
	void BindEvent(EventType l_type,
		EventInfo l_info = EventInfo())
	{
		m_events.emplace_back(l_type, l_info);
	}

	Events m_events;
	std::string m_name;
	int c; // brojac evenata koji se dogadaju

	EventDetails m_details;
};

using Bindings = std::unordered_map<std::string, Binding*>;
using Callbacks = std::unordered_map<std::string,
	std::function<void(EventDetails*)>>;

class EventManager {
public:
	EventManager();
	~EventManager();

	bool AddBinding(Binding* l_binding);
	bool RemoveBinding(std::string l_name);
	void SetFocus(const bool& l_focus) {
		m_hasFocus = l_focus;
	}

	template<class T>
	bool AddCallback(const std::string& l_name,
		void(T::*l_func)(EventDetails*), T* l_instance) {
		auto temp = std::bind(l_func, l_instance,
			std::placeholders::_1);
		return m_callbacks.emplace(l_name, temp).second;
	}

	void RemoveCallback(const std::string& l_name) {
		m_callbacks.erase(l_name);
	}

	void HandleEvent(sf::Event& l_event);
	void Update();

	sf::Vector2i GetMousePos(sf::RenderWindow* l_wind = nullptr) {
		return (l_wind ? sf::Mouse::getPosition(*l_wind)
			: sf::Mouse::getPosition());
	}

private:
	void LoadBindings();

	Bindings m_bindings;
	Callbacks m_callbacks;
	bool m_hasFocus;
};

EventManager::EventManager()
	: m_hasFocus(true) {
	LoadBindings();
}

EventManager::~EventManager() {
	for (auto& itr : m_bindings) {
		delete itr.second;
		itr.second = nullptr;
	}
}

// dodaje binding ako vec ne postoji takav s istim imenom
// vraca bool - korisno zbog provjere gresaka
bool EventManager::AddBinding(Binding* l_binding) {
	if (m_bindings.find(l_binding->m_name) != m_bindings.end())
		return false;

	return m_bindings.emplace(l_binding->m_name,
		l_binding).second;
}

bool EventManager::RemoveBinding(std::string l_name) {
	auto itr = m_bindings.find(l_name);
	if (itr == m_bindings.end())
		return false;
	// prvo oslobodimo vrijednost u paru kljuc-vrijednost
	// a onda uklanjamo pokazivac iz spremnika m_bindings
	delete itr->second;
	m_bindings.erase(itr);
	return true;
}

// kod izvlacenja dogadaja u svakoj iteraciji trebamo
// pogledati ima li neki koji nam je od interesa
void EventManager::HandleEvent(sf::Event& l_event) {
	for (auto& b_itr : m_bindings) {
		Binding* bind = b_itr.second;
		for (auto& e_itr : bind->m_events) {
			EventType sfmlEvent = (EventType)l_event.type;
			if (e_itr.first != sfmlEvent)
				continue; // ne odgovara tip dogadaja
			if (sfmlEvent == EventType::KeyDown ||
				sfmlEvent == EventType::KeyUp)
			{
				if (e_itr.second.m_code == l_event.key.code) {
					// ako odgovara kod tipke - povecamo brojac:
					if (bind->m_details.m_keyCode != -1) {
						bind->m_details.m_keyCode = e_itr.second.m_code;
					}
					++(bind->c);
					break;
				}
			}
			else if (sfmlEvent == EventType::MButtonDown ||
				sfmlEvent == EventType::MButtonUp)
			{
				if (e_itr.second.m_code == l_event.mouseButton.button) {
					// odgovara tip dogadaja i sad kod tipke - pov. brojac
					bind->m_details.m_mouse.x = l_event.mouseButton.x;
					bind->m_details.m_mouse.y = l_event.mouseButton.y;
					if (bind->m_details.m_keyCode != -1)
						bind->m_details.m_keyCode = e_itr.second.m_code;
					++(bind->c);
					break; // prekida petlju po bind->m_events
				}
			}
			else {
				// ne trebaju dodatne provjere
				if (sfmlEvent == EventType::MouseWheel) {
					bind->m_details.m_mouseWheelDelta
						= l_event.mouseWheel.delta;
				}
				else if (sfmlEvent == EventType::WindowResized) {
					bind->m_details.m_size.x = l_event.size.width;
					bind->m_details.m_size.y = l_event.size.height;
				}
				else if (sfmlEvent == EventType::TextEntered) {
					bind->m_details.m_textEntered = l_event.text.unicode;
				}
				++(bind->c);
			}
		}
	}
}


void EventManager::Update() {
	if (!m_hasFocus)
		return;
	for (auto& b_itr : m_bindings) {
		Binding* bind = b_itr.second;
		for (auto& e_itr : bind->m_events) {
			switch (e_itr.first) {
			case(EventType::Keyboard):
				if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key(e_itr.second.m_code)))
				{
					if (bind->m_details.m_keyCode != -1) {
						bind->m_details.m_keyCode = e_itr.second.m_code;
					}
					++(bind->c);
				}
				break;
			case(EventType::Mouse):
				if (sf::Mouse::isButtonPressed(sf::Mouse::Button(e_itr.second.m_code)))
				{
					if (bind->m_details.m_keyCode != -1) {
						bind->m_details.m_keyCode = e_itr.second.m_code;
					}
					++(bind->c);
				}
				break;
			case(EventType::Joystick):
				// tu moze nadogradnja u buducnosti :)
				break;
			}
		}

		// ako se sve dogodilo sto treba (brojac to broji)
		// pozovemo odgovarajuci callback
		if (bind->m_events.size() == bind->c) {
			auto callItr = m_callbacks.find(bind->m_name);
			if (callItr != m_callbacks.end()) {
				callItr->second(&bind->m_details);
			}
		}
		bind->c = 0;
		bind->m_details.Clear();
	}
}

// npr. u datoteci "keys.cfg":
// Window_close 0:0
// Fullscreen_toggle 5:89
// Move 9:0 24:38

void EventManager::LoadBindings() {
	std::string delimiter = ":";
	std::string file = "Keys.cfg";

	std::ifstream bindings;
	bindings.open(file);
	if (!bindings.is_open()) {
		std::cout << "Neuspjelo otvaranje datoteke "
			<< file << "!" << std::endl;
		return;
	}

	std::string linija;
	while (std::getline(bindings, linija)) {
		std::stringstream keystream(linija);
		// ime callbacka
		std::string callbackName;
		keystream >> callbackName;

		Binding* bind = new Binding(callbackName);
		// dodaju se svi parovi tip:kod
		while (!keystream.eof()) {
			std::string keyval;
			keystream >> keyval;
			int start = 0;
			int end = keyval.find(delimiter);
			if (end == std::string::npos) {
				delete bind;
				bind = nullptr;
				break;
			}
			// dva broja - tip i kod
			EventType type = EventType(stoi(keyval.substr(start, end - start)));
			int code = stoi(keyval.substr(end + delimiter.length(),
				keyval.find(delimiter, end + delimiter.length())));

			EventInfo eventInfo;
			eventInfo.m_code = code;

			bind->BindEvent(type, eventInfo);
		}

		if (!AddBinding(bind))
			delete bind;
		bind = nullptr;
	}

	// zatvaranje datoteke
	bindings.close();
}