#include "uielements.h"

#include <GLFW/glfw3.h>
#include <algorithm>
#include <glm/gtc/type_ptr.hpp>
#include <string>

#include "constants.h"
#include "fonts.h"
#include "input.h"
#include "internal.h"
#include "internal/ui.h"
#include "ui.h"
#include "unicode.h"
#include "utils.h"

namespace kek {

TextElement::TextElement(UIValue x, UIValue y, std::shared_ptr<Font> font)
	: UIElement(x, y) {
	this->text = new TextObject(font, "Text");
	this->sizePixels = KEK_DEFAULT_FONT_SIZE_PIXELS;
	this->color = Colors::WHITE;
	this->textMode = TextMode::ORIGIN;
	this->textBounds = TextBounds::SMALLEST;
}

TextElement::TextElement(UIValue x, UIValue y)
	: TextElement(x, y, kekData.ui->defaultFont) {}

TextElement::~TextElement() {
	delete text;
}

UIElementType TextElement::getType() {
	return UIElementType::TEXT;
}

UIBounds TextElement::getBounds() {
	TextMetrics metrics = text->getMetrics(sizePixels);

	int w = metrics.width, h;
	switch(textBounds) {
		case TextBounds::SMALLEST:
		default:
			h = metrics.height;
			break;
		case TextBounds::LINE:
			FontMetrics metrics = text->getFont()->getMetrics(sizePixels);
			h = metrics.lineHeight;
			break;
	}

	switch(textMode) {
		case TextMode::BASELINE:
			return UIBounds(offsetX(w, origin), 0, w, h);
		case TextMode::ORIGIN:
		default:
			return offsetUIBounds(w, h, origin);
	}
}

void TextElement::setText(std::string text) {
	this->text->setText(text);
}

std::string TextElement::getText() {
	return text->getText();
}

void TextElement::draw(UIPoint screenPos, glm::mat4 projection) {
	switch(textMode) {
		case TextMode::BASELINE:
			text->getFont()->drawText(text, projection, screenPos.x, screenPos.y, sizePixels, color);
			break;
		case TextMode::ORIGIN:
			switch(textBounds) {
				case TextBounds::SMALLEST:
				default:
					text->getFont()->drawTextFromOrigin(text, projection, screenPos.x, screenPos.y, sizePixels, color);
					break;
				case TextBounds::LINE:
					FontMetrics metrics = text->getFont()->getMetrics(sizePixels);
					int offsetY = metrics.ascender;
					text->getFont()->drawText(text, projection, screenPos.x, screenPos.y + offsetY, sizePixels, color);
					break;
			}

			break;
	}
}

static float rectangleVerts[] = {
	0.0f,
	1.0f,
	1.0f,
	1.0f,
	1.0f,
	0.0f,
	1.0f,
	0.0f,
	0.0f,
	0.0f,
	0.0f,
	1.0f,
};

RectangleElement::RectangleElement(UIValue x, UIValue y, UIValue w, UIValue h)
	: UIElement(x, y) {
	this->w = w;
	this->h = h;
	color = Colors::BLACK;

	glCreateVertexArrays(1, &vao);
	glCreateBuffers(1, &vbo);

	glNamedBufferData(vbo, sizeof(rectangleVerts), rectangleVerts, GL_STATIC_DRAW);

	glVertexArrayVertexBuffer(vao, KEK_UI_VERTEX_BUFFER_BINDING, vbo, 0, 2 * sizeof(float));

	glEnableVertexArrayAttrib(vao, KEK_UI_RECT_VERTEX_SHADER_IN_POSITION);
	glVertexArrayAttribFormat(vao, KEK_UI_RECT_VERTEX_SHADER_IN_POSITION, 2, GL_FLOAT, GL_FALSE, 0);
	glVertexArrayAttribBinding(vao, KEK_UI_RECT_VERTEX_SHADER_IN_POSITION, KEK_UI_VERTEX_BUFFER_BINDING);
}

RectangleElement::~RectangleElement() {
	glDeleteVertexArrays(1, &vao);
	glDeleteBuffers(1, &vbo);
}

UIElementType RectangleElement::getType() {
	return UIElementType::RECTANGLE;
}

UIBounds RectangleElement::getBounds() {
	return offsetUIBounds(uiToScreen(w), uiToScreen(h), origin);
}

void RectangleElement::draw(UIPoint screenPos, glm::mat4 projection) {
	kekData.ui->rectangleShader->use();

	UIBounds offset = getBounds();

	glUniformMatrix4fv(glGetUniformLocation(kekData.ui->rectangleShader->id, "projection"), 1, GL_FALSE, glm::value_ptr(projection));
	glm::vec4 bounds = glm::vec4(screenPos.x, screenPos.y, offset.w, offset.h);
	glUniform4fv(glGetUniformLocation(kekData.ui->rectangleShader->id, "bounds"), 1, glm::value_ptr(bounds));
	glUniform4fv(glGetUniformLocation(kekData.ui->rectangleShader->id, "rectColor"), 1, color.valuePointer());

	glBindVertexArray(vao);
	glDrawArrays(GL_TRIANGLES, 0, 6);
}

ButtonElement::ButtonElement(UIValue x, UIValue y, UIValue w, UIValue h)
	: RectangleElement(x, y, w, h) {
	clickable = true;

	color = Colors::CYAN;
	hoverColor = Colors::CYAN.darker();

	text = new TextElement(uiPw(0.5), uiPh(0.5));
	text->origin = Origin::CENTER;
	addChild(text);
}

ButtonElement::~ButtonElement() {
	delete text;
}

UIElementType ButtonElement::getType() {
	return UIElementType::BUTTON;
}

void ButtonElement::click(UIPoint pos, UIPoint screenPos, GLFWMouseButton button) {
	onClick();
}

void ButtonElement::draw(UIPoint screenPos, glm::mat4 projection) {
	RectangleElement::color = hovering ? hoverColor : color;
	RectangleElement::draw(screenPos, projection);
}

TextFieldElement::TextFieldElement(UIValue x, UIValue y, UIValue w, std::shared_ptr<Font> font)
	: RectangleElement(x, y, w, uiPx(0)) {
	this->enableClipping = true;
	this->focusable = true;
	this->color = Colors::WHITE;
	this->focusColor = Colors::GRAY;
	this->text = "";

	this->textElement = new TextElement(uiPx(0), uiPx(0));
	textElement->textBounds = TextBounds::LINE;
	textElement->color = Colors::BLACK;
	textElement->setText(text);
	addChild(textElement);

	int textH = textElement->getBounds().h;
	h = uiPx(textH);

	this->cursor = new RectangleElement(uiPx(0), uiPx(2), uiPx(1), uiPx(textH - 4));
	cursor->color = Colors::BLACK;
	addChild(cursor);

	this->capture = KEK_INVALID_ID;
}

TextFieldElement::TextFieldElement(UIValue x, UIValue y, UIValue w)
	: TextFieldElement(x, y, w, kekData.ui->defaultFont) {
}

TextFieldElement::~TextFieldElement() {
	Input::uncaptureKeyboardInput(capture);
	capture = KEK_INVALID_ID;

	delete textElement;
	delete cursor;
}

void TextFieldElement::updateText(std::u32string newText) {
	this->text = Unicode::convertU32ToStd(newText);
	this->textElement->setText(this->text);

	UIBounds bounds = getBounds();
	TextMetrics m = textElement->text->getMetrics(0, newText.length() - cursorPos, textElement->sizePixels);
	TextMetrics fullM = textElement->text->getMetrics(0, newText.length(), textElement->sizePixels);

	// Set offset to keep cursor visible
	float offsetX = -this->textElement->x.pixels;
	float minOffsetX = std::max(m.width - bounds.w + 1, 0);
	float maxOffsetX = m.width;

	// Adjust offset to make sure the text field is always fully utilized (if possible)
	float adjust = bounds.w - (fullM.width - offsetX);
	if(adjust > 0) offsetX -= adjust;

	offsetX = clamp(offsetX, minOffsetX, maxOffsetX);

	this->cursor->x = uiPx(m.width - offsetX);
	this->textElement->x = uiPx(-offsetX);
}

UIElementType TextFieldElement::getType() {
	return UIElementType::TEXT_FIELD;
}

void TextFieldElement::focusEnter() {
	cursor->visible = true;
	lastCharTyped = glfwGetTime();

	capture = Input::captureKeyboardInput(
		KeyCharCallback([](KeyCharEvent event, void *data) {
			TextFieldElement *_this = (TextFieldElement *) data;

			std::u32string str = Unicode::convertStdToU32(_this->text);
			switch(event.codepoint) {
				case KEK_INPUT_DELETE:
					if(_this->cursorPos == str.length()) return;
					str = str.erase(str.length() - _this->cursorPos - 1, 1);
					break;
				case KEK_INPUT_DELETE_FORWARD:
					if(_this->cursorPos == 0) return;
					str = str.erase(str.length() - _this->cursorPos, 1);
					_this->cursorPos--;
					break;
				default:
					str.insert(str.length() - _this->cursorPos, 1, event.codepoint);
					break;
			}

			_this->lastCharTyped = glfwGetTime();
			_this->updateText(str);
		},
			this),
		KeyCallback([](KeyEvent event, void *data) {
			TextFieldElement *_this = (TextFieldElement *) data;

			if(event.action == GLFW_PRESS || event.action == GLFW_REPEAT) {
				switch(event.key) {
					case GLFW_KEY_ENTER:
						Input::uncaptureKeyboardInput(_this->capture);
						_this->onSubmit(_this->text);
						break;
					case GLFW_KEY_LEFT: {
						std::u32string str = Unicode::convertStdToU32(_this->text);
						if(_this->cursorPos < str.length()) {
							_this->cursorPos++;
							_this->updateText(str);
							_this->lastCharTyped = glfwGetTime();
						}
						break;
					}
					case GLFW_KEY_RIGHT: {
						std::u32string str = Unicode::convertStdToU32(_this->text);
						if(_this->cursorPos > 0) {
							_this->cursorPos--;
							_this->updateText(str);
							_this->lastCharTyped = glfwGetTime();
						}
						break;
					}
				}
			}
		},
			this),
		Callable([](void *data) {
			TextFieldElement *_this = (TextFieldElement *) data;
			UI::unfocusElement(_this);
		},
			this));
}

void TextFieldElement::focusExit() {
	cursor->visible = false;

	Input::uncaptureKeyboardInput(capture);
	capture = KEK_INVALID_ID;

	cursorPos = 0;
	updateText(Unicode::convertStdToU32(this->text));
}

void TextFieldElement::draw(UIPoint screenPos, glm::mat4 projection) {
	RectangleElement::color = focused ? focusColor : color;
	RectangleElement::draw(screenPos, projection);

	double time = glfwGetTime();
	cursor->visible = focused ? (time - lastCharTyped < 0.5 || (int) time % 2 == 0) : false;
}

void TextFieldElement::setText(std::string text) {
	this->cursorPos = 0;
	updateText(Unicode::convertStdToU32(text));
}

UIWindowTitleBar::UIWindowTitleBar(UIValue height)
	: RectangleElement(uiPx(0), uiPx(0), uiPw(1), height) {
	this->color = Colors::GRAY;
	this->draggable = true;

	text = new TextElement(x, uiPh(0.5));
	text->setText("Window title");
	text->textMode = TextMode::ORIGIN;
	text->origin = Origin::LEFT_CENTER;
	addChild(text);

	closeButton = new ButtonElement(uiPw(1) - uiPx(30), uiPx(0), uiPx(30), uiPh(1));
	closeButton->text->setText("X");
	closeButton->color = Colors::RED;
	closeButton->hoverColor = Colors::RED.darker();
	addChild(closeButton);
}

void UIWindowTitleBar::dragEnter(UIPoint pos, UIPoint screenPos) {
	// TODO
}

void UIWindowTitleBar::drag(UIPoint pos, UIPoint screenPos) {
	// TODO
}

void UIWindowTitleBar::dragExit(UIPoint pos, UIPoint screenPos) {
	// TODO
}

UIWindow::UIWindow(UIValue x, UIValue y, UIValue w, UIValue h)
	: UIElement(x, y) {
	this->w = w;
	this->h = h;

	this->background = new RectangleElement(x, y, w, h);
	background->color = Colors::GRAY.darker();
	addChild(background);

	this->titleBar = new UIWindowTitleBar(uiPx(25));
	addChild(titleBar);

	titleBar->closeButton->onClick = Callable([](void *element) {
		UI::removeElement((UIElement *) element);
	},
		this);
}

UIWindow::~UIWindow() {
}

UIElementType UIWindow::getType() {
	return UIElementType::WINDOW;
}

UIBounds UIWindow::getBounds() {
	return offsetUIBounds(uiToScreen(w), uiToScreen(h), origin);
}

void UIWindow::draw(UIPoint screenPos, glm::mat4 projection) {
}

}
