///|/ Copyright (c) Prusa Research 2021 - 2023 Oleksandra Iushchenko @YuSanka, David Kocík @kocikdav, Vojtěch Bubník @bubnikv, Lukáš Matěna @lukasmatena, Lukáš Hejl @hejllukas
///|/ Copyright (c) 2021 odaki @odaki
///|/
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
///|/
#include "HintNotification.hpp"
#include "ImGuiPureWrap.hpp"
#include "format.hpp"
#include "I18N.hpp"
#include "GUI_ObjectList.hpp"
#include "GLCanvas3D.hpp"
#include "MainFrame.hpp"
#include "Tab.hpp"
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/PrintConfig.hpp"

#include <map>

#include <boost/algorithm/string/replace.hpp>
#include <boost/filesystem.hpp>
#include <boost/nowide/fstream.hpp>
#include <boost/log/trivial.hpp>
#include <boost/property_tree/ini_parser.hpp>

#include <cereal/archives/binary.hpp>
#include <cereal/types/string.hpp>
#include <cereal/types/vector.hpp>

#define HINTS_CEREAL_VERSION 1
// structure for writing used hints into binary file with version
struct HintsCerealData
{
	std::vector<std::string> my_data;
	// cereal will supply the version automatically when loading or saving
	// The version number comes from the CEREAL_CLASS_VERSION macro
	template<class Archive>
	void serialize(Archive& ar, std::uint32_t const version)
	{
		// You can choose different behaviors depending on the version
		// This is useful if you need to support older variants of your codebase
		// interacting with newer ones
		if (version > HINTS_CEREAL_VERSION)
			throw Slic3r::IOError("Version of hints.cereal is higher than current version.");
		else
			ar(my_data);
	}
};
// version of used hints binary file
CEREAL_CLASS_VERSION(HintsCerealData, HINTS_CEREAL_VERSION);

namespace Slic3r {
namespace GUI {

const std::string BOLD_MARKER_START      = "<b>"; 
const std::string BOLD_MARKER_END        = "</b>";
const std::string HYPERTEXT_MARKER_START = "<a>";
const std::string HYPERTEXT_MARKER_END   = "</a>";

namespace {
inline void push_style_color(ImGuiCol idx, const ImVec4& col, bool fading_out, float current_fade_opacity)
{
	if (fading_out)
		ImGui::PushStyleColor(idx, ImVec4(col.x, col.y, col.z, col.w * current_fade_opacity));
	else
		ImGui::PushStyleColor(idx, col);
}

void write_used_binary(const std::vector<std::string>& ids)
{
	boost::nowide::ofstream file((boost::filesystem::path(data_dir()) / "cache" / "hints.cereal").string(), std::ios::binary);
	cereal::BinaryOutputArchive archive(file);
		HintsCerealData cd { ids };
	try
	{
		archive(cd);
	}
	catch (const std::exception& ex)
	{
		BOOST_LOG_TRIVIAL(error) << "Failed to write to hints.cereal. " << ex.what();
	}
}
void read_used_binary(std::vector<std::string>& ids)
{
	boost::filesystem::path path(boost::filesystem::path(data_dir()) / "cache" / "hints.cereal");
	if (!boost::filesystem::exists(path)) {
		BOOST_LOG_TRIVIAL(warning) << "Failed to load to hints.cereal. File does not exists. " << path.string();
		return;
	}
	boost::nowide::ifstream file(path.string(), std::ios::binary);
	cereal::BinaryInputArchive archive(file);
	HintsCerealData cd;
	try
	{
		archive(cd);
	}
	catch (const std::exception& ex)
	{
		BOOST_LOG_TRIVIAL(error) << "Failed to load to hints.cereal. " << ex.what();
		return;
	}
	ids = cd.my_data;
}
enum TagCheckResult
{
	TagCheckAffirmative,
	TagCheckNegative,
	TagCheckNotCompatible
};
// returns if in mode defined by tag
TagCheckResult tag_check_mode(const std::string& tag)
{
	std::vector<std::string> allowed_tags = {"simple", "advanced", "expert"};
	if (std::find(allowed_tags.begin(), allowed_tags.end(), tag) != allowed_tags.end())
	{
		ConfigOptionMode config_mode = wxGetApp().get_mode();
		if (config_mode == ConfigOptionMode::comSimple)        return (tag == "simple"   ? TagCheckAffirmative : TagCheckNegative);
		else if (config_mode == ConfigOptionMode::comAdvanced) return (tag == "advanced" ? TagCheckAffirmative : TagCheckNegative);
		else if (config_mode == ConfigOptionMode::comExpert)   return (tag == "expert"   ? TagCheckAffirmative : TagCheckNegative);
	}
	return TagCheckNotCompatible;
}

TagCheckResult tag_check_tech(const std::string& tag)
{
	std::vector<std::string> allowed_tags = { "FFF", "MMU", "SLA" };
	if (std::find(allowed_tags.begin(), allowed_tags.end(), tag) != allowed_tags.end()) {
		const PrinterTechnology tech = wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology();
		if (tech == ptFFF) {
			// MMU / FFF
			bool is_mmu = wxGetApp().extruders_edited_cnt() > 1;
			if (tag == "MMU") return (is_mmu ? TagCheckAffirmative : TagCheckNegative);
			return (tag == "FFF" ? TagCheckAffirmative : TagCheckNegative);
		} else {
			// SLA
			return (tag == "SLA" ? TagCheckAffirmative : TagCheckNegative);
		}
	}
	return TagCheckNotCompatible;
}

TagCheckResult tag_check_system(const std::string& tag)
{
	std::vector<std::string> allowed_tags = { "Windows", "Linux", "OSX" };
	if (std::find(allowed_tags.begin(), allowed_tags.end(), tag) != allowed_tags.end()) {
		if (tag =="Windows")
#ifdef WIN32
			return TagCheckAffirmative;
#else 
			return TagCheckNegative;
#endif // WIN32

		if (tag == "Linux")
#ifdef __linux__
			return TagCheckAffirmative;
#else 
			return TagCheckNegative;
#endif // __linux__

		if (tag == "OSX")
#ifdef __APPLE__
			return TagCheckAffirmative;
#else 
			return TagCheckNegative;
#endif // __apple__
	}
	return TagCheckNotCompatible;
}

TagCheckResult tag_check_material(const std::string& tag)
{
	if (const GUI::Tab* tab = wxGetApp().get_tab(Preset::Type::TYPE_FILAMENT)) {
		// search PrintConfig filament_type to find if allowed tag
		if (wxGetApp().app_config->get("filament_type").find(tag)) {
			const Preset& preset = tab->m_presets->get_edited_preset();
			const auto* opt = preset.config.opt<ConfigOptionStrings>("filament_type");
			if (opt->values[0] == tag)
				return TagCheckAffirmative;
			return TagCheckNegative;
		}
		return TagCheckNotCompatible;
	}
	/* TODO: SLA materials
	else if (const GUI::Tab* tab = wxGetApp().get_tab(Preset::Type::TYPE_SLA_MATERIAL)) {
		//if (wxGetApp().app_config->get("material_type").find(tag)) {
			const Preset& preset = tab->m_presets->get_edited_preset();
			const auto* opt = preset.config.opt<ConfigOptionStrings>("material_type");
			if (opt->values[0] == tag)
				return TagCheckAffirmative;
			return TagCheckNegative;
		//}
		return TagCheckNotCompatible;
	}*/
	return TagCheckNotCompatible;
}

// return true if NOT in disabled mode.
bool tags_check(const std::string& disabled_tags, const std::string& enabled_tags)
{
	if (disabled_tags.empty() && enabled_tags.empty())
		return true;
	// enabled tags must ALL return affirmative or check fails
	if (!enabled_tags.empty()) {
		std::string tag;
		for (size_t i = 0; i < enabled_tags.size(); i++) {
			if (enabled_tags[i] == ' ') {
				tag.erase();
				continue;
			}
			if (enabled_tags[i] != ';') {
				tag += enabled_tags[i];
			}
			if (enabled_tags[i] == ';' || i == enabled_tags.size() - 1) {
				if (!tag.empty()) {
					TagCheckResult result;
					result = tag_check_mode(tag);
					if (result == TagCheckResult::TagCheckNegative)
						return false;
					if (result == TagCheckResult::TagCheckAffirmative)
						continue;
					result = tag_check_tech(tag);
					if (result == TagCheckResult::TagCheckNegative)
						return false;
					if (result == TagCheckResult::TagCheckAffirmative)
						continue;
					result = tag_check_system(tag);
					if (result == TagCheckResult::TagCheckNegative)
						return false;
					if (result == TagCheckResult::TagCheckAffirmative)
						continue;
					result = tag_check_material(tag);
					if (result == TagCheckResult::TagCheckNegative)
						return false;
					if (result == TagCheckResult::TagCheckAffirmative)
						continue;
					BOOST_LOG_TRIVIAL(error) << "Hint Notification: Tag " << tag << " in enabled_tags not compatible.";
					// non compatible in enabled means return false since all enabled must be affirmative.
					return false;
				}
			}
		}
	}
	// disabled tags must all NOT return affirmative or check fails
	if (!disabled_tags.empty()) {
		std::string tag;
		for (size_t i = 0; i < disabled_tags.size(); i++) {
			if (disabled_tags[i] == ' ') {
				tag.erase();
				continue;
			}
			if (disabled_tags[i] != ';') {
				tag += disabled_tags[i];
			}
			if (disabled_tags[i] == ';' || i == disabled_tags.size() - 1) {
				if (!tag.empty()) {
					TagCheckResult result;
					result = tag_check_mode(tag);
					if (result == TagCheckResult::TagCheckNegative)
						continue;
					if (result == TagCheckResult::TagCheckAffirmative)
						return false;
					result = tag_check_tech(tag);
					if (result == TagCheckResult::TagCheckNegative)
						continue;
					if (result == TagCheckResult::TagCheckAffirmative)
						return false;
					result = tag_check_system(tag);
					if (result == TagCheckResult::TagCheckAffirmative)
						return false;
					if (result == TagCheckResult::TagCheckNegative)
						continue;
					result = tag_check_material(tag);
					if (result == TagCheckResult::TagCheckAffirmative)
						return false;
					if (result == TagCheckResult::TagCheckNegative)
						continue;
					BOOST_LOG_TRIVIAL(error) << "Hint Notification: Tag " << tag << " in disabled_tags not compatible.";
				}
			}
		}
	}
	return true;
}
void launch_browser_if_allowed(const std::string& url)
{
	wxGetApp().open_browser_with_warning_dialog(url);
}
} //namespace
HintDatabase::~HintDatabase()
{
	if (m_initialized) {
		write_used_binary(m_used_ids);
	}
}
void HintDatabase::uninit()
{
	if (m_initialized) {
		write_used_binary(m_used_ids);
	}
	m_initialized = false;
	m_loaded_hints.clear();
	m_sorted_hints = false;
	m_used_ids.clear();
	 m_used_ids_loaded = false;
}
void HintDatabase::init()
{
	load_hints_from_file(std::move(boost::filesystem::path(resources_dir()) / "data" / "hints.ini"));
    m_initialized = true;
}
void HintDatabase::load_hints_from_file(const boost::filesystem::path& path)
{
	namespace pt = boost::property_tree;
	pt::ptree tree;
 	boost::nowide::ifstream ifs(path.string());
	try {
		pt::read_ini(ifs, tree);
	}
	catch (const boost::property_tree::ini_parser::ini_parser_error& err) {
		throw Slic3r::RuntimeError(format("Failed loading hints file \"%1%\"\nError: \"%2%\" at line %3%", path, err.message(), err.line()).c_str());
	}

 	for (const auto& section : tree) {
		if (boost::starts_with(section.first, "hint:")) {
			// create std::map with tree data 
			std::map<std::string, std::string> dict;
			for (const auto& data : section.second) {
				dict.emplace(data.first, data.second.data());
			}
			// unique id string [hint:id] (trim "hint:")
			std::string id_string = section.first.substr(5);
			id_string = std::to_string(std::hash<std::string>{}(id_string));
			// unescaping and translating all texts and saving all data common for all hint types 
			std::string fulltext;
			std::string text1;
			std::string hypertext_text;
			std::string follow_text;
			// tags
			std::string disabled_tags;
			std::string enabled_tags;
			// optional link to documentation (accessed from button)
			std::string documentation_link;
			// randomized weighted order variables
			size_t      weight = 1;
			bool		was_displayed = is_used(id_string);
			//unescape text1
			unescape_string_cstyle(dict["text"], fulltext);
			fulltext = into_u8(_(fulltext));
#ifdef __APPLE__
			boost::replace_all(fulltext, "Ctrl+", "⌘");
#endif //__APPLE__
			// replace <b> and </b> for imgui markers
			std::string marker_s(1, ImGui::ColorMarkerStart);
			std::string marker_e(1, ImGui::ColorMarkerEnd);
			// start marker
			size_t marker_pos = fulltext.find(BOLD_MARKER_START);
			while (marker_pos != std::string::npos) {
				fulltext.replace(marker_pos, 3, marker_s);
				marker_pos = fulltext.find(BOLD_MARKER_START, marker_pos);
			}
			// end marker
			marker_pos = fulltext.find(BOLD_MARKER_END);
			while (marker_pos != std::string::npos) {
				fulltext.replace(marker_pos, 4, marker_e);
				marker_pos = fulltext.find(BOLD_MARKER_END, marker_pos);
			}
			// divide fulltext
			size_t hypertext_start = fulltext.find(HYPERTEXT_MARKER_START);
			if (hypertext_start != std::string::npos) {
				//hypertext exists
				fulltext.erase(hypertext_start, HYPERTEXT_MARKER_START.size());
				if (fulltext.find(HYPERTEXT_MARKER_START) != std::string::npos) {
					// This must not happen - only 1 hypertext allowed
					BOOST_LOG_TRIVIAL(error) << "Hint notification with multiple hypertexts: " << dict["text"];
					continue;
				}
				size_t hypertext_end = fulltext.find(HYPERTEXT_MARKER_END);
				if (hypertext_end == std::string::npos) {
					// hypertext was not correctly ended
					BOOST_LOG_TRIVIAL(error) << "Hint notification without hypertext end marker: " << dict["text"];
					continue;
				}
				fulltext.erase(hypertext_end, HYPERTEXT_MARKER_END.size());
				if (fulltext.find(HYPERTEXT_MARKER_END) != std::string::npos) {
					// This must not happen - only 1 hypertext end allowed
					BOOST_LOG_TRIVIAL(error) << "Hint notification with multiple hypertext end markers: " << dict["text"];
					continue;
				}
				
				text1          = fulltext.substr(0, hypertext_start);
				hypertext_text = fulltext.substr(hypertext_start, hypertext_end - hypertext_start);
				follow_text    = fulltext.substr(hypertext_end);
			} else {
				text1 = fulltext;
			}
			
			if (dict.find("disabled_tags") != dict.end()) {
				disabled_tags = dict["disabled_tags"];
			}
			if (dict.find("enabled_tags") != dict.end()) {
				enabled_tags = dict["enabled_tags"];
			}
			if (dict.find("documentation_link") != dict.end()) {
				documentation_link = dict["documentation_link"];
			}

			if (dict.find("weight") != dict.end()) {
				weight = (size_t)std::max(1, std::atoi(dict["weight"].c_str()));
			}

			// create HintData
			if (dict.find("hypertext_type") != dict.end()) {
				//link to internet
				if(dict["hypertext_type"] == "link") {
					std::string	hypertext_link = dict["hypertext_link"];
					HintData	hint_data{ id_string, text1, weight, was_displayed, hypertext_text, follow_text, disabled_tags, enabled_tags, false, documentation_link, [hypertext_link]() { launch_browser_if_allowed(hypertext_link); }  };
					m_loaded_hints.emplace_back(hint_data);
				// highlight settings
				} else if (dict["hypertext_type"] == "settings") {
					std::string		opt = dict["hypertext_settings_opt"];
					Preset::Type	type = static_cast<Preset::Type>(std::atoi(dict["hypertext_settings_type"].c_str()));
					std::wstring	category = boost::nowide::widen(dict["hypertext_settings_category"]);
					HintData		hint_data{ id_string, text1, weight, was_displayed, hypertext_text, follow_text, disabled_tags, enabled_tags, true, documentation_link, [opt, type, category]() { GUI::wxGetApp().jump_to_option(opt, type, category); } };
					m_loaded_hints.emplace_back(hint_data);
				// open preferences
				} else if(dict["hypertext_type"] == "preferences") {
					std::string	page = dict["hypertext_preferences_page"];
					std::string	item = dict["hypertext_preferences_item"];
					HintData	hint_data{ id_string, text1, weight, was_displayed, hypertext_text, follow_text, disabled_tags, enabled_tags, false, documentation_link, [page, item]() { wxGetApp().open_preferences(item, page); } };
					m_loaded_hints.emplace_back(hint_data);
				} else if (dict["hypertext_type"] == "plater") {
					std::string	item = dict["hypertext_plater_item"];
					HintData	hint_data{ id_string, text1, weight, was_displayed, hypertext_text, follow_text, disabled_tags, enabled_tags, true, documentation_link, [item]() { wxGetApp().plater()->canvas3D()->highlight_toolbar_item(item); } };
					m_loaded_hints.emplace_back(hint_data);
				} else if (dict["hypertext_type"] == "gizmo") {
					std::string	item = dict["hypertext_gizmo_item"];
					HintData	hint_data{ id_string, text1, weight, was_displayed, hypertext_text, follow_text, disabled_tags, enabled_tags, true, documentation_link, [item]() { wxGetApp().plater()->canvas3D()->highlight_gizmo(item); } };
					m_loaded_hints.emplace_back(hint_data);
				}
				else if (dict["hypertext_type"] == "gallery") {
					HintData	hint_data{ id_string, text1, weight, was_displayed, hypertext_text, follow_text, disabled_tags, enabled_tags, false, documentation_link, []() {
						// Deselect all objects, otherwise gallery wont show.
						wxGetApp().plater()->canvas3D()->deselect_all();
						wxGetApp().obj_list()->load_shape_object_from_gallery(); } 
					};
					m_loaded_hints.emplace_back(hint_data);
				} else if (dict["hypertext_type"] == "menubar") {
					wxString menu(_(dict["hypertext_menubar_menu_name"]));
					wxString item(_(dict["hypertext_menubar_item_name"]));
					HintData	hint_data{ id_string, text1, weight, was_displayed, hypertext_text, follow_text, disabled_tags, enabled_tags, true, documentation_link, [menu, item]() { wxGetApp().mainframe->open_menubar_item(menu, item); } };
					m_loaded_hints.emplace_back(hint_data);
				}
			} else {
				// plain text without hypertext
				HintData hint_data{ id_string, text1, weight, was_displayed, hypertext_text, follow_text, disabled_tags, enabled_tags, false, documentation_link };
				m_loaded_hints.emplace_back(hint_data);
			}
		}
	}
}
HintData* HintDatabase::get_hint(bool new_hint/* = true*/)
{
    if (! m_initialized) {
        init();
		new_hint = true;
    }
	if (m_loaded_hints.empty())
	{
		BOOST_LOG_TRIVIAL(error) << "There were no hints loaded from hints.ini file.";
		return nullptr;
	}

	try
	{
		if (new_hint)
			m_hint_id = get_next();
	}
	catch (const std::exception&)
	{
		return nullptr;
	}
	


    return &m_loaded_hints[m_hint_id];
}

size_t HintDatabase::get_next()
{
	if (!m_sorted_hints)
	{
		auto compare_wieght = [](const HintData& a, const HintData& b){ return a.weight < b.weight; };
		std::sort(m_loaded_hints.begin(), m_loaded_hints.end(), compare_wieght);
		m_sorted_hints = true;
		srand(time(NULL));
	}
	std::vector<size_t> candidates; // index in m_loaded_hints
	// total weight
	size_t total_weight = 0;
	for (size_t i = 0; i < m_loaded_hints.size(); i++) {
		if (!m_loaded_hints[i].was_displayed && tags_check(m_loaded_hints[i].disabled_tags, m_loaded_hints[i].enabled_tags)) {
			candidates.emplace_back(i);
			total_weight += m_loaded_hints[i].weight;
		}
	}
	// all were shown
	if (total_weight == 0) {
		clear_used();
	 	for (size_t i = 0; i < m_loaded_hints.size(); i++) {
			m_loaded_hints[i].was_displayed = false;
			if (tags_check(m_loaded_hints[i].disabled_tags, m_loaded_hints[i].enabled_tags)) {
				candidates.emplace_back(i);
				total_weight += m_loaded_hints[i].weight;
			}
		}
	}
	if (total_weight == 0) {
		BOOST_LOG_TRIVIAL(error) << "Hint notification random number generator failed. No suitable hint was found.";
		throw std::exception();
	}
	size_t random_number = rand() % total_weight + 1;
	size_t current_weight = 0;
	for (size_t i = 0; i < candidates.size(); i++) {
		current_weight += m_loaded_hints[candidates[i]].weight;
		if (random_number <= current_weight) {
			set_used(m_loaded_hints[candidates[i]].id_string);
			m_loaded_hints[candidates[i]].was_displayed = true;
			return candidates[i];
		}
	}
	BOOST_LOG_TRIVIAL(error) << "Hint notification random number generator failed.";
	throw std::exception();
}

bool HintDatabase::is_used(const std::string& id)
{
	// load used ids from file
	if (!m_used_ids_loaded) {
		read_used_binary(m_used_ids);
		m_used_ids_loaded = true;
	}
	// check if id is in used
	for (const std::string& used_id : m_used_ids) {
		if (used_id == id)
		{
			return true;
		}
	}
	return false;
}
void HintDatabase::set_used(const std::string& id)
{
	// check needed?
	if (!is_used(id))
	{
		m_used_ids.emplace_back(id);
	}
}
void HintDatabase::clear_used()
{
	m_used_ids.clear();
}

void NotificationManager::HintNotification::count_spaces()
{
	//determine line width 
	m_line_height = ImGui::CalcTextSize("A").y;

	
	std::string text;
	text = ImGui::WarningMarker; 
	float picture_width = ImGui::CalcTextSize(text.c_str()).x;
	m_left_indentation = picture_width * 1.5f + m_line_height / 2;
	
	// no left button picture
	//m_left_indentation = m_line_height;

	if (m_documentation_link.empty())
		m_window_width_offset = m_left_indentation + m_line_height * 3.f;
	else 
		m_window_width_offset = m_left_indentation + m_line_height * 5.5f;

	m_window_width = m_line_height * 25;
}

void NotificationManager::HintNotification::count_lines()
{
	std::string text = m_text1;
	size_t      last_end = 0;
	m_lines_count = 0;

	if (text.empty())
		return;

	m_endlines.clear();
	while (last_end < text.length() - 1)
	{
		size_t next_hard_end = text.find_first_of('\n', last_end);
		if (next_hard_end != std::string::npos && ImGui::CalcTextSize(text.substr(last_end, next_hard_end - last_end).c_str()).x < m_window_width - m_window_width_offset) {
			//next line is ended by '/n'
			m_endlines.push_back(next_hard_end);
			last_end = next_hard_end + 1;
		}
		else {
			// find next suitable endline
			if (ImGui::CalcTextSize(text.substr(last_end).c_str()).x >= m_window_width - m_window_width_offset) {
				// more than one line till end
				size_t next_space = text.find_first_of(' ', last_end);
				if (next_space > 0 && next_space < text.length()) {
					size_t next_space_candidate = text.find_first_of(' ', next_space + 1);
					while (next_space_candidate > 0 && ImGui::CalcTextSize(text.substr(last_end, next_space_candidate - last_end).c_str()).x < m_window_width - m_window_width_offset) {
						next_space = next_space_candidate;
						next_space_candidate = text.find_first_of(' ', next_space + 1);
					}
				} else {
					next_space = text.length();
				}
				// when one word longer than line.
				if (ImGui::CalcTextSize(text.substr(last_end, next_space - last_end).c_str()).x > m_window_width - m_window_width_offset ||
					ImGui::CalcTextSize(text.substr(last_end, next_space - last_end).c_str()).x < (m_window_width - m_window_width_offset) / 5 * 3
				    ) {
					float width_of_a = ImGui::CalcTextSize("a").x;
					int letter_count = (int)((m_window_width - m_window_width_offset) / width_of_a);
					while (last_end + letter_count < text.size() && ImGui::CalcTextSize(text.substr(last_end, letter_count).c_str()).x < m_window_width - m_window_width_offset) {
						letter_count += get_utf8_sequence_length(text, last_end + letter_count);
					}
					m_endlines.push_back(last_end + letter_count);
					last_end += letter_count;
				} else {
					m_endlines.push_back(next_space);
					last_end = next_space + 1;
				}
			}
			else {
				m_endlines.push_back(text.length());
				last_end = text.length();
			}

		}
		m_lines_count++;
	}
	int prev_end = m_endlines.size() > 1 ? m_endlines[m_endlines.size() - 2] : 0;
	int size_of_last_line = ImGui::CalcTextSize(text.substr(prev_end, last_end - prev_end).c_str()).x;
	// hypertext calculation
	if (!m_hypertext.empty()) {
		if (size_of_last_line + ImGui::CalcTextSize(m_hypertext.c_str()).x > m_window_width - m_window_width_offset) {
			// hypertext on new line
			size_of_last_line = ImGui::CalcTextSize((m_hypertext + "  ").c_str()).x;
			m_endlines.push_back(last_end);
			m_lines_count++;
		} else {
			size_of_last_line += ImGui::CalcTextSize((m_hypertext + "  ").c_str()).x;
		}
	}
	if (!m_text2.empty()) {
		text						= m_text2;
		last_end					= 0;
		m_endlines2.clear();
		// if size_of_last_line too large to fit anything
		size_t first_end = std::min(text.find_first_of('\n'), text.find_first_of(' '));
		if (size_of_last_line >= m_window_width - m_window_width_offset - ImGui::CalcTextSize(text.substr(0, first_end).c_str()).x) {
			m_endlines2.push_back(0);
			size_of_last_line = 0;
		}
		while (last_end < text.length() - 1)
		{
			size_t next_hard_end = text.find_first_of('\n', last_end);
			if (next_hard_end != std::string::npos && ImGui::CalcTextSize(text.substr(last_end, next_hard_end - last_end).c_str()).x < m_window_width - m_window_width_offset - size_of_last_line) {
				//next line is ended by '/n'
				m_endlines2.push_back(next_hard_end);
				last_end = next_hard_end + 1;
			}
			else {
				// find next suitable endline
				if (ImGui::CalcTextSize(text.substr(last_end).c_str()).x >= m_window_width - m_window_width_offset - size_of_last_line) {
					// more than one line till end
					size_t next_space = text.find_first_of(' ', last_end);
					if (next_space > 0) {
						size_t next_space_candidate = text.find_first_of(' ', next_space + 1);
						while (next_space_candidate > 0 && ImGui::CalcTextSize(text.substr(last_end, next_space_candidate - last_end).c_str()).x < m_window_width - m_window_width_offset - size_of_last_line) {
							next_space = next_space_candidate;
							next_space_candidate = text.find_first_of(' ', next_space + 1);
						}
					}
					else {
						next_space = text.length();
					}
					// when one word longer than line.
					if (ImGui::CalcTextSize(text.substr(last_end, next_space - last_end).c_str()).x > m_window_width - m_window_width_offset - size_of_last_line ||
						ImGui::CalcTextSize(text.substr(last_end, next_space - last_end).c_str()).x + size_of_last_line < (m_window_width - m_window_width_offset) / 5 * 3
						) {
						float width_of_a = ImGui::CalcTextSize("a").x;
						int letter_count = (int)((m_window_width - m_window_width_offset - size_of_last_line) / width_of_a);
						while (last_end + letter_count < text.size() && ImGui::CalcTextSize(text.substr(last_end, letter_count).c_str()).x < m_window_width - m_window_width_offset - size_of_last_line) {
							letter_count += get_utf8_sequence_length(text, last_end + letter_count);
						}
						m_endlines2.push_back(last_end + letter_count);
						last_end += letter_count;
					}
					else {
						m_endlines2.push_back(next_space);
						last_end = next_space + 1;
					}
				}
				else {
					m_endlines2.push_back(text.length());
					last_end = text.length();
				}

			}
			if (size_of_last_line == 0) // if first line is continuation of previous text, do not add to line count.
				m_lines_count++;
			size_of_last_line = 0; // should countain value only for first line (with hypertext) 
			
		}
	}
}

void NotificationManager::HintNotification::init()
{
	// Do not init closing notification
	if (is_finished())
		return;

	count_spaces();
	count_lines();

	m_multiline = true;

	m_notification_start = GLCanvas3D::timestamp_now();
	if (m_state == EState::Unknown)
		m_state = EState::Shown;
}

void NotificationManager::HintNotification::set_next_window_size()
{
	/*
	m_window_height = m_multiline ?
		(m_lines_count + 1.f) * m_line_height :
		4.f * m_line_height;
	m_window_height += 1 * m_line_height; // top and bottom
	*/

	m_window_height = std::max((m_lines_count + 1.f) * m_line_height, 5.f * m_line_height);
}

bool NotificationManager::HintNotification::on_text_click()
{
	if (m_hypertext_callback != nullptr && (!m_runtime_disable || tags_check(m_disabled_tags, m_enabled_tags)))
		m_hypertext_callback();
	return false;
}

void NotificationManager::HintNotification::render_text(const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y)
{
    if (!m_has_hint_data) {
        retrieve_data();
	}

	float	x_offset = m_left_indentation;
	int		last_end = 0;
	float	starting_y = (m_lines_count < 4 ? m_line_height / 2 * (4 - m_lines_count + 1) : m_line_height / 2);
	float	shift_y = m_line_height;
	std::string line;

	for (size_t i = 0; i < (m_multiline ? /*m_lines_count*/m_endlines.size() : 2); i++) {
		line.clear();
		ImGui::SetCursorPosX(x_offset);
		ImGui::SetCursorPosY(starting_y + i * shift_y);
		if (m_endlines.size() > i && m_text1.size() >= m_endlines[i]) {
			if (i == 1 && m_endlines.size() > 2 && !m_multiline) {
				// second line with "more" hypertext
				line = m_text1.substr(m_endlines[0] + (m_text1[m_endlines[0]] == '\n' || m_text1[m_endlines[0]] == ' ' ? 1 : 0), m_endlines[1] - m_endlines[0] - (m_text1[m_endlines[0]] == '\n' || m_text1[m_endlines[0]] == ' ' ? 1 : 0));
				while (ImGui::CalcTextSize(line.c_str()).x > m_window_width - m_window_width_offset - ImGui::CalcTextSize((".." + _u8L("More")).c_str()).x) {
					line = line.substr(0, line.length() - 1);
				}
				line += "..";
			} else {
				// regural line
				line = m_text1.substr(last_end, m_endlines[i] - last_end);	
			}
			// first line is headline (for hint notification it must be divided by \n)
			if (m_text1.find('\n') >= m_endlines[i]) {
				line = ImGui::ColorMarkerStart + line + ImGui::ColorMarkerEnd;
			}
			// Add ImGui::ColorMarkerStart if there is ImGui::ColorMarkerEnd first (start was at prev line)
			if (line.find_first_of(ImGui::ColorMarkerEnd) < line.find_first_of(ImGui::ColorMarkerStart)) {
				line = ImGui::ColorMarkerStart + line;
			}

			last_end = m_endlines[i];
			if (m_text1.size() > m_endlines[i])
				last_end += (m_text1[m_endlines[i]] == '\n' || m_text1[m_endlines[i]] == ' ' ? 1 : 0);
			ImGuiPureWrap::text(line.c_str());
		}
			
	}
	//hyperlink text
	if (!m_multiline && m_lines_count > 2) {
		render_hypertext(x_offset + ImGui::CalcTextSize((line + " ").c_str()).x, starting_y + shift_y, _u8L("More"), true);
	} else if (!m_hypertext.empty()) {
		render_hypertext(x_offset + ImGui::CalcTextSize((line + (line.empty()? "": " ")).c_str()).x, starting_y + (m_endlines.size() - 1) * shift_y, m_hypertext);
	}

	// text2
	if (!m_text2.empty() && m_multiline) {
		starting_y += (m_endlines.size() - 1) * shift_y;
		last_end = 0;
		for (size_t i = 0; i < (m_multiline ? m_endlines2.size() : 2); i++) {
			if (i == 0) //first line X is shifted by hypertext
				ImGui::SetCursorPosX(x_offset + ImGui::CalcTextSize((line + m_hypertext + (line.empty() ? " " : "  ")).c_str()).x);
			else
				ImGui::SetCursorPosX(x_offset);

			ImGui::SetCursorPosY(starting_y + i * shift_y);
			line.clear();
			if (m_endlines2.size() > i && m_text2.size() >= m_endlines2[i]) {

				// regural line
				line = m_text2.substr(last_end, m_endlines2[i] - last_end);

				// Add ImGui::ColorMarkerStart if there is ImGui::ColorMarkerEnd first (start was at prev line)
				if (line.find_first_of(ImGui::ColorMarkerEnd) < line.find_first_of(ImGui::ColorMarkerStart)) {
					line = ImGui::ColorMarkerStart + line;
				}

				last_end = m_endlines2[i];
				if (m_text2.size() > m_endlines2[i])
					last_end += (m_text2[m_endlines2[i]] == '\n' || m_text2[m_endlines2[i]] == ' ' ? 1 : 0);
				ImGuiPureWrap::text(line.c_str());
			}

		}
	}
}

void NotificationManager::HintNotification::render_close_button(const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y)
{
	ImVec2 win_size(win_size_x, win_size_y);
	ImVec2 win_pos(win_pos_x, win_pos_y);
	ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f));
	ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f));
	push_style_color(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity);
	push_style_color(ImGuiCol_TextSelectedBg, ImVec4(0, .75f, .75f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity);
	ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(.0f, .0f, .0f, .0f));


	std::string button_text;
	button_text = ImGui::CloseNotifButton;

	if (ImGui::IsMouseHoveringRect(ImVec2(win_pos.x - win_size.x / 10.f, win_pos.y),
		ImVec2(win_pos.x, win_pos.y + win_size.y - 2 * m_line_height),
		true))
	{
		button_text = ImGui::CloseNotifHoverButton;
	}
	ImVec2 button_pic_size = ImGui::CalcTextSize(button_text.c_str());
	ImVec2 button_size(button_pic_size.x * 1.25f, button_pic_size.y * 1.25f);
	m_close_b_w = button_size.y;
	if (m_lines_count <= 3) {
		m_close_b_y = win_size.y / 2 - button_size.y * 1.25f;
		ImGui::SetCursorPosX(win_size.x - m_line_height * 2.75f);
		ImGui::SetCursorPosY(m_close_b_y);
	} else {
		ImGui::SetCursorPosX(win_size.x - m_line_height * 2.75f);
		ImGui::SetCursorPosY(win_size.y / 2 - button_size.y);
	}
	if (ImGuiPureWrap::button(button_text.c_str(), button_size.x, button_size.y))
	{
		close();
	}
	
	//invisible large button
	ImGui::SetCursorPosX(win_size.x - m_line_height * 2.35f);
	ImGui::SetCursorPosY(0);
	if (ImGuiPureWrap::button(" ", m_line_height * 2.125, win_size.y -  2 * m_line_height))
	{
		close();
	}
	
	ImGui::PopStyleColor(5);


	//render_right_arrow_button(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y);
	render_logo(win_size_x, win_size_y, win_pos_x, win_pos_y);
	render_preferences_button(win_pos_x, win_pos_y);
	if (!m_documentation_link.empty() && !wxGetApp().app_config->get_bool("suppress_hyperlinks"))
	{
		render_documentation_button(win_size_x, win_size_y, win_pos_x, win_pos_y);
	}
	
}

void NotificationManager::HintNotification::render_preferences_button(const float win_pos_x, const float win_pos_y)
{
	
	ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f));
	ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f));
	push_style_color(ImGuiCol_ButtonActive, ImGui::GetStyleColorVec4(ImGuiCol_WindowBg), m_state == EState::FadingOut, m_current_fade_opacity);
	push_style_color(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity);
	push_style_color(ImGuiCol_TextSelectedBg, ImVec4(0, .75f, .75f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity);

	std::string button_text;
	button_text = ImGui::PreferencesButton;
	//hover
	if (ImGui::IsMouseHoveringRect(ImVec2(win_pos_x - m_window_width / 15.f, win_pos_y + m_window_height - 1.75f * m_line_height),
		ImVec2(win_pos_x, win_pos_y + m_window_height),
		true)) {
		button_text = ImGui::PreferencesHoverButton;
		// tooltip
		long time_now = wxGetLocalTime();
		if (m_prefe_hover_time > 0 && m_prefe_hover_time < time_now) {
			ImGui::PushStyleColor(ImGuiCol_PopupBg, ImGuiPureWrap::COL_WINDOW_BACKGROUND);
			ImGui::BeginTooltip();
			ImGuiPureWrap::text(_u8L("Open Preferences."));
			ImGui::EndTooltip();
			ImGui::PopStyleColor();
		}
		if (m_prefe_hover_time == 0)
			m_prefe_hover_time = time_now;
	} else
		m_prefe_hover_time = 0;

	ImVec2 button_pic_size = ImGui::CalcTextSize(button_text.c_str());
	ImVec2 button_size(button_pic_size.x * 1.25f, button_pic_size.y * 1.25f);
	ImGui::SetCursorPosX(m_window_width - m_line_height * 1.75f);
	if (m_lines_count <= 3) {
		ImGui::SetCursorPosY(m_close_b_y + m_close_b_w / 4.f * 7.f);
	} else {
		ImGui::SetCursorPosY(m_window_height - button_size.y - m_close_b_w / 4.f);
	}
	if (ImGuiPureWrap::button(button_text.c_str(), button_size.x, button_size.y))
	{
		wxGetApp().open_preferences("show_hints", "GUI");
	}

	ImGui::PopStyleColor(5);
	// preferences button is in place of minimize button
	m_minimize_b_visible = true;	
}
void NotificationManager::HintNotification::render_right_arrow_button(const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y)
{
	// Used for debuging

	ImVec2 win_size(win_size_x, win_size_y);
	ImVec2 win_pos(win_pos_x, win_pos_y);
	ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f));
	ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f));
	push_style_color(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity);
	push_style_color(ImGuiCol_TextSelectedBg, ImVec4(0, .75f, .75f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity);
	ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(.0f, .0f, .0f, .0f));

	std::string button_text;
	button_text = ImGui::RightArrowButton;
	
	ImVec2 button_pic_size = ImGui::CalcTextSize(button_text.c_str());
	ImVec2 button_size(button_pic_size.x * 1.25f, button_pic_size.y * 1.25f);

	ImGui::SetCursorPosX(m_window_width - m_line_height * 3.f);
	if (m_lines_count <= 3)
		ImGui::SetCursorPosY(m_close_b_y + m_close_b_w / 4.f * 7.f);
	else
		ImGui::SetCursorPosY(m_window_height - button_size.y - m_close_b_w / 4.f);
	if (ImGuiPureWrap::button(button_text.c_str(), button_size.x * 0.8f, button_size.y * 1.f))
	{
		retrieve_data();
	}

	ImGui::PopStyleColor(5);
}
void NotificationManager::HintNotification::render_logo(const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y)
{
	std::string placeholder_text;
	placeholder_text = ImGui::EjectButton;
	ImVec2 button_pic_size = ImGui::CalcTextSize(placeholder_text.c_str());
	std::wstring text;
	text = ImGui::ClippyMarker;
	ImGui::SetCursorPosX(button_pic_size.x / 3);
	ImGui::SetCursorPosY(win_size_y / 2 - button_pic_size.y * 2.f);
	ImGuiPureWrap::text(text.c_str());
}
void NotificationManager::HintNotification::render_documentation_button(const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y)
{
	ImVec2 win_size(win_size_x, win_size_y);
	ImVec2 win_pos(win_pos_x, win_pos_y);
	ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f));
	ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f));
	push_style_color(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity);
	push_style_color(ImGuiCol_TextSelectedBg, ImVec4(0, .75f, .75f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity);
	ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(.0f, .0f, .0f, .0f));

	std::wstring button_text;
	button_text = ImGui::DocumentationButton;
	std::string placeholder_text;
	placeholder_text = ImGui::EjectButton;

	if (ImGui::IsMouseHoveringRect(ImVec2(win_pos.x - m_line_height * 5.f, win_pos.y),
		ImVec2(win_pos.x - m_line_height * 2.5f, win_pos.y + win_size.y - 2 * m_line_height),
		true))
	{
		button_text = ImGui::DocumentationHoverButton;
		// tooltip
		long time_now = wxGetLocalTime();
		if (m_docu_hover_time > 0 && m_docu_hover_time < time_now) {
			ImGui::PushStyleColor(ImGuiCol_PopupBg, ImGuiPureWrap::COL_WINDOW_BACKGROUND);
			ImGui::BeginTooltip();
			ImGuiPureWrap::text(_u8L("Open Documentation in web browser."));
			ImGui::EndTooltip();
			ImGui::PopStyleColor();
		}
		if (m_docu_hover_time == 0)
			m_docu_hover_time = time_now;
	}
	else
		m_docu_hover_time = 0;

	ImVec2 button_pic_size = ImGui::CalcTextSize(placeholder_text.c_str());
	ImVec2 button_size(button_pic_size.x * 1.25f, button_pic_size.y * 1.25f);
	ImGui::SetCursorPosX(win_size.x - m_line_height * 5.0f);
	ImGui::SetCursorPosY(win_size.y / 2 - button_size.y);
	if (ImGuiPureWrap::button(button_text.c_str(), button_size.x, button_size.y))
	{
		open_documentation();
	}

	//invisible large button
	ImGui::SetCursorPosX(win_size.x - m_line_height * 4.625f);
	ImGui::SetCursorPosY(0);
	if (ImGuiPureWrap::button("  ", m_line_height * 2.f, win_size.y - 2 * m_line_height))
	{
		open_documentation();
	}

	ImGui::PopStyleColor(5);
}

void NotificationManager::HintNotification::open_documentation()
{
	if (!m_documentation_link.empty())
	{
		launch_browser_if_allowed(m_documentation_link);
	}
}
void NotificationManager::HintNotification::retrieve_data(bool new_hint/* = true*/)
{
    HintData* hint_data = HintDatabase::get_instance().get_hint(new_hint);
	if (hint_data == nullptr)
		 close();

	if(hint_data != nullptr)
    {
        NotificationData nd { NotificationType::DidYouKnowHint,
						      NotificationLevel::HintNotificationLevel,
							  0,
						      hint_data->text,
							  hint_data->hypertext, nullptr,
							  hint_data->follow_text };
		m_hypertext_callback = hint_data->callback;
		m_disabled_tags      = hint_data->disabled_tags;
		m_enabled_tags       = hint_data->enabled_tags;
		m_runtime_disable    = hint_data->runtime_disable;
		m_documentation_link = hint_data->documentation_link;
        m_has_hint_data      = true;
		update(nd);
    }
}
} //namespace Slic3r 
} //namespace GUI 
