#include <psemek/ui/file_dialog.hpp>

#include <psemek/ui/window.hpp>
#include <psemek/ui/grid_layout.hpp>
#include <psemek/ui/button.hpp>
#include <psemek/ui/label.hpp>
#include <psemek/ui/scroller.hpp>
#include <psemek/ui/edit.hpp>
#include <psemek/ui/frame.hpp>
#include <psemek/util/to_string.hpp>
#include <psemek/util/recursive.hpp>
#include <psemek/io/memory_stream.hpp>
#include <psemek/io/file_stream.hpp>
#include <psemek/util/unicode.hpp>
#include <psemek/util/common_directories.hpp>
#include <psemek/util/to_shared.hpp>

#include <psemek/ui/resources/folder_png.hpp>
#include <psemek/ui/resources/file_png.hpp>
#include <psemek/ui/resources/back_png.hpp>

namespace psemek::ui
{

	namespace
	{

		struct frame_style_fix
			: ui::single_container
		{
			struct shape const & shape() const override
			{
				return child()->shape();
			}

			void reshape(math::box<float, 2> const & bbox) override
			{
				child()->reshape(bbox);
			}

			math::box<float, 2> size_constraints() const override
			{
				return child()->size_constraints();
			}

			math::interval<float> width_constraints(float height) const override
			{
				return child()->width_constraints(height);
			}

			math::interval<float> height_constraints(float width) const override
			{
				return child()->height_constraints(width);
			}

			void style_updated() const override
			{
				ui::single_container::style_updated();

				auto style = merged_style();
				auto new_style = std::make_shared<struct style>();
				new_style->bg_color = style->action_color;

				child()->set_own_style(new_style);
			}

			void draw(painter &) const override
			{}
		};

		struct file_dialog_image_provider
			: image_provider
		{
			file_dialog_image_provider()
			{
				back_.load(gfx::read_image<gfx::color_rgba>(io::memory_istream(resources::back_png.data)));
				back_.linear_filter();

				folder_.load(gfx::read_image<gfx::color_rgba>(io::memory_istream(resources::folder_png.data)));
				folder_.linear_filter();

				file_.load(gfx::read_image<gfx::color_rgba>(io::memory_istream(resources::file_png.data)));
				file_.linear_filter();
			}

			gfx::texture_view_2d get(std::string_view const & id) const override
			{
				if (id == "back")
					return gfx::texture_view_2d{&back_};
				if (id == "folder")
					return gfx::texture_view_2d{&folder_};
				if (id == "file")
					return gfx::texture_view_2d{&file_};
				return {};
			}

		private:
			gfx::texture_2d back_;
			gfx::texture_2d folder_;
			gfx::texture_2d file_;
		};

		std::string replace(std::string str, std::string const & match, std::string const & replacement)
		{
			for (std::size_t i = 0; (i = str.find(match, i)) != std::string::npos;)
			{
				str.replace(i, match.size(), replacement);
				i += replacement.size();
			}
			return str;
		}

		std::string escape(std::string str)
		{
			str = replace(std::move(str), "\\", "\\\\");
			str = replace(std::move(str), "[", "\\[");
			str = replace(std::move(str), "]", "\\]");
			return str;
		}

	}

	std::shared_ptr<window> make_file_dialog(file_dialog_options const & options)
	{
		auto window = options.element_factory.make_window(options.caption);

		auto main_layout = options.element_factory.make_grid_layout();
		main_layout->set_size(4, options.extra_widget ? 3 : 2);
		main_layout->set_column_weight(0, 0.f);
		if (options.extra_widget)
			main_layout->set_column_weight(2, 0.f);
		main_layout->set_row_weight(0, 0.f);
		main_layout->set_row_weight(2, 0.f);
		main_layout->set_row_weight(3, 0.f);

		auto image_provider = std::make_shared<file_dialog_image_provider>();

		auto current_path = std::make_shared<std::filesystem::path>();
		auto selected_path = std::make_shared<std::filesystem::path>();

		auto back_button = options.element_factory.make_button(image_provider->get("back"));
		back_button->icon()->set_downscale(false);

		auto back_button_style = std::make_shared<style>();
		back_button_style->inner_margin = math::vector{2, 2};
		back_button->set_style(back_button_style);

		auto common_paths_label = options.element_factory.make_label("");

		auto common_paths = util::to_shared(util::common_directories());
		{
			std::string str;
			int index = 0;
			for (auto const & dir : *common_paths)
			{
				str += util::to_string("[link:", index++, "]", escape(dir.first), "[/link]\n");
			}
			common_paths_label->set_tagged_text(str);
		}

		auto current_path_label = options.element_factory.make_label("");
		current_path_label->set_valign(label::valignment::center);

		auto directory_view_frame_style_fix = std::make_shared<frame_style_fix>();
		auto directory_view_frame = options.element_factory.make_frame();
		directory_view_frame->set_fixed_size(math::vector{600.f, 400.f});
		auto directory_view_scroller = options.element_factory.make_scroller();
		auto directory_view = options.element_factory.make_label("");

		auto directory_view_style = std::make_shared<style>();
		directory_view_style->link_color = gfx::white.as_color_rgba();
		directory_view->set_style(directory_view_style);

		auto path_edit = options.element_factory.make_edit();
		path_edit->set_enabled(options.type == file_dialog_type::save);

		auto bottom_layout = options.element_factory.make_grid_layout();
		bottom_layout->set_outer_margin(false);
		bottom_layout->set_size(1, 4);
		bottom_layout->set_column_weight(0, 0.f);
		bottom_layout->set_column_weight(2, 0.f);
		bottom_layout->set_column_weight(3, 0.f);

		auto status_label = options.element_factory.make_label("");

		auto status_label_style = std::make_shared<style>();
		status_label_style->text_color = gfx::red.as_color_rgba();
		status_label->set_style(status_label_style);

		auto confirm_button = options.element_factory.make_button(options.type == file_dialog_type::save ? "Save" : "Open");
		auto cancel_button = options.element_factory.make_button("Cancel");

		confirm_button->set_hidden(true);

		bottom_layout->set(0, 1, status_label);
		bottom_layout->set(0, 2, confirm_button);
		bottom_layout->set(0, 3, cancel_button);

		directory_view_scroller->set_child(directory_view);
		directory_view_frame->set_child(directory_view_scroller);
		directory_view_frame_style_fix->set_child(directory_view_frame);

		main_layout->set(0, 0, back_button);
		main_layout->set(1, 0, common_paths_label);
		main_layout->set(0, 1, current_path_label);
		main_layout->set(1, 1, directory_view_frame_style_fix);
		main_layout->set(2, 1, path_edit);
		main_layout->set(3, 1, bottom_layout);
		if (options.extra_widget)
			main_layout->set(1, 2, options.extra_widget);

		window->set_child(main_layout);

		directory_view->set_image_provider(image_provider.get());
		directory_view->data() = image_provider;

		auto set_path_callback = std::make_shared<std::function<void(std::filesystem::path)>>();

		*set_path_callback = [=, type = options.type, on_visited = options.on_visited](std::filesystem::path new_path)
		{
			new_path = std::filesystem::canonical(std::filesystem::absolute(new_path));

			try
			{
				[[maybe_unused]] auto test_iterator = std::filesystem::directory_iterator(new_path);
			}
			catch (std::filesystem::filesystem_error const & error)
			{
				status_label->set_text(error.code().message());
				return;
			}

			if (on_visited)
				on_visited(new_path);

			*current_path = new_path;

			status_label->set_text("");

			back_button->on_click([set_path_callback, new_path]{
				(*set_path_callback)(new_path.parent_path());
			});

			{
				std::string path_str;

				int index = 0;
				for (auto const & component : new_path)
				{
					if (!new_path.root_name().empty() && component == new_path.root_directory())
						continue;

					path_str += "[link:" + util::to_string(++index) + "]" + escape(component.string()) + "[/link]";
					if (!new_path.root_name().empty() || component != new_path.root_directory())
						path_str += escape(std::string(1, char(std::filesystem::path::preferred_separator)));
				}
				current_path_label->set_tagged_text(std::move(path_str));

				current_path_label->on_link_click([set_path_callback, new_path, count = index](std::string_view const & value){
					int index = util::from_string<int>(std::string{value});
					auto path = new_path;
					for (int i = 0; i < count - index; ++i)
						path = path.parent_path();
					(*set_path_callback)(path);
				});
			}

			{
				std::string contents_str;

				struct directory_entry
				{
					bool folder;
					std::filesystem::path path;
				};

				std::vector<directory_entry> entries;

				for (auto const & entry : std::filesystem::directory_iterator(new_path))
					entries.push_back({std::filesystem::is_directory(entry.path()), entry.path()});

				std::sort(entries.begin(), entries.end(), [](directory_entry const & e1, directory_entry const & e2){
					if (e1.folder && !e2.folder)
						return true;

					if (!e1.folder && e2.folder)
						return false;

					return e1.path.filename().string() < e2.path.filename().string();
				});

				int index = 0;
				for (auto const & entry : entries)
				{
					if (entry.folder)
						contents_str += "[image:folder]";
					else
						contents_str += "[image:file]";

					contents_str += "[link:" + util::to_string(index++) + "]";
					contents_str += escape(entry.path.filename().string()) + "[/link]\n";
				}
				directory_view->set_tagged_text(std::move(contents_str));
				directory_view_scroller->set_position(scroller::direction::vertical, 0.f, false);

				directory_view->on_link_click([=, entries = std::move(entries)](std::string_view const & value){
					int index = util::from_string<int>(std::string{value});
					if (entries[index].folder)
						(*set_path_callback)(entries[index].path);
					else
					{
						auto path = entries[index].path;

						try
						{
							if (type == file_dialog_type::load)
							{
								[[maybe_unused]] io::file_istream test(path);
							}
							else
							{
								[[maybe_unused]] io::file_ostream test(path, io::file_ostream::append);
							}
							path_edit->set_text(path.filename().string());
							status_label->set_text("");
							*selected_path = path;
						}
						catch (std::exception const & error)
						{
							status_label->set_text(error.what());
						}
					}
				});
			}
		};

		common_paths_label->on_link_click([common_paths, set_path_callback](std::string const & link){
			int index = util::from_string<int>(link);
			(*set_path_callback)((*common_paths)[index].second);
		});

		path_edit->on_text_changed([=](std::u32string_view const & text_u32){
			std::string text = util::to_utf8(std::u32string(text_u32));
			*selected_path = *current_path / text;
			confirm_button->set_hidden(text.empty());
		});

		cancel_button->on_click([=]{
			window->close();
		});

		window->on_close(options.on_canceled);

		confirm_button->on_click([=, on_selected = options.on_selected]{
			window->close();
			if (on_selected)
				on_selected(*selected_path);
		});

		(*set_path_callback)(options.path);

		return window;
	}

}
