#include "UiManager.h"
#include "utils/Utils.h"
#include "render/TextRenderer.h"
#include <fstream>
#include <iostream>
#include <algorithm>
#include <sstream>
#include "GameConstants.h"

namespace ZL {

	using json = nlohmann::json;


	static int countWrappedLines(const std::string& text) {
		if (text.empty()) return 0;
		int lines = 1;
		for (char c : text) {
			if (c == '\n') ++lines;
		}
		return lines;
	}

	static std::string limitLines(const std::string& text, int maxLines) {
		if (maxLines <= 0) return text;
		std::string out;
		int lines = 1;
		for (char c : text) {
			if (c == '\n') {
				if (lines >= maxLines) {
					out += "...";
					return out;
				}
				++lines;
			}
			out.push_back(c);
		}
		return out;
	}

	static std::string wrapTextByPixels(const std::string& input, const TextRenderer& textRenderer, float maxWidthPx, float scale, int maxLines = 0) {
		if (input.empty() || maxWidthPx <= 1.0f) return input;

		std::string output;
		std::string currentLine;
		std::string currentWord;
		auto flushLine = [&]() {
			if (!currentLine.empty()) {
				if (!output.empty()) output.push_back('\n');
				output += currentLine;
				currentLine.clear();
			}
		};
		auto pushWord = [&](const std::string& word) {
			if (word.empty()) return;
			if (currentLine.empty()) {
				currentLine = word;
				return;
			}
			const std::string candidate = currentLine + " " + word;
			if (textRenderer.measureTextWidth(candidate, scale) <= maxWidthPx) {
				currentLine = candidate;
			} else {
				flushLine();
				currentLine = word;
			}
		};

		for (size_t i = 0; i < input.size(); ++i) {
			const char ch = input[i];
			if (ch == '\n') {
				pushWord(currentWord);
				currentWord.clear();
				flushLine();
				continue;
			}
			if (ch == ' ' || ch == '\t' || ch == '\r') {
				pushWord(currentWord);
				currentWord.clear();
				continue;
			}
			currentWord.push_back(ch);
		}

		pushWord(currentWord);
		flushLine();

		return limitLines(output, maxLines);
	}

	static float applyEasing(const std::string& easing, float t) {
		if (easing == "easein") {
			return t * t;
		}
		else if (easing == "easeout") {
			float inv = 1.0f - t;
			return 1.0f - inv * inv;
		}
		return t;
	}

	void UiButton::buildMesh() {
		mesh.data.PositionData.clear();
		mesh.data.TexCoordData.clear();

		float x0 = rect.x;
		float y0 = rect.y;
		float x1 = rect.x + rect.w;
		float y1 = rect.y + rect.h;

		mesh.data.PositionData.push_back({ x0, y0, 0 });
		mesh.data.TexCoordData.push_back({ 0, 0 });

		mesh.data.PositionData.push_back({ x0, y1, 0 });
		mesh.data.TexCoordData.push_back({ 0, 1 });

		mesh.data.PositionData.push_back({ x1, y1, 0 });
		mesh.data.TexCoordData.push_back({ 1, 1 });

		mesh.data.PositionData.push_back({ x0, y0, 0 });
		mesh.data.TexCoordData.push_back({ 0, 0 });

		mesh.data.PositionData.push_back({ x1, y1, 0 });
		mesh.data.TexCoordData.push_back({ 1, 1 });

		mesh.data.PositionData.push_back({ x1, y0, 0 });
		mesh.data.TexCoordData.push_back({ 1, 0 });

		mesh.RefreshVBO();
	}

	void UiButton::draw(Renderer& renderer) const {
		if (!texNormal) return;
		const std::shared_ptr<Texture>* tex = &texNormal;
		switch (state) {
		case ButtonState::Normal: tex = &texNormal; break;
		case ButtonState::Hover: tex = &texHover; break;
		case ButtonState::Pressed: tex = &texPressed; break;
		case ButtonState::Disabled: tex = &texDisabled; break;
		}
		if (!(*tex)) return;

		renderer.PushMatrix();
		renderer.TranslateMatrix({ animOffsetX, animOffsetY, 0.0f });
		renderer.ScaleMatrix({ animScaleX, animScaleY, 1.0f });

		renderer.RenderUniform1i(textureUniformName, 0);
		glBindTexture(GL_TEXTURE_2D, (*tex)->getTexID());
		renderer.DrawVertexRenderStruct(mesh);
		renderer.PopMatrix();
	}

	void UiTextButton::buildMesh() {
		mesh.data.PositionData.clear();
		mesh.data.TexCoordData.clear();

		float x0 = rect.x;
		float y0 = rect.y;
		float x1 = rect.x + rect.w;
		float y1 = rect.y + rect.h;

		mesh.data.PositionData.push_back({ x0, y0, 0 });
		mesh.data.TexCoordData.push_back({ 0, 0 });

		mesh.data.PositionData.push_back({ x0, y1, 0 });
		mesh.data.TexCoordData.push_back({ 0, 1 });

		mesh.data.PositionData.push_back({ x1, y1, 0 });
		mesh.data.TexCoordData.push_back({ 1, 1 });

		mesh.data.PositionData.push_back({ x0, y0, 0 });
		mesh.data.TexCoordData.push_back({ 0, 0 });

		mesh.data.PositionData.push_back({ x1, y1, 0 });
		mesh.data.TexCoordData.push_back({ 1, 1 });

		mesh.data.PositionData.push_back({ x1, y0, 0 });
		mesh.data.TexCoordData.push_back({ 1, 0 });

		mesh.RefreshVBO();
	}

	void UiTextButton::draw(Renderer& renderer) const {
		
		// Draw background texture (optional)
		const std::shared_ptr<Texture>* tex = nullptr;
		switch (state) {
		case ButtonState::Normal:   if (texNormal)   tex = &texNormal;   break;
		case ButtonState::Hover:    tex = texHover   ? &texHover   : (texNormal   ? &texNormal   : nullptr); break;
		case ButtonState::Pressed:  tex = texPressed ? &texPressed : (texNormal   ? &texNormal   : nullptr); break;
		case ButtonState::Disabled: tex = texDisabled ? &texDisabled : (texNormal ? &texNormal   : nullptr); break;
		}
		glDisable(GL_DEPTH_TEST);
		if (tex && *tex) {
			renderer.shaderManager.PushShader(defaultShaderName);

			renderer.PushMatrix();
			renderer.TranslateMatrix({ animOffsetX, animOffsetY, 0.0f });
			renderer.ScaleMatrix({ animScaleX, animScaleY, 1.0f });

			renderer.RenderUniform1i(textureUniformName, 0);
			glBindTexture(GL_TEXTURE_2D, (*tex)->getTexID());
			renderer.DrawVertexRenderStruct(mesh);
			renderer.PopMatrix();
			renderer.shaderManager.PopShader();

		}

		
		// Draw text on top (uses absolute coords, add anim offset manually)
		// use left padding, which is required for inventory/quest lists.
		if (textRenderer && !text.empty()) {
			float tx = rect.x + rect.w / 2.0f + animOffsetX;
			if (!textCentered) {
				tx = rect.x + textPaddingX + animOffsetX;
			}
			const float ty = rect.y + rect.h * 0.5f + textPaddingY + animOffsetY;
			textRenderer->drawText(text, tx, ty, 1.0f, textCentered, color);
		}
		glEnable(GL_DEPTH_TEST);
	}

	void UiTextView::draw(Renderer& renderer) const {
		(void)renderer;
		if (!textRenderer || text.empty()) {
			return;
		}

		const float scale = 1.0f;

		// Backward compatibility:
		// Old UI files, including the original inventory panel, positioned TextView
		// around the rect center. If a TextView does not explicitly request wrapping,
		// top alignment, padding or line limiting, keep that old behavior.
		const bool usesModernRectText = wrap || topAligned || paddingX != 0.0f || paddingY != 0.0f || maxLines > 0;
		if (!usesModernRectText) {
			textRenderer->drawText(
				text,
				rect.x + rect.w * 0.5f,
				rect.y + rect.h * 0.5f,
				scale,
				centered,
				color
			);
			return;
		}

		const float availableWidth = max(1.0f, rect.w - paddingX * 2.0f);
		const std::string finalText = wrap
			? wrapTextByPixels(text, *textRenderer, availableWidth, scale, maxLines)
			: limitLines(text, maxLines);

		float tx = centered ? rect.x + rect.w * 0.5f : rect.x + paddingX;
		float ty = rect.y + rect.h * 0.5f;

		if (topAligned) {
			// TextRenderer expects a baseline position. This offset places the first
			// visible line close to the top inside the TextView rectangle.
			ty = rect.y + rect.h - paddingY - static_cast<float>(fontSize);
		}

		textRenderer->drawText(finalText, tx, ty, scale, centered, color);
	}

	void UiSlider::buildTrackMesh() {
		trackMesh.data.PositionData.clear();
		trackMesh.data.TexCoordData.clear();

		float x0 = rect.x;
		float y0 = rect.y;
		float x1 = rect.x + rect.w;
		float y1 = rect.y + rect.h;

		trackMesh.data.PositionData.push_back({ x0, y0, 0 });
		trackMesh.data.TexCoordData.push_back({ 0, 0 });

		trackMesh.data.PositionData.push_back({ x0, y1, 0 });
		trackMesh.data.TexCoordData.push_back({ 0, 1 });

		trackMesh.data.PositionData.push_back({ x1, y1, 0 });
		trackMesh.data.TexCoordData.push_back({ 1, 1 });

		trackMesh.data.PositionData.push_back({ x0, y0, 0 });
		trackMesh.data.TexCoordData.push_back({ 0, 0 });

		trackMesh.data.PositionData.push_back({ x1, y1, 0 });
		trackMesh.data.TexCoordData.push_back({ 1, 1 });

		trackMesh.data.PositionData.push_back({ x1, y0, 0 });
		trackMesh.data.TexCoordData.push_back({ 1, 0 });

		trackMesh.RefreshVBO();
	}

	void UiSlider::buildKnobMesh() {
		knobMesh.data.PositionData.clear();
		knobMesh.data.TexCoordData.clear();

		float kw = vertical ? rect.w * 4.0f : rect.w * 0.5f;
		float kh = vertical ? rect.w * 4.0f : rect.h * 0.5f;

		float cx = rect.x + rect.w * 0.5f;
		float cy = rect.y + (vertical ? (value * rect.h) : (rect.h * 0.5f));

		float x0 = cx - kw * 0.5f;
		float y0 = cy - kh * 0.5f;
		float x1 = cx + kw * 0.5f;
		float y1 = cy + kh * 0.5f;

		knobMesh.data.PositionData.push_back({ x0, y0, 0 });
		knobMesh.data.TexCoordData.push_back({ 0, 0 });

		knobMesh.data.PositionData.push_back({ x0, y1, 0 });
		knobMesh.data.TexCoordData.push_back({ 0, 1 });

		knobMesh.data.PositionData.push_back({ x1, y1, 0 });
		knobMesh.data.TexCoordData.push_back({ 1, 1 });

		knobMesh.data.PositionData.push_back({ x0, y0, 0 });
		knobMesh.data.TexCoordData.push_back({ 0, 0 });

		knobMesh.data.PositionData.push_back({ x1, y1, 0 });
		knobMesh.data.TexCoordData.push_back({ 1, 1 });

		knobMesh.data.PositionData.push_back({ x1, y0, 0 });
		knobMesh.data.TexCoordData.push_back({ 1, 0 });

		knobMesh.RefreshVBO();
	}

	void UiSlider::draw(Renderer& renderer) const {
		renderer.RenderUniform1i(textureUniformName, 0);
		if (texTrack) {
			glBindTexture(GL_TEXTURE_2D, texTrack->getTexID());
			renderer.DrawVertexRenderStruct(trackMesh);
		}
		if (texKnob) {
			glBindTexture(GL_TEXTURE_2D, texKnob->getTexID());
			renderer.DrawVertexRenderStruct(knobMesh);
		}
	}

	void UiStaticImage::buildMesh() {
		mesh.data.PositionData.clear();
		mesh.data.TexCoordData.clear();

		float x0 = rect.x;
		float y0 = rect.y;
		float x1 = rect.x + rect.w;
		float y1 = rect.y + rect.h;

		mesh.data.PositionData.push_back({ x0, y0, 0 });
		mesh.data.TexCoordData.push_back({ 0, 0 });

		mesh.data.PositionData.push_back({ x0, y1, 0 });
		mesh.data.TexCoordData.push_back({ 0, 1 });

		mesh.data.PositionData.push_back({ x1, y1, 0 });
		mesh.data.TexCoordData.push_back({ 1, 1 });

		mesh.data.PositionData.push_back({ x0, y0, 0 });
		mesh.data.TexCoordData.push_back({ 0, 0 });

		mesh.data.PositionData.push_back({ x1, y1, 0 });
		mesh.data.TexCoordData.push_back({ 1, 1 });

		mesh.data.PositionData.push_back({ x1, y0, 0 });
		mesh.data.TexCoordData.push_back({ 1, 0 });

		mesh.RefreshVBO();
	}

	void UiStaticImage::draw(Renderer& renderer) const {
		if (!texture) return;

		renderer.RenderUniform1i(textureUniformName, 0);
		glBindTexture(GL_TEXTURE_2D, texture->getTexID());
		renderer.DrawVertexRenderStruct(mesh);
	}

	void UiTextField::draw(Renderer& renderer) const {
		if (textRenderer) {
			float textX = rect.x + 10.0f;
			float textY = rect.y + rect.h / 2.0f;

			if (text.empty()) {
				textRenderer->drawText(placeholder, textX, textY, 1.0f, false, placeholderColor);
			}
			else {
				textRenderer->drawText(text, textX, textY, 1.0f, false, color);
			}
		}
	}

	std::shared_ptr<UiNode> parseNode(const json& j, Renderer& renderer, const std::string& zipFile) {
		auto node = std::make_shared<UiNode>();

		// 1. Определяем тип контейнера и ориентацию
		std::string typeStr = j.value("type", "FrameLayout"); // По умолчанию FrameLayout
		if (typeStr == "LinearLayout") {
			node->layoutType = LayoutType::Linear;
		}
		else {
			node->layoutType = LayoutType::Frame;
		}

		if (j.contains("name")) node->name = j["name"].get<std::string>();

		// 2. Читаем размеры во временные "локальные" поля
		// Это критически важно: мы не пишем сразу в screenRect, 
		// так как LinearLayout их пересчитает.
		node->localX = j.value("x", 0.0f);
		node->localY = j.value("y", 0.0f);
		if (j.contains("width")) {
			if (j["width"].is_string() && j["width"] == "match_parent") {
				node->width = -1.0f; // Наш маркер для match_parent
			}
			else {
				node->width = j["width"].get<float>();
			}
		}
		else
		{
			node->width = 0.0f;
		}
		if (j.contains("height")) {
			if (j["height"].is_string() && j["height"] == "match_parent") {
				node->height = -1.0f; // Наш маркер для match_parent
			}
			else {
				node->height = j["height"].get<float>();
			}
		}
		else
		{
			node->height = 0.0f;
		}
	
		// 3. Параметры компоновки
		if (j.contains("orientation")) {
			std::string orient = j["orientation"].get<std::string>();
			node->orientation = (orient == "horizontal") ? Orientation::Horizontal : Orientation::Vertical;
		}
		node->spacing = j.value("spacing", 0.0f);

		if (j.contains("horizontal_align")) {
			std::string halign = j["horizontal_align"];
			if (halign == "center") node->layoutSettings.hAlign = HorizontalAlign::Center;
			else if (halign == "right") node->layoutSettings.hAlign = HorizontalAlign::Right;
		}

		if (j.contains("vertical_align")) {
			std::string valign = j["vertical_align"];
			if (valign == "center") node->layoutSettings.vAlign = VerticalAlign::Center;
			else if (valign == "bottom") node->layoutSettings.vAlign = VerticalAlign::Bottom;
		}

		if (j.contains("horizontal_gravity")) {
			std::string hg = j["horizontal_gravity"].get<std::string>();
			if (hg == "right") node->layoutSettings.hGravity = HorizontalGravity::Right;
			else if (hg == "center") node->layoutSettings.hGravity = HorizontalGravity::Center;
			else node->layoutSettings.hGravity = HorizontalGravity::Left;
		}

		// Читаем Vertical Gravity
		if (j.contains("vertical_gravity")) {
			std::string vg = j["vertical_gravity"].get<std::string>();
			if (vg == "bottom") node->layoutSettings.vGravity = VerticalGravity::Bottom;
			else if (vg == "center") node->layoutSettings.vGravity = VerticalGravity::Center;
			else node->layoutSettings.vGravity = VerticalGravity::Top;
		}

		// Подготавливаем базовый rect для компонентов (кнопок и т.д.)
		// На этапе парсинга мы даем им "желаемый" размер
		UiRect initialRect = { node->localX, node->localY, node->width, node->height };


		if (typeStr == "Button") {
			auto btn = std::make_shared<UiButton>();
			btn->name = node->name;
			btn->rect = initialRect;

			if (!j.contains("textures") || !j["textures"].is_object()) {
				std::cerr << "UiManager: Button '" << btn->name << "' missing textures" << std::endl;
				throw std::runtime_error("UI button textures missing");
			}
			auto t = j["textures"];
			auto loadTex = [&](const std::string& key)->std::shared_ptr<Texture> {
				if (!t.contains(key) || !t[key].is_string()) return nullptr;
				std::string path = t[key].get<std::string>();
				try {
					std::cout << "UiManager: loading texture for button '" << btn->name << "' : " << path << " Zip file: " << zipFile << std::endl;
					return renderer.textureManager.LoadFromPng(path, zipFile);
				}
				catch (const std::exception& e) {
					std::cerr << "UiManager: failed load texture " << path << " : " << e.what() << std::endl;
					throw std::runtime_error("UI texture load failed: " + path);
				}
				};

			btn->texNormal = loadTex("normal");
			btn->texHover = loadTex("hover");
			btn->texPressed = loadTex("pressed");
			btn->texDisabled = loadTex("disabled");

			btn->border = j.value("border", 0.0f);

			node->button = btn;
		}
		else if (typeStr == "Slider") {
			auto s = std::make_shared<UiSlider>();
			s->name = node->name;
			s->rect = initialRect;

			if (!j.contains("textures") || !j["textures"].is_object()) {
				std::cerr << "UiManager: Slider '" << s->name << "' missing textures" << std::endl;
				throw std::runtime_error("UI slider textures missing");
			}
			auto t = j["textures"];
			auto loadTex = [&](const std::string& key)->std::shared_ptr<Texture> {
				if (!t.contains(key) || !t[key].is_string()) return nullptr;
				std::string path = t[key].get<std::string>();
				try {
					std::cout << "UiManager: --loading texture for slider '" << s->name << "' : " << path << " Zip file: " << zipFile << std::endl;
					return renderer.textureManager.LoadFromPng(path, zipFile);
				}
				catch (const std::exception& e) {
					std::cerr << "UiManager: failed load texture " << path << " : " << e.what() << std::endl;
					throw std::runtime_error("UI texture load failed: " + path);
				}
				};

			s->texTrack = loadTex("track");
			s->texKnob = loadTex("knob");

			if (j.contains("value")) s->value = j["value"].get<float>();
			if (j.contains("orientation")) {
				std::string orient = j["orientation"].get<std::string>();
				std::transform(orient.begin(), orient.end(), orient.begin(), ::tolower);
				s->vertical = (orient != "horizontal");
			}

			node->slider = s;
		}
		else if (typeStr == "TextField") {
			auto tf = std::make_shared<UiTextField>();
			tf->name = node->name;
			tf->rect = initialRect;

			if (j.contains("placeholder")) tf->placeholder = j["placeholder"].get<std::string>();
			if (j.contains("fontPath")) tf->fontPath = j["fontPath"].get<std::string>();
			if (j.contains("fontSize")) tf->fontSize = j["fontSize"].get<int>();
			if (j.contains("maxLength")) tf->maxLength = j["maxLength"].get<int>();

			if (j.contains("color") && j["color"].is_array() && j["color"].size() == 4) {
				for (int i = 0; i < 4; ++i) {
					tf->color[i] = j["color"][i].get<float>();
				}
			}
			if (j.contains("placeholderColor") && j["placeholderColor"].is_array() && j["placeholderColor"].size() == 4) {
				for (int i = 0; i < 4; ++i) {
					tf->placeholderColor[i] = j["placeholderColor"][i].get<float>();
				}
			}
			if (j.contains("backgroundColor") && j["backgroundColor"].is_array() && j["backgroundColor"].size() == 4) {
				for (int i = 0; i < 4; ++i) {
					tf->backgroundColor[i] = j["backgroundColor"][i].get<float>();
				}
			}
			if (j.contains("borderColor") && j["borderColor"].is_array() && j["borderColor"].size() == 4) {
				for (int i = 0; i < 4; ++i) {
					tf->borderColor[i] = j["borderColor"][i].get<float>();
				}
			}

			tf->textRenderer = std::make_unique<TextRenderer>();
			if (!tf->textRenderer->init(renderer, tf->fontPath, tf->fontSize, zipFile)) {
				std::cerr << "Failed to init TextRenderer for TextField: " << tf->name << std::endl;
			}

			node->textField = tf;
		}

		if (typeStr == "TextButton") {
			auto tb = std::make_shared<UiTextButton>();
			tb->name = node->name;
			tb->rect = initialRect;
			tb->border = j.value("border", 0.0f);

			// Textures are optional
			if (j.contains("textures") && j["textures"].is_object()) {
				auto t = j["textures"];
				auto loadTex = [&](const std::string& key) -> std::shared_ptr<Texture> {
					if (!t.contains(key) || !t[key].is_string()) return nullptr;
					std::string path = t[key].get<std::string>();
					try {
						auto data = CreateTextureDataFromPng(path.c_str(), zipFile.c_str());
						return std::make_shared<Texture>(data);
					}
					catch (const std::exception& e) {
						std::cerr << "UiManager: TextButton '" << tb->name << "' failed to load texture " << path << ": " << e.what() << std::endl;
						return nullptr;
					}
				};
				tb->texNormal   = loadTex("normal");
				tb->texHover    = loadTex("hover");
				tb->texPressed  = loadTex("pressed");
				tb->texDisabled = loadTex("disabled");
			}

			if (j.contains("text"))     tb->text     = j["text"].get<std::string>();
			if (j.contains("fontPath")) tb->fontPath = j["fontPath"].get<std::string>();
			if (j.contains("fontSize")) tb->fontSize = j["fontSize"].get<int>();
			if (j.contains("textCentered")) tb->textCentered = j["textCentered"].get<bool>();
			if (j.contains("textPaddingX")) tb->textPaddingX = j["textPaddingX"].get<float>();
			if (j.contains("textPaddingY")) tb->textPaddingY = j["textPaddingY"].get<float>();
			if (j.contains("color") && j["color"].is_array() && j["color"].size() == 4) {
				for (int i = 0; i < 4; ++i) tb->color[i] = j["color"][i].get<float>();
			}

			tb->textRenderer = std::make_unique<TextRenderer>();
			if (!tb->textRenderer->init(renderer, tb->fontPath, tb->fontSize, zipFile)) {
				std::cerr << "UiManager: Failed to init TextRenderer for TextButton: " << tb->name << std::endl;
			}

			node->textButton = tb;
		}

		if (j.contains("animations") && j["animations"].is_object()) {
			for (auto it = j["animations"].begin(); it != j["animations"].end(); ++it) {
				std::string animName = it.key();
				const auto& animDef = it.value();
				UiNode::AnimSequence seq;
				if (animDef.contains("repeat") && animDef["repeat"].is_boolean()) seq.repeat = animDef["repeat"].get<bool>();
				if (animDef.contains("steps") && animDef["steps"].is_array()) {
					for (const auto& step : animDef["steps"]) {
						UiNode::AnimStep s;
						if (step.contains("type") && step["type"].is_string()) {
							s.type = step["type"].get<std::string>();
							std::transform(s.type.begin(), s.type.end(), s.type.begin(), ::tolower);
						}
						if (step.contains("to") && step["to"].is_array() && step["to"].size() >= 2) {
							s.toX = step["to"][0].get<float>();
							s.toY = step["to"][1].get<float>();
						}
						if (step.contains("duration")) {
							s.durationMs = step["duration"].get<float>() * 1000.0f;
						}
						if (step.contains("easing") && step["easing"].is_string()) {
							s.easing = step["easing"].get<std::string>();
							std::transform(s.easing.begin(), s.easing.end(), s.easing.begin(), ::tolower);
						}
						seq.steps.push_back(s);
					}
				}
				node->animations[animName] = std::move(seq);
			}
		}

		if (typeStr == "StaticImage") {
			auto img = std::make_shared<UiStaticImage>();
			img->name = node->name;
			img->rect = initialRect;

			std::string texPath;
			if (j.contains("texture") && j["texture"].is_string()) {
				texPath = j["texture"].get<std::string>();
			}
			else if (j.contains("textures") && j["textures"].is_object() && j["textures"].contains("normal")) {
				texPath = j["textures"]["normal"].get<std::string>();
			}

			if (!texPath.empty()) {
				try {
					img->texture = renderer.textureManager.LoadFromPng(texPath, zipFile);
				}
				catch (const std::exception& e) {
					std::cerr << "UiManager: failed load texture for StaticImage '" << img->name << "' : " << e.what() << std::endl;
				}
			}

			node->staticImage = img;
		}

		if (typeStr == "TextView") {
			auto tv = std::make_shared<UiTextView>();

			tv->name = node->name;
			tv->rect = initialRect;
			if (j.contains("text")) tv->text = j["text"].get<std::string>();
			if (j.contains("fontPath")) tv->fontPath = j["fontPath"].get<std::string>();
			if (j.contains("fontSize")) tv->fontSize = j["fontSize"].get<int>();
			if (j.contains("color") && j["color"].is_array() && j["color"].size() == 4) {
				for (int i = 0; i < 4; ++i) {
					tv->color[i] = j["color"][i].get<float>();
				}
			}
			if (j.contains("centered")) tv->centered = j["centered"].get<bool>();
			if (j.contains("wrap")) tv->wrap = j["wrap"].get<bool>();
			if (j.contains("topAligned")) tv->topAligned = j["topAligned"].get<bool>();
			if (j.contains("paddingX")) tv->paddingX = j["paddingX"].get<float>();
			if (j.contains("paddingY")) tv->paddingY = j["paddingY"].get<float>();
			if (j.contains("maxLines")) tv->maxLines = j["maxLines"].get<int>();

			tv->textRenderer = std::make_unique<TextRenderer>();
			if (!tv->textRenderer->init(renderer, tv->fontPath, tv->fontSize, zipFile)) {
				std::cerr << "Failed to init TextRenderer for TextView: " << tv->name << std::endl;
			}

			node->textView = tv;
		}

		if (j.contains("children") && j["children"].is_array()) {
			for (const auto& ch : j["children"]) {
				node->children.push_back(parseNode(ch, renderer, zipFile));
			}
		}

		return node;
	}

	std::shared_ptr<UiNode> loadUiFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile)
	{
		std::shared_ptr<UiNode> root;

		std::string content;
		try {
			if (zipFile.empty()) {
				content = readTextFile(path);
			}
			else {
				auto buf = readFileFromZIP(path, zipFile);
				if (buf.empty()) {
					std::cerr << "UiManager: failed to read " << path << " from zip " << zipFile << std::endl;
					throw std::runtime_error("Failed to load UI file: " + path);
				}
				content.assign(buf.begin(), buf.end());
			}
		}
		catch (const std::exception& e) {
			std::cerr << "UiManager: failed to open " << path << " : " << e.what() << std::endl;
			throw std::runtime_error("Failed to load UI file: " + path);
		}

		json j;
		try {
			j = json::parse(content);
		}
		catch (const std::exception& e) {
			std::cerr << "UiManager: json parse error: " << e.what() << std::endl;
			throw std::runtime_error("Failed to load UI file: " + path);
		}

		if (!j.contains("root") || !j["root"].is_object()) {
			std::cerr << "UiManager: root node missing or invalid" << std::endl;
			throw std::runtime_error("Failed to load UI file: " + path);
		}


		root = parseNode(j["root"], renderer, zipFile);

		return root;
	}

	void UiManager::replaceRoot(std::shared_ptr<UiNode> newRoot) {
		root = newRoot;
		layoutNode(
			root,
			0.0f, 0.0f,                         // parentX, parentY (экран начинается с 0,0)
			Environment::projectionWidth,       // parentW
			Environment::projectionHeight,      // parentH
			root->localX,                       // finalLocalX
			root->localY                        // finalLocalY
		);
		buttons.clear();
		textButtons.clear();
		sliders.clear();
		textViews.clear();
		textFields.clear();
		staticImages.clear();
		collectButtonsAndSliders(root);

		nodeActiveAnims.clear();

		for (auto& b : buttons) {
			b->buildMesh();
		}
		for (auto& tb : textButtons) {
			tb->buildMesh();
		}
		for (auto& s : sliders) {
			s->buildTrackMesh();
			s->buildKnobMesh();
		}
		for (auto& img : staticImages) {
			img->buildMesh();
		}
	}

	void UiManager::loadFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile) {
		
		std::shared_ptr<UiNode> newRoot = loadUiFromFile(path, renderer, zipFile);
		replaceRoot(newRoot);
	}

	void UiManager::appendFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile) {
		std::shared_ptr<UiNode> extraRoot = loadUiFromFile(path, renderer, zipFile);
		if (!extraRoot) {
			std::cerr << "UiManager: appendFromFile failed: " << path << std::endl;
			return;
		}

		if (!root) {
			replaceRoot(extraRoot);
			return;
		}

		for (auto& child : extraRoot->children) {
			root->children.push_back(child);
		}

		replaceRoot(root);
	}


	void UiManager::layoutNode(const std::shared_ptr<UiNode>& node, float parentX, float parentY, float parentW, float parentH, float finalLocalX, float finalLocalY) {

		node->screenRect.w = (node->width < 0) ? parentW : node->width;
		node->screenRect.h = (node->height < 0) ? parentH : node->height;

		// ТЕПЕРЬ используем переданные координаты, а не node->localX напрямую
		node->screenRect.x = parentX + finalLocalX;
		node->screenRect.y = parentY + finalLocalY;

		float currentW = node->screenRect.w;
		float currentH = node->screenRect.h;

		if (node->layoutType == LayoutType::Linear) {
			float totalContentWidth = 0;
			float totalContentHeight = 0;

			// Предварительный расчет занимаемого места всеми детьми
			for (size_t i = 0; i < node->children.size(); ++i) {
				if (node->orientation == Orientation::Vertical) {
					totalContentHeight += node->children[i]->height;
					if (i < node->children.size() - 1) totalContentHeight += node->spacing;
				}
				else {
					totalContentWidth += node->children[i]->width;
					if (i < node->children.size() - 1) totalContentWidth += node->spacing;
				}
			}

			float startX = 0;
			float startY = currentH;

			if (node->orientation == Orientation::Vertical) {
				if (node->layoutSettings.vAlign == VerticalAlign::Center) {
					startY = (currentH + totalContentHeight) / 2.0f;
				}
				else if (node->layoutSettings.vAlign == VerticalAlign::Bottom) {
					startY = totalContentHeight;
				}
			}

			// Горизонтальное выравнивание всего блока
			if (node->orientation == Orientation::Horizontal) {
				if (node->layoutSettings.hAlign == HorizontalAlign::Center) {
					startX = (currentW - totalContentWidth) / 2.0f;
				}
				else if (node->layoutSettings.hAlign == HorizontalAlign::Right) {
					startX = currentW - totalContentWidth;
				}
			}

			float cursorX = startX;
			float cursorY = startY;

			for (auto& child : node->children) {

				float childW = (child->width < 0) ? currentW : child->width;
				float childH = (child->height < 0) ? currentH : child->height;

				if (node->orientation == Orientation::Vertical) {
					cursorY -= childH; // используем вычисленный childH

					float childX = 0;
					float freeSpaceX = currentW - childW;
					if (node->layoutSettings.hAlign == HorizontalAlign::Center) childX = freeSpaceX / 2.0f;
					else if (node->layoutSettings.hAlign == HorizontalAlign::Right) childX = freeSpaceX;

					child->localX = childX;
					child->localY = cursorY;
					cursorY -= node->spacing;
				}
				else {
					child->localX = cursorX;

					// Вертикальное выравнивание внутри "строки" (Cross-axis alignment)
					float childY = 0;
					float freeSpaceY = currentH - childH;

					if (node->layoutSettings.vAlign == VerticalAlign::Center) {
						childY = freeSpaceY / 2.0f;
					}
					else if (node->layoutSettings.vAlign == VerticalAlign::Top) {
						childY = freeSpaceY; // Прижимаем к верхнему краю (т.к. Y растет вверх)
					}
					else if (node->layoutSettings.vAlign == VerticalAlign::Bottom) {
						childY = 0; // Прижимаем к нижнему краю
					}

					child->localY = childY;

					// Сдвигаем курсор вправо для следующего элемента
					cursorX += childW + node->spacing;
				}
				layoutNode(child, node->screenRect.x, node->screenRect.y, currentW, currentH, child->localX, child->localY);
			}
		}
		else {
			for (auto& child : node->children) {
				float childW = (child->width < 0) ? currentW : child->width;
				float childH = (child->height < 0) ? currentH : child->height;

				float fLX = child->localX;
				float fLY = child->localY;

				if (child->layoutSettings.hGravity == HorizontalGravity::Right) {
					fLX = currentW - childW - child->localX;
				}
				else if (child->layoutSettings.hGravity == HorizontalGravity::Center) {
					fLX = (currentW - childW) / 2.0f + child->localX;
				}
				if (child->layoutSettings.vGravity == VerticalGravity::Top) {
					fLY = currentH - childH - child->localY;
				}
				else if (child->layoutSettings.vGravity == VerticalGravity::Center) {
					fLY = (currentH - childH) / 2.0f + child->localY;
				}

				// Передаем рассчитанные fLX, fLY в рекурсию
				layoutNode(child, node->screenRect.x, node->screenRect.y, currentW, currentH, fLX, fLY);
			}
		}

		// Обновляем меши визуальных компонентов
		syncComponentRects(node);
	}

	void UiManager::syncComponentRects(const std::shared_ptr<UiNode>& node) {
		if (!node) return;

		// 1. Обновляем кнопку
		if (node->button) {
			node->button->rect = node->screenRect;
			node->button->buildMesh();
		}

		if (node->textButton) {
			node->textButton->rect = node->screenRect;
			node->textButton->buildMesh();
		}

		// 2. Обновляем слайдер
		if (node->slider) {
			node->slider->rect = node->screenRect;
			node->slider->buildTrackMesh();
			node->slider->buildKnobMesh();
		}

		// 3. Обновляем текстовое поле (TextView)
		if (node->textView) {
			node->textView->rect = node->screenRect;
			// Если в TextView реализован кэш меша для текста, его нужно обновить здесь
			// node->textView->rebuildText(); 
		}

		// 4. Обновляем поле ввода (TextField)
		if (node->textField) {
			node->textField->rect = node->screenRect;
			// Аналогично для курсора и фонового меша
		}

		// 5. Обновляем статическое изображение
		if (node->staticImage) {
			node->staticImage->rect = node->screenRect;
			node->staticImage->buildMesh();
		}
	}

	void UiManager::updateAllLayouts() {
		if (!root) return;

		// Запускаем расчет от корня, передавая размеры экрана как "родительские"
		layoutNode(
			root,
			0.0f, 0.0f,                         // parentX, parentY (экран начинается с 0,0)
			Environment::projectionWidth,       // parentW
			Environment::projectionHeight,      // parentH
			root->localX,                       // finalLocalX
			root->localY                        // finalLocalY
		);
	}

	void UiManager::collectButtonsAndSliders(const std::shared_ptr<UiNode>& node) {
		if (node->button) {
			buttons.push_back(node->button);
		}
		if (node->textButton) {
			textButtons.push_back(node->textButton);
		}
		if (node->slider) {
			sliders.push_back(node->slider);
		}
		if (node->textView) {
			textViews.push_back(node->textView);
		}
		if (node->textField) {
			textFields.push_back(node->textField);
		}
		if (node->staticImage) {
			staticImages.push_back(node->staticImage);
		}
		for (auto& c : node->children) collectButtonsAndSliders(c);
	}

	bool UiManager::setButtonCallback(const std::string& name, std::function<void(const std::string&)> cb) {
		auto b = findButton(name);
		if (!b) {
			std::cerr << "UiManager: setButtonCallback failed, button not found: " << name << std::endl;
			return false;
		}
		b->onClick = std::move(cb);
		return true;
	}

	bool UiManager::setButtonPressCallback(const std::string& name, std::function<void(const std::string&)> cb) {
		auto b = findButton(name);
		if (!b) {
			std::cerr << "UiManager: setButtonPressCallback failed, button not found: " << name << std::endl;
			return false;
		}
		b->onPress = std::move(cb);
		return true;
	}

	bool UiManager::addSlider(const std::string& name, const UiRect& rect, Renderer& renderer, const std::string& zipFile,
		const std::string& trackPath, const std::string& knobPath, float initialValue, bool vertical) {

		auto s = std::make_shared<UiSlider>();
		s->name = name;
		s->rect = rect;
		s->value = std::clamp(initialValue, 0.0f, 1.0f);
		s->vertical = vertical;

		try {
			if (!trackPath.empty())
				s->texTrack = renderer.textureManager.LoadFromPng(trackPath, zipFile);
			if (!knobPath.empty())
				s->texKnob = renderer.textureManager.LoadFromPng(knobPath, zipFile);
		}
		catch (const std::exception& e) {
			std::cerr << "UiManager: addSlider failed to load textures: " << e.what() << std::endl;
			return false;
		}

		s->buildTrackMesh();
		s->buildKnobMesh();

		sliders.push_back(s);
		return true;
	}

	std::shared_ptr<UiSlider> UiManager::findSlider(const std::string& name) {
		for (auto& s : sliders) if (s->name == name) return s;
		return nullptr;
	}

	bool UiManager::setSliderCallback(const std::string& name, std::function<void(const std::string&, float)> cb) {
		auto s = findSlider(name);
		if (!s) {
			std::cerr << "UiManager: setSliderCallback failed, slider not found: " << name << std::endl;
			return false;
		}
		s->onValueChanged = std::move(cb);
		return true;
	}

	bool UiManager::setSliderValue(const std::string& name, float value) {
		auto s = findSlider(name);
		if (!s) return false;
		value = std::clamp(value, 0.0f, 1.0f);
		if (fabs(s->value - value) < 1e-6f) return true;
		s->value = value;
		s->buildKnobMesh();
		if (s->onValueChanged) s->onValueChanged(s->name, s->value);
		return true;
	}

	std::shared_ptr<UiTextField> UiManager::findTextField(const std::string& name) {
		for (auto& tf : textFields) if (tf->name == name) return tf;
		return nullptr;
	}

	bool UiManager::setTextFieldCallback(const std::string& name, std::function<void(const std::string&, const std::string&)> cb) {
		auto tf = findTextField(name);
		if (!tf) {
			std::cerr << "UiManager: setTextFieldCallback failed, textfield not found: " << name << std::endl;
			return false;
		}
		tf->onTextChanged = std::move(cb);
		return true;
	}

	std::string UiManager::getTextFieldValue(const std::string& name) {
		auto tf = findTextField(name);
		if (!tf) return "";
		return tf->text;
	}

	bool UiManager::pushMenuFromSavedRoot(std::shared_ptr<UiNode> newRoot)
	{
		MenuState prev;
		prev.root = root;
		prev.buttons = buttons;
		prev.textButtons = textButtons;
		prev.sliders = sliders;
		prev.textViews = textViews;
		prev.textFields = textFields;
		prev.staticImages = staticImages;
		prev.pressedButtons = pressedButtons;
		prev.pressedTextButtons = pressedTextButtons;
		prev.pressedSliders = pressedSliders;
		prev.focusedTextField = focusedTextField;
		prev.path = "";

		prev.animCallbacks = animCallbacks;

		try {
			nodeActiveAnims.clear();
			animCallbacks.clear();
			focusedTextField = nullptr;
			for (auto& b : buttons) {
				if (b) {
					b->animOffsetX = 0.0f;
					b->animOffsetY = 0.0f;
					b->animScaleX = 1.0f;
					b->animScaleY = 1.0f;
				}
			}
			for (auto& tb : textButtons) {
				if (tb) {
					tb->animOffsetX = 0.0f;
					tb->animOffsetY = 0.0f;
					tb->animScaleX = 1.0f;
					tb->animScaleY = 1.0f;
				}
			}

			replaceRoot(newRoot);
			menuStack.push_back(std::move(prev));
			return true;
		}
		catch (const std::exception& e) {
			std::cerr << "UiManager: pushMenuFromFile failed to load from root : " << e.what() << std::endl;
			animCallbacks = prev.animCallbacks;
			return false;
		}
	}

	bool UiManager::pushMenuFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile) {
		auto newRoot = loadUiFromFile(path, renderer, zipFile);
		return pushMenuFromSavedRoot(newRoot);
	}

	bool UiManager::popMenu() {
		if (menuStack.empty()) {
			std::cerr << "UiManager: popMenu called but menu stack is empty" << std::endl;
			return false;
		}
		auto s = menuStack.back();
		menuStack.pop_back();

		nodeActiveAnims.clear();

		root = s.root;
		buttons = s.buttons;
		textButtons = s.textButtons;
		sliders = s.sliders;
		textViews = s.textViews;
		textFields = s.textFields;
		staticImages = s.staticImages;
		pressedButtons = s.pressedButtons;
		pressedTextButtons = s.pressedTextButtons;
		pressedSliders = s.pressedSliders;
		focusedTextField = s.focusedTextField;

		animCallbacks = s.animCallbacks;

		for (auto& b : buttons) {
			if (b) {
				b->animOffsetX = 0.0f;
				b->animOffsetY = 0.0f;
				b->animScaleX = 1.0f;
				b->animScaleY = 1.0f;
				b->buildMesh();
			}
		}
		for (auto& tb : textButtons) {
			if (tb) {
				tb->animOffsetX = 0.0f;
				tb->animOffsetY = 0.0f;
				tb->animScaleX = 1.0f;
				tb->animScaleY = 1.0f;
				tb->buildMesh();
			}
		}

		for (auto& sl : sliders) {
			if (sl) {
				sl->buildTrackMesh();
				sl->buildKnobMesh();
			}
		}
		return true;
	}

	void UiManager::clearMenuStack() {
		menuStack.clear();
	}

	void UiManager::draw(Renderer& renderer) {
		renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, -1, 1);
		renderer.PushMatrix();
		renderer.LoadIdentity();

		std::function<void(const std::shared_ptr<UiNode>&)> drawNode = 
			[&](const std::shared_ptr<UiNode>& node) {
			if (!node || !node->visible) return;

			renderer.PushMatrix();
			renderer.TranslateMatrix({ node->screenRect.x + node->screenRect.w * 0.5f, 
										node->screenRect.y + node->screenRect.h * 0.5f, 0.0f });
			renderer.ScaleMatrix({ node->scaleX, node->scaleY, 1.0f });
			renderer.TranslateMatrix({ -(node->screenRect.x + node->screenRect.w * 0.5f), 
										-(node->screenRect.y + node->screenRect.h * 0.5f), 0.0f });

			
			// 1. Сначала изображения
			if (node->staticImage) {
				node->staticImage->draw(renderer);
			}
			
			// 2. Потом кнопки
			if (node->button) {
				node->button->draw(renderer);
			}
			if (node->textButton) {
				node->textButton->draw(renderer);
			}
			
			// 3. Потом слайдеры
			if (node->slider) {
				node->slider->draw(renderer);
			}
			
			// 4. Потом текстовые поля
			if (node->textField) {
				node->textField->draw(renderer);
			}
			
			// 5. ПОСЛЕДНИЙ - ТЕКСТ (поверх всего!)
			if (node->textView) {
				node->textView->draw(renderer);
			}

			// Рисуем детей
			for (auto& child : node->children) {
				drawNode(child);
			}

			renderer.PopMatrix();
		};

		drawNode(root);

		renderer.PopMatrix();
		renderer.PopProjectionMatrix();
	}

	static std::shared_ptr<UiNode> findNodeByName(const std::shared_ptr<UiNode>& node, const std::string& name) {
		if (!node) return nullptr;
		if (!name.empty() && node->name == name) return node;
		for (auto& c : node->children) {
			auto r = findNodeByName(c, name);
			if (r) return r;
		}
		return nullptr;
	}

	void UiManager::update(float deltaMs) {
		if (!root) return;

		std::vector<std::pair<std::shared_ptr<UiNode>, size_t>> animationsToRemove;
		std::vector<std::function<void()>> pendingCallbacks;

		for (auto& kv : nodeActiveAnims) {
			auto node = kv.first;
			auto& activeList = kv.second;

			for (size_t i = 0; i < activeList.size(); ++i) {
				auto& act = activeList[i];
				if (!act.seq) {
					animationsToRemove.push_back({ node, i });
					continue;
				}

				const auto& steps = act.seq->steps;

				if (act.stepIndex >= steps.size()) {
					if (act.repeat) {
						if (node->button) {
							node->button->animOffsetX = act.origOffsetX;
							node->button->animOffsetY = act.origOffsetY;
							node->button->animScaleX = act.origScaleX;
							node->button->animScaleY = act.origScaleY;
						}
						if (node->textButton) {
							node->textButton->animOffsetX = act.origOffsetX;
							node->textButton->animOffsetY = act.origOffsetY;
							node->textButton->animScaleX = act.origScaleX;
							node->textButton->animScaleY = act.origScaleY;
						}
						act.stepIndex = 0;
						act.elapsedMs = 0.0f;
						act.stepStarted = false;
					}
					else {
						if (act.onComplete) {
							pendingCallbacks.push_back(act.onComplete);
						}
						animationsToRemove.push_back({ node, i });
					}
					continue;
				}

				const auto& step = steps[act.stepIndex];

				if (step.durationMs <= 0.0f) {
					if (step.type == "move") {
						if (node->button) {
							node->button->animOffsetX = step.toX;
							node->button->animOffsetY = step.toY;
						}
						if (node->textButton) {
							node->textButton->animOffsetX = step.toX;
							node->textButton->animOffsetY = step.toY;
						}
					}
					else if (step.type == "scale") {
						if (node->button) {
							node->button->animScaleX = step.toX;
							node->button->animScaleY = step.toY;
						}
						if (node->textButton) {
							node->textButton->animScaleX = step.toX;
							node->textButton->animScaleY = step.toY;
						}
					}
					act.stepIndex++;
					act.elapsedMs = 0.0f;
					act.stepStarted = false;
					continue;
				}

				if (!act.stepStarted && act.stepIndex == 0 && act.elapsedMs == 0.0f) {
					if (node->button) {
						act.origOffsetX = node->button->animOffsetX;
						act.origOffsetY = node->button->animOffsetY;
						act.origScaleX = node->button->animScaleX;
						act.origScaleY = node->button->animScaleY;
					}
					else if (node->textButton) {
						act.origOffsetX = node->textButton->animOffsetX;
						act.origOffsetY = node->textButton->animOffsetY;
						act.origScaleX = node->textButton->animScaleX;
						act.origScaleY = node->textButton->animScaleY;
					}
					else {
						act.origOffsetX = act.origOffsetY = 0.0f;
						act.origScaleX = act.origScaleY = 1.0f;
					}
				}

				float prevElapsed = act.elapsedMs;
				act.elapsedMs += deltaMs;

				if (!act.stepStarted && prevElapsed == 0.0f) {
					if (node->button) {
						act.startOffsetX = node->button->animOffsetX;
						act.startOffsetY = node->button->animOffsetY;
						act.startScaleX = node->button->animScaleX;
						act.startScaleY = node->button->animScaleY;
					}
					else if (node->textButton) {
						act.startOffsetX = node->textButton->animOffsetX;
						act.startOffsetY = node->textButton->animOffsetY;
						act.startScaleX = node->textButton->animScaleX;
						act.startScaleY = node->textButton->animScaleY;
					}
					else {
						act.startOffsetX = act.startOffsetY = 0.0f;
						act.startScaleX = act.startScaleY = 1.0f;
					}
					if (step.type == "move") {
						act.endOffsetX = step.toX;
						act.endOffsetY = step.toY;
					}
					else if (step.type == "scale") {
						act.endScaleX = step.toX;
						act.endScaleY = step.toY;
					}
					act.stepStarted = true;
				}
				float t = (step.durationMs > 0.0f) ? (act.elapsedMs / step.durationMs) : 1.0f;
				if (t > 1.0f) t = 1.0f;
				float te = applyEasing(step.easing, t);

				if (step.type == "move") {
					float nx = act.startOffsetX + (act.endOffsetX - act.startOffsetX) * te;
					float ny = act.startOffsetY + (act.endOffsetY - act.startOffsetY) * te;
					if (node->button) {
						node->button->animOffsetX = nx;
						node->button->animOffsetY = ny;
					}
					if (node->textButton) {
						node->textButton->animOffsetX = nx;
						node->textButton->animOffsetY = ny;
					}
				}
				else if (step.type == "scale") {
					float sx = act.startScaleX + (act.endScaleX - act.startScaleX) * te;
					float sy = act.startScaleY + (act.endScaleY - act.startScaleY) * te;
					if (node->button) {
						node->button->animScaleX = sx;
						node->button->animScaleY = sy;
					}
					if (node->textButton) {
						node->textButton->animScaleX = sx;
						node->textButton->animScaleY = sy;
					}
				}
				else if (step.type == "wait") {
					//wait
				}
				if (act.elapsedMs >= step.durationMs) {
					act.stepIndex++;
					act.elapsedMs = 0.0f;
					act.stepStarted = false;
				}
			}
		}

		for (auto it = animationsToRemove.rbegin(); it != animationsToRemove.rend(); ++it) {
			auto& [node, index] = *it;
			if (nodeActiveAnims.find(node) != nodeActiveAnims.end()) {
				auto& animList = nodeActiveAnims[node];
				if (index < animList.size()) {
					animList.erase(animList.begin() + index);
				}
				if (animList.empty()) {
					nodeActiveAnims.erase(node);
				}
			}
		}

		for (auto& cb : pendingCallbacks) {
			try {
				cb();
			}
			catch (...) {
				std::cerr << "UiManager: animation onComplete callback threw exception" << std::endl;
			}
		}
	}

	void UiManager::onTouchMove(int64_t fingerId, int x, int y) {
		// Hover state updates only make sense for mouse (single pointer)
		if (fingerId == MOUSE_FINGER_ID) {
			for (auto& b : buttons) {
				if (b->state != ButtonState::Disabled)
				{
					if (b->rect.containsConsideringBorder((float)x, (float)y, b->border)) {
						if (b->state != ButtonState::Pressed) b->state = ButtonState::Hover;
					}
					else {
						if (b->state != ButtonState::Pressed) b->state = ButtonState::Normal;
					}
				}
			}
			for (auto& tb : textButtons) {
				if (tb->state != ButtonState::Disabled)
				{
					if (tb->rect.containsConsideringBorder((float)x, (float)y, tb->border)) {
						if (tb->state != ButtonState::Pressed) tb->state = ButtonState::Hover;
					}
					else {
						if (tb->state != ButtonState::Pressed) tb->state = ButtonState::Normal;
					}
				}
			}
		}

		auto it = pressedSliders.find(fingerId);
		if (it != pressedSliders.end()) {
			auto s = it->second;
			float t;
			if (s->vertical) {
				t = (y - s->rect.y) / s->rect.h;
			}
			else {
				t = (x - s->rect.x) / s->rect.w;
			}
			if (t < 0.0f) t = 0.0f;
			if (t > 1.0f) t = 1.0f;
			s->value = t;
			s->buildKnobMesh();
			if (s->onValueChanged) s->onValueChanged(s->name, s->value);
		}
	}


	void UiManager::onTouchDown(int64_t fingerId, int x, int y) {
		for (auto& b : buttons) {
			if (b->state != ButtonState::Disabled)
			{
				if (b->rect.containsConsideringBorder((float)x, (float)y, b->border)) {
					b->state = ButtonState::Pressed;
					pressedButtons[fingerId] = b;
					if (b->onPress) b->onPress(b->name);
					break; // a single finger can only press one button
				}
			}
		}

		for (auto& tb : textButtons) {
			if (tb->state != ButtonState::Disabled)
			{
				if (tb->rect.containsConsideringBorder((float)x, (float)y, tb->border)) {
					tb->state = ButtonState::Pressed;
					pressedTextButtons[fingerId] = tb;
					if (tb->onPress) tb->onPress(tb->name);
					break;
				}
			}
		}

		for (auto& s : sliders) {
			if (s->rect.contains((float)x, (float)y)) {
				pressedSliders[fingerId] = s;
				float t;
				if (s->vertical) {
					t = (y - s->rect.y) / s->rect.h;
				}
				else {
					t = (x - s->rect.x) / s->rect.w;
				}
				if (t < 0.0f) t = 0.0f;
				if (t > 1.0f) t = 1.0f;
				s->value = t;
				s->buildKnobMesh();
				if (s->onValueChanged) s->onValueChanged(s->name, s->value);
				break;
			}
		}

		for (auto& tf : textFields) {
			if (tf->rect.contains((float)x, (float)y)) {
				focusedTextField = tf;
				tf->focused = true;
			}
			else {
				tf->focused = false;
			}
		}
	}

	void UiManager::onTouchUp(int64_t fingerId, int x, int y) {
		std::vector<std::shared_ptr<UiButton>> clicked;
		std::vector<std::shared_ptr<UiTextButton>> clickedText;

		auto btnIt = pressedButtons.find(fingerId);
		if (btnIt != pressedButtons.end()) {
			auto b = btnIt->second;
			if (b) {
				bool contains = b->rect.contains((float)x, (float)y);
				if (b->state == ButtonState::Pressed) {
					if (contains) {
						clicked.push_back(b);
					}
					// On mouse: leave Hover if still over button. On touch: always Normal.
				b->state = (contains && fingerId == MOUSE_FINGER_ID) ? ButtonState::Hover : ButtonState::Normal;
				}
			}
			pressedButtons.erase(btnIt);
		}

		auto tbIt = pressedTextButtons.find(fingerId);
		if (tbIt != pressedTextButtons.end()) {
			auto tb = tbIt->second;
			if (tb) {
				bool contains = tb->rect.contains((float)x, (float)y);
				if (tb->state == ButtonState::Pressed) {
					if (contains) {
						clickedText.push_back(tb);
					}
					tb->state = (contains && fingerId == MOUSE_FINGER_ID) ? ButtonState::Hover : ButtonState::Normal;
				}
			}
			pressedTextButtons.erase(tbIt);
		}

		pressedSliders.erase(fingerId);

		for (auto& b : clicked) {
			if (b->onClick) {
				b->onClick(b->name);
			}
		}
		for (auto& tb : clickedText) {
			if (tb->onClick) {
				tb->onClick(tb->name);
			}
		}
	}

	void UiManager::onKeyPress(unsigned char key) {
		if (!focusedTextField) return;

		if (key >= 32 && key <= 126) {
			if (focusedTextField->text.length() < (size_t)focusedTextField->maxLength) {
				focusedTextField->text += key;
				if (focusedTextField->onTextChanged) {
					focusedTextField->onTextChanged(focusedTextField->name, focusedTextField->text);
				}
			}
		}
	}

	void UiManager::onKeyBackspace() {
		if (!focusedTextField) return;

		if (!focusedTextField->text.empty()) {
			focusedTextField->text.pop_back();
			if (focusedTextField->onTextChanged) {
				focusedTextField->onTextChanged(focusedTextField->name, focusedTextField->text);
			}
		}
	}

	std::shared_ptr<UiButton> UiManager::findButton(const std::string& name) {
		for (auto& b : buttons) if (b->name == name) return b;
		return nullptr;
	}

	bool UiManager::startAnimationOnNode(const std::string& nodeName, const std::string& animName) {
		if (!root) return false;
		auto node = findNodeByName(root, nodeName);
		if (!node) return false;
		auto it = node->animations.find(animName);
		if (it == node->animations.end()) return false;

		ActiveAnim aa;
		aa.name = animName;
		aa.seq = &it->second;
		aa.stepIndex = 0;
		aa.elapsedMs = 0.0f;
		aa.repeat = it->second.repeat;
		aa.stepStarted = false;
		if (node->button) {
			aa.origOffsetX = node->button->animOffsetX;
			aa.origOffsetY = node->button->animOffsetY;
			aa.origScaleX = node->button->animScaleX;
			aa.origScaleY = node->button->animScaleY;
		}
		else if (node->textButton) {
			aa.origOffsetX = node->textButton->animOffsetX;
			aa.origOffsetY = node->textButton->animOffsetY;
			aa.origScaleX = node->textButton->animScaleX;
			aa.origScaleY = node->textButton->animScaleY;
		}
		auto cbIt = animCallbacks.find({ nodeName, animName });
		if (cbIt != animCallbacks.end()) aa.onComplete = cbIt->second;
		nodeActiveAnims[node].push_back(std::move(aa));
		return true;
	}

	bool UiManager::stopAnimationOnNode(const std::string& nodeName, const std::string& animName) {
		if (!root) return false;
		auto node = findNodeByName(root, nodeName);
		if (!node) return false;
		auto it = nodeActiveAnims.find(node);
		if (it != nodeActiveAnims.end()) {
			auto& animList = it->second;
			for (auto animIt = animList.begin(); animIt != animList.end(); ) {
				if (animIt->name == animName) {
					animIt = animList.erase(animIt);
				}
				else {
					++animIt;
				}
			}
			if (animList.empty()) {
				nodeActiveAnims.erase(it);
			}

			return true;
		}

		return false;
	}

	void UiManager::startAnimation(const std::string& animName) {
		if (!root) return;
		std::function<void(const std::shared_ptr<UiNode>&)> traverse = [&](const std::shared_ptr<UiNode>& n) {
			if (!n) return;
			auto it = n->animations.find(animName);
			if (it != n->animations.end()) {
				ActiveAnim aa;
				aa.name = animName;
				aa.seq = &it->second;
				aa.stepIndex = 0;
				aa.elapsedMs = 0.0f;
				aa.repeat = it->second.repeat;
				aa.stepStarted = false;
				if (n->button) {
					aa.origOffsetX = n->button->animOffsetX;
					aa.origOffsetY = n->button->animOffsetY;
					aa.origScaleX = n->button->animScaleX;
					aa.origScaleY = n->button->animScaleY;
				}
				else if (n->textButton) {
					aa.origOffsetX = n->textButton->animOffsetX;
					aa.origOffsetY = n->textButton->animOffsetY;
					aa.origScaleX = n->textButton->animScaleX;
					aa.origScaleY = n->textButton->animScaleY;
				}
				auto cbIt = animCallbacks.find({ n->name, animName });
				if (cbIt != animCallbacks.end()) aa.onComplete = cbIt->second;
				nodeActiveAnims[n].push_back(std::move(aa));
			}
			for (auto& c : n->children) traverse(c);
			};
		traverse(root);
	}

	bool UiManager::setAnimationCallback(const std::string& nodeName, const std::string& animName, std::function<void()> cb) {
		animCallbacks[{nodeName, animName}] = std::move(cb);
		return true;
	}

	std::shared_ptr<UiStaticImage> UiManager::findStaticImage(const std::string& name) {
		for (auto& img : staticImages) {
			if (img->name == name) return img;
		}
		return nullptr;
	}

	std::shared_ptr<UiTextView> UiManager::findTextView(const std::string& name) {
		for (auto& tv : textViews) {
			if (tv->name == name) return tv;
		}
		return nullptr;
	}

	bool UiManager::setText(const std::string& name, const std::string& newText) {
		auto tv = findTextView(name);
		if (!tv) {
			return false;
		}
		tv->text = newText;
		return true;
	}

	bool UiManager::setTextColor(const std::string& name, const std::array<float, 4>& color) {
		auto tv = findTextView(name);
		if (!tv) {
			return false;
		}
		tv->color = color;
		return true;
	}

	std::shared_ptr<UiTextButton> UiManager::findTextButton(const std::string& name) {
		for (auto& tb : textButtons) if (tb->name == name) return tb;
		return nullptr;
	}

	bool UiManager::setTextButtonCallback(const std::string& name, std::function<void(const std::string&)> cb) {
		auto tb = findTextButton(name);
		if (!tb) {
			std::cerr << "UiManager: setTextButtonCallback failed, textButton not found: " << name << std::endl;
			return false;
		}
		tb->onClick = std::move(cb);
		return true;
	}

	bool UiManager::setTextButtonPressCallback(const std::string& name, std::function<void(const std::string&)> cb) {
		auto tb = findTextButton(name);
		if (!tb) {
			std::cerr << "UiManager: setTextButtonPressCallback failed, textButton not found: " << name << std::endl;
			return false;
		}
		tb->onPress = std::move(cb);
		return true;
	}

	bool UiManager::setTextButtonText(const std::string& name, const std::string& newText) {
		auto tb = findTextButton(name);
		if (!tb) return false;
		tb->text = newText;
		return true;
	}

	bool UiManager::setTextButtonColor(const std::string& name, const std::array<float, 4>& color) {
		auto tb = findTextButton(name);
		if (!tb) return false;
		tb->color = color;
		return true;
	}

	std::shared_ptr<UiNode> UiManager::findNode(const std::string& name) {
	if (!root) return nullptr;
	return findNodeByName(root, name);
}

	bool UiManager::setNodeVisible(const std::string& nodeName, bool visible) {
		if (!root) return false;
		auto node = findNodeByName(root, nodeName);
		if (!node) return false;
		node->visible = visible;
		return true;
	}

	bool UiManager::getNodeVisible(const std::string& nodeName) {
		if (!root) return false;
		auto node = findNodeByName(root, nodeName);
		if (!node) return false;
		return node->visible;
	}
} // namespace ZL