/*
 * Copyright 2016 - 2024 Gary Talent (gary@drinkingtea.net). All rights reserved.
 */

#include <imgui.h>
#include <lodepng.h>

#include <ox/std/point.hpp>
#include <keel/media.hpp>

#include "tilesheeteditor-imgui.hpp"

namespace nostalgia::core {

namespace ig = studio::ig;

static ox::Vector<uint32_t> normalizePixelSizes(
		ox::Vector<uint8_t> const&inPixels,
		int const bpp) noexcept {
	uint_t const bytesPerTile = bpp == 8 ? PixelsPerTile : PixelsPerTile / 2;
	ox::Vector<uint32_t> outPixels;
	if (bytesPerTile == 64) { // 8 BPP
		outPixels.resize(inPixels.size());
		for (std::size_t i = 0; i < inPixels.size(); ++i) {
			outPixels[i] = inPixels[i];
		}
	} else { // 4 BPP
		outPixels.resize(inPixels.size() * 2);
		for (std::size_t i = 0; i < inPixels.size(); ++i) {
			outPixels[i * 2 + 0] = inPixels[i] & 0xF;
			outPixels[i * 2 + 1] = inPixels[i] >> 4;
		}
	}
	return outPixels;
}

static ox::Vector<uint32_t> normalizePixelArrangement(
		ox::Vector<uint32_t> const&inPixels,
		int cols,
		int scale) {
	auto const scalePt = ox::Point{scale, scale};
	auto const width = cols * TileWidth;
	auto const height = static_cast<int>(inPixels.size()) / width;
	auto const dstWidth = width * scale;
	ox::Vector<uint32_t> outPixels(static_cast<size_t>((width * scale) * (height * scale)));
	for (std::size_t dstIdx = 0; dstIdx < outPixels.size(); ++dstIdx) {
		auto const dstPt = ox::Point{
			static_cast<int>(dstIdx) % dstWidth,
			static_cast<int>(dstIdx) / dstWidth};
		auto const srcPt = dstPt / scalePt;
		auto const srcIdx = ptToIdx(srcPt, cols);
		outPixels[dstIdx] = inPixels[srcIdx];
	}
	return outPixels;
}

static ox::Error toPngFile(
		ox::CStringView const&path,
		ox::Vector<uint32_t> &&pixels,
		Palette const&pal,
		size_t page,
		unsigned width,
		unsigned height) noexcept {
	for (auto &c : pixels) {
		c = color32(color(pal, page, c)) | static_cast<Color32>(0XFF << 24);
	}
	constexpr auto fmt = LCT_RGBA;
	return OxError(static_cast<ox::ErrorCode>(
			lodepng_encode_file(
					path.c_str(),
					reinterpret_cast<uint8_t const*>(pixels.data()),
					width,
					height,
					fmt,
					8)));
}

TileSheetEditorImGui::TileSheetEditorImGui(studio::StudioContext &sctx, ox::CRStringView path):
		Editor(path),
		m_sctx(sctx),
		m_tctx(m_sctx.tctx),
		m_view(m_sctx, path, *undoStack()),
		m_model(m_view.model()) {
	oxIgnoreError(setPaletteSelection());
	// connect signal/slots
	undoStack()->changeTriggered.connect(this, &TileSheetEditorImGui::markUnsavedChanges);
	m_subsheetEditor.inputSubmitted.connect(this, &TileSheetEditorImGui::updateActiveSubsheet);
	m_exportMenu.inputSubmitted.connect(this, &TileSheetEditorImGui::exportSubhseetToPng);
	m_model.paletteChanged.connect(this, &TileSheetEditorImGui::setPaletteSelection);
}

void TileSheetEditorImGui::exportFile() {
	m_exportMenu.show();
}

void TileSheetEditorImGui::cut() {
	m_model.cut();
}

void TileSheetEditorImGui::copy() {
	m_model.copy();
}

void TileSheetEditorImGui::paste() {
	m_model.paste();
}

void TileSheetEditorImGui::keyStateChanged(turbine::Key key, bool down) {
	if (!down) {
		return;
	}
	if (key == turbine::Key::Escape) {
		m_subsheetEditor.close();
		m_exportMenu.close();
	}
	auto const popupOpen = m_subsheetEditor.isOpen() && m_exportMenu.isOpen();
	auto const pal = m_model.pal();
	if (!popupOpen) {
		auto const colorCnt = pal.pages[m_model.palettePage()].size();
		if (key == turbine::Key::Alpha_D) {
			m_tool = Tool::Draw;
			setCopyEnabled(false);
			setCutEnabled(false);
			setPasteEnabled(false);
			m_model.clearSelection();
		} else if (key == turbine::Key::Alpha_S) {
			m_tool = Tool::Select;
			setCopyEnabled(true);
			setCutEnabled(true);
			setPasteEnabled(true);
		} else if (key == turbine::Key::Alpha_F) {
			m_tool = Tool::Fill;
			setCopyEnabled(false);
			setCutEnabled(false);
			setPasteEnabled(false);
			m_model.clearSelection();
		} else if (key >= turbine::Key::Num_1 && key <= turbine::Key::Num_9) {
			if (turbine::buttonDown(m_tctx, turbine::Key::Mod_Alt)) {
				auto const idx = ox::min<std::size_t>(
						static_cast<uint32_t>(key - turbine::Key::Num_1), m_model.pal().pages.size() - 1);
				m_model.setPalettePage(idx);
			} else if (key <= turbine::Key::Num_0 + colorCnt) {
				auto const idx = ox::min<std::size_t>(static_cast<uint32_t>(key - turbine::Key::Num_1), colorCnt - 1);
				m_view.setPalIdx(idx);
			}
		} else if (key == turbine::Key::Num_0) {
			if (turbine::buttonDown(m_tctx, turbine::Key::Mod_Alt)) {
				auto const idx = ox::min<std::size_t>(
						static_cast<uint32_t>(key - turbine::Key::Num_1 + 9), m_model.pal().pages.size() - 1);
				m_model.setPalettePage(idx);
			} else if (colorCnt >= 10) {
				auto const idx = ox::min<std::size_t>(
						static_cast<uint32_t>(key - turbine::Key::Num_1 + 9), colorCnt - 1);
				m_view.setPalIdx(idx);
			}
		}
	}
}

void TileSheetEditorImGui::draw(studio::StudioContext&) noexcept {
	auto const paneSize = ImGui::GetContentRegionAvail();
	auto const tileSheetParentSize = ImVec2(paneSize.x - m_palViewWidth, paneSize.y);
	auto const fbSize = ox::Vec2(tileSheetParentSize.x - 16, tileSheetParentSize.y - 16);
	ImGui::BeginChild("TileSheetView", tileSheetParentSize, true);
	{
		drawTileSheet(fbSize);
	}
	ImGui::EndChild();
	ImGui::SameLine();
	ImGui::BeginChild("Controls", ImVec2(m_palViewWidth - 8, paneSize.y), true);
	{
		auto const controlsSize = ImGui::GetContentRegionAvail();
		ImGui::BeginChild("ToolBox", ImVec2(m_palViewWidth - 24, 30), true);
		{
			auto const btnSz = ImVec2(45, 14);
			if (ImGui::Selectable("Select", m_tool == Tool::Select, 0, btnSz)) {
				m_tool = Tool::Select;
			}
			ImGui::SameLine();
			if (ImGui::Selectable("Draw", m_tool == Tool::Draw, 0, btnSz)) {
				m_tool = Tool::Draw;
				m_model.clearSelection();
			}
			ImGui::SameLine();
			if (ImGui::Selectable("Fill", m_tool == Tool::Fill, 0, btnSz)) {
				m_tool = Tool::Fill;
				m_model.clearSelection();
			}
		}
		ImGui::EndChild();
		auto const ySize = controlsSize.y - 38;
		// draw palette/color picker
		ImGui::BeginChild("Palette", ImVec2(m_palViewWidth - 24, ySize / 2.f), true);
		{
			drawPaletteSelector();
		}
		ImGui::EndChild();
		ImGui::BeginChild("SubSheets", ImVec2(m_palViewWidth - 24, ySize / 2.f), true);
		{
			static constexpr auto btnHeight = ig::BtnSz.y;
			auto const btnSize = ImVec2(btnHeight, btnHeight);
			if (ig::PushButton("+", btnSize)) {
				auto insertOnIdx = m_model.activeSubSheetIdx();
				auto const&parent = m_model.activeSubSheet();
				m_model.addSubsheet(insertOnIdx);
				insertOnIdx.emplace_back(parent.subsheets.size() - 1);
				m_model.setActiveSubsheet(insertOnIdx);
			}
			ImGui::SameLine();
			if (ig::PushButton("-", btnSize)) {
				auto const&activeSubsheetIdx = m_model.activeSubSheetIdx();
				if (!activeSubsheetIdx.empty()) {
					m_model.rmSubsheet(activeSubsheetIdx);
				}
			}
			ImGui::SameLine();
			if (ig::PushButton("Edit")) {
				showSubsheetEditor();
			}
			ImGui::SameLine();
			if (ig::PushButton("Export")) {
				m_exportMenu.show();
			}
			TileSheet::SubSheetIdx path;
			static constexpr auto flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_NoBordersInBody;
			if (ImGui::BeginTable("Subsheets", 4, flags)) {
				ImGui::TableSetupColumn("Subsheet", ImGuiTableColumnFlags_NoHide);
				ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_WidthFixed, 25);
				ImGui::TableSetupColumn("Columns", ImGuiTableColumnFlags_WidthFixed, 50);
				ImGui::TableSetupColumn("Rows", ImGuiTableColumnFlags_WidthFixed, 50);
				ImGui::TableHeadersRow();
				drawSubsheetSelector(m_view.img().subsheet, path);
				ImGui::EndTable();
			}
		}
		ImGui::EndChild();
	}
	ImGui::EndChild();
	m_subsheetEditor.draw(m_tctx);
	m_exportMenu.draw(m_sctx);
}

void TileSheetEditorImGui::drawSubsheetSelector(
		TileSheet::SubSheet &subsheet, TileSheet::SubSheetIdx &path) {
	constexpr auto indentReduce = 14;
	ImGui::TableNextRow(0, 5);
	using Str = ox::BasicString<100>;
	auto pathStr = ox::join<Str>("##", path).value;
	auto lbl = ox::sfmt<Str>("{}##{}", subsheet.name, pathStr);
	auto const rowSelected = path == m_model.activeSubSheetIdx();
	auto const flags = ImGuiTreeNodeFlags_SpanFullWidth
	                 | ImGuiTreeNodeFlags_OpenOnArrow
	                 | ImGuiTreeNodeFlags_DefaultOpen
	                 | (subsheet.subsheets.empty() ? ImGuiTreeNodeFlags_Leaf : 0)
	                 | (rowSelected ? ImGuiTreeNodeFlags_Selected : 0);
	ImGui::TableNextColumn();
	auto const open = ImGui::TreeNodeEx(lbl.c_str(), flags);
	ImGui::SameLine();
	if (ImGui::IsItemClicked()) {
		m_model.setActiveSubsheet(path);
	}
	if (ImGui::IsMouseDoubleClicked(0) && ImGui::IsItemHovered()) {
		showSubsheetEditor();
	}
	ImGui::TableNextColumn();
	ImGui::Text("%d", subsheet.id);
	if (subsheet.subsheets.empty()) {
		ImGui::TableNextColumn();
		ImGui::Text("%d", subsheet.columns);
		ImGui::TableNextColumn();
		ImGui::Text("%d", subsheet.rows);
	} else {
		ImGui::TableNextColumn();
		ImGui::Text("--");
		ImGui::TableNextColumn();
		ImGui::Text("--");
	}
	if (open) {
		for (auto i = 0ul; auto &child : subsheet.subsheets) {
			path.push_back(i);
			ImGui::PushID(static_cast<int>(i));
			ImGui::Indent(-indentReduce);
			drawSubsheetSelector(child, path);
			ImGui::Indent(indentReduce);
			ImGui::PopID();
			path.pop_back();
			++i;
		}
		ImGui::TreePop();
	}
}

[[nodiscard]]
ox::Vec2 TileSheetEditorImGui::clickPos(ImVec2 const&winPos, ox::Vec2 clickPos) noexcept {
	clickPos.x -= winPos.x + 10;
	clickPos.y -= winPos.y + 10;
	return clickPos;
}

ox::Error TileSheetEditorImGui::saveItem() noexcept {
	return m_model.saveFile();
}

void TileSheetEditorImGui::showSubsheetEditor() noexcept {
	auto const&sheet = m_model.activeSubSheet();
	if (!sheet.subsheets.empty()) {
		m_subsheetEditor.show(sheet.name, -1, -1);
	} else {
		m_subsheetEditor.show(sheet.name, sheet.columns, sheet.rows);
	}
}

ox::Error TileSheetEditorImGui::exportSubhseetToPng(int scale) noexcept {
	oxRequire(path, studio::saveFile({{"PNG", "png"}}));
	// subsheet to png
	auto const&img = m_model.img();
	auto const&s = m_model.activeSubSheet();
	auto const&pal = m_model.pal();
	auto const width = s.columns * TileWidth;
	auto const height = s.rows * TileHeight;
	auto pixels = normalizePixelSizes(s.pixels, img.bpp);
	pixels = normalizePixelArrangement(pixels, s.columns, scale);
	auto const err = toPngFile(
			path,
			std::move(pixels),
			pal,
			m_model.palettePage(),
			static_cast<unsigned>(width * scale),
			static_cast<unsigned>(height * scale));
	if (err) {
		oxErrorf("Tilesheet export failed: {}", toStr(err));
	}
	return err;
}

void TileSheetEditorImGui::drawTileSheet(ox::Vec2 const&fbSize) noexcept {
	auto const winPos = ImGui::GetWindowPos();
	auto const fbSizei = ox::Size(static_cast<int>(fbSize.x), static_cast<int>(fbSize.y));
	if (m_framebuffer.width != fbSizei.width || m_framebuffer.height != fbSizei.height) {
		glutils::resizeInitFrameBuffer(m_framebuffer, fbSizei.width, fbSizei.height);
		m_view.resizeView(fbSize);
	} else if (m_view.updated()) {
		m_view.ackUpdate();
	}
	glBindFramebuffer(GL_FRAMEBUFFER, m_framebuffer);
	// clear screen and draw
	glViewport(0, 0, fbSizei.width, fbSizei.height);
	m_view.draw();
	glBindFramebuffer(GL_FRAMEBUFFER, 0);
	ImGui::Image(
		std::bit_cast<ImTextureID>(uintptr_t{m_framebuffer.color.id}),
		static_cast<ImVec2>(fbSize),
		ImVec2(0, 1),
		ImVec2(1, 0));
	// handle input, this must come after drawing
	auto const&io = ImGui::GetIO();
	auto const mousePos = ox::Vec2(io.MousePos);
	if (ImGui::IsItemHovered()) {
		auto const wheel = io.MouseWheel;
		auto const wheelh = io.MouseWheelH;
		if (wheel != 0) {
			const auto zoomMod = ox::defines::OS == ox::OS::Darwin ?
			                     io.KeySuper : turbine::buttonDown(m_tctx, turbine::Key::Mod_Ctrl);
			m_view.scrollV(fbSize, wheel, zoomMod);
		}
		if (wheelh != 0) {
			m_view.scrollH(fbSize, wheelh);
		}
		if (io.MouseDown[0] && m_prevMouseDownPos != mousePos) {
			m_prevMouseDownPos = mousePos;
			switch (m_tool) {
				case Tool::Draw:
					m_view.clickDraw(fbSize, clickPos(winPos, mousePos));
					break;
				case Tool::Fill:
					m_view.clickFill(fbSize, clickPos(winPos, mousePos));
					break;
				case Tool::Select:
					m_view.clickSelect(fbSize, clickPos(winPos, mousePos));
					break;
				case Tool::None:
					break;
			}
		}
	}
	if (ImGui::BeginPopupContextItem("TileMenu", ImGuiPopupFlags_MouseButtonRight)) {
		auto const popupPos = ox::Vec2(ImGui::GetWindowPos());
		if (ImGui::MenuItem("Insert Tile")) {
			m_view.insertTile(fbSize, clickPos(winPos, popupPos));
		}
		if (ImGui::MenuItem("Delete Tile")) {
			m_view.deleteTile(fbSize, clickPos(winPos, popupPos));
		}
		ImGui::EndPopup();
	}
	if (io.MouseReleased[0]) {
		m_prevMouseDownPos = {-1, -1};
		m_view.releaseMouseButton();
	}
}

void TileSheetEditorImGui::drawPaletteSelector() noexcept {
	auto const&files = m_sctx.project->fileList(core::FileExt_npal);
	auto const comboWidthSub = 62;
	ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - comboWidthSub);
	if (ig::ComboBox("Palette", files, m_selectedPaletteIdx)) {
		oxLogError(m_model.setPalette(files[m_selectedPaletteIdx]));
	}
	auto const pages = m_model.pal().pages.size();
	if (pages > 1) {
		ImGui::Indent(20);
		ox::Array<char, 10> numStr;
		ox::itoa(m_model.palettePage() + 1, numStr.data());
		ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - comboWidthSub);
		if (ImGui::BeginCombo("Page", numStr.data(), 0)) {
			for (auto n = 0u; n < pages; ++n) {
				auto const selected = (m_model.palettePage() == n);
				ox::itoa(n + 1, numStr.data());
				if (ImGui::Selectable(numStr.data(), selected) && m_model.palettePage() != n) {
					m_model.setPalettePage(n);
				}
				if (selected) {
					ImGui::SetItemDefaultFocus();
				}
			}
			ImGui::EndCombo();
		}
		ImGui::Indent(-20);
	}
	// header
	if (ImGui::BeginTable("PaletteTable", 3, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) {
		ImGui::TableSetupColumn("No.", 0, 0.45f);
		ImGui::TableSetupColumn("", 0, 0.22f);
		ImGui::TableSetupColumn("Color16", 0, 3);
		ImGui::TableHeadersRow();
		{
			auto const&pal = m_model.pal();
			for (auto i = 0u; auto c: pal.pages[m_model.palettePage()]) {
				ImGui::PushID(static_cast<int>(i));
				// Column: color idx
				ImGui::TableNextColumn();
				auto const label = ox::BString<8>() + (i + 1);
				auto const rowSelected = i == m_view.palIdx();
				if (ImGui::Selectable(label.c_str(), rowSelected, ImGuiSelectableFlags_SpanAllColumns)) {
					m_view.setPalIdx(i);
				}
				// Column: color RGB
				ImGui::TableNextColumn();
				auto ic = ImGui::GetColorU32(ImVec4(redf(c), greenf(c), bluef(c), 1));
				ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, ic);
				ImGui::TableNextColumn();
				ImGui::Text("(%02d, %02d, %02d)", red16(c), green16(c), blue16(c));
				ImGui::TableNextRow();
				ImGui::PopID();
				++i;
			}
		}
		ImGui::EndTable();
	}
}

ox::Error TileSheetEditorImGui::updateActiveSubsheet(ox::StringView const&name, int cols, int rows) noexcept {
	return m_model.updateSubsheet(m_model.activeSubSheetIdx(), name, cols, rows);
}

ox::Error TileSheetEditorImGui::setPaletteSelection() noexcept {
	auto const&palPath = m_model.palPath();
	auto const&palList = m_sctx.project->fileList(core::FileExt_npal);
	for (std::size_t i = 0; auto const&pal : palList) {
		if (palPath == pal) {
			m_selectedPaletteIdx = i;
			break;
		}
		++i;
	}
	return {};
}

ox::Error TileSheetEditorImGui::markUnsavedChanges(studio::UndoCommand const*) noexcept {
	setUnsavedChanges(true);
	return {};
}

void TileSheetEditorImGui::SubSheetEditor::draw(turbine::Context &sctx) noexcept {
	constexpr auto popupName = "Edit Subsheet";
	if (!m_show) {
		return;
	}
	auto const modSize = m_cols > 0;
	auto constexpr popupWidth = 235.f;
	auto const popupHeight = modSize ? 130.f : 85.f;
	auto const popupSz = ImVec2(popupWidth, popupHeight);
	if (ig::BeginPopup(sctx, popupName, m_show, popupSz)) {
		ImGui::InputText("Name", m_name.data(), m_name.cap());
		if (modSize) {
			ImGui::InputInt("Columns", &m_cols);
			ImGui::InputInt("Rows", &m_rows);
		}
		if (ig::PopupControlsOkCancel(popupWidth, m_show) == ig::PopupResponse::OK) {
			inputSubmitted.emit(m_name, m_cols, m_rows);
		}
		ImGui::EndPopup();
	}
}

void TileSheetEditorImGui::SubSheetEditor::close() noexcept {
	m_show = false;
}

void TileSheetEditorImGui::ExportMenu::draw(studio::StudioContext &sctx) noexcept {
	constexpr auto popupName = "Export Tile Sheet";
	if (!m_show) {
		return;
	}
	constexpr auto popupWidth = 235.f;
	constexpr auto popupHeight = 85.f;
	constexpr auto popupSz = ImVec2(popupWidth, popupHeight);
	if (ig::BeginPopup(sctx.tctx, popupName, m_show, popupSz)) {
		ImGui::InputInt("Scale", &m_scale);
		m_scale = ox::clamp(m_scale, 1, 50);
		if (ig::PopupControlsOkCancel(popupWidth, m_show) == ig::PopupResponse::OK) {
			inputSubmitted.emit(m_scale);
		}
		ImGui::EndPopup();
	}
}

void TileSheetEditorImGui::ExportMenu::close() noexcept {
	m_show = false;
}

}
