/* Copyright (c) 2019-2025, Arm Limited and Contributors
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * Licensed under the Apache License, Version 2.0 the "License";
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "direct_window.h"

#include <fcntl.h>
#include <sys/stat.h>

#include "core/instance.h"

#include "platform/headless_window.h"
#include "platform/unix/direct_window.h"

namespace vkb
{
namespace
{
static KeyCode key_map[] = {
    KeyCode::Unknown,
    KeyCode::Unknown,
    KeyCode::Unknown,
    KeyCode::Unknown,
    KeyCode::Unknown,
    KeyCode::Unknown,
    KeyCode::Unknown,
    KeyCode::Unknown,
    KeyCode::Backspace,
    KeyCode::Tab,
    KeyCode::Unknown,
    KeyCode::Unknown,
    KeyCode::Unknown,
    KeyCode::Enter,
    KeyCode::Unknown,
    KeyCode::Unknown,
    KeyCode::Unknown,
    KeyCode::Unknown,
    KeyCode::Unknown,
    KeyCode::Unknown,
    KeyCode::Unknown,
    KeyCode::Unknown,
    KeyCode::Unknown,
    KeyCode::Unknown,
    KeyCode::Unknown,
    KeyCode::Unknown,
    KeyCode::Unknown,
    KeyCode::Escape,
    KeyCode::Unknown,
    KeyCode::Unknown,
    KeyCode::Unknown,
    KeyCode::Unknown,
    KeyCode::Space,
    KeyCode::_1,
    KeyCode::Apostrophe,
    KeyCode::Backslash,
    KeyCode::_4,
    KeyCode::_5,
    KeyCode::_7,
    KeyCode::Apostrophe,
    KeyCode::_9,
    KeyCode::_0,
    KeyCode::_8,
    KeyCode::Equal,
    KeyCode::Comma,
    KeyCode::Minus,
    KeyCode::Period,
    KeyCode::Slash,
    KeyCode::_0,
    KeyCode::_1,
    KeyCode::_2,
    KeyCode::_3,
    KeyCode::_4,
    KeyCode::_5,
    KeyCode::_6,
    KeyCode::_7,
    KeyCode::_8,
    KeyCode::_9,
    KeyCode::Semicolon,
    KeyCode::Semicolon,
    KeyCode::Comma,
    KeyCode::Equal,
    KeyCode::Period,
    KeyCode::Slash,
    KeyCode::_2,
    KeyCode::A,
    KeyCode::B,
    KeyCode::C,
    KeyCode::D,
    KeyCode::E,
    KeyCode::F,
    KeyCode::G,
    KeyCode::H,
    KeyCode::I,
    KeyCode::J,
    KeyCode::K,
    KeyCode::L,
    KeyCode::M,
    KeyCode::N,
    KeyCode::O,
    KeyCode::P,
    KeyCode::Q,
    KeyCode::R,
    KeyCode::S,
    KeyCode::T,
    KeyCode::U,
    KeyCode::V,
    KeyCode::W,
    KeyCode::X,
    KeyCode::Y,
    KeyCode::Z,
    KeyCode::LeftBracket,
    KeyCode::Backslash,
    KeyCode::RightBracket,
    KeyCode::_6,
    KeyCode::Minus,
    KeyCode::GraveAccent,
    KeyCode::A,
    KeyCode::B,
    KeyCode::C,
    KeyCode::D,
    KeyCode::E,
    KeyCode::F,
    KeyCode::G,
    KeyCode::H,
    KeyCode::I,
    KeyCode::J,
    KeyCode::K,
    KeyCode::L,
    KeyCode::M,
    KeyCode::N,
    KeyCode::O,
    KeyCode::P,
    KeyCode::Q,
    KeyCode::R,
    KeyCode::S,
    KeyCode::T,
    KeyCode::U,
    KeyCode::V,
    KeyCode::W,
    KeyCode::X,
    KeyCode::Y,
    KeyCode::Z,
    KeyCode::LeftBracket,
    KeyCode::Backslash,
    KeyCode::RightBracket,
    KeyCode::GraveAccent,
    KeyCode::Backspace};

static KeyCode map_multichar_key(int tty_fd, KeyCode initial)
{
	static std::map<std::string, KeyCode> mc_map;        // Static for one-time-init
	if (mc_map.size() == 0)
	{
		mc_map["[A"]   = KeyCode::Up;
		mc_map["[B"]   = KeyCode::Down;
		mc_map["[C"]   = KeyCode::Right;
		mc_map["[D"]   = KeyCode::Left;
		mc_map["[2~"]  = KeyCode::Insert;
		mc_map["[3~"]  = KeyCode::DelKey;
		mc_map["[5~"]  = KeyCode::PageUp;
		mc_map["[6~"]  = KeyCode::PageDown;
		mc_map["[H"]   = KeyCode::Home;
		mc_map["[F"]   = KeyCode::End;
		mc_map["OP"]   = KeyCode::F1;
		mc_map["OQ"]   = KeyCode::F2;
		mc_map["OR"]   = KeyCode::F3;
		mc_map["OS"]   = KeyCode::F4;
		mc_map["[15~"] = KeyCode::F5;
		mc_map["[17~"] = KeyCode::F6;
		mc_map["[18~"] = KeyCode::F7;
		mc_map["[19~"] = KeyCode::F8;
		mc_map["[20~"] = KeyCode::F9;
		mc_map["[21~"] = KeyCode::F10;
		mc_map["[23~"] = KeyCode::F11;
		mc_map["[24~"] = KeyCode::F12;
	}

	char        key;
	std::string buf;

	while (::read(tty_fd, &key, 1) == 1)
	{
		buf += key;
	}

	if (buf.size() == 0)
	{
		return initial;        // Nothing new read, just return the initial char
	}

	// Is it a code we recognise?
	auto iter = mc_map.find(buf);
	if (iter != mc_map.end())
	{
		return iter->second;
	}

	return KeyCode::Unknown;
}

}        // namespace
DirectWindow::DirectWindow(Platform *platform, const Window::Properties &properties) :
    Window(properties),
    platform{platform}
{
	// Setup tty for reading keyboard from console
	if ((tty_fd = open("/dev/tty", O_RDWR | O_NDELAY)) > 0)
	{
		tcgetattr(tty_fd, &termio_prev);
		tcgetattr(tty_fd, &termio);
		cfmakeraw(&termio);
		termio.c_lflag |= ISIG;
		termio.c_oflag |= OPOST | ONLCR;
		termio.c_cc[VMIN]  = 1;
		termio.c_cc[VTIME] = 0;

		if (tcsetattr(tty_fd, TCSANOW, &termio) == -1)
			LOGW("Failed to set attribs for '/dev/tty'");
	}

	platform->set_focus(true);
}

DirectWindow::~DirectWindow()
{
	if (tty_fd > 0)
	{
		tcsetattr(tty_fd, TCSANOW, &termio_prev);
	}
}

VkSurfaceKHR DirectWindow::create_surface(vkb::core::InstanceC &instance)
{
	return create_surface(instance.get_handle(), instance.get_first_gpu().get_handle());
}

// Local structure for holding display candidate information
struct Candidate
{
	VkDisplayKHR                  display;
	VkDisplayPropertiesKHR        display_props;
	VkDisplayModePropertiesKHR    mode;
	VkDisplayPlaneCapabilitiesKHR caps;
	uint32_t                      plane_index;
	uint32_t                      stack_index;
};

// Helper template to get a vector of properties using the Vulkan idiom of
// querying size, then querying data
template <typename T, typename F, typename... Targs>
static std::vector<T> get_props(F func, Targs... args)
{
	std::vector<T> result;
	uint32_t       count;

	VK_CHECK(func(args..., &count, nullptr));
	if (count > 0)
	{
		result.resize(count);
		VK_CHECK(func(args..., &count, result.data()));
	}
	return result;
}

// Find all valid display candidates in the system
static std::vector<Candidate> find_display_candidates(VkPhysicalDevice phys_dev, Window::Mode window_mode)
{
	// Possible display config candidates
	std::vector<Candidate> candidates;

	// Find all the displays connected to this platform
	auto display_properties = get_props<VkDisplayPropertiesKHR>(vkGetPhysicalDeviceDisplayPropertiesKHR, phys_dev);

	// Find all the display planes
	auto plane_properties = get_props<VkDisplayPlanePropertiesKHR>(vkGetPhysicalDeviceDisplayPlanePropertiesKHR, phys_dev);

	// Build a list of candidates
	for (uint32_t plane_index = 0; plane_index < plane_properties.size(); plane_index++)
	{
		const VkDisplayPlanePropertiesKHR &plane_props = plane_properties[plane_index];

		// Find the list of displays compatible with the plane
		auto supported_displays = get_props<VkDisplayKHR>(vkGetDisplayPlaneSupportedDisplaysKHR, phys_dev, plane_index);

		for (const auto &display : supported_displays)
		{
			auto props = std::find_if(display_properties.cbegin(), display_properties.cend(),
			                          [display](const VkDisplayPropertiesKHR &p) { return p.display == display; });
			if (props == display_properties.end())
			{
				continue;
			}

			// Cannot use if already on another display
			if (plane_props.currentDisplay != VK_NULL_HANDLE && plane_props.currentDisplay != display)
			{
				continue;
			}

			// Cannot use if identity transform is unsupported
			if ((props->supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR) == 0)
			{
				continue;
			}

			// Find all the display modes for the display
			auto modes = get_props<VkDisplayModePropertiesKHR>(vkGetDisplayModePropertiesKHR, phys_dev, display);

			for (auto mode : modes)
			{
				// Query capabilities of this mode/plane combination
				VkDisplayPlaneCapabilitiesKHR caps;
				VK_CHECK(vkGetDisplayPlaneCapabilitiesKHR(phys_dev, mode.displayMode, plane_index, &caps));

				// Check for opaque alpha support
				if ((caps.supportedAlpha & VK_DISPLAY_PLANE_ALPHA_OPAQUE_BIT_KHR) == 0)
				{
					continue;
				}

				// Eliminate modes that don't fit the plane capabilities
				if (mode.parameters.visibleRegion.width > caps.maxDstExtent.width ||
				    mode.parameters.visibleRegion.height > caps.maxDstExtent.height ||
				    mode.parameters.visibleRegion.width < caps.minDstExtent.width ||
				    mode.parameters.visibleRegion.height < caps.minDstExtent.height)
				{
					continue;
				}

				if (window_mode == Window::Mode::Fullscreen ||
				    window_mode == Window::Mode::FullscreenBorderless)
				{
					// For full-screen modes (where the src image is the same size as the
					// display) we must also check the src extents are valid.
					if (mode.parameters.visibleRegion.width > caps.maxSrcExtent.width ||
					    mode.parameters.visibleRegion.height > caps.maxSrcExtent.height ||
					    mode.parameters.visibleRegion.width < caps.minSrcExtent.width ||
					    mode.parameters.visibleRegion.height < caps.minSrcExtent.height)
					{
						continue;
					}
				}

				// Add to list of candidates
				candidates.push_back(Candidate{display, *props, mode, caps, plane_index,
				                               plane_properties[plane_index].currentStackIndex});
			}
		}
	}

	return candidates;
}

VkSurfaceKHR DirectWindow::create_surface(VkInstance instance, VkPhysicalDevice phys_dev)
{
	if (instance == VK_NULL_HANDLE || phys_dev == VK_NULL_HANDLE)
	{
		return VK_NULL_HANDLE;
	}

	// Find possible display config candidates
	std::vector<Candidate> candidates = find_display_candidates(phys_dev, properties.mode);
	if (candidates.empty())
	{
		LOGE("Direct-to-display: No compatible display candidates found");
		return VK_NULL_HANDLE;
	}

	// 'candidates' contains valid potential display configs. Find the best one.
	// Look for the candidate with the closest resolution to our requested width & height.
	uint32_t best_candidate = 0;
	uint32_t wanted_area    = properties.extent.width * properties.extent.height;
	uint32_t best_diff      = ~0;

	for (size_t c = 0; c < candidates.size(); c++)
	{
		const Candidate &cand = candidates[c];

		uint32_t area = cand.display_props.physicalResolution.width *
		                cand.display_props.physicalResolution.height;

		uint32_t diff = abs(static_cast<int32_t>(area) - static_cast<int32_t>(wanted_area));

		if (diff < best_diff)
		{
			best_diff      = diff;
			best_candidate = c;
		}
	}

	const Candidate &best = candidates[best_candidate];

	// Get the full display mode extent
	full_extent.width  = best.mode.parameters.visibleRegion.width;
	full_extent.height = best.mode.parameters.visibleRegion.height;

	VkExtent2D image_extent;
	if (properties.mode == Window::Mode::Fullscreen ||
	    properties.mode == Window::Mode::FullscreenBorderless)
	{
		// Fullscreen & Borderless options create a surface that matches the display size
		image_extent.width  = full_extent.width;
		image_extent.height = full_extent.height;
	}
	else
	{
		// Normal and stretch options create a surface at the requested width & height
		// clamped to the plane capabilities in play
		image_extent.width  = std::min(best.caps.maxSrcExtent.width,
		                               std::max(properties.extent.width, best.caps.minSrcExtent.width));
		image_extent.height = std::min(best.caps.maxSrcExtent.height,
		                               std::max(properties.extent.height, best.caps.minSrcExtent.height));
	}

	// Calculate the display DPI
	constexpr float mm_per_inch = 25.4f;

	dpi = mm_per_inch * best.display_props.physicalResolution.width / best.display_props.physicalDimensions.width;

	// Create the surface
	VkDisplaySurfaceCreateInfoKHR surface_create_info{};
	surface_create_info.sType           = VK_STRUCTURE_TYPE_DISPLAY_SURFACE_CREATE_INFO_KHR;
	surface_create_info.displayMode     = best.mode.displayMode;
	surface_create_info.planeIndex      = best.plane_index;
	surface_create_info.planeStackIndex = best.stack_index;
	surface_create_info.transform       = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
	surface_create_info.alphaMode       = VK_DISPLAY_PLANE_ALPHA_OPAQUE_BIT_KHR;
	surface_create_info.imageExtent     = image_extent;

	VkSurfaceKHR surface;
	VK_CHECK(vkCreateDisplayPlaneSurfaceKHR(instance, &surface_create_info, nullptr, &surface));

	return surface;
}

void DirectWindow::process_events()
{
	if (tty_fd > 0)
	{
		if (key_down != KeyCode::Unknown)
		{
			// Signal release for the key we previously reported as down
			// (we don't get separate press & release from the terminal)
			platform->input_event(KeyInputEvent{key_down, KeyAction::Up});
			key_down = KeyCode::Unknown;
		}

		// See if there is a new keypress
		uint8_t key   = 0;
		int     bytes = ::read(tty_fd, &key, 1);

		if (bytes > 0 && key > 0 && (key < sizeof(key_map) / sizeof(KeyCode)))
		{
			// Map the key
			key_down = key_map[key];

			// Is this potentially a multi-character code?
			if (key_down == KeyCode::Escape)
			{
				key_down = map_multichar_key(tty_fd, key_down);
			}

			// Signal the press
			platform->input_event(KeyInputEvent{key_down, KeyAction::Down});
		}
	}
}

bool DirectWindow::should_close()
{
	return !keep_running;
}

void DirectWindow::close()
{
	keep_running = false;
}

float DirectWindow::get_dpi_factor() const
{
	const float win_base_density = 96.0f;
	return dpi / win_base_density;
}

bool DirectWindow::get_display_present_info(VkDisplayPresentInfoKHR *info,
                                            uint32_t src_width, uint32_t src_height) const
{
	// Only stretch mode needs to supply a VkDisplayPresentInfoKHR
	if (properties.mode != Window::Mode::FullscreenStretch || !info)
		return false;

	info->sType      = VK_STRUCTURE_TYPE_DISPLAY_PRESENT_INFO_KHR;
	info->pNext      = nullptr;
	info->srcRect    = {0, 0, src_width, src_height};
	info->dstRect    = {0, 0, full_extent.width, full_extent.height};
	info->persistent = false;

	return true;
}

std::vector<const char *> DirectWindow::get_required_surface_extensions() const
{
	return {VK_KHR_DISPLAY_EXTENSION_NAME};
}

}        // namespace vkb
