/* === S Y N F I G ========================================================= */
/*!	\file dockmanager.cpp
**	\brief Template File
**
**	\legal
**	Copyright (c) 2002-2005 Robert B. Quattlebaum Jr., Adrian Bentley
**	Copyright (c) 2007, 2008 Chris Moore
**
**	This file is part of Synfig.
**
**	Synfig is free software: you can redistribute it and/or modify
**	it under the terms of the GNU General Public License as published by
**	the Free Software Foundation, either version 2 of the License, or
**	(at your option) any later version.
**
**	Synfig is distributed in the hope that it will be useful,
**	but WITHOUT ANY WARRANTY; without even the implied warranty of
**	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
**	GNU General Public License for more details.
**
**	You should have received a copy of the GNU General Public License
**	along with Synfig.  If not, see <https://www.gnu.org/licenses/>.
**	\endlegal
*/
/* ========================================================================= */

/* === H E A D E R S ======================================================= */

#ifdef USING_PCH
#	include "pch.h"
#else
#ifdef HAVE_CONFIG_H
#	include <config.h>
#endif

#include "docks/dockmanager.h"

#include <stdexcept>

#include <gdkmm/general.h>
#if GTK_CHECK_VERSION(3,22,0)
#include <gdkmm/monitor.h>
#endif

#include <gtkmm/paned.h>
#include <gtkmm/window.h>

#include <gui/app.h>
#include <gui/canvasview.h>
#include <gui/docks/dockable.h>
#include <gui/docks/dockbook.h>
#include <gui/docks/dockdialog.h>
#include <gui/localization.h>
#include <gui/mainwindow.h>

#include <synfig/general.h>

#include <synfigapp/settings.h>
#include <synfigapp/main.h>

#endif

/* === U S I N G =========================================================== */

using namespace studio;

/* === M A C R O S ========================================================= */

/* === P R O C E D U R E S ================================================= */

std::map<Gtk::Container*, bool> DockManager::containers_to_remove_;

namespace studio {
	class DockLinkPoint {
	public:
		Gtk::Bin *bin;
		Gtk::Paned *paned;
		Gtk::Window *window;
		bool is_first;

		DockLinkPoint(): bin(nullptr), paned(nullptr), window(nullptr), is_first(false) { }
		explicit DockLinkPoint(Gtk::Bin* bin): bin(bin), paned(nullptr), window(nullptr), is_first(false) { }
		explicit DockLinkPoint(Gtk::Paned* paned, bool is_first): bin(nullptr), paned(paned), window(nullptr), is_first(is_first) { }
		explicit DockLinkPoint(Gtk::Window* window): bin(nullptr), paned(nullptr), window(window), is_first(false) { }
		explicit DockLinkPoint(Gtk::Widget& widget) {
			Gtk::Container *container = widget.get_parent();
			bin = dynamic_cast<Gtk::Bin*>(container);
			paned = dynamic_cast<Gtk::Paned*>(container);
			window = dynamic_cast<Gtk::Window*>(container);
			is_first = paned && paned->get_child1() == &widget;
		}

		bool is_valid() { return bin || paned || window; }

		void unlink() {
			if (paned && is_first && paned->get_child1())
				paned->remove(*paned->get_child1());
			else
			if (paned && !is_first && paned->get_child2())
				paned->remove(*paned->get_child2());
			else
			if (window)
				window->remove();
			if (bin)
				bin->remove();
		}

		void link(Gtk::Widget &widget)
		{
			if (paned && is_first)
				paned->pack1(widget, true, false);
			else
			if (paned && !is_first)
				paned->pack2(widget, true, false);
			else
			if (window)
				window->add(widget);
			else
			if (bin)
				bin->add(widget);
		}
	};
}

class studio::DockSettings : public synfigapp::Settings
{
	DockManager* dock_manager;

public:
	DockSettings(DockManager* dock_manager):dock_manager(dock_manager)
	{
		synfigapp::Main::settings().add_domain(this,"workspace");
	}

	virtual ~DockSettings()
	{
		synfigapp::Main::settings().remove_domain("workspace");
	}

	virtual bool get_raw_value(const synfig::String& key_, synfig::String& value)const override
	{
		try
		{
			if (key_ == "layout")
			{
				value = dock_manager->save_layout_to_string();
				return true;
			}
		}catch (...) { return false; }
		return synfigapp::Settings::get_raw_value(key_,value);
	}

	virtual bool set_value(const synfig::String& key_,const synfig::String& value) override
	{
		try
		{
			if (key_ == "layout")
			{
				dock_manager->load_layout_from_string(value);
				return true;
			}
		}catch (...) { return false; }
		return synfigapp::Settings::set_value(key_,value);
	}

	virtual KeyList get_key_list()const override
	{
		synfigapp::Settings::KeyList ret(synfigapp::Settings::get_key_list());
		ret.push_back("layout");
		return ret;
	}
};

/* === M E T H O D S ======================================================= */

DockManager::DockManager():
	dock_settings(new DockSettings(this))
{
}

DockManager::~DockManager()
{
	while(!dock_dialog_list_.empty())
	{
		dock_dialog_list_.back()->close();
	}
	while(!dockable_list_.empty())
	{
		Dockable* dockable(dockable_list_.back());
		// synfig::info("DockManager::~DockManager(): Deleting dockable \"%s\"",dockable->get_name().c_str());
		dockable_list_.pop_back();
		delete dockable;
	}
}

void
DockManager::register_dockable(Dockable& x)
{
	dockable_list_.push_back(&x);
	signal_dockable_registered()(&x);
}

bool
DockManager::unregister_dockable(Dockable& x)
{
	for(std::list<Dockable*>::iterator iter = dockable_list_.begin(); iter != dockable_list_.end(); ++iter)
	{
		if (&x == *iter)
		{
			remove_widget_recursive(x);
			dockable_list_.erase(iter);
			signal_dockable_unregistered()(&x);
			update_window_titles();
			return true;
		}
	}
	return false;
}

Dockable&
DockManager::find_dockable(const synfig::String& x)
{
	std::list<Dockable*>::iterator iter;
	for(iter=dockable_list_.begin();iter!=dockable_list_.end();++iter)
		if((*iter)->get_name()==x)
			return **iter;

	throw std::runtime_error("DockManager::find_dockable(): not found");
}

void
DockManager::present(synfig::String x)
{
	try
	{
		find_dockable(x).present();
	}
	catch(...)
	{
	}
}

DockDialog&
DockManager::find_dock_dialog(int id)
{
	std::list<DockDialog*>::iterator iter;
	for(iter=dock_dialog_list_.begin();iter!=dock_dialog_list_.end();++iter)
		if((*iter)->get_id()==id)
			return **iter;

	DockDialog* dock_dialog(new DockDialog());
	dock_dialog->set_id(id);
	return *dock_dialog;
}

const DockDialog&
DockManager::find_dock_dialog(int id)const
{
	std::list<DockDialog*>::const_iterator iter;
	for(iter=dock_dialog_list_.begin();iter!=dock_dialog_list_.end();++iter)
		if((*iter)->get_id()==id)
			return **iter;

	throw std::runtime_error("DockManager::find_dock_dialog(int id)const: not found");
}

void
DockManager::show_all_dock_dialogs()
{
	std::list<DockDialog*>::iterator iter;
	for(iter=dock_dialog_list_.begin();iter!=dock_dialog_list_.end();++iter)
		(*iter)->present();
}

bool
DockManager::swap_widgets(Gtk::Widget &widget1, Gtk::Widget &widget2)
{
	DockLinkPoint point1(widget1);
	DockLinkPoint point2(widget2);
	if (point1.is_valid() && point2.is_valid())
	{
		point1.unlink();
		point2.unlink();
		point1.link(widget2);
		point2.link(widget1);
		return true;
	}
	return false;
}

void
DockManager::remove_empty_container_recursive(Gtk::Container &container)
{
	containers_to_remove_.erase(&container);
	Gtk::Paned *paned = dynamic_cast<Gtk::Paned*>(&container);
	Gtk::Window *window = dynamic_cast<Gtk::Window*>(&container);
	DockBook *book = dynamic_cast<DockBook*>(&container);

	if (paned)
	{
		if (paned->get_child1() && paned->get_child2()) return;
		Gtk::Widget *child = paned->get_child1() ? paned->get_child1() : paned->get_child2();
		if (child)
		{
			DockLinkPoint link(*paned);
			if (link.is_valid())
			{
				paned->remove(*child);
				link.unlink();
				link.link(*child);
				delete paned;
			}
		}
		else
		{
			remove_widget_recursive(*paned);
			delete paned;
			return;
		}
	}
	else
	if (window)
	{
		if (!window->get_child())
			window->close();
	}
	else
	if (book)
	{
		if (!book->allow_empty && book->get_n_pages() == 0)
		{
			remove_widget_recursive(*book);
			delete book;
		}
	}
}

void
DockManager::remove_widget_recursive(Gtk::Widget &widget)
{
	Gtk::Container *container = widget.get_parent();
	if (container)
	{
		container->remove(widget);
		remove_empty_container_recursive(*container);
	}
}

bool
DockManager::add_widget(Gtk::Widget &dest_widget, Gtk::Widget &src_widget, bool vertical, bool first)
{
	if (&src_widget == &dest_widget) return false;

	// check for src widget is parent for dest_widget
	for(Gtk::Widget* parent = src_widget.get_parent(); parent; parent = parent->get_parent())
		if (parent == &dest_widget)
			return swap_widgets(src_widget, dest_widget);

	// unlink dest_widget
	DockLinkPoint dest_link(dest_widget);
	if (!dest_link.is_valid()) return false;
	dest_link.unlink();

	// unlink src_widget
	remove_widget_recursive(src_widget);

	// create new paned and link all
	Gtk::Paned *paned = manage(new Gtk::Paned(vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL));
	paned->show();
	DockLinkPoint(paned, first).link(src_widget);
	DockLinkPoint(paned, !first).link(dest_widget);
	dest_link.link(*paned);
	return true;
}

bool
DockManager::add_dockable(Gtk::Widget &dest_widget, Dockable &dockable, bool vertical, bool first)
{
	DockBook *book = manage(new DockBook());
	book->show();
	if (add_widget(dest_widget, *book, vertical, first))
	{
		book->add(dockable);
		return true;
	}
	delete book;
	return false;
}

bool DockManager::read_separator(std::string &x)
{
	size_t pos = x.find_first_of("|]");
	if (pos == std::string::npos) { x.clear(); return false; }
	if (x[pos] == '|') { x = x.substr(pos+1); return true; }
	if (x[pos] == ']') x = x.substr(pos+1);
	return false;
}

std::string DockManager::read_string(std::string &x)
{
	size_t pos = x.find_first_of("|]");
	std::string res = x.substr(0, pos);
	if (pos == std::string::npos) x.clear(); else x = x.substr(pos);
	return res;
}

std::string DockManager::extract_dockable_name(std::string &x) const
{
	size_t pos = x.find(',');
	std::string res = x.substr(0, pos);
	if (pos == std::string::npos)
		x.clear();
	else
		x = x.substr(pos+1); // skip comma
	return res;
}

int DockManager::read_int(std::string &x)
{
	return strtol(read_string(x).c_str(), nullptr, 10);
}

bool DockManager::read_bool(std::string &x)
{
	return read_string(x) == "true";
}

static void check_and_fix_window_position(int &x, int &y, int &width, int &height)
{
	// At least magic_mininum_length x magic_mininum_length pixels of window must be shown on display
	constexpr int magic_mininum_length = 50;

	// First & easy checks
	if (width < magic_mininum_length)
		width = magic_mininum_length;
	if (height < magic_mininum_length)
		height = magic_mininum_length;

	if (y + height < magic_mininum_length)
		y = 0;
	if (x + width < magic_mininum_length)
		x = 0;

	// How much of window is visible on monitor layout?
	auto display = Gdk::Display::get_default();
	const Gdk::Rectangle window_rect(x,y,width,height);
	Gdk::Rectangle window_visible_rect;

	Gdk::Rectangle monitor_rect;

#if GTK_CHECK_VERSION(3,22,0)
	int n_monitors = display->get_n_monitors();
	auto monitor_at_topleft = display->get_monitor_at_point(x, y);
	monitor_at_topleft->get_geometry(monitor_rect);
#else
	auto screen = display->get_default_screen();
	int n_monitors = screen->get_n_monitors();
	auto monitor_at_topleft_idx = screen->get_monitor_at_point(x, y);
	screen->get_monitor_geometry(monitor_at_topleft_idx, monitor_rect);
#endif

	// adjust topleft point to lie in the monitor
	if (x < monitor_rect.get_x() || x > monitor_rect.get_x() + monitor_rect.get_width())
		x = monitor_rect.get_x();
	if (y < monitor_rect.get_y() || y > monitor_rect.get_y() + monitor_rect.get_height())
		y = monitor_rect.get_y();

	for (int i = 0; i < n_monitors; i++) {
		Gdk::Rectangle rect;
#if GTK_CHECK_VERSION(3,22,0)
		display->get_monitor(i)->get_geometry(rect);
#else
		screen->get_monitor_geometry(i, rect);
#endif

		rect.intersect(window_rect);

		// Is the minimum required size and pos in this monitor? Ok, then, no need to fix anything more
		if (rect.get_height() >= magic_mininum_length && rect.get_width() >= magic_mininum_length)
			return;

		window_visible_rect.join(rect);
	}

	// If minimum visible area is too small, move the window...
	bool bad_visible_height = window_visible_rect.get_height() < magic_mininum_length;
	bool bad_visible_width = window_visible_rect.get_width() < magic_mininum_length;
	if (bad_visible_height || bad_visible_width) {
		if (bad_visible_height)
			y = monitor_rect.get_y();
		if (bad_visible_width)
			y = monitor_rect.get_x();
	}

}

Gtk::Widget* DockManager::read_widget(std::string &x)
{
	bool hor = x.substr(0, 5) == "[hor|";
	bool vert = x.substr(0, 6) == "[vert|";

	// paned
	if (hor || vert)
	{
		// skip "[hor|" or "[vert|"
		x = x.substr(1);
		if (!read_separator(x)) return nullptr;

		int size = read_int(x);
		if (!read_separator(x)) return nullptr;

		Gtk::Widget* first = nullptr;
		Gtk::Widget* second = nullptr;

		first = read_widget(x);
		if (!read_separator(x)) return first;
		second = read_widget(x);
		read_separator(x);

		if (!first && !second) return nullptr;
		if (first && !second) return first;
		if (!first && second) return second;

		// create paned
		Gtk::Paned *paned = manage(new Gtk::Paned(vert ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL));
		paned->pack1(*first,  true, false);
		paned->pack2(*second, true, false);
		paned->set_position(size);
		paned->show();
		return paned;
	}
	else
	if (x.substr(0, 6) == "[book|")
	{
		// skip "[book|"
		x = x.substr(1);
		if (!read_separator(x)) return nullptr;

		DockBook* book = nullptr;
		do
		{
			std::string dockable_name_params = read_string(x);
			std::string name = extract_dockable_name(dockable_name_params);
			if (!name.empty())
			{
				Dockable &dockable = find_dockable(name);
				if (!dockable_name_params.empty())
					dockable.read_layout_string(dockable_name_params);
				Gtk::Container *container = dockable.get_parent();
				if (container) {
					container->remove(dockable);
					containers_to_remove_[container] = true;
				}
				if (!book) { book = manage(new DockBook()); book->show(); }
				book->add(dockable);
			}			
		} while (read_separator(x));

		return book;
	}
	else
	if (x.substr(0, 8) == "[dialog|")
	{
		// skip "[dialog|"
		x = x.substr(1);
		if (!read_separator(x)) return nullptr;

		int left = read_int(x);
		if (!read_separator(x)) return nullptr;
		int top = read_int(x);
		if (!read_separator(x)) return nullptr;
		int width = read_int(x);
		if (!read_separator(x)) return nullptr;
		int height = read_int(x);
		if (!read_separator(x)) return nullptr;

		check_and_fix_window_position(left, top, width, height);

		Gtk::Widget *widget = read_widget(x);
		read_separator(x);

		if (!widget) return nullptr;

		DockDialog *dialog = new DockDialog();
		dialog->add(*widget);
		dialog->move(left, top);
		dialog->set_default_size(width, height);
		dialog->resize(width, height);

		return nullptr;
	}
	else
	if (x.substr(0, 12) == "[mainwindow|")
	{
		// skip "[mainwindow|"
		x = x.substr(1);
		if (!read_separator(x)) return nullptr;

		int left = read_int(x);
		if (!read_separator(x)) return nullptr;
		int top = read_int(x);
		if (!read_separator(x)) return nullptr;
		int width = read_int(x);
		if (!read_separator(x)) return nullptr;
		int height = read_int(x);
		if (!read_separator(x)) return nullptr;

		Gtk::Widget *widget = read_widget(x);
		read_separator(x);

		if (!widget) return nullptr;

		check_and_fix_window_position(left, top, width, height);

		Gtk::Widget *child = App::main_window->root().get_child();
		App::main_window->root().remove();
		if (child && child != &App::main_window->main_dock_book())
			delete child;
		App::main_window->root().add(*widget);

		App::main_window->move(left, top);
		App::main_window->set_default_size(width, height);
		App::main_window->resize(width, height);

		return nullptr;
	}
	else
	if (x.substr(0, 14) == "[mainnotebook]")
	{
		x = x.substr(14);
		if (App::main_window->main_dock_book().get_parent())
			App::main_window->main_dock_book().get_parent()->remove(App::main_window->main_dock_book());
		return &App::main_window->main_dock_book();
	}

	return nullptr;
}

void DockManager::write_string(std::string &x, const std::string &str)
	{ x += str; }
void DockManager::write_separator(std::string &x, bool continue_)
	{ write_string(x, continue_ ? "|" : "]"); }
void DockManager::write_int(std::string &x, int i)
	{ write_string(x, synfig::strprintf("%d", i)); }
void DockManager::write_bool(std::string &x, bool b)
	{ write_string(x, b ? "true" : "false"); }

void DockManager::write_widget(std::string &x, Gtk::Widget* widget)
{
	Gtk::Paned *paned = dynamic_cast<Gtk::Paned*>(widget);
	DockBook *book = dynamic_cast<DockBook*>(widget);
	DockDialog *dialog = dynamic_cast<DockDialog*>(widget);

	if (!widget)
	{
		return;
	}
	else
	if (widget == App::main_window)
	{
		write_string(x, "[mainwindow|");
		int left = 0, top = 0, width = 0, height = 0;
		App::main_window->get_position(left, top);
		App::main_window->get_size(width, height);
		write_int(x, left);
		write_separator(x);
		write_int(x, top);
		write_separator(x);
		write_int(x, width);
		write_separator(x);
		write_int(x, height);
		write_separator(x);

		write_widget(x, App::main_window->root().get_child());
		write_separator(x, false);
	}
	else
	if (widget == &App::main_window->main_dock_book())
	{
		write_string(x, "[mainnotebook]");
	}
	else
	if (dialog)
	{
		write_string(x, "[dialog|");
		int left = 0, top = 0, width = 0, height = 0;
		dialog->get_position(left, top);
		dialog->get_size(width, height);
		write_int(x, left);
		write_separator(x);
		write_int(x, top);
		write_separator(x);
		write_int(x, width);
		write_separator(x);
		write_int(x, height);
		write_separator(x);

		write_widget(x, dialog->get_child());
		write_separator(x, false);
	}
	else
	if (paned)
	{
		write_string(x, paned->get_orientation() == Gtk::ORIENTATION_HORIZONTAL ? "[hor|" : "[vert|");
		write_int(x, paned->get_position());
		write_separator(x);
		write_widget(x, paned->get_child1());
		write_separator(x);
		write_widget(x, paned->get_child2());
		write_separator(x, false);
	}
	else
	if (book)
	{
		write_string(x, "[book");
		for(int i = 0; i < book->get_n_pages(); ++i)
		{
			Dockable *dockable = dynamic_cast<Dockable*>(book->get_nth_page(i));
			if (dockable)
			{
				write_separator(x);
				std::string name_params = dockable->get_name();
				std::string params;
				dockable->write_layout_string(params);
				if (!params.empty()) {
					if (params.find_first_of("]|") != std::string::npos) {
						synfig::warning("Ignoring %s's layout info: it must not have ] or | characters.", dockable->get_name().c_str());
					} else {
						name_params += "," + params;
					}
				}
				write_string(x, name_params);
			}
		}
		write_separator(x, false);
	}
}

std::string DockManager::save_widget_to_string(Gtk::Widget *widget)
{
	std::string res;
	write_widget(res, widget);
	return res;
}

Gtk::Widget* DockManager::load_widget_from_string(const std::string &x)
{
	std::string copy(x);
	Gtk::Widget *widget = read_widget(copy);
	while (!containers_to_remove_.empty())
		remove_empty_container_recursive(*containers_to_remove_.begin()->first);
	return widget;
}

std::string DockManager::save_layout_to_string()
{
	std::string res;
	for (std::list<DockDialog*>::iterator i = dock_dialog_list_.begin(); i != dock_dialog_list_.end(); ++i) {
		write_widget(res, *i);
		write_separator(res);
	}
	write_widget(res, App::main_window);
	return res;
}

void DockManager::load_layout_from_string(const std::string &x)
{
	std::string copy(x);
	do
	{
		read_widget(copy);
	} while (read_separator(copy));
	while (!containers_to_remove_.empty())
		remove_empty_container_recursive(*containers_to_remove_.begin()->first);
}

std::string DockManager::layout_from_template(const std::string &tpl, float dx, float dy, float sx, float sy)
{
	std::string res;
	size_t pos_begin;
	size_t pos_end = 0;
	while(true)
	{
		pos_begin = tpl.find_first_of('%', pos_end);
		if (pos_begin == std::string::npos)
			{ res+=tpl.substr(pos_end); break; }
		res+=tpl.substr(pos_end, pos_begin-pos_end);
		pos_end = tpl.find_first_of("xyXY", pos_begin);
		if (pos_end == std::string::npos) break;
		float f = (float)strtol(tpl.c_str()+pos_begin+1, nullptr, 10);
		if (tpl[pos_end] == 'X') res += synfig::strprintf("%d", (int)roundf(dx+f*sx/100.f));
		if (tpl[pos_end] == 'Y') res += synfig::strprintf("%d", (int)roundf(dy+f*sy/100.f));
		if (tpl[pos_end] == 'x') res += synfig::strprintf("%d", (int)roundf(f*sx/100.f));
		if (tpl[pos_end] == 'y') res += synfig::strprintf("%d", (int)roundf(f*sy/100.f));
		pos_end++;
	}
	return res;
}

void DockManager::set_dock_area_visibility(bool visible, DockBook* source)
{
	for (auto iter = dockable_list_.begin(); iter != dockable_list_.end(); ++iter) {
		Dockable * dockable = *iter;
		if (!dockable->is_visible())
			continue;
		DockBook * book = dynamic_cast<DockBook*>((*iter)->get_parent());
		if (book)
			book->set_dock_area_visibility(visible, source);
	}
}

void
DockManager::update_window_titles()
{
	// build maps
	typedef std::map< CanvasView::ActivationIndex, CanvasView* > CanvasViewMap;
	typedef std::map< Glib::RefPtr<Gdk::Window>, std::string > TitleMap;
	CanvasViewMap canvas_view_map;
	TitleMap title_map;
	for (std::list<Dockable*>::iterator i = dockable_list_.begin(); i != dockable_list_.end(); ++i) {
		if ((*i)->get_parent_window())
		{
			title_map[(*i)->get_parent_window()] = (*i)->get_parent_window() == App::main_window->get_window()
			                                     ? _("Synfig Studio") : _("Dock Panel");
			CanvasView *canvas_view = dynamic_cast<CanvasView*>(*i);
			if (canvas_view)
				canvas_view_map[canvas_view->get_activation_index()] = canvas_view;
		}
	}

	// prepare titles
	for (CanvasViewMap::iterator i = canvas_view_map.begin(); i != canvas_view_map.end(); ++i)
		title_map[ i->second->get_parent_window() ] =
			i->second->get_local_name() + " - " + _("Synfig Studio");

	// set titles
	for (TitleMap::iterator i = title_map.begin(); i != title_map.end(); ++i)
		i->first->set_title(i->second);
}
