/*
 * MIT License
 *
 * Copyright (c) 2017 Serge Zaitsev
 * Copyright (c) 2022 Steffen André Langnes
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
#ifndef WEBVIEW_H
#define WEBVIEW_H

#ifndef WEBVIEW_API
#define WEBVIEW_API extern
#endif

#ifndef WEBVIEW_VERSION_MAJOR
// The current library major version.
#define WEBVIEW_VERSION_MAJOR 0
#endif

#ifndef WEBVIEW_VERSION_MINOR
// The current library minor version.
#define WEBVIEW_VERSION_MINOR 10
#endif

#ifndef WEBVIEW_VERSION_PATCH
// The current library patch version.
#define WEBVIEW_VERSION_PATCH 0
#endif

#ifndef WEBVIEW_VERSION_PRE_RELEASE
// SemVer 2.0.0 pre-release labels prefixed with "-".
#define WEBVIEW_VERSION_PRE_RELEASE ""
#endif

#ifndef WEBVIEW_VERSION_BUILD_METADATA
// SemVer 2.0.0 build metadata prefixed with "+".
#define WEBVIEW_VERSION_BUILD_METADATA ""
#endif

// Utility macro for stringifying a macro argument.
#define WEBVIEW_STRINGIFY(x) #x

// Utility macro for stringifying the result of a macro argument expansion.
#define WEBVIEW_EXPAND_AND_STRINGIFY(x) WEBVIEW_STRINGIFY(x)

// SemVer 2.0.0 version number in MAJOR.MINOR.PATCH format.
#define WEBVIEW_VERSION_NUMBER                                                                \
	WEBVIEW_EXPAND_AND_STRINGIFY(WEBVIEW_VERSION_MAJOR)                                       \
	"." WEBVIEW_EXPAND_AND_STRINGIFY(WEBVIEW_VERSION_MINOR) "." WEBVIEW_EXPAND_AND_STRINGIFY( \
	    WEBVIEW_VERSION_PATCH)

// Holds the elements of a MAJOR.MINOR.PATCH version number.
typedef struct
{
	// Major version.
	unsigned int major;
	// Minor version.
	unsigned int minor;
	// Patch version.
	unsigned int patch;
} webview_version_t;

// Holds the library's version information.
typedef struct
{
	// The elements of the version number.
	webview_version_t version;
	// SemVer 2.0.0 version number in MAJOR.MINOR.PATCH format.
	char version_number[32];
	// SemVer 2.0.0 pre-release labels prefixed with "-" if specified, otherwise
	// an empty string.
	char pre_release[48];
	// SemVer 2.0.0 build metadata prefixed with "+", otherwise an empty string.
	char build_metadata[48];
} webview_version_info_t;

#ifdef __cplusplus
extern "C"
{
#endif

	typedef void* webview_t;

	// Creates a new webview instance. If debug is non-zero - developer tools will
	// be enabled (if the platform supports them). Window parameter can be a
	// pointer to the native window handle. If it's non-null - then child WebView
	// is embedded into the given parent window. Otherwise a new window is created.
	// Depending on the platform, a GtkWindow, NSWindow or HWND pointer can be
	// passed here. Returns null on failure. Creation can fail for various reasons
	// such as when required runtime dependencies are missing or when window creation
	// fails.
	WEBVIEW_API webview_t webview_create(int debug, void* window);

	// Destroys a webview and closes the native window.
	WEBVIEW_API void webview_destroy(webview_t w);

	// Runs the main loop until it's terminated. After this function exits - you
	// must destroy the webview.
	WEBVIEW_API void webview_run(webview_t w);

	// Stops the main loop. It is safe to call this function from another other
	// background thread.
	WEBVIEW_API void webview_terminate(webview_t w);

	// Posts a function to be executed on the main thread. You normally do not need
	// to call this function, unless you want to tweak the native window.
	WEBVIEW_API void webview_dispatch(webview_t w, void (*fn)(webview_t w, void* arg), void* arg);

	// Returns a native window handle pointer. When using GTK backend the pointer
	// is GtkWindow pointer, when using Cocoa backend the pointer is NSWindow
	// pointer, when using Win32 backend the pointer is HWND pointer.
	WEBVIEW_API void* webview_get_window(webview_t w);

	// Updates the title of the native window. Must be called from the UI thread.
	WEBVIEW_API void webview_set_title(webview_t w, const char* title);

// Window size hints
#define WEBVIEW_HINT_NONE 0  // Width and height are default size
#define WEBVIEW_HINT_MIN 1   // Width and height are minimum bounds
#define WEBVIEW_HINT_MAX 2   // Width and height are maximum bounds
#define WEBVIEW_HINT_FIXED 3 // Window size can not be changed by a user
	// Updates native window size. See WEBVIEW_HINT constants.
	WEBVIEW_API void webview_set_size(webview_t w, int width, int height, int hints);

	// Navigates webview to the given URL. URL may be a properly encoded data URI.
	// Examples:
	// webview_navigate(w, "https://github.com/webview/webview");
	// webview_navigate(w, "data:text/html,%3Ch1%3EHello%3C%2Fh1%3E");
	// webview_navigate(w, "data:text/html;base64,PGgxPkhlbGxvPC9oMT4=");
	WEBVIEW_API void webview_navigate(webview_t w, const char* url);

	// Set webview HTML directly.
	// Example: webview_set_html(w, "<h1>Hello</h1>");
	WEBVIEW_API void webview_set_html(webview_t w, const char* html);

	// Injects JavaScript code at the initialization of the new page. Every time
	// the webview will open a the new page - this initialization code will be
	// executed. It is guaranteed that code is executed before window.onload.
	WEBVIEW_API void webview_init(webview_t w, const char* js);

	// Evaluates arbitrary JavaScript code. Evaluation happens asynchronously, also
	// the result of the expression is ignored. Use RPC bindings if you want to
	// receive notifications about the results of the evaluation.
	WEBVIEW_API void webview_eval(webview_t w, const char* js);

	// Binds a native C callback so that it will appear under the given name as a
	// global JavaScript function. Internally it uses webview_init(). Callback
	// receives a request string and a user-provided argument pointer. Request
	// string is a JSON array of all the arguments passed to the JavaScript
	// function.
	WEBVIEW_API void webview_bind(webview_t w, const char* name,
	                              void (*fn)(const char* seq, const char* req, void* arg),
	                              void* arg);

	// Removes a native C callback that was previously set by webview_bind.
	WEBVIEW_API void webview_unbind(webview_t w, const char* name);

	// Allows to return a value from the native binding. Original request pointer
	// must be provided to help internal RPC engine match requests with responses.
	// If status is zero - result is expected to be a valid JSON result value.
	// If status is not zero - result is an error JSON object.
	WEBVIEW_API void webview_return(webview_t w, const char* seq, int status, const char* result);

	// Get the library's version information.
	// @since 0.10
	WEBVIEW_API const webview_version_info_t* webview_version();

#ifdef __cplusplus
}

#ifndef WEBVIEW_HEADER

#if !defined(WEBVIEW_GTK) && !defined(WEBVIEW_COCOA) && !defined(WEBVIEW_EDGE)
#if defined(__APPLE__)
#define WEBVIEW_COCOA
#elif defined(__unix__)
#define WEBVIEW_GTK
#elif defined(_WIN32)
#define WEBVIEW_EDGE
#else
#error "please, specify webview backend"
#endif
#endif

#ifndef WEBVIEW_DEPRECATED
#if __cplusplus >= 201402L
#define WEBVIEW_DEPRECATED(reason) [[deprecated(reason)]]
#elif defined(_MSC_VER)
#define WEBVIEW_DEPRECATED(reason) __declspec(deprecated(reason))
#else
#define WEBVIEW_DEPRECATED(reason) __attribute__((deprecated(reason)))
#endif
#endif

#ifndef WEBVIEW_DEPRECATED_PRIVATE
#define WEBVIEW_DEPRECATED_PRIVATE WEBVIEW_DEPRECATED("Private API should not be used")
#endif

#include <array>
#include <atomic>
#include <functional>
#include <future>
#include <map>
#include <string>
#include <utility>
#include <vector>
#include <locale>
#include <codecvt>
#include <cstring>

namespace webview
{

	using dispatch_fn_t = std::function<void()>;

	namespace detail
	{

		// The library's version information.
		constexpr const webview_version_info_t library_version_info{
			{ WEBVIEW_VERSION_MAJOR, WEBVIEW_VERSION_MINOR, WEBVIEW_VERSION_PATCH },
			WEBVIEW_VERSION_NUMBER,
			WEBVIEW_VERSION_PRE_RELEASE,
			WEBVIEW_VERSION_BUILD_METADATA
		};

		inline int json_parse_c(const char* s, size_t sz, const char* key, size_t keysz,
		                        const char** value, size_t* valuesz)
		{
			enum
			{
				JSON_STATE_VALUE,
				JSON_STATE_LITERAL,
				JSON_STATE_STRING,
				JSON_STATE_ESCAPE,
				JSON_STATE_UTF8
			} state = JSON_STATE_VALUE;
			const char* k = nullptr;
			int index = 1;
			int depth = 0;
			int utf8_bytes = 0;

			*value = nullptr;
			*valuesz = 0;

			if (key == nullptr)
			{
				index = static_cast<decltype(index)>(keysz);
				if (index < 0)
				{
					return -1;
				}
				keysz = 0;
			}

			for (; sz > 0; s++, sz--)
			{
				enum
				{
					JSON_ACTION_NONE,
					JSON_ACTION_START,
					JSON_ACTION_END,
					JSON_ACTION_START_STRUCT,
					JSON_ACTION_END_STRUCT
				} action = JSON_ACTION_NONE;
				auto c = static_cast<unsigned char>(*s);
				switch (state)
				{
					case JSON_STATE_VALUE:
						if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == ',' || c == ':')
						{
							continue;
						}
						else if (c == '"')
						{
							action = JSON_ACTION_START;
							state = JSON_STATE_STRING;
						}
						else if (c == '{' || c == '[')
						{
							action = JSON_ACTION_START_STRUCT;
						}
						else if (c == '}' || c == ']')
						{
							action = JSON_ACTION_END_STRUCT;
						}
						else if (c == 't' || c == 'f' || c == 'n' || c == '-' ||
						         (c >= '0' && c <= '9'))
						{
							action = JSON_ACTION_START;
							state = JSON_STATE_LITERAL;
						}
						else
						{
							return -1;
						}
						break;
					case JSON_STATE_LITERAL:
						if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == ',' ||
						    c == ']' || c == '}' || c == ':')
						{
							state = JSON_STATE_VALUE;
							s--;
							sz++;
							action = JSON_ACTION_END;
						}
						else if (c < 32 || c > 126)
						{
							return -1;
						} // fallthrough
					case JSON_STATE_STRING:
						if (c < 32 || (c > 126 && c < 192))
						{
							return -1;
						}
						else if (c == '"')
						{
							action = JSON_ACTION_END;
							state = JSON_STATE_VALUE;
						}
						else if (c == '\\')
						{
							state = JSON_STATE_ESCAPE;
						}
						else if (c >= 192 && c < 224)
						{
							utf8_bytes = 1;
							state = JSON_STATE_UTF8;
						}
						else if (c >= 224 && c < 240)
						{
							utf8_bytes = 2;
							state = JSON_STATE_UTF8;
						}
						else if (c >= 240 && c < 247)
						{
							utf8_bytes = 3;
							state = JSON_STATE_UTF8;
						}
						else if (c >= 128 && c < 192)
						{
							return -1;
						}
						break;
					case JSON_STATE_ESCAPE:
						if (c == '"' || c == '\\' || c == '/' || c == 'b' || c == 'f' || c == 'n' ||
						    c == 'r' || c == 't' || c == 'u')
						{
							state = JSON_STATE_STRING;
						}
						else
						{
							return -1;
						}
						break;
					case JSON_STATE_UTF8:
						if (c < 128 || c > 191)
						{
							return -1;
						}
						utf8_bytes--;
						if (utf8_bytes == 0)
						{
							state = JSON_STATE_STRING;
						}
						break;
					default:
						return -1;
				}

				if (action == JSON_ACTION_END_STRUCT)
				{
					depth--;
				}

				if (depth == 1)
				{
					if (action == JSON_ACTION_START || action == JSON_ACTION_START_STRUCT)
					{
						if (index == 0)
						{
							*value = s;
						}
						else if (keysz > 0 && index == 1)
						{
							k = s;
						}
						else
						{
							index--;
						}
					}
					else if (action == JSON_ACTION_END || action == JSON_ACTION_END_STRUCT)
					{
						if (*value != nullptr && index == 0)
						{
							*valuesz = (size_t)(s + 1 - *value);
							return 0;
						}
						else if (keysz > 0 && k != nullptr)
						{
							if (keysz == (size_t)(s - k - 1) && memcmp(key, k + 1, keysz) == 0)
							{
								index = 0;
							}
							else
							{
								index = 2;
							}
							k = nullptr;
						}
					}
				}

				if (action == JSON_ACTION_START_STRUCT)
				{
					depth++;
				}
			}
			return -1;
		}

		inline std::string json_escape(const std::string& s)
		{
			// TODO: implement
			return '"' + s + '"';
		}

		inline int json_unescape(const char* s, size_t n, char* out)
		{
			int r = 0;
			if (*s++ != '"')
			{
				return -1;
			}
			while (n > 2)
			{
				char c = *s;
				if (c == '\\')
				{
					s++;
					n--;
					switch (*s)
					{
						case 'b':
							c = '\b';
							break;
						case 'f':
							c = '\f';
							break;
						case 'n':
							c = '\n';
							break;
						case 'r':
							c = '\r';
							break;
						case 't':
							c = '\t';
							break;
						case '\\':
							c = '\\';
							break;
						case '/':
							c = '/';
							break;
						case '\"':
							c = '\"';
							break;
						default: // TODO: support unicode decoding
							return -1;
					}
				}
				if (out != nullptr)
				{
					*out++ = c;
				}
				s++;
				n--;
				r++;
			}
			if (*s != '"')
			{
				return -1;
			}
			if (out != nullptr)
			{
				*out = '\0';
			}
			return r;
		}

		inline std::string json_parse(const std::string& s, const std::string& key, const int index)
		{
			const char* value = nullptr;
			size_t value_sz = 0;
			if (key.empty())
			{
				json_parse_c(s.c_str(), s.length(), nullptr, index, &value, &value_sz);
			}
			else
			{
				json_parse_c(s.c_str(), s.length(), key.c_str(), key.length(), &value, &value_sz);
			}
			if (value != nullptr)
			{
				if (value[0] != '"')
				{
					return { value, value_sz };
				}
				int n = json_unescape(value, value_sz, nullptr);
				if (n > 0)
				{
					char* decoded = new char[n + 1];
					json_unescape(value, value_sz, decoded);
					std::string result(decoded, n);
					delete[] decoded;
					return result;
				}
			}
			return "";
		}

	} // namespace detail

	WEBVIEW_DEPRECATED_PRIVATE
	inline int json_parse_c(const char* s, size_t sz, const char* key, size_t keysz,
	                        const char** value, size_t* valuesz)
	{
		return detail::json_parse_c(s, sz, key, keysz, value, valuesz);
	}

	WEBVIEW_DEPRECATED_PRIVATE
	inline std::string json_escape(const std::string& s)
	{
		return detail::json_escape(s);
	}

	WEBVIEW_DEPRECATED_PRIVATE
	inline int json_unescape(const char* s, size_t n, char* out)
	{
		return detail::json_unescape(s, n, out);
	}

	WEBVIEW_DEPRECATED_PRIVATE
	inline std::string json_parse(const std::string& s, const std::string& key, const int index)
	{
		return detail::json_parse(s, key, index);
	}

} // namespace webview

#if defined(WEBVIEW_GTK)
//
// ====================================================================
//
// This implementation uses webkit2gtk backend. It requires gtk+3.0 and
// webkit2gtk-4.0 libraries. Proper compiler flags can be retrieved via:
//
//   pkg-config --cflags --libs gtk+-3.0 webkit2gtk-4.0
//
// ====================================================================
//
#include <JavaScriptCore/JavaScript.h>
#include <gtk/gtk.h>
#include <webkit2/webkit2.h>

namespace webview
{
	namespace detail
	{

		class gtk_webkit_engine
		{
		  public:
			gtk_webkit_engine(bool debug, void* window) : m_window(static_cast<GtkWidget*>(window))
			{
				if (gtk_init_check(nullptr, nullptr) == FALSE)
				{
					return;
				}
				m_window = static_cast<GtkWidget*>(window);
				if (m_window == nullptr)
				{
					m_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
				}
				g_signal_connect(G_OBJECT(m_window), "destroy",
				                 G_CALLBACK(+[](GtkWidget*, gpointer arg) {
					                 static_cast<gtk_webkit_engine*>(arg)->terminate();
				                 }),
				                 this);
				// Initialize webview widget
				m_webview = webkit_web_view_new();
				WebKitUserContentManager* manager =
				    webkit_web_view_get_user_content_manager(WEBKIT_WEB_VIEW(m_webview));
				g_signal_connect(manager, "script-message-received::external",
				                 G_CALLBACK(+[](WebKitUserContentManager*,
				                                WebKitJavascriptResult* r, gpointer arg) {
					                 auto* w = static_cast<gtk_webkit_engine*>(arg);
					                 char* s = get_string_from_js_result(r);
					                 w->on_message(s);
					                 g_free(s);
				                 }),
				                 this);
				webkit_user_content_manager_register_script_message_handler(manager, "external");
				init("window.external={invoke:function(s){window.webkit.messageHandlers."
				     "external.postMessage(s);}}");

				gtk_container_add(GTK_CONTAINER(m_window), GTK_WIDGET(m_webview));
				gtk_widget_grab_focus(GTK_WIDGET(m_webview));

				WebKitSettings* settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(m_webview));
				webkit_settings_set_javascript_can_access_clipboard(settings, true);
				if (debug)
				{
					webkit_settings_set_enable_write_console_messages_to_stdout(settings, true);
					webkit_settings_set_enable_developer_extras(settings, true);
				}

				gtk_widget_show_all(m_window);
			}
			virtual ~gtk_webkit_engine() = default;
			void* window()
			{
				return (void*)m_window;
			}
			void run()
			{
				gtk_main();
			}
			void terminate()
			{
				gtk_main_quit();
			}
			void dispatch(std::function<void()> f)
			{
				g_idle_add_full(G_PRIORITY_HIGH_IDLE, (GSourceFunc)([](void* f) -> int {
					                (*static_cast<dispatch_fn_t*>(f))();
					                return G_SOURCE_REMOVE;
				                }),
				                new std::function<void()>(f),
				                [](void* f) { delete static_cast<dispatch_fn_t*>(f); });
			}

			void set_title(const std::string& title)
			{
				gtk_window_set_title(GTK_WINDOW(m_window), title.c_str());
			}

			void set_size(int width, int height, int hints)
			{
				gtk_window_set_resizable(GTK_WINDOW(m_window), hints != WEBVIEW_HINT_FIXED);
				if (hints == WEBVIEW_HINT_NONE)
				{
					gtk_window_resize(GTK_WINDOW(m_window), width, height);
				}
				else if (hints == WEBVIEW_HINT_FIXED)
				{
					gtk_widget_set_size_request(m_window, width, height);
				}
				else
				{
					GdkGeometry g;
					g.min_width = g.max_width = width;
					g.min_height = g.max_height = height;
					GdkWindowHints h =
					    (hints == WEBVIEW_HINT_MIN ? GDK_HINT_MIN_SIZE : GDK_HINT_MAX_SIZE);
					// This defines either MIN_SIZE, or MAX_SIZE, but not both:
					gtk_window_set_geometry_hints(GTK_WINDOW(m_window), nullptr, &g, h);
				}
			}

			void navigate(const std::string& url)
			{
				webkit_web_view_load_uri(WEBKIT_WEB_VIEW(m_webview), url.c_str());
			}

			void add_navigate_listener(std::function<void(const std::string&, void*)> callback,
			                           void* arg)
			{
				g_signal_connect(WEBKIT_WEB_VIEW(m_webview), "load-changed",
				                 G_CALLBACK(on_load_changed), this);
				navigateCallbackArg = arg;
				navigateCallback = std::move(callback);
			}

			void add_scheme_handler(const std::string& scheme,
			                        std::function<void(const std::string&, void*)> callback,
			                        void* arg)
			{
				auto view = WEBKIT_WEB_VIEW(m_webview);
				auto context = webkit_web_view_get_context(view);

				scheme_handlers.insert({ scheme, { .arg = arg, .fkt = callback } });
				webkit_web_context_register_uri_scheme(context, scheme.c_str(), scheme_handler,
				                                       static_cast<gpointer>(this), nullptr);
			}

			void set_html(const std::string& html)
			{
				webkit_web_view_load_html(WEBKIT_WEB_VIEW(m_webview), html.c_str(), nullptr);
			}

			void init(const std::string& js)
			{
				WebKitUserContentManager* manager =
				    webkit_web_view_get_user_content_manager(WEBKIT_WEB_VIEW(m_webview));
				webkit_user_content_manager_add_script(
				    manager, webkit_user_script_new(
				                 js.c_str(), WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
				                 WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nullptr, nullptr));
			}

			void eval(const std::string& js)
			{
				webkit_web_view_run_javascript(WEBKIT_WEB_VIEW(m_webview), js.c_str(), nullptr,
				                               nullptr, nullptr);
			}

		  private:
			virtual void on_message(const std::string& msg) = 0;

			struct handler_t
			{
				void* arg;
				std::function<void(const std::string&, void*)> fkt;
			};

			std::map<std::string, handler_t> scheme_handlers;

			void scheme_handler_call(const std::string& scheme, const std::string& url)
			{
				auto handler = scheme_handlers.find(scheme);
				if (handler != scheme_handlers.end())
				{
					const auto& arg = handler->second;
					arg.fkt(url, arg.arg);
				}
			}

			static void scheme_handler(WebKitURISchemeRequest* request, gpointer user_data)
			{
				auto _this = static_cast<gtk_webkit_engine*>(user_data);

				auto scheme = webkit_uri_scheme_request_get_scheme(request);
				auto uri = webkit_uri_scheme_request_get_uri(request);
				_this->scheme_handler_call(scheme, uri);
			}

			static char* get_string_from_js_result(WebKitJavascriptResult* r)
			{
				char* s = nullptr;
#if WEBKIT_MAJOR_VERSION >= 2 && WEBKIT_MINOR_VERSION >= 22
				JSCValue* value = webkit_javascript_result_get_js_value(r);
				s = jsc_value_to_string(value);
#else
				JSGlobalContextRef ctx = webkit_javascript_result_get_global_context(r);
				JSValueRef value = webkit_javascript_result_get_value(r);
				JSStringRef js = JSValueToStringCopy(ctx, value, nullptr);
				size_t n = JSStringGetMaximumUTF8CStringSize(js);
				s = g_new(char, n);
				JSStringGetUTF8CString(js, s, n);
				JSStringRelease(js);
#endif
				return s;
			}

			GtkWidget* m_window;
			GtkWidget* m_webview;

			void* navigateCallbackArg = nullptr;
			std::function<void(const std::string&, void*)> navigateCallback = nullptr;

			static void on_load_changed(WebKitWebView* web_view, WebKitLoadEvent load_event,
			                            gpointer arg)
			{
				if (load_event == WEBKIT_LOAD_FINISHED)
				{
					auto inst = static_cast<gtk_webkit_engine*>(arg);
					inst->navigateCallback(webkit_web_view_get_uri(web_view),
					                       inst->navigateCallbackArg);
				}
			}
		};

	} // namespace detail

	using browser_engine = detail::gtk_webkit_engine;

} // namespace webview

#elif defined(WEBVIEW_COCOA)

//
// ====================================================================
//
// This implementation uses Cocoa WKWebView backend on macOS. It is
// written using ObjC runtime and uses WKWebView class as a browser runtime.
// You should pass "-framework Webkit" flag to the compiler.
//
// ====================================================================
//

#include <CoreGraphics/CoreGraphics.h>
#include <objc/NSObjCRuntime.h>
#include <objc/objc-runtime.h>

namespace webview
{
	namespace detail
	{
		namespace objc
		{

			// A convenient template function for unconditionally casting the specified
			// C-like function into a function that can be called with the given return
			// type and arguments. Caller takes full responsibility for ensuring that
			// the function call is valid. It is assumed that the function will not
			// throw exceptions.
			template <typename Result, typename Callable, typename... Args>
			Result invoke(Callable callable, Args... args) noexcept
			{
				return reinterpret_cast<Result (*)(Args...)>(callable)(args...);
			}

			// Calls objc_msgSend.
			template <typename Result, typename... Args> Result msg_send(Args... args) noexcept
			{
				return invoke<Result>(objc_msgSend, args...);
			}

		} // namespace objc

		enum NSBackingStoreType : NSUInteger
		{
			NSBackingStoreBuffered = 2
		};

		enum NSWindowStyleMask : NSUInteger
		{
			NSWindowStyleMaskTitled = 1,
			NSWindowStyleMaskClosable = 2,
			NSWindowStyleMaskMiniaturizable = 4,
			NSWindowStyleMaskResizable = 8
		};

		enum NSApplicationActivationPolicy : NSInteger
		{
			NSApplicationActivationPolicyRegular = 0
		};

		enum WKUserScriptInjectionTime : NSInteger
		{
			WKUserScriptInjectionTimeAtDocumentStart = 0
		};

		enum NSModalResponse : NSInteger
		{
			NSModalResponseOK = 1
		};

		// Convenient conversion of string literals.
		inline id operator"" _cls(const char* s, std::size_t)
		{
			return (id)objc_getClass(s);
		}
		inline SEL operator"" _sel(const char* s, std::size_t)
		{
			return sel_registerName(s);
		}
		inline id operator"" _str(const char* s, std::size_t)
		{
			return objc::msg_send<id>("NSString"_cls, "stringWithUTF8String:"_sel, s);
		}

		class cocoa_wkwebview_engine
		{
		  public:
			cocoa_wkwebview_engine(bool debug, void* window)
			    : m_debug{ debug }, m_parent_window{ window }
			{
				auto app = get_shared_application();
				auto delegate = create_app_delegate();
				objc_setAssociatedObject(delegate, "webview", (id)this, OBJC_ASSOCIATION_ASSIGN);
				objc::msg_send<void>(app, "setDelegate:"_sel, delegate);

				// See comments related to application lifecycle in create_app_delegate().
				if (window)
				{
					on_application_did_finish_launching(delegate, app);
				}
				else
				{
					// Start the main run loop so that the app delegate gets the
					// NSApplicationDidFinishLaunchingNotification notification after the run
					// loop has started in order to perform further initialization.
					// We need to return from this constructor so this run loop is only
					// temporary.
					objc::msg_send<void>(app, "run"_sel);
				}
			}
			virtual ~cocoa_wkwebview_engine() = default;
			void* window()
			{
				return (void*)m_window;
			}
			void terminate()
			{
				auto app = get_shared_application();
				objc::msg_send<void>(app, "terminate:"_sel, nullptr);
			}
			void run()
			{
				auto app = get_shared_application();
				objc::msg_send<void>(app, "run"_sel);
			}
			void dispatch(std::function<void()> f)
			{
				dispatch_async_f(dispatch_get_main_queue(), new dispatch_fn_t(f),
				                 (dispatch_function_t)([](void* arg) {
					                 auto f = static_cast<dispatch_fn_t*>(arg);
					                 (*f)();
					                 delete f;
				                 }));
			}
			void set_title(const std::string& title)
			{
				objc::msg_send<void>(
				    m_window, "setTitle:"_sel,
				    objc::msg_send<id>("NSString"_cls, "stringWithUTF8String:"_sel, title.c_str()));
			}
			void set_size(int width, int height, int hints)
			{
				auto style = static_cast<NSWindowStyleMask>(NSWindowStyleMaskTitled |
				                                            NSWindowStyleMaskClosable |
				                                            NSWindowStyleMaskMiniaturizable);
				if (hints != WEBVIEW_HINT_FIXED)
				{
					style = static_cast<NSWindowStyleMask>(style | NSWindowStyleMaskResizable);
				}
				objc::msg_send<void>(m_window, "setStyleMask:"_sel, style);

				if (hints == WEBVIEW_HINT_MIN)
				{
					objc::msg_send<void>(m_window, "setContentMinSize:"_sel,
					                     CGSizeMake(width, height));
				}
				else if (hints == WEBVIEW_HINT_MAX)
				{
					objc::msg_send<void>(m_window, "setContentMaxSize:"_sel,
					                     CGSizeMake(width, height));
				}
				else
				{
					objc::msg_send<void>(m_window, "setFrame:display:animate:"_sel,
					                     CGRectMake(0, 0, width, height), YES, NO);
				}
				objc::msg_send<void>(m_window, "center"_sel);
			}
			void navigate(const std::string& url)
			{
				auto nsurl = objc::msg_send<id>(
				    "NSURL"_cls, "URLWithString:"_sel,
				    objc::msg_send<id>("NSString"_cls, "stringWithUTF8String:"_sel, url.c_str()));

				objc::msg_send<void>(
				    m_webview, "loadRequest:"_sel,
				    objc::msg_send<id>("NSURLRequest"_cls, "requestWithURL:"_sel, nsurl));
			}

			void add_navigate_listener(std::function<void(const std::string&, void*)> callback,
			                           void* arg)
			{
				m_navigateCallback = callback;
				m_navigateCallbackArg = arg;
			}

			void set_html(const std::string& html)
			{
				objc::msg_send<void>(
				    m_webview, "loadHTMLString:baseURL:"_sel,
				    objc::msg_send<id>("NSString"_cls, "stringWithUTF8String:"_sel, html.c_str()),
				    nullptr);
			}
			void init(const std::string& js)
			{
				// Equivalent Obj-C:
				// [m_manager addUserScript:[[WKUserScript alloc] initWithSource:[NSString
				// stringWithUTF8String:js.c_str()]
				// injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]]
				objc::msg_send<void>(
				    m_manager, "addUserScript:"_sel,
				    objc::msg_send<id>(
				        objc::msg_send<id>("WKUserScript"_cls, "alloc"_sel),
				        "initWithSource:injectionTime:forMainFrameOnly:"_sel,
				        objc::msg_send<id>("NSString"_cls, "stringWithUTF8String:"_sel, js.c_str()),
				        WKUserScriptInjectionTimeAtDocumentStart, YES));
			}
			void eval(const std::string& js)
			{
				objc::msg_send<void>(
				    m_webview, "evaluateJavaScript:completionHandler:"_sel,
				    objc::msg_send<id>("NSString"_cls, "stringWithUTF8String:"_sel, js.c_str()),
				    nullptr);
			}

		  private:
			virtual void on_message(const std::string& msg) = 0;
			id create_app_delegate()
			{
				// Note: Avoid registering the class name "AppDelegate" as it is the
				// default name in projects created with Xcode, and using the same name
				// causes objc_registerClassPair to crash.
				auto cls =
				    objc_allocateClassPair((Class) "NSResponder"_cls, "WebviewAppDelegate", 0);
				class_addProtocol(cls, objc_getProtocol("NSTouchBarProvider"));
				class_addMethod(cls, "applicationShouldTerminateAfterLastWindowClosed:"_sel,
				                (IMP)(+[](id, SEL, id) -> BOOL { return 1; }), "c@:@");
				// If the library was not initialized with an existing window then the user
				// is likely managing the application lifecycle and we would not get the
				// "applicationDidFinishLaunching:" message and therefore do not need to
				// add this method.
				if (!m_parent_window)
				{
					class_addMethod(cls, "applicationDidFinishLaunching:"_sel,
					                (IMP)(+[](id self, SEL, id notification) {
						                auto app = objc::msg_send<id>(notification, "object"_sel);
						                auto w = get_associated_webview(self);
						                w->on_application_did_finish_launching(self, app);
					                }),
					                "v@:@");
				}
				objc_registerClassPair(cls);
				return objc::msg_send<id>((id)cls, "new"_sel);
			}
			id create_script_message_handler()
			{
				auto cls = objc_allocateClassPair((Class) "NSResponder"_cls,
				                                  "WebkitScriptMessageHandler", 0);
				class_addProtocol(cls, objc_getProtocol("WKScriptMessageHandler"));
				class_addMethod(cls, "userContentController:didReceiveScriptMessage:"_sel,
				                (IMP)(+[](id self, SEL, id, id msg) {
					                auto w = get_associated_webview(self);
					                w->on_message(objc::msg_send<const char*>(
					                    objc::msg_send<id>(msg, "body"_sel), "UTF8String"_sel));
				                }),
				                "v@:@@");
				objc_registerClassPair(cls);
				auto instance = objc::msg_send<id>((id)cls, "new"_sel);
				objc_setAssociatedObject(instance, "webview", (id)this, OBJC_ASSOCIATION_ASSIGN);
				return instance;
			}
			static id create_webkit_ui_delegate()
			{
				auto cls = objc_allocateClassPair((Class) "NSObject"_cls, "WebkitUIDelegate", 0);
				class_addProtocol(cls, objc_getProtocol("WKUIDelegate"));
				class_addMethod(
				    cls,
				    "webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:"_sel,
				    (IMP)(+[](id, SEL, id, id parameters, id, id completion_handler) {
					    auto allows_multiple_selection =
					        objc::msg_send<BOOL>(parameters, "allowsMultipleSelection"_sel);
					    auto allows_directories =
					        objc::msg_send<BOOL>(parameters, "allowsDirectories"_sel);

					    // Show a panel for selecting files.
					    auto panel = objc::msg_send<id>("NSOpenPanel"_cls, "openPanel"_sel);
					    objc::msg_send<void>(panel, "setCanChooseFiles:"_sel, YES);
					    objc::msg_send<void>(panel, "setCanChooseDirectories:"_sel,
					                         allows_directories);
					    objc::msg_send<void>(panel, "setAllowsMultipleSelection:"_sel,
					                         allows_multiple_selection);
					    auto modal_response =
					        objc::msg_send<NSModalResponse>(panel, "runModal"_sel);

					    // Get the URLs for the selected files. If the modal was canceled
					    // then we pass null to the completion handler to signify
					    // cancellation.
					    id urls = modal_response == NSModalResponseOK
					                  ? objc::msg_send<id>(panel, "URLs"_sel)
					                  : nullptr;

					    // Invoke the completion handler block.
					    auto sig = objc::msg_send<id>("NSMethodSignature"_cls,
					                                  "signatureWithObjCTypes:"_sel, "v@?@");
					    auto invocation = objc::msg_send<id>(
					        "NSInvocation"_cls, "invocationWithMethodSignature:"_sel, sig);
					    objc::msg_send<void>(invocation, "setTarget:"_sel, completion_handler);
					    objc::msg_send<void>(invocation, "setArgument:atIndex:"_sel, &urls, 1);
					    objc::msg_send<void>(invocation, "invoke"_sel);
				    }),
				    "v@:@@@@");
				objc_registerClassPair(cls);
				return objc::msg_send<id>((id)cls, "new"_sel);
			}
			id create_webkit_navigation_delegate()
			{
				auto cls =
				    objc_allocateClassPair((Class) "NSObject"_cls, "WebkitNavigationDelegate", 0);
				class_addProtocol(cls, objc_getProtocol("WKNavigationDelegate"));
				class_addMethod(cls, "webView:didFinishNavigation:"_sel,
				                (IMP)(+[](id delegate, SEL sel, id webview, id navigation) {
					                auto w = get_associated_webview(delegate);
					                auto url = objc::msg_send<id>(webview, "URL"_sel);
					                auto nstr = objc::msg_send<id>(url, "absoluteString"_sel);
					                auto str = objc::msg_send<char*>(nstr, "UTF8String"_sel);
					                w->m_navigateCallback(str, w->m_navigateCallbackArg);
				                }),
				                "v@:@");
				objc_registerClassPair(cls);
				auto instance = objc::msg_send<id>((id)cls, "new"_sel);
				objc_setAssociatedObject(instance, "webview", (id)this, OBJC_ASSOCIATION_ASSIGN);
				return instance;
			}
			static id get_shared_application()
			{
				return objc::msg_send<id>("NSApplication"_cls, "sharedApplication"_sel);
			}
			static cocoa_wkwebview_engine* get_associated_webview(id object)
			{
				auto w = (cocoa_wkwebview_engine*)objc_getAssociatedObject(object, "webview");
				assert(w);
				return w;
			}
			static id get_main_bundle() noexcept
			{
				return objc::msg_send<id>("NSBundle"_cls, "mainBundle"_sel);
			}
			static bool is_app_bundled() noexcept
			{
				auto bundle = get_main_bundle();
				if (!bundle)
				{
					return false;
				}
				auto bundle_path = objc::msg_send<id>(bundle, "bundlePath"_sel);
				auto bundled = objc::msg_send<BOOL>(bundle_path, "hasSuffix:"_sel, ".app"_str);
				return !!bundled;
			}
			void on_application_did_finish_launching(id /*delegate*/, id app)
			{
				// See comments related to application lifecycle in create_app_delegate().
				if (!m_parent_window)
				{
					// Stop the main run loop so that we can return
					// from the constructor.
					objc::msg_send<void>(app, "stop:"_sel, nullptr);
				}

				// Activate the app if it is not bundled.
				// Bundled apps launched from Finder are activated automatically but
				// otherwise not. Activating the app even when it has been launched from
				// Finder does not seem to be harmful but calling this function is rarely
				// needed as proper activation is normally taken care of for us.
				// Bundled apps have a default activation policy of
				// NSApplicationActivationPolicyRegular while non-bundled apps have a
				// default activation policy of NSApplicationActivationPolicyProhibited.
				if (!is_app_bundled())
				{
					// "setActivationPolicy:" must be invoked before
					// "activateIgnoringOtherApps:" for activation to work.
					objc::msg_send<void>(app, "setActivationPolicy:"_sel,
					                     NSApplicationActivationPolicyRegular);
					// Activate the app regardless of other active apps.
					// This can be obtrusive so we only do it when necessary.
					objc::msg_send<void>(app, "activateIgnoringOtherApps:"_sel, YES);
				}

				// Main window
				if (!m_parent_window)
				{
					m_window = objc::msg_send<id>("NSWindow"_cls, "alloc"_sel);
					auto style = NSWindowStyleMaskTitled;
					m_window = objc::msg_send<id>(
					    m_window, "initWithContentRect:styleMask:backing:defer:"_sel,
					    CGRectMake(0, 0, 0, 0), style, NSBackingStoreBuffered, NO);
				}
				else
				{
					m_window = (id)m_parent_window;
				}

				// Webview
				auto config = objc::msg_send<id>("WKWebViewConfiguration"_cls, "new"_sel);
				m_manager = objc::msg_send<id>(config, "userContentController"_sel);
				m_webview = objc::msg_send<id>("WKWebView"_cls, "alloc"_sel);

				if (m_debug)
				{
					// Equivalent Obj-C:
					// [[config preferences] setValue:@YES forKey:@"developerExtrasEnabled"];
					objc::msg_send<id>(
					    objc::msg_send<id>(config, "preferences"_sel), "setValue:forKey:"_sel,
					    objc::msg_send<id>("NSNumber"_cls, "numberWithBool:"_sel, YES),
					    "developerExtrasEnabled"_str);
				}

				// Equivalent Obj-C:
				// [[config preferences] setValue:@YES forKey:@"fullScreenEnabled"];
				objc::msg_send<id>(objc::msg_send<id>(config, "preferences"_sel),
				                   "setValue:forKey:"_sel,
				                   objc::msg_send<id>("NSNumber"_cls, "numberWithBool:"_sel, YES),
				                   "fullScreenEnabled"_str);

				// Equivalent Obj-C:
				// [[config preferences] setValue:@YES forKey:@"javaScriptCanAccessClipboard"];
				objc::msg_send<id>(objc::msg_send<id>(config, "preferences"_sel),
				                   "setValue:forKey:"_sel,
				                   objc::msg_send<id>("NSNumber"_cls, "numberWithBool:"_sel, YES),
				                   "javaScriptCanAccessClipboard"_str);

				// Equivalent Obj-C:
				// [[config preferences] setValue:@YES forKey:@"DOMPasteAllowed"];
				objc::msg_send<id>(objc::msg_send<id>(config, "preferences"_sel),
				                   "setValue:forKey:"_sel,
				                   objc::msg_send<id>("NSNumber"_cls, "numberWithBool:"_sel, YES),
				                   "DOMPasteAllowed"_str);

				auto ui_delegate = create_webkit_ui_delegate();
				objc::msg_send<void>(m_webview, "initWithFrame:configuration:"_sel,
				                     CGRectMake(0, 0, 0, 0), config);
				objc::msg_send<void>(m_webview, "setUIDelegate:"_sel, ui_delegate);

				auto navigation_delegate = create_webkit_navigation_delegate();
				objc::msg_send<void>(m_webview, "setNavigationDelegate:"_sel, navigation_delegate);
				auto script_message_handler = create_script_message_handler();
				objc::msg_send<void>(m_manager, "addScriptMessageHandler:name:"_sel,
				                     script_message_handler, "external"_str);

				init(R""(
      window.external = {
        invoke: function(s) {
          window.webkit.messageHandlers.external.postMessage(s);
        },
      };
      )"");
				objc::msg_send<void>(m_window, "setContentView:"_sel, m_webview);
				objc::msg_send<void>(m_window, "makeKeyAndOrderFront:"_sel, nullptr);
			}
			bool m_debug;
			void* m_parent_window;
			id m_window;
			id m_webview;
			id m_manager;
			void* m_navigateCallbackArg = nullptr;
			std::function<void(const std::string&, void*)> m_navigateCallback = 0;
		};

	} // namespace detail

	using browser_engine = detail::cocoa_wkwebview_engine;

} // namespace webview

#elif defined(WEBVIEW_EDGE)

//
// ====================================================================
//
// This implementation uses Win32 API to create a native window. It
// uses Edge/Chromium webview2 backend as a browser engine.
//
// ====================================================================
//

#define WIN32_LEAN_AND_MEAN
#include <shlobj.h>
#include <shlwapi.h>
#include <stdlib.h>
#include <windows.h>

#include "WebView2.h"

#ifdef _MSC_VER
#pragma comment(lib, "advapi32.lib")
#pragma comment(lib, "ole32.lib")
#pragma comment(lib, "shell32.lib")
#pragma comment(lib, "shlwapi.lib")
#pragma comment(lib, "user32.lib")
#pragma comment(lib, "version.lib")
#endif

namespace webview
{
	namespace detail
	{

		using msg_cb_t = std::function<void(const std::string)>;

		// Converts a narrow (UTF-8-encoded) string into a wide (UTF-16-encoded) string.
		inline std::wstring widen_string(const std::string& input)
		{
			if (input.empty())
			{
				return std::wstring();
			}
			UINT cp = CP_UTF8;
			DWORD flags = MB_ERR_INVALID_CHARS;
			auto input_c = input.c_str();
			auto input_length = static_cast<int>(input.size());
			auto required_length =
			    MultiByteToWideChar(cp, flags, input_c, input_length, nullptr, 0);
			if (required_length > 0)
			{
				std::wstring output(static_cast<std::size_t>(required_length), L'\0');
				if (MultiByteToWideChar(cp, flags, input_c, input_length, &output[0],
				                        required_length) > 0)
				{
					return output;
				}
			}
			// Failed to convert string from UTF-8 to UTF-16
			return std::wstring();
		}

		// Converts a wide (UTF-16-encoded) string into a narrow (UTF-8-encoded) string.
		inline std::string narrow_string(const std::wstring& input)
		{
			if (input.empty())
			{
				return std::string();
			}
			UINT cp = CP_UTF8;
			DWORD flags = WC_ERR_INVALID_CHARS;
			auto input_c = input.c_str();
			auto input_length = static_cast<int>(input.size());
			auto required_length =
			    WideCharToMultiByte(cp, flags, input_c, input_length, nullptr, 0, nullptr, nullptr);
			if (required_length > 0)
			{
				std::string output(static_cast<std::size_t>(required_length), '\0');
				if (WideCharToMultiByte(cp, flags, input_c, input_length, &output[0],
				                        required_length, nullptr, nullptr) > 0)
				{
					return output;
				}
			}
			// Failed to convert string from UTF-16 to UTF-8
			return std::string();
		}

		// Parses a version string with 1-4 integral components, e.g. "1.2.3.4".
		// Missing or invalid components default to 0, and excess components are ignored.
		template <typename T>
		std::array<unsigned int, 4> parse_version(const std::basic_string<T>& version) noexcept
		{
			auto parse_component = [](auto sb, auto se) -> unsigned int {
				try
				{
					auto n = std::stol(std::basic_string<T>(sb, se));
					return n < 0 ? 0 : n;
				}
				catch (std::exception&)
				{
					return 0;
				}
			};
			auto end = version.end();
			auto sb = version.begin(); // subrange begin
			auto se = sb;              // subrange end
			unsigned int ci = 0;       // component index
			std::array<unsigned int, 4> components{};
			while (sb != end && se != end && ci < components.size())
			{
				if (*se == static_cast<T>('.'))
				{
					components[ci++] = parse_component(sb, se);
					sb = ++se;
					continue;
				}
				++se;
			}
			if (sb < se && ci < components.size())
			{
				components[ci] = parse_component(sb, se);
			}
			return components;
		}

		template <typename T, std::size_t Length>
		auto parse_version(const T (&version)[Length]) noexcept
		{
			return parse_version(std::basic_string<T>(version, Length));
		}

		std::wstring get_file_version_string(const std::wstring& file_path) noexcept
		{
			DWORD dummy_handle; // Unused
			DWORD info_buffer_length = GetFileVersionInfoSizeW(file_path.c_str(), &dummy_handle);
			if (info_buffer_length == 0)
			{
				return std::wstring();
			}
			std::vector<char> info_buffer;
			info_buffer.reserve(info_buffer_length);
			if (!GetFileVersionInfoW(file_path.c_str(), 0, info_buffer_length, info_buffer.data()))
			{
				return std::wstring();
			}
			auto sub_block = L"\\StringFileInfo\\040904B0\\ProductVersion";
			LPWSTR version = nullptr;
			unsigned int version_length = 0;
			if (!VerQueryValueW(info_buffer.data(), sub_block, reinterpret_cast<LPVOID*>(&version),
			                    &version_length))
			{
				return std::wstring();
			}
			if (!version || version_length == 0)
			{
				return std::wstring();
			}
			return std::wstring(version, version_length);
		}

		// A wrapper around COM library initialization. Calls CoInitializeEx in the
		// constructor and CoUninitialize in the destructor.
		class com_init_wrapper
		{
		  public:
			com_init_wrapper(DWORD dwCoInit)
			{
				// We can safely continue as long as COM was either successfully
				// initialized or already initialized.
				// RPC_E_CHANGED_MODE means that CoInitializeEx was already called with
				// a different concurrency model.
				switch (CoInitializeEx(nullptr, dwCoInit))
				{
					case S_OK:
					case S_FALSE:
						m_initialized = true;
						break;
				}
			}

			~com_init_wrapper()
			{
				if (m_initialized)
				{
					CoUninitialize();
					m_initialized = false;
				}
			}

			com_init_wrapper(const com_init_wrapper& other) = delete;
			com_init_wrapper& operator=(const com_init_wrapper& other) = delete;
			com_init_wrapper(com_init_wrapper&& other) = delete;
			com_init_wrapper& operator=(com_init_wrapper&& other) = delete;

			bool is_initialized() const
			{
				return m_initialized;
			}

		  private:
			bool m_initialized = false;
		};

		// Holds a symbol name and associated type for code clarity.
		template <typename T> class library_symbol
		{
		  public:
			using type = T;

			constexpr explicit library_symbol(const char* name) : m_name(name)
			{
			}
			constexpr const char* get_name() const
			{
				return m_name;
			}

		  private:
			const char* m_name;
		};

		// Loads a native shared library and allows one to get addresses for those
		// symbols.
		class native_library
		{
		  public:
			explicit native_library(const wchar_t* name) : m_handle(LoadLibraryW(name))
			{
			}

			~native_library()
			{
				if (m_handle)
				{
					FreeLibrary(m_handle);
					m_handle = nullptr;
				}
			}

			native_library(const native_library& other) = delete;
			native_library& operator=(const native_library& other) = delete;
			native_library(native_library&& other) = default;
			native_library& operator=(native_library&& other) = default;

			// Returns true if the library is currently loaded; otherwise false.
			operator bool() const
			{
				return is_loaded();
			}

			// Get the address for the specified symbol or nullptr if not found.
			template <typename Symbol> typename Symbol::type get(const Symbol& symbol) const
			{
				if (is_loaded())
				{
					return reinterpret_cast<typename Symbol::type>(
					    GetProcAddress(m_handle, symbol.get_name()));
				}
				return nullptr;
			}

			// Returns true if the library is currently loaded; otherwise false.
			bool is_loaded() const
			{
				return !!m_handle;
			}

			void detach()
			{
				m_handle = nullptr;
			}

		  private:
			HMODULE m_handle = nullptr;
		};

		struct user32_symbols
		{
			using DPI_AWARENESS_CONTEXT = HANDLE;
			using SetProcessDpiAwarenessContext_t = BOOL(WINAPI*)(DPI_AWARENESS_CONTEXT);
			using SetProcessDPIAware_t = BOOL(WINAPI*)();

			static constexpr auto SetProcessDpiAwarenessContext =
			    library_symbol<SetProcessDpiAwarenessContext_t>("SetProcessDpiAwarenessContext");
			static constexpr auto SetProcessDPIAware =
			    library_symbol<SetProcessDPIAware_t>("SetProcessDPIAware");
		};

		struct shcore_symbols
		{
			typedef enum
			{
				PROCESS_PER_MONITOR_DPI_AWARE = 2
			} PROCESS_DPI_AWARENESS;
			using SetProcessDpiAwareness_t = HRESULT(WINAPI*)(PROCESS_DPI_AWARENESS);

			static constexpr auto SetProcessDpiAwareness =
			    library_symbol<SetProcessDpiAwareness_t>("SetProcessDpiAwareness");
		};

		class reg_key
		{
		  public:
			explicit reg_key(HKEY root_key, const wchar_t* sub_key, DWORD options,
			                 REGSAM sam_desired)
			{
				HKEY handle;
				auto status = RegOpenKeyExW(root_key, sub_key, options, sam_desired, &handle);
				if (status == ERROR_SUCCESS)
				{
					m_handle = handle;
				}
			}

			explicit reg_key(HKEY root_key, const std::wstring& sub_key, DWORD options,
			                 REGSAM sam_desired)
			    : reg_key(root_key, sub_key.c_str(), options, sam_desired)
			{
			}

			virtual ~reg_key()
			{
				if (m_handle)
				{
					RegCloseKey(m_handle);
					m_handle = nullptr;
				}
			}

			reg_key(const reg_key& other) = delete;
			reg_key& operator=(const reg_key& other) = delete;
			reg_key(reg_key&& other) = delete;
			reg_key& operator=(reg_key&& other) = delete;

			bool is_open() const
			{
				return !!m_handle;
			}
			bool get_handle() const
			{
				return m_handle;
			}

			std::wstring query_string(const wchar_t* name) const
			{
				DWORD buf_length = 0;
				// Get the size of the data in bytes.
				auto status =
				    RegQueryValueExW(m_handle, name, nullptr, nullptr, nullptr, &buf_length);
				if (status != ERROR_SUCCESS && status != ERROR_MORE_DATA)
				{
					return std::wstring();
				}
				// Read the data.
				std::wstring result(buf_length / sizeof(wchar_t), 0);
				auto buf = reinterpret_cast<LPBYTE>(&result[0]);
				status = RegQueryValueExW(m_handle, name, nullptr, nullptr, buf, &buf_length);
				if (status != ERROR_SUCCESS)
				{
					return std::wstring();
				}
				// Remove trailing null-characters.
				for (std::size_t length = result.size(); length > 0; --length)
				{
					if (result[length - 1] != 0)
					{
						result.resize(length);
						break;
					}
				}
				return result;
			}

		  private:
			HKEY m_handle = nullptr;
		};

		inline bool enable_dpi_awareness()
		{
			auto user32 = native_library(L"user32.dll");
			if (auto fn = user32.get(user32_symbols::SetProcessDpiAwarenessContext))
			{
				if (fn(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE))
				{
					return true;
				}
				return GetLastError() == ERROR_ACCESS_DENIED;
			}
			if (auto shcore = native_library(L"shcore.dll"))
			{
				if (auto fn = shcore.get(shcore_symbols::SetProcessDpiAwareness))
				{
					auto result = fn(shcore_symbols::PROCESS_PER_MONITOR_DPI_AWARE);
					return result == S_OK || result == E_ACCESSDENIED;
				}
			}
			if (auto fn = user32.get(user32_symbols::SetProcessDPIAware))
			{
				return !!fn();
			}
			return true;
		}

// Enable built-in WebView2Loader implementation by default.
#ifndef WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL
#define WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL 1
#endif

// Link WebView2Loader.dll explicitly by default only if the built-in
// implementation is enabled.
#ifndef WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK
#define WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL
#endif

// Explicit linking of WebView2Loader.dll should be used along with
// the built-in implementation.
#if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1 && WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK != 1
#undef WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK
#error Please set WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK=1.
#endif

#if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1
		// Gets the last component of a Windows native file path.
		// For example, if the path is "C:\a\b" then the result is "b".
		template <typename T>
		std::basic_string<T> get_last_native_path_component(const std::basic_string<T>& path)
		{
			if (auto pos = path.find_last_of(static_cast<T>('\\'));
			    pos != std::basic_string<T>::npos)
			{
				return path.substr(pos + 1);
			}
			return std::basic_string<T>();
		}
#endif /* WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL */

		template <typename T> struct cast_info_t
		{
			using type = T;
			IID iid;
		};

		namespace mswebview2
		{
			static constexpr IID IID_ICoreWebView2CreateCoreWebView2ControllerCompletedHandler{
				0x6C4819F3, 0xC9B7, 0x4260, 0x81, 0x27, 0xC9, 0xF5, 0xBD, 0xE7, 0xF6, 0x8C
			};
			static constexpr IID IID_ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler{
				0x4E8A3389, 0xC9D8, 0x4BD2, 0xB6, 0xB5, 0x12, 0x4F, 0xEE, 0x6C, 0xC1, 0x4D
			};
			static constexpr IID IID_ICoreWebView2PermissionRequestedEventHandler{
				0x15E1C6A3, 0xC72A, 0x4DF3, 0x91, 0xD7, 0xD0, 0x97, 0xFB, 0xEC, 0x6B, 0xFD
			};
			static constexpr IID IID_ICoreWebView2WebMessageReceivedEventHandler{
				0x57213F19, 0x00E6, 0x49FA, 0x8E, 0x07, 0x89, 0x8E, 0xA0, 0x1E, 0xCB, 0xD2
			};

#if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1
			enum class webview2_runtime_type
			{
				installed = 0,
				embedded = 1
			};

			namespace webview2_symbols
			{
				using CreateWebViewEnvironmentWithOptionsInternal_t = HRESULT(STDMETHODCALLTYPE*)(
				    bool, webview2_runtime_type, PCWSTR, IUnknown*,
				    ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler*);
				using DllCanUnloadNow_t = HRESULT(STDMETHODCALLTYPE*)();

				static constexpr auto CreateWebViewEnvironmentWithOptionsInternal =
				    library_symbol<CreateWebViewEnvironmentWithOptionsInternal_t>(
				        "CreateWebViewEnvironmentWithOptionsInternal");
				static constexpr auto DllCanUnloadNow =
				    library_symbol<DllCanUnloadNow_t>("DllCanUnloadNow");
			} // namespace webview2_symbols
#endif /* WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL */

#if WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK == 1
			namespace webview2_symbols
			{
				using CreateCoreWebView2EnvironmentWithOptions_t = HRESULT(STDMETHODCALLTYPE*)(
				    PCWSTR, PCWSTR, ICoreWebView2EnvironmentOptions*,
				    ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler*);
				using GetAvailableCoreWebView2BrowserVersionString_t =
				    HRESULT(STDMETHODCALLTYPE*)(PCWSTR, LPWSTR*);

				static constexpr auto CreateCoreWebView2EnvironmentWithOptions =
				    library_symbol<CreateCoreWebView2EnvironmentWithOptions_t>(
				        "CreateCoreWebView2EnvironmentWithOptions");
				static constexpr auto GetAvailableCoreWebView2BrowserVersionString =
				    library_symbol<GetAvailableCoreWebView2BrowserVersionString_t>(
				        "GetAvailableCoreWebView2BrowserVersionString");
			} // namespace webview2_symbols
#endif /* WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK */

			class loader
			{
			  public:
				HRESULT create_environment_with_options(
				    PCWSTR browser_dir, PCWSTR user_data_dir,
				    ICoreWebView2EnvironmentOptions* env_options,
				    ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler* created_handler)
				    const
				{
#if WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK == 1
					if (m_lib.is_loaded())
					{
						if (auto fn = m_lib.get(
						        webview2_symbols::CreateCoreWebView2EnvironmentWithOptions))
						{
							return fn(browser_dir, user_data_dir, env_options, created_handler);
						}
					}
#if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1
					return create_environment_with_options_impl(browser_dir, user_data_dir,
					                                            env_options, created_handler);
#else
					return S_FALSE;
#endif
#else
					return ::CreateCoreWebView2EnvironmentWithOptions(browser_dir, user_data_dir,
					                                                  env_options, created_handler);
#endif /* WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK */
				}

				HRESULT
				get_available_browser_version_string(PCWSTR browser_dir, LPWSTR* version) const
				{
#if WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK == 1
					if (m_lib.is_loaded())
					{
						if (auto fn = m_lib.get(
						        webview2_symbols::GetAvailableCoreWebView2BrowserVersionString))
						{
							return fn(browser_dir, version);
						}
					}
#if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1
					return get_available_browser_version_string_impl(browser_dir, version);
#else
					return S_FALSE;
#endif
#else
					return ::GetAvailableCoreWebView2BrowserVersionString(browser_dir, version);
#endif /* WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK */
				}

			  private:
#if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1
				struct client_info_t
				{
					bool found = false;
					std::wstring dll_path;
					std::wstring version;
					webview2_runtime_type runtime_type;
				};

				HRESULT create_environment_with_options_impl(
				    PCWSTR browser_dir, PCWSTR user_data_dir,
				    ICoreWebView2EnvironmentOptions* env_options,
				    ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler* created_handler)
				    const
				{
					auto found_client = find_available_client(browser_dir);
					if (!found_client.found)
					{
						return -1;
					}
					auto client_dll = native_library(found_client.dll_path.c_str());
					if (auto fn = client_dll.get(
					        webview2_symbols::CreateWebViewEnvironmentWithOptionsInternal))
					{
						return fn(true, found_client.runtime_type, user_data_dir, env_options,
						          created_handler);
					}
					if (auto fn = client_dll.get(webview2_symbols::DllCanUnloadNow))
					{
						if (!fn())
						{
							client_dll.detach();
						}
					}
					return ERROR_SUCCESS;
				}

				HRESULT
				get_available_browser_version_string_impl(PCWSTR browser_dir, LPWSTR* version) const
				{
					if (!version)
					{
						return -1;
					}
					auto found_client = find_available_client(browser_dir);
					if (!found_client.found)
					{
						return -1;
					}
					auto info_length_bytes =
					    found_client.version.size() * sizeof(found_client.version[0]);
					auto info = static_cast<LPWSTR>(CoTaskMemAlloc(info_length_bytes));
					if (!info)
					{
						return -1;
					}
					CopyMemory(info, found_client.version.c_str(), info_length_bytes);
					*version = info;
					return 0;
				}

				client_info_t find_available_client(PCWSTR browser_dir) const
				{
					if (browser_dir)
					{
						return find_embedded_client(api_version, browser_dir);
					}
					auto found_client =
					    find_installed_client(api_version, true, default_release_channel_guid);
					if (!found_client.found)
					{
						found_client =
						    find_installed_client(api_version, false, default_release_channel_guid);
					}
					return found_client;
				}

				std::wstring make_client_dll_path(const std::wstring& dir) const
				{
					auto dll_path = dir;
					if (!dll_path.empty())
					{
						auto last_char = dir[dir.size() - 1];
						if (last_char != L'\\' && last_char != L'/')
						{
							dll_path += L'\\';
						}
					}
					dll_path += L"EBWebView\\";
#if defined(_M_X64) || defined(__x86_64__)
					dll_path += L"x64";
#elif defined(_M_IX86) || defined(__i386__)
					dll_path += L"x86";
#elif defined(_M_ARM64) || defined(__aarch64__)
					dll_path += L"arm64";
#else
#error WebView2 integration for this platform is not yet supported.
#endif
					dll_path += L"\\EmbeddedBrowserWebView.dll";
					return dll_path;
				}

				client_info_t find_installed_client(unsigned int min_api_version, bool system,
				                                    const std::wstring& release_channel) const
				{
					std::wstring sub_key = client_state_reg_sub_key;
					sub_key += release_channel;
					auto root_key = system ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER;
					reg_key key(root_key, sub_key, 0, KEY_READ | KEY_WOW64_32KEY);
					if (!key.is_open())
					{
						return {};
					}
					auto ebwebview_value = key.query_string(L"EBWebView");

					auto client_version_string = get_last_native_path_component(ebwebview_value);
					auto client_version = parse_version(client_version_string);
					if (client_version[2] < min_api_version)
					{
						// Our API version is greater than the runtime API version.
						return {};
					}

					auto client_dll_path = make_client_dll_path(ebwebview_value);
					return { true, client_dll_path, client_version_string,
						     webview2_runtime_type::installed };
				}

				client_info_t find_embedded_client(unsigned int min_api_version,
				                                   const std::wstring& dir) const
				{
					auto client_dll_path = make_client_dll_path(dir);

					auto client_version_string = get_file_version_string(client_dll_path);
					auto client_version = parse_version(client_version_string);
					if (client_version[2] < min_api_version)
					{
						// Our API version is greater than the runtime API version.
						return {};
					}

					return { true, client_dll_path, client_version_string,
						     webview2_runtime_type::embedded };
				}

				// The minimum WebView2 API version we need regardless of the SDK release
				// actually used. The number comes from the SDK release version,
				// e.g. 1.0.1150.38. To be safe the SDK should have a number that is greater
				// than or equal to this number. The Edge browser webview client must
				// have a number greater than or equal to this number.
				static constexpr unsigned int api_version = 1150;

				static constexpr auto client_state_reg_sub_key =
				    L"SOFTWARE\\Microsoft\\EdgeUpdate\\ClientState\\";

				// GUID for the stable release channel.
				static constexpr auto stable_release_guid =
				    L"{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}";

				static constexpr auto default_release_channel_guid = stable_release_guid;
#endif /* WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL */

#if WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK == 1
				native_library m_lib{ L"WebView2Loader.dll" };
#endif
			};

			namespace cast_info
			{
				static constexpr auto controller_completed =
				    cast_info_t<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>{
					    IID_ICoreWebView2CreateCoreWebView2ControllerCompletedHandler
				    };

				static constexpr auto environment_completed =
				    cast_info_t<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>{
					    IID_ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler
				    };

				static constexpr auto message_received =
				    cast_info_t<ICoreWebView2WebMessageReceivedEventHandler>{
					    IID_ICoreWebView2WebMessageReceivedEventHandler
				    };

				static constexpr auto permission_requested =
				    cast_info_t<ICoreWebView2PermissionRequestedEventHandler>{
					    IID_ICoreWebView2PermissionRequestedEventHandler
				    };
			} // namespace cast_info
		}     // namespace mswebview2

		class webview2_com_handler
		    : public ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler,
		      public ICoreWebView2CreateCoreWebView2ControllerCompletedHandler,
		      public ICoreWebView2WebMessageReceivedEventHandler,
		      public ICoreWebView2PermissionRequestedEventHandler,
		      public ICoreWebView2NavigationCompletedEventHandler
		{
			using webview2_com_handler_cb_t =
			    std::function<void(ICoreWebView2Controller*, ICoreWebView2* webview)>;

		  public:
			webview2_com_handler(HWND hwnd, msg_cb_t msgCb, webview2_com_handler_cb_t cb)
			    : m_window(hwnd), m_msgCb(msgCb), m_cb(cb)
			{
			}

			virtual ~webview2_com_handler() = default;
			webview2_com_handler(const webview2_com_handler& other) = delete;
			webview2_com_handler& operator=(const webview2_com_handler& other) = delete;
			webview2_com_handler(webview2_com_handler&& other) = delete;
			webview2_com_handler& operator=(webview2_com_handler&& other) = delete;

			ULONG STDMETHODCALLTYPE AddRef()
			{
				return ++m_ref_count;
			}
			ULONG STDMETHODCALLTYPE Release()
			{
				if (m_ref_count > 1)
				{
					return --m_ref_count;
				}
				delete this;
				return 0;
			}
			HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID* ppv)
			{
				using namespace mswebview2::cast_info;

				if (!ppv)
				{
					return E_POINTER;
				}

				// All of the COM interfaces we implement should be added here regardless
				// of whether they are required.
				// This is just to be on the safe side in case the WebView2 Runtime ever
				// requests a pointer to an interface we implement.
				// The WebView2 Runtime must at the very least be able to get a pointer to
				// ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler when we use
				// our custom WebView2 loader implementation, and observations have shown
				// that it is the only interface requested in this case. None have been
				// observed to be requested when using the official WebView2 loader.

				if (cast_if_equal_iid(riid, controller_completed, ppv) ||
				    cast_if_equal_iid(riid, environment_completed, ppv) ||
				    cast_if_equal_iid(riid, message_received, ppv) ||
				    cast_if_equal_iid(riid, permission_requested, ppv))
				{
					return S_OK;
				}

				return E_NOINTERFACE;
			}
			HRESULT STDMETHODCALLTYPE Invoke(HRESULT res, ICoreWebView2Environment* env)
			{
				if (SUCCEEDED(res))
				{
					res = env->CreateCoreWebView2Controller(m_window, this);
					if (SUCCEEDED(res))
					{
						return S_OK;
					}
				}
				try_create_environment();
				return S_OK;
			}
			HRESULT STDMETHODCALLTYPE Invoke(HRESULT res, ICoreWebView2Controller* controller)
			{
				if (FAILED(res))
				{
					// See try_create_environment() regarding
					// HRESULT_FROM_WIN32(ERROR_INVALID_STATE).
					// The result is E_ABORT if the parent window has been destroyed already.
					switch (res)
					{
						case HRESULT_FROM_WIN32(ERROR_INVALID_STATE):
						case E_ABORT:
							return S_OK;
					}
					try_create_environment();
					return S_OK;
				}

				ICoreWebView2* webview;
				::EventRegistrationToken token;
				controller->get_CoreWebView2(&webview);
				webview->add_WebMessageReceived(this, &token);
				webview->add_PermissionRequested(this, &token);
				webview->add_NavigationCompleted(this, &token);

				m_cb(controller, webview);
				return S_OK;
			}
			HRESULT STDMETHODCALLTYPE Invoke(ICoreWebView2* sender,
			                                 ICoreWebView2WebMessageReceivedEventArgs* args)
			{
				LPWSTR message;
				args->TryGetWebMessageAsString(&message);
				m_msgCb(narrow_string(message));
				sender->PostWebMessageAsString(message);

				CoTaskMemFree(message);
				return S_OK;
			}
			HRESULT STDMETHODCALLTYPE Invoke(ICoreWebView2* sender,
			                                 ICoreWebView2PermissionRequestedEventArgs* args)
			{
				COREWEBVIEW2_PERMISSION_KIND kind;
				args->get_PermissionKind(&kind);
				if (kind == COREWEBVIEW2_PERMISSION_KIND_CLIPBOARD_READ)
				{
					args->put_State(COREWEBVIEW2_PERMISSION_STATE_ALLOW);
				}
				return S_OK;
			}
			HRESULT STDMETHODCALLTYPE Invoke(ICoreWebView2* sender,
			                                 ICoreWebView2NavigationCompletedEventArgs* args)
			{
				PWSTR uri = nullptr;
				auto hr = sender->get_Source(&uri);
				if (SUCCEEDED(hr))
				{
					auto curi = std::wstring_convert<std::codecvt_utf8<wchar_t> >().to_bytes(uri);
					if (navigateCallback)
						navigateCallback(curi, navigateCallbackArg);
				}
				CoTaskMemFree(uri);
				return hr;
			}

			// Checks whether the specified IID equals the IID of the specified type and
			// if so casts the "this" pointer to T and returns it. Returns nullptr on
			// mismatching IIDs.
			// If ppv is specified then the pointer will also be assigned to *ppv.
			template <typename T>
			T* cast_if_equal_iid(REFIID riid, const cast_info_t<T>& info,
			                     LPVOID* ppv = nullptr) noexcept
			{
				T* ptr = nullptr;
				if (IsEqualIID(riid, info.iid))
				{
					ptr = static_cast<T*>(this);
					ptr->AddRef();
				}
				if (ppv)
				{
					*ppv = ptr;
				}
				return ptr;
			}

			// Set the function that will perform the initiating logic for creating
			// the WebView2 environment.
			void set_attempt_handler(std::function<HRESULT()> attempt_handler) noexcept
			{
				m_attempt_handler = attempt_handler;
			}

			// Retry creating a WebView2 environment.
			// The initiating logic for creating the environment is defined by the
			// caller of set_attempt_handler().
			void try_create_environment() noexcept
			{
				// WebView creation fails with HRESULT_FROM_WIN32(ERROR_INVALID_STATE) if
				// a running instance using the same user data folder exists, and the
				// Environment objects have different EnvironmentOptions.
				// Source:
				// https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2environment?view=webview2-1.0.1150.38
				if (m_attempts < m_max_attempts)
				{
					++m_attempts;
					auto res = m_attempt_handler();
					if (SUCCEEDED(res))
					{
						return;
					}
					// Not entirely sure if this error code only applies to
					// CreateCoreWebView2Controller so we check here as well.
					if (res == HRESULT_FROM_WIN32(ERROR_INVALID_STATE))
					{
						return;
					}
					try_create_environment();
					return;
				}
				// Give up.
				m_cb(nullptr, nullptr);
			}

			void STDMETHODCALLTYPE add_navigate_listener(
			    std::function<void(const std::string&, void*)> callback, void* arg)
			{
				navigateCallback = std::move(callback);
				navigateCallbackArg = arg;
			}

		  private:
			HWND m_window;
			msg_cb_t m_msgCb;
			webview2_com_handler_cb_t m_cb;
			std::atomic<ULONG> m_ref_count{ 1 };
			std::function<HRESULT()> m_attempt_handler;
			unsigned int m_max_attempts = 5;
			unsigned int m_attempts = 0;
			void* navigateCallbackArg = nullptr;
			std::function<void(const std::string&, void*)> navigateCallback = 0;
		};

		class win32_edge_engine
		{
		  public:
			win32_edge_engine(bool debug, void* window)
			{
				if (!is_webview2_available())
				{
					return;
				}
				if (!m_com_init.is_initialized())
				{
					return;
				}
				enable_dpi_awareness();
				if (window == nullptr)
				{
					HINSTANCE hInstance = GetModuleHandle(nullptr);
					HICON icon = (HICON)LoadImage(hInstance, IDI_APPLICATION, IMAGE_ICON,
					                              GetSystemMetrics(SM_CXICON),
					                              GetSystemMetrics(SM_CYICON), LR_DEFAULTCOLOR);

					WNDCLASSEXW wc;
					ZeroMemory(&wc, sizeof(WNDCLASSEX));
					wc.cbSize = sizeof(WNDCLASSEX);
					wc.hInstance = hInstance;
					wc.lpszClassName = L"webview";
					wc.hIcon = icon;
					wc.lpfnWndProc =
					    (WNDPROC)(+[](HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) -> LRESULT {
						    auto w = (win32_edge_engine*)GetWindowLongPtr(hwnd, GWLP_USERDATA);
						    switch (msg)
						    {
							    case WM_SIZE:
								    w->resize(hwnd);
								    break;
							    case WM_CLOSE:
								    DestroyWindow(hwnd);
								    break;
							    case WM_DESTROY:
								    w->terminate();
								    break;
							    case WM_GETMINMAXINFO:
							    {
								    auto lpmmi = (LPMINMAXINFO)lp;
								    if (w == nullptr)
								    {
									    return 0;
								    }
								    if (w->m_maxsz.x > 0 && w->m_maxsz.y > 0)
								    {
									    lpmmi->ptMaxSize = w->m_maxsz;
									    lpmmi->ptMaxTrackSize = w->m_maxsz;
								    }
								    if (w->m_minsz.x > 0 && w->m_minsz.y > 0)
								    {
									    lpmmi->ptMinTrackSize = w->m_minsz;
								    }
							    }
							    break;
							    default:
								    return DefWindowProcW(hwnd, msg, wp, lp);
						    }
						    return 0;
					    });
					RegisterClassExW(&wc);
					m_window = CreateWindowW(L"webview", L"", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,
					                         CW_USEDEFAULT, 640, 480, nullptr, nullptr, hInstance,
					                         nullptr);
					if (m_window == nullptr)
					{
						return;
					}
					SetWindowLongPtr(m_window, GWLP_USERDATA, (LONG_PTR)this);
				}
				else
				{
					m_window = *(static_cast<HWND*>(window));
				}

				ShowWindow(m_window, SW_SHOW);
				UpdateWindow(m_window);
				SetFocus(m_window);

				auto cb = std::bind(&win32_edge_engine::on_message, this, std::placeholders::_1);

				embed(m_window, debug, cb);
				resize(m_window);
				m_controller->MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC);
			}

			virtual ~win32_edge_engine()
			{
				if (m_com_handler)
				{
					m_com_handler->Release();
					m_com_handler = nullptr;
				}
				if (m_webview)
				{
					m_webview->Release();
					m_webview = nullptr;
				}
				if (m_controller)
				{
					m_controller->Release();
					m_controller = nullptr;
				}
			}

			win32_edge_engine(const win32_edge_engine& other) = delete;
			win32_edge_engine& operator=(const win32_edge_engine& other) = delete;
			win32_edge_engine(win32_edge_engine&& other) = delete;
			win32_edge_engine& operator=(win32_edge_engine&& other) = delete;

			void run()
			{
				MSG msg;
				BOOL res;
				while ((res = GetMessage(&msg, nullptr, 0, 0)) != -1)
				{
					if (msg.hwnd)
					{
						TranslateMessage(&msg);
						DispatchMessage(&msg);
						continue;
					}
					if (msg.message == WM_APP)
					{
						auto f = (dispatch_fn_t*)(msg.lParam);
						(*f)();
						delete f;
					}
					else if (msg.message == WM_QUIT)
					{
						return;
					}
				}
			}
			void* window()
			{
				return (void*)m_window;
			}
			void terminate()
			{
				PostQuitMessage(0);
			}
			void dispatch(dispatch_fn_t f)
			{
				PostThreadMessage(m_main_thread, WM_APP, 0, (LPARAM) new dispatch_fn_t(f));
			}

			void set_title(const std::string& title)
			{
				SetWindowTextW(m_window, widen_string(title).c_str());
			}

			void set_size(int width, int height, int hints)
			{
				auto style = GetWindowLong(m_window, GWL_STYLE);
				if (hints == WEBVIEW_HINT_FIXED)
				{
					style &= ~(WS_THICKFRAME | WS_MAXIMIZEBOX);
				}
				else
				{
					style |= (WS_THICKFRAME | WS_MAXIMIZEBOX);
				}
				SetWindowLong(m_window, GWL_STYLE, style);

				if (hints == WEBVIEW_HINT_MAX)
				{
					m_maxsz.x = width;
					m_maxsz.y = height;
				}
				else if (hints == WEBVIEW_HINT_MIN)
				{
					m_minsz.x = width;
					m_minsz.y = height;
				}
				else
				{
					RECT r;
					r.left = r.top = 0;
					r.right = width;
					r.bottom = height;
					AdjustWindowRect(&r, WS_OVERLAPPEDWINDOW, 0);
					SetWindowPos(m_window, nullptr, r.left, r.top, r.right - r.left,
					             r.bottom - r.top,
					             SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOMOVE | SWP_FRAMECHANGED);
					resize(m_window);
				}
			}

			void navigate(const std::string& url)
			{
				auto wurl = widen_string(url);
				m_webview->Navigate(wurl.c_str());
			}

			void init(const std::string& js)
			{
				auto wjs = widen_string(js);
				m_webview->AddScriptToExecuteOnDocumentCreated(wjs.c_str(), nullptr);
			}

			void eval(const std::string& js)
			{
				auto wjs = widen_string(js);
				m_webview->ExecuteScript(wjs.c_str(), nullptr);
			}

			void add_navigate_listener(std::function<void(const std::string&, void*)> callback,
			                           void* arg)
			{
				m_com_handler->add_navigate_listener(callback, arg);
			}

			void set_html(const std::string& html)
			{
				m_webview->NavigateToString(widen_string(html).c_str());
			}

		  private:
			bool embed(HWND wnd, bool debug, msg_cb_t cb)
			{
				std::atomic_flag flag = ATOMIC_FLAG_INIT;
				flag.test_and_set();

				wchar_t currentExePath[MAX_PATH];
				GetModuleFileNameW(nullptr, currentExePath, MAX_PATH);
				wchar_t* currentExeName = PathFindFileNameW(currentExePath);

				wchar_t dataPath[MAX_PATH];
				if (!SUCCEEDED(SHGetFolderPathW(nullptr, CSIDL_APPDATA, nullptr, 0, dataPath)))
				{
					return false;
				}
				wchar_t userDataFolder[MAX_PATH];
				PathCombineW(userDataFolder, dataPath, currentExeName);

				m_com_handler = new webview2_com_handler(
				    wnd, cb, [&](ICoreWebView2Controller* controller, ICoreWebView2* webview) {
					    if (!controller || !webview)
					    {
						    flag.clear();
						    return;
					    }
					    controller->AddRef();
					    webview->AddRef();
					    m_controller = controller;
					    m_webview = webview;
					    flag.clear();
				    });

				m_com_handler->set_attempt_handler([&] {
					return m_webview2_loader.create_environment_with_options(
					    nullptr, userDataFolder, nullptr, m_com_handler);
				});
				m_com_handler->try_create_environment();

				MSG msg = {};
				while (flag.test_and_set() && GetMessage(&msg, nullptr, 0, 0))
				{
					TranslateMessage(&msg);
					DispatchMessage(&msg);
				}
				if (!m_controller || !m_webview)
				{
					return false;
				}
				ICoreWebView2Settings* settings = nullptr;
				auto res = m_webview->get_Settings(&settings);
				if (res != S_OK)
				{
					return false;
				}
				res = settings->put_AreDevToolsEnabled(debug ? TRUE : FALSE);
				if (res != S_OK)
				{
					return false;
				}
				init("window.external={invoke:s=>window.chrome.webview.postMessage(s)}");
				return true;
			}

			void resize(HWND wnd)
			{
				if (m_controller == nullptr)
				{
					return;
				}
				RECT bounds;
				GetClientRect(wnd, &bounds);
				m_controller->put_Bounds(bounds);
			}

			bool is_webview2_available() const noexcept
			{
				LPWSTR version_info = nullptr;
				auto res =
				    m_webview2_loader.get_available_browser_version_string(nullptr, &version_info);
				// The result will be equal to HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)
				// if the WebView2 runtime is not installed.
				auto ok = SUCCEEDED(res) && version_info;
				if (version_info)
				{
					CoTaskMemFree(version_info);
				}
				return ok;
			}

			virtual void on_message(const std::string& msg) = 0;

			// The app is expected to call CoInitializeEx before
			// CreateCoreWebView2EnvironmentWithOptions.
			// Source:
			// https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/webview2-idl#createcorewebview2environmentwithoptions
			com_init_wrapper m_com_init{ COINIT_APARTMENTTHREADED };
			HWND m_window = nullptr;
			POINT m_minsz = POINT{ 0, 0 };
			POINT m_maxsz = POINT{ 0, 0 };
			DWORD m_main_thread = GetCurrentThreadId();
			ICoreWebView2* m_webview = nullptr;
			ICoreWebView2Controller* m_controller = nullptr;
			webview2_com_handler* m_com_handler = nullptr;
			mswebview2::loader m_webview2_loader;
		};

	} // namespace detail

	using browser_engine = detail::win32_edge_engine;

} // namespace webview

#endif /* WEBVIEW_GTK, WEBVIEW_COCOA, WEBVIEW_EDGE */

namespace webview
{

	class webview : public browser_engine
	{
	  public:
		webview(bool debug = false, void* wnd = nullptr) : browser_engine(debug, wnd)
		{
		}

		void navigate(const std::string& url)
		{
			if (url.empty())
			{
				browser_engine::navigate("about:blank");
				return;
			}
			browser_engine::navigate(url);
		}

		using binding_t = std::function<void(std::string, std::string, void*)>;
		class binding_ctx_t
		{
		  public:
			binding_ctx_t(binding_t callback, void* arg) : callback(callback), arg(arg)
			{
			}
			// This function is called upon execution of the bound JS function
			binding_t callback;
			// This user-supplied argument is passed to the callback
			void* arg;
		};

		using sync_binding_t = std::function<std::string(std::string)>;

		// Synchronous bind
		void bind(const std::string& name, sync_binding_t fn)
		{
			auto wrapper = [this, fn](const std::string& seq, const std::string& req,
			                          void* /*arg*/) { resolve(seq, 0, fn(req)); };
			bind(name, wrapper, nullptr);
		}

		// Asynchronous bind
		void bind(const std::string& name, binding_t fn, void* arg)
		{
			if (bindings.count(name) > 0)
			{
				return;
			}
			bindings.emplace(name, binding_ctx_t(fn, arg));
			auto js = "(function() { var name = '" + name + "';" + R""(
      var RPC = window._rpc = (window._rpc || {nextSeq: 1});
      window[name] = function() {
        var seq = RPC.nextSeq++;
        var promise = new Promise(function(resolve, reject) {
          RPC[seq] = {
            resolve: resolve,
            reject: reject,
          };
        });
        window.external.invoke(JSON.stringify({
          id: seq,
          method: name,
          params: Array.prototype.slice.call(arguments),
        }));
        return promise;
      }
    })())"";
			init(js);
			eval(js);
		}

		void unbind(const std::string& name)
		{
			auto found = bindings.find(name);
			if (found != bindings.end())
			{
				auto js = "delete window['" + name + "'];";
				init(js);
				eval(js);
				bindings.erase(found);
			}
		}

		void resolve(const std::string& seq, int status, const std::string& result)
		{
			dispatch([seq, status, result, this]() {
				if (status == 0)
				{
					eval("window._rpc[" + seq + "].resolve(" + result + "); delete window._rpc[" +
					     seq + "]");
				}
				else
				{
					eval("window._rpc[" + seq + "].reject(" + result + "); delete window._rpc[" +
					     seq + "]");
				}
			});
		}

	  private:
		void on_message(const std::string& msg) override
		{
			auto seq = detail::json_parse(msg, "id", 0);
			auto name = detail::json_parse(msg, "method", 0);
			auto args = detail::json_parse(msg, "params", 0);
			auto found = bindings.find(name);
			if (found == bindings.end())
			{
				return;
			}
			const auto& context = found->second;
			context.callback(seq, args, context.arg);
		}

		std::map<std::string, binding_ctx_t> bindings;
	};
} // namespace webview

WEBVIEW_API webview_t webview_create(int debug, void* wnd)
{
	auto w = new webview::webview(debug, wnd);
	if (!w->window())
	{
		delete w;
		return nullptr;
	}
	return w;
}

WEBVIEW_API void webview_destroy(webview_t w)
{
	delete static_cast<webview::webview*>(w);
}

WEBVIEW_API void webview_run(webview_t w)
{
	static_cast<webview::webview*>(w)->run();
}

WEBVIEW_API void webview_terminate(webview_t w)
{
	static_cast<webview::webview*>(w)->terminate();
}

WEBVIEW_API void webview_dispatch(webview_t w, void (*fn)(webview_t, void*), void* arg)
{
	static_cast<webview::webview*>(w)->dispatch([=]() { fn(w, arg); });
}

WEBVIEW_API void* webview_get_window(webview_t w)
{
	return static_cast<webview::webview*>(w)->window();
}

WEBVIEW_API void webview_set_title(webview_t w, const char* title)
{
	static_cast<webview::webview*>(w)->set_title(title);
}

WEBVIEW_API void webview_set_size(webview_t w, int width, int height, int hints)
{
	static_cast<webview::webview*>(w)->set_size(width, height, hints);
}

WEBVIEW_API void webview_navigate(webview_t w, const char* url)
{
	static_cast<webview::webview*>(w)->navigate(url);
}

WEBVIEW_API void webview_set_html(webview_t w, const char* html)
{
	static_cast<webview::webview*>(w)->set_html(html);
}

WEBVIEW_API void webview_init(webview_t w, const char* js)
{
	static_cast<webview::webview*>(w)->init(js);
}

WEBVIEW_API void webview_eval(webview_t w, const char* js)
{
	static_cast<webview::webview*>(w)->eval(js);
}

WEBVIEW_API void webview_bind(webview_t w, const char* name,
                              void (*fn)(const char* seq, const char* req, void* arg), void* arg)
{
	static_cast<webview::webview*>(w)->bind(
	    name,
	    [=](const std::string& seq, const std::string& req, void* arg) {
		    fn(seq.c_str(), req.c_str(), arg);
	    },
	    arg);
}

WEBVIEW_API void webview_unbind(webview_t w, const char* name)
{
	static_cast<webview::webview*>(w)->unbind(name);
}

WEBVIEW_API void webview_return(webview_t w, const char* seq, int status, const char* result)
{
	static_cast<webview::webview*>(w)->resolve(seq, status, result);
}

WEBVIEW_API const webview_version_info_t* webview_version()
{
	return &webview::detail::library_version_info;
}

#endif /* WEBVIEW_HEADER */
#endif /* __cplusplus */
#endif /* WEBVIEW_H */
