#include <psemek/sdl2/window.hpp>
#include <psemek/sdl2/init.hpp>
#include <psemek/math/box.hpp>
#include <psemek/log/log.hpp>

#if defined(PSEMEK_GRAPHICS_API_OPENGL)
#include <psemek/gfx/gl.hpp>
#include <psemek/gfx/init.hpp>
#elif defined(PSEMEK_GRAPHICS_API_WEBGPU)
#include <psemek/wgpu/instance.hpp>
#include <psemek/wgpu/logging.hpp>
#include <psemek/wgpu/version.hpp>
#ifdef __APPLE__
extern "C" void * metal_layer_from_nswindow(void * nswindow);
#endif
#endif

#include <SDL2/SDL_syswm.h>

namespace psemek::sdl2
{

	window::window(psemek::app::application::options const & options)
		: sdl_init_(init(SDL_INIT_EVENTS | SDL_INIT_VIDEO))
	{
		std::uint32_t flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE;
		if (options.highdpi) flags |= SDL_WINDOW_ALLOW_HIGHDPI;

#if defined(PSEMEK_GRAPHICS_API_OPENGL)
		SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
		SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, gl::sys::major_version());
		SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, gl::sys::minor_version());
		SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
		SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
		SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
		SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);
		SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
		SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);

		if (options.multisampling == 0)
		{
			SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 0);
		}
		else
		{
			SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1);
			SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, options.multisampling);
		}

		flags |= SDL_WINDOW_OPENGL;
#endif

		SDL_DisplayMode display_mode;
		SDL_GetCurrentDisplayMode(0, &display_mode);

		int x = SDL_WINDOWPOS_UNDEFINED;
		int y = SDL_WINDOWPOS_UNDEFINED;

		if (false)
		{
			display_mode.w = 1024;
			display_mode.h = 576;
			flags ^= SDL_WINDOW_RESIZABLE;
			x = 0;
			y = 0;
		}

		window_ = SDL_CreateWindow(options.name.data(), x, y, display_mode.w, display_mode.h, flags);
		if (!window_)
			sdl2::fail("Failed to create window: ");

#if defined(PSEMEK_GRAPHICS_API_OPENGL)
		gl_context_ = SDL_GL_CreateContext(window_);
		if (!gl_context_)
			sdl2::fail("Failed to create OpenGL context: ");

		SDL_GL_MakeCurrent(window_, gl_context_);

		gfx::init();
#elif defined(PSEMEK_GRAPHICS_API_WEBGPU)
		wgpu::setup_logging(log::level::error);

		wgpu_instance_ = wgpu::instance::create({});

		SDL_SysWMinfo wminfo;
		SDL_VERSION(&wminfo.version);
		SDL_GetWindowWMInfo(window_, &wminfo);

		wgpu::surface::descriptor surface_descriptor;
#if defined(_WIN32)
		surface_descriptor.chain = {wgpu::surface::from_windows_hwnd{ .hinstance = wminfo.info.win.hinstance, .hwnd = wminfo.info.win.window }};
#elif defined(__linux__)
		surface_descriptor.chain = {wgpu::surface::from_xlib_window{ .display = wminfo.info.x11.display, .window = wminfo.info.x11.window }};
#elif defined(__APPLE__)
		surface_descriptor.chain = {wgpu::surface::from_metal_layer{ .layer = metal_layer_from_nswindow(wminfo.info.cocoa.window) }};
#else
	#error Unknown platform
#endif
		wgpu_surface_ = wgpu_instance_.create_surface(surface_descriptor);

		wgpu::adapter::request_options adapter_request_options;
		// adapter_request_options.feature_level = wgpu::feature_level::compatibility;
		adapter_request_options.compatible_surface = wgpu_surface_;
		adapter_request_options.backend_type = wgpu::backend_type::undefined;

		wgpu_instance_.request_adapter(wgpu::callback_mode::allow_process_events, adapter_request_options, [this](wgpu::adapter::request_status status, wgpu::adapter adapter_in, std::string_view message)
		{
			if (status != wgpu::adapter::request_status::success)
				throw std::runtime_error("Failed to request WebGPU adapter: " + std::string(message));

			wgpu_adapter_ = std::move(adapter_in);
		});

		[[maybe_unused]] auto adapter_limits = wgpu_adapter_.get_limits();

		wgpu::device::descriptor device_descriptor
		{
			.required_features = options.required_features,
			.required_limits = std::nullopt,
			.lost_callback = [](wgpu::device::lost_reason reason, std::string_view message)
			{
				std::ostringstream os;
				os << "WebGPU device lost, reason: " << static_cast<std::uint32_t>(reason) << ", message: " << message;
				auto str = os.str();
				log::error() << str;
				// This will most probably panic in wgpu-native internals
				throw util::exception(std::move(str));
			},
			.lost_callback_mode = wgpu::callback_mode::allow_process_events,
			.uncaptured_error_callback = [](wgpu::error_type type, std::string_view message)
			{
				std::ostringstream os;
				os << "Uncaptured WebGPU error, type: " << static_cast<std::uint32_t>(type) << ", message: " << message;
				auto str = os.str();
				log::error() << str;
				// This will most probably panic in wgpu-native internals
				throw util::exception(std::move(str));
			},
		};

		if (options.required_limits || options.required_native_limits)
			device_descriptor.required_limits = wgpu::device::required_limits{};

		if (options.required_limits)
			device_descriptor.required_limits->limits = *options.required_limits;

		if (options.required_native_limits)
			device_descriptor.required_limits->chain.push_back(wgpu::native_limits{*options.required_native_limits});

		wgpu_adapter_.request_device(wgpu::callback_mode::allow_process_events, device_descriptor, [this](wgpu::device::request_status status, wgpu::device device_in, std::string_view message)
		{
			if (status != wgpu::device::request_status::success)
				throw std::runtime_error("Failed to request WebGPU device: " + std::string(message));
			wgpu_device_ = std::move(device_in);
		});

		[[maybe_unused]] auto device_limits = wgpu_device_.get_limits();

		auto adapter_info = wgpu_adapter_.get_info();
		std::string adapter_backend_str;
		switch (adapter_info.backend_type)
		{
		case wgpu::backend_type::undefined:
			adapter_backend_str = "undefined";
			break;
		case wgpu::backend_type::null:
			adapter_backend_str = "(null)";
			break;
		case wgpu::backend_type::webgpu:
			adapter_backend_str = "WebGPU";
			break;
		case wgpu::backend_type::d3d11:
			adapter_backend_str = "D3D11";
			break;
		case wgpu::backend_type::d3d12:
			adapter_backend_str = "D3D12";
			break;
		case wgpu::backend_type::metal:
			adapter_backend_str = "Metal";
			break;
		case wgpu::backend_type::vulkan:
			adapter_backend_str = "Vulkan";
			break;
		case wgpu::backend_type::opengl:
			adapter_backend_str = "OpenGL";
			break;
		case wgpu::backend_type::opengles:
			adapter_backend_str = "OpenGL ES";
			break;
		}

		log::info() << "Initialized WebGPU: " << adapter_info.device << ", " << adapter_backend_str << " backend";
		log::info() << "Using wgpu-native version " << wgpu::get_version();
#endif
	}

	math::vector<int, 2> window::size() const
	{
		math::vector<int, 2> result;
		SDL_GL_GetDrawableSize(window_, &result[0], &result[1]);
		return result;
	}

	void window::show()
	{
		SDL_ShowWindow(window_);
	}

	void window::swap()
	{
#if defined(PSEMEK_GRAPHICS_API_OPENGL)
		SDL_GL_SwapWindow(window_);
#endif
	}

	void window::show_cursor(bool show)
	{
		SDL_ShowCursor(show ? SDL_TRUE : SDL_FALSE);
	}

	void window::relative_mouse_mode(bool mode)
	{
		log::info() << "Relative mouse mode " << (mode ? "on" : "off");
		SDL_SetRelativeMouseMode(mode ? SDL_TRUE: SDL_FALSE);
	}

	void window::vsync([[maybe_unused]] bool on)
	{
#if defined(PSEMEK_GRAPHICS_API_OPENGL)
		log::info() << "Turning VSync " << (on ? "on" : "off");
		if (on)
		{
			// try adaptive vsync
			if (SDL_GL_SetSwapInterval(-1) != 0)
			{
				// failed, try usual vsync then
				SDL_GL_SetSwapInterval(1);
			}
		}
		else
		{
			SDL_GL_SetSwapInterval(0);
		}
#endif
	}

	void window::windowed(bool on)
	{
		log::info() << "Entering " << (on ? "windowed" : "fullscreen") << " mode";

		SDL_SetWindowFullscreen(window_, on ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP);
		SDL_SetWindowBordered(window_, on ? SDL_TRUE : SDL_FALSE);
		SDL_RestoreWindow(window_);
		if (on)
		{
			SDL_Rect display_usable_bounds;
			SDL_GetDisplayUsableBounds(0, &display_usable_bounds);

			int top_border, left_border, bottom_border, right_border;
			SDL_GetWindowBordersSize(window_, &top_border, &left_border, &bottom_border, &right_border);

			math::box<int, 2> bounds_rect;
			bounds_rect[0] = {display_usable_bounds.x, display_usable_bounds.x + display_usable_bounds.w};
			bounds_rect[1] = {display_usable_bounds.y, display_usable_bounds.y + display_usable_bounds.h};

			log::info() << "Display usable bounds: " << bounds_rect;
			log::info() << "Window borders: left " << left_border << ", right " << right_border << ", top " << top_border << ", bottom " << bottom_border;

			math::box<int, 2> window_rect = bounds_rect;
			window_rect[0].min += left_border;
			window_rect[0].max -= right_border;
			window_rect[1].min += top_border;
			window_rect[1].max -= bottom_border;

			log::info() << "Computed window rect: " << window_rect;

			SDL_SetWindowSize(window_, window_rect.dimensions()[0], window_rect.dimensions()[1]);
			SDL_SetWindowPosition(window_, window_rect[0].min, window_rect[1].min);
		}
	}

	window::~window()
	{
#if defined(PSEMEK_SDL2_OPENGL)
		if (gl_context_)
			SDL_GL_DeleteContext(gl_context_);
#endif
		if (window_)
			SDL_DestroyWindow(window_);
	}

}
