// license:BSD-3-Clause
// copyright-holders:Aaron Giles, Vas Crabb
//============================================================
//
//  debugview.c - Win32 debug window handling
//
//============================================================

#include "emu.h"
#include "debugviewinfo.h"

#include "debugwininfo.h"
#include "uimetrics.h"
#include "debugger.h"
#include "debug/debugcon.h"
#include "debug/debugcpu.h"

#include "strconv.h"

#include "winutil.h"

#include <mmsystem.h>


// debugger view styles
#define DEBUG_VIEW_STYLE    WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN
#define DEBUG_VIEW_STYLE_EX 0

// combo box styles
#define COMBO_BOX_STYLE     WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST | WS_VSCROLL
#define COMBO_BOX_STYLE_EX  0

// horizontal scroll bar styles
#define HSCROLL_STYLE       WS_CHILD | WS_VISIBLE | SBS_HORZ
#define HSCROLL_STYLE_EX    0

// vertical scroll bar styles
#define VSCROLL_STYLE       WS_CHILD | WS_VISIBLE | SBS_VERT
#define VSCROLL_STYLE_EX    0


bool debugview_info::s_window_class_registered = false;


debugview_info::debugview_info(debugger_windows_interface &debugger, debugwin_info &owner, HWND parent, debug_view_type type) :
	debugbase_info(debugger),
	m_owner(owner),
	m_view(nullptr),
	m_wnd(nullptr),
	m_hscroll(nullptr),
	m_vscroll(nullptr),
	m_contextmenu(nullptr)
{
	register_window_class();

	// create the child view
	m_wnd = CreateWindowEx(DEBUG_VIEW_STYLE_EX, TEXT("MAMEDebugView"), nullptr, DEBUG_VIEW_STYLE,
			0, 0, 100, 100, parent, nullptr, GetModuleHandleUni(), this);
	if (!m_wnd)
		goto cleanup;

	// create the scroll bars
	m_hscroll = CreateWindowEx(HSCROLL_STYLE_EX, TEXT("SCROLLBAR"), nullptr, HSCROLL_STYLE,
			0, 0, 100, CW_USEDEFAULT, m_wnd, nullptr, GetModuleHandleUni(), this);
	m_vscroll = CreateWindowEx(VSCROLL_STYLE_EX, TEXT("SCROLLBAR"), nullptr, VSCROLL_STYLE,
			0, 0, CW_USEDEFAULT, 100, m_wnd, nullptr, GetModuleHandleUni(), this);
	if (!m_hscroll || !m_vscroll)
		goto cleanup;

	// create the debug view
	m_view = machine().debug_view().alloc_view(type, &debugview_info::static_update, this);
	if (!m_view)
		goto cleanup;

	return;

cleanup:
	if (m_hscroll)
		DestroyWindow(m_hscroll);
	m_hscroll = nullptr;
	if (m_vscroll)
		DestroyWindow(m_vscroll);
	m_vscroll = nullptr;
	if (m_wnd)
		DestroyWindow(m_wnd);
	m_wnd = nullptr;
	if (m_view)
		machine().debug_view().free_view(*m_view);
	m_view = nullptr;
}


debugview_info::~debugview_info()
{
	if (m_contextmenu)
		DestroyMenu(m_contextmenu);
	if (m_wnd)
		DestroyWindow(m_wnd);
	if (m_view)
		machine().debug_view().free_view(*m_view);
}


bool debugview_info::is_valid() const
{
	return m_view && m_hscroll && m_vscroll && m_wnd;
}


uint32_t debugview_info::prefwidth() const
{
	return (m_view->total_size().x * metrics().debug_font_width()) + metrics().vscroll_width();
}


uint32_t debugview_info::maxwidth()
{
	uint32_t max = m_view->total_size().x;
	debug_view_source const *const cursource = m_view->source();
	for (auto &source : m_view->source_list())
	{
		m_view->set_source(*source);
		uint32_t const chars = m_view->total_size().x;
		if (max < chars)
			max = chars;
	}
	if (cursource)
		m_view->set_source(*cursource);
	return (max * metrics().debug_font_width()) + metrics().vscroll_width();
}


void debugview_info::get_bounds(RECT &bounds) const
{
	GetWindowRect(m_wnd, &bounds);
}


void debugview_info::set_bounds(RECT const &newbounds)
{
	// account for the edges and set the bounds
	if (m_wnd)
		smart_set_window_bounds(m_wnd, GetParent(m_wnd), newbounds);

	// update
	update();
}


void debugview_info::send_vscroll(int delta)
{
	if (m_vscroll)
	{
		int message_type = SB_LINEUP;
		if (delta < 0)
		{
			message_type = SB_LINEDOWN;
			delta = -delta;
		}
		while (delta > 0)
		{
			SendMessage(m_wnd, WM_VSCROLL, message_type, (LPARAM)m_vscroll);
			delta--;
		}
	}
}


void debugview_info::send_pageup()
{
	if (m_vscroll)
		SendMessage(m_wnd, WM_VSCROLL, SB_PAGELEFT, (LPARAM)m_vscroll);
}


void debugview_info::send_pagedown()
{
	if (m_vscroll)
		SendMessage(m_wnd, WM_VSCROLL, SB_PAGERIGHT, (LPARAM)m_vscroll);
}


char const *debugview_info::source_name() const
{
	if (m_view)
	{
		debug_view_source const *const source = m_view->source();
		if (source)
			return source->name();
	}
	return "";
}


device_t *debugview_info::source_device() const
{
	if (m_view)
	{
		debug_view_source const *const source = m_view->source();
		if (source)
			return source->device();
	}
	return nullptr;
}


bool debugview_info::source_is_visible_cpu() const
{
	if (m_view)
	{
		const debug_view_source *const source = m_view->source();
		return source && (machine().debugger().console().get_visible_cpu() == source->device());
	}
	return false;
}


bool debugview_info::set_source_index(int index)
{
	if (m_view)
	{
		const debug_view_source *const source = m_view->source(index);
		if (source)
		{
			m_view->set_source(*source);
			return true;
		}
	}
	return false;
}


bool debugview_info::set_source_for_device(device_t &device)
{
	if (m_view)
	{
		const debug_view_source *const source = m_view->source_for_device(&device);
		if (source)
		{
			m_view->set_source(*source);
			return true;
		}
	}
	return false;
}


bool debugview_info::set_source_for_visible_cpu()
{
	device_t *const curcpu = machine().debugger().console().get_visible_cpu();
	if (curcpu)
		return set_source_for_device(*curcpu);
	else
		return false;
}


HWND debugview_info::create_source_combobox(HWND parent, LONG_PTR userdata)
{
	// create a combo box
	HWND const result = CreateWindowEx(COMBO_BOX_STYLE_EX, TEXT("COMBOBOX"), nullptr, COMBO_BOX_STYLE,
			0, 0, 100, 1000, parent, nullptr, GetModuleHandleUni(), nullptr);
	SetWindowLongPtr(result, GWLP_USERDATA, userdata);
	SendMessage(result, WM_SETFONT, (WPARAM)metrics().debug_font(), (LPARAM)FALSE);

	// populate the combobox
	debug_view_source const *const cursource = m_view->source();
	int maxlength = 0;
	for (auto &source : m_view->source_list())
	{
		int const length = strlen(source->name());
		if (length > maxlength)
			maxlength = length;
		auto t_name = osd::text::to_tstring(source->name());
		SendMessage(result, CB_ADDSTRING, 0, (LPARAM)t_name.c_str());
	}
	if (cursource)
	{
		SendMessage(result, CB_SETCURSEL, m_view->source_index(*cursource), 0);
		SendMessage(result, CB_SETDROPPEDWIDTH, ((maxlength + 2) * metrics().debug_font_width()) + metrics().vscroll_width(), 0);
		m_view->set_source(*cursource);
	}
	return result;
}


void debugview_info::add_items_to_context_menu(HMENU menu)
{
	AppendMenu(menu, MF_ENABLED, ID_CONTEXT_COPY_VISIBLE, TEXT("Copy Visible"));
	AppendMenu(menu, MF_ENABLED, ID_CONTEXT_PASTE, TEXT("Paste"));
}


void debugview_info::update_context_menu(HMENU menu)
{
	EnableMenuItem(menu, ID_CONTEXT_PASTE, MF_BYCOMMAND | (IsClipboardFormatAvailable(CF_UNICODETEXT) ? MF_ENABLED : MF_GRAYED));
}


void debugview_info::handle_context_menu(unsigned command)
{
	switch (command)
	{
	case ID_CONTEXT_COPY_VISIBLE:
		{
			// get visible text
			debug_view_xy const visarea = m_view->visible_size();
			debug_view_char const *viewdata = m_view->viewdata();
			if (!viewdata)
			{
				PlaySound(TEXT("SystemAsterisk"), nullptr, SND_SYNC);
				break;
			}

			// turn into a plain string, trimming trailing whitespace
			std::wstring text;
			for (uint32_t row = 0; row < visarea.y; row++, viewdata += visarea.x)
			{
				std::wstring::size_type const start = text.length();
				for (uint32_t col = 0; col < visarea.x; ++col)
					text += wchar_t(viewdata[col].byte);
				std::wstring::size_type const nonblank = text.find_last_not_of(L"\t\n\v\r ");
				if (nonblank != std::wstring::npos)
					text.resize((std::max)(start, nonblank + 1));
				text += L"\r\n";
			}

			// copy to the clipboard
			HGLOBAL const clip = GlobalAlloc(GMEM_MOVEABLE, (text.length() + 1) * sizeof(wchar_t));
			if (!clip)
			{
				PlaySound(TEXT("SystemAsterisk"), nullptr, SND_SYNC);
				break;
			}
			if (!OpenClipboard(m_wnd))
			{
				GlobalFree(clip);
				PlaySound(TEXT("SystemAsterisk"), nullptr, SND_SYNC);
				break;
			}
			EmptyClipboard();
			LPWSTR const lock = reinterpret_cast<LPWSTR>(GlobalLock(clip));
			std::copy_n(text.c_str(), text.length() + 1, lock);
			GlobalUnlock(clip);
			if (!SetClipboardData(CF_UNICODETEXT, clip))
			{
				GlobalFree(clip);
				PlaySound(TEXT("SystemAsterisk"), nullptr, SND_SYNC);
			}
			CloseClipboard();
			break;
		}

	case ID_CONTEXT_PASTE:
		if (!IsClipboardFormatAvailable(CF_UNICODETEXT) || !OpenClipboard(m_wnd))
		{
			PlaySound(TEXT("SystemAsterisk"), nullptr, SND_SYNC);
		}
		else
		{
			HGLOBAL const clip = GetClipboardData(CF_UNICODETEXT);
			LPCWSTR lock = clip ? reinterpret_cast<LPCWSTR>(GlobalLock(clip)) : nullptr;
			if (!clip || !lock)
			{
				PlaySound(TEXT("SystemAsterisk"), nullptr, SND_SYNC);
			}
			else
			{
				try
				{
					while (*lock)
					{
						if ((32 <= *lock) && (127 >= *lock))
							m_view->process_char(*lock);
						++lock;
					}
				}
				catch (...)
				{
					GlobalUnlock(clip);
					CloseClipboard();
					throw;
				}
				GlobalUnlock(clip);
			}
			CloseClipboard();
		}
		break;

	default:
		osd_printf_warning("debugview_info: unhandled context menu item %u\n", command);
	}
}


void debugview_info::draw_contents(HDC windc)
{
	debug_view_char const *viewdata = m_view->viewdata();
	debug_view_xy const visarea = m_view->visible_size();

	// get the client rect
	RECT client;
	GetClientRect(m_wnd, &client);
	bool const need_filldown = client.bottom > (metrics().debug_font_height() * visarea.y);

	// create a compatible DC and an offscreen bitmap
	HDC const dc = CreateCompatibleDC(windc);
	if (!dc)
		return;
	HBITMAP const bitmap = CreateCompatibleBitmap(windc, client.right, client.bottom);
	if (!bitmap)
	{
		DeleteDC(dc);
		return;
	}
	HGDIOBJ const oldbitmap = SelectObject(dc, bitmap);
	bool const show_hscroll = m_hscroll && IsWindowVisible(m_hscroll);
	if (show_hscroll)
		client.bottom -= metrics().hscroll_height();

	// set the font
	HGDIOBJ const oldfont = SelectObject(dc, metrics().debug_font());
	COLORREF const oldfgcolor = GetTextColor(dc);
	int const oldbkmode = GetBkMode(dc);
	SetBkMode(dc, TRANSPARENT);

	// iterate over rows and columns
	for (uint32_t row = 0; row < visarea.y; row++)
	{
		bool do_filldown = (row == (visarea.y - 1)) && need_filldown;

		// loop twice; once to fill the background and once to draw the text
		for (int iter = 0; iter < 2; iter++)
		{
			COLORREF fgcolor;
			COLORREF bgcolor = RGB(0xff,0xff,0xff);
			HBRUSH bgbrush = nullptr;
			int last_attrib = -1;
			TCHAR buffer[256];
			int count = 0;
			RECT bounds;

			// initialize the text bounds
			bounds.left = bounds.right = 0;
			bounds.top = row * metrics().debug_font_height();
			bounds.bottom = bounds.top + metrics().debug_font_height();

			// start with a brush on iteration #0
			if (iter == 0)
				bgbrush = CreateSolidBrush(bgcolor);

			// iterate over columns
			for (uint32_t col = 0; col < visarea.x; col++)
			{
				// if the attribute changed, adjust the colors
				if (viewdata[col].attrib != last_attrib)
				{
					COLORREF oldbg = bgcolor;

					// pick new background color
					bgcolor = RGB(0xff,0xff,0xff);
					if (viewdata[col].attrib & DCA_VISITED) bgcolor = RGB(0xc6, 0xe2, 0xff);
					if (viewdata[col].attrib & DCA_ANCILLARY) bgcolor = RGB(0xe0,0xe0,0xe0);
					if (viewdata[col].attrib & DCA_SELECTED) bgcolor = RGB(0xff,0x80,0x80);
					if (viewdata[col].attrib & DCA_CURRENT) bgcolor = RGB(0xff,0xff,0x00);
					if ((viewdata[col].attrib & DCA_SELECTED) && (viewdata[col].attrib & DCA_CURRENT)) bgcolor = RGB(0xff,0xc0,0x80);

					// pick new foreground color
					fgcolor = RGB(0x00,0x00,0x00);
					if (viewdata[col].attrib & DCA_CHANGED) fgcolor = RGB(0xff,0x00,0x00);
					if (viewdata[col].attrib & DCA_INVALID) fgcolor = RGB(0x00,0x00,0xff);
					if (viewdata[col].attrib & DCA_DISABLED) fgcolor = RGB((GetRValue(fgcolor) + GetRValue(bgcolor)) / 2, (GetGValue(fgcolor) + GetGValue(bgcolor)) / 2, (GetBValue(fgcolor) + GetBValue(bgcolor)) / 2);
					if (viewdata[col].attrib & DCA_COMMENT) fgcolor = RGB(0x00,0x80,0x00);

					// flush any pending drawing
					if (count > 0)
					{
						bounds.right = bounds.left + (count * metrics().debug_font_width());
						if (iter == 0)
						{
							FillRect(dc, &bounds, bgbrush);
							if (do_filldown)
							{
								COLORREF const filldown = (last_attrib & DCA_ANCILLARY) ? RGB(0xe0,0xe0,0xe0) : RGB(0xff,0xff,0xff);
								if (oldbg != filldown)
								{
									DeleteObject(bgbrush);
									bgbrush = CreateSolidBrush(filldown);
									oldbg = filldown;
								}
								RECT padding = bounds;
								padding.top = padding.bottom;
								padding.bottom = client.bottom;
								FillRect(dc, &padding, bgbrush);
							}
						}
						else
						{
							ExtTextOut(dc, bounds.left, bounds.top, 0, nullptr, buffer, count, nullptr);
						}
						bounds.left = bounds.right;
						count = 0;
					}

					// set the new colors
					if (iter == 1)
					{
						SetTextColor(dc, fgcolor);
					}
					else if (oldbg != bgcolor)
					{
						DeleteObject(bgbrush);
						bgbrush = CreateSolidBrush(bgcolor);
					}
					last_attrib = viewdata[col].attrib;
				}

				// add this character to the buffer
				buffer[count++] = viewdata[col].byte;
			}

			// flush any remaining stuff
			bounds.right = bounds.left + (count * metrics().debug_font_width());
			if (iter == 0)
			{
				// erase to the end of the line
				bounds.right = client.right;
				FillRect(dc, &bounds, bgbrush);
				if (do_filldown)
				{
					COLORREF const filldown = (last_attrib & DCA_ANCILLARY) ? RGB(0xe0,0xe0,0xe0) : RGB(0xff,0xff,0xff);
					if (bgcolor != filldown)
					{
						DeleteObject(bgbrush);
						bgbrush = CreateSolidBrush(filldown);
					}
					bounds.top = bounds.bottom;
					bounds.bottom = client.bottom;
					FillRect(dc, &bounds, bgbrush);
				}
				DeleteObject(bgbrush);
			}
			else if (count > 0)
			{
				ExtTextOut(dc, bounds.left, bounds.top, 0, nullptr, buffer, count, nullptr);
			}
		}

		// advance viewdata
		viewdata += visarea.x;
	}

	// prevent garbage from showing in the corner
	if (show_hscroll)
	{
		client.top = client.bottom;
		client.bottom = client.top + metrics().hscroll_height();
		FillRect(dc, &client, (HBRUSH)GetStockObject(WHITE_BRUSH));
	}

	// reset the font
	SetBkMode(dc, oldbkmode);
	SetTextColor(dc, oldfgcolor);
	SelectObject(dc, oldfont);

	// blit the final results
	BitBlt(windc, 0, 0, client.right, client.bottom, dc, 0, 0, SRCCOPY);

	// undo the offscreen stuff
	SelectObject(dc, oldbitmap);
	DeleteObject(bitmap);
	DeleteDC(dc);
}


void debugview_info::update()
{
	SCROLLINFO scrollinfo;

	// get the updated total rows/cols and left row/col
	debug_view_xy const totalsize = m_view->total_size();
	debug_view_xy topleft = m_view->visible_position();

	// get the view window bounds
	RECT bounds;
	GetClientRect(m_wnd, &bounds);
	debug_view_xy visiblesize;
	visiblesize.x = (bounds.right - bounds.left) / metrics().debug_font_width();
	visiblesize.y = (bounds.bottom - bounds.top) / metrics().debug_font_height();

	// determine if we need to show the scrollbars
	bool const fit_hscroll = (bounds.bottom - bounds.top) > metrics().hscroll_height();
	bool show_hscroll = fit_hscroll && (totalsize.x > visiblesize.x);
	if (show_hscroll)
	{
		bounds.bottom -= metrics().hscroll_height();
		visiblesize.y = (bounds.bottom - bounds.top) / metrics().debug_font_height();
	}
	bool const fit_vscroll = (bounds.right - bounds.left) > metrics().vscroll_width();
	bool const show_vscroll = fit_vscroll && (totalsize.y > visiblesize.y);
	if (show_vscroll)
	{
		bounds.right -= metrics().vscroll_width();
		visiblesize.x = (bounds.right - bounds.left) / metrics().debug_font_width();
		if (fit_hscroll && !show_hscroll && (totalsize.x > visiblesize.x))
		{
			bounds.bottom -= metrics().hscroll_height();
			visiblesize.y = (bounds.bottom - bounds.top) / metrics().debug_font_height();
			show_hscroll = true;
		}
	}

	// compute the bounds of the scrollbars
	RECT vscroll_bounds;
	GetClientRect(m_wnd, &vscroll_bounds);
	vscroll_bounds.left = vscroll_bounds.right - metrics().vscroll_width();
	if (show_hscroll)
		vscroll_bounds.bottom -= metrics().hscroll_height();
	RECT hscroll_bounds;
	GetClientRect(m_wnd, &hscroll_bounds);
	hscroll_bounds.top = hscroll_bounds.bottom - metrics().hscroll_height();
	if (show_vscroll)
		hscroll_bounds.right -= metrics().vscroll_width();

	// if we hid the scrollbars, make sure we reset the top/left corners
	if (topleft.y + visiblesize.y > totalsize.y)
		topleft.y = std::max(totalsize.y - visiblesize.y, 0);
	if (topleft.x + visiblesize.x > totalsize.x)
		topleft.x = std::max(totalsize.x - visiblesize.x, 0);

	// fill out the scroll info struct for the vertical scrollbar
	if (m_vscroll)
	{
		scrollinfo.cbSize = sizeof(scrollinfo);
		scrollinfo.fMask = SIF_PAGE | SIF_POS | SIF_RANGE;
		scrollinfo.nMin = 0;
		scrollinfo.nMax = totalsize.y - 1;
		scrollinfo.nPage = visiblesize.y;
		scrollinfo.nPos = topleft.y;
		SetScrollInfo(m_vscroll, SB_CTL, &scrollinfo, TRUE);
	}

	// fill out the scroll info struct for the horizontal scrollbar
	if (m_hscroll)
	{
		scrollinfo.cbSize = sizeof(scrollinfo);
		scrollinfo.fMask = SIF_PAGE | SIF_POS | SIF_RANGE;
		scrollinfo.nMin = 0;
		scrollinfo.nMax = totalsize.x - 1;
		scrollinfo.nPage = visiblesize.x;
		scrollinfo.nPos = topleft.x;
		SetScrollInfo(m_hscroll, SB_CTL, &scrollinfo, TRUE);
	}

	// update window info
	if (((bounds.right - bounds.left) > (visiblesize.x * metrics().debug_font_width())) && ((topleft.x + visiblesize.x) < totalsize.x))
		visiblesize.x++;
	if (((bounds.bottom - bounds.top) > (visiblesize.y * metrics().debug_font_height())) && ((topleft.y + visiblesize.y) < totalsize.y))
		visiblesize.y++;
	m_view->set_visible_size(visiblesize);
	m_view->set_visible_position(topleft);

	// invalidate the bounds
	InvalidateRect(m_wnd, nullptr, FALSE);

	// adjust the bounds of the scrollbars and show/hide them
	if (m_vscroll)
	{
		if (show_vscroll)
			smart_set_window_bounds(m_vscroll, m_wnd, vscroll_bounds);
		smart_show_window(m_vscroll, show_vscroll);
	}
	if (m_hscroll)
	{
		if (show_hscroll)
			smart_set_window_bounds(m_hscroll, m_wnd, hscroll_bounds);
		smart_show_window(m_hscroll, show_hscroll);
	}
}


uint32_t debugview_info::process_scroll(WORD type, HWND wnd)
{
	// get the current info
	SCROLLINFO scrollinfo;
	scrollinfo.cbSize = sizeof(scrollinfo);
	scrollinfo.fMask = SIF_PAGE | SIF_POS | SIF_RANGE | SIF_TRACKPOS;
	GetScrollInfo(wnd, SB_CTL, &scrollinfo);

	// by default we stay put
	int32_t result = scrollinfo.nPos;

	// determine the maximum value
	int32_t const maxval = (scrollinfo.nMax > scrollinfo.nPage) ? (scrollinfo.nMax - scrollinfo.nPage + 1) : 0;

	// handle the message
	switch (type)
	{
	case SB_THUMBTRACK:
		result = scrollinfo.nTrackPos;
		break;

	case SB_LEFT:
		result = 0;
		break;

	case SB_RIGHT:
		result = maxval;
		break;

	case SB_LINELEFT:
		result -= 1;
		break;

	case SB_LINERIGHT:
		result += 1;
		break;

	case SB_PAGELEFT:
		result -= scrollinfo.nPage - 1;
		break;

	case SB_PAGERIGHT:
		result += scrollinfo.nPage - 1;
		break;
	}

	// generic rangecheck
	if (result < 0)
		result = 0;
	if (result > maxval)
		result = maxval;

	// set the new position
	scrollinfo.fMask = SIF_POS;
	scrollinfo.nPos = result;
	SetScrollInfo(wnd, SB_CTL, &scrollinfo, TRUE);

	return uint32_t(result);
}


bool debugview_info::process_context_menu(int x, int y)
{
	// don't show a menu if not in client rect
	RECT clientrect;
	GetClientRect(m_wnd, &clientrect);
	POINT loc{ x, y };
	ScreenToClient(m_wnd, &loc);
	if (!PtInRect(&clientrect, loc))
		return false;

	// create the context menu if we haven’t already
	if (!m_contextmenu)
	{
		m_contextmenu = CreatePopupMenu();
		if (!m_contextmenu)
			return false;
		add_items_to_context_menu(m_contextmenu);
	}

	// show the context menu
	update_context_menu(m_contextmenu);
	BOOL const command(TrackPopupMenu(
				m_contextmenu,
				(GetSystemMetrics(SM_MENUDROPALIGNMENT) ? TPM_RIGHTALIGN : TPM_LEFTALIGN) | TPM_LEFTBUTTON | TPM_NONOTIFY | TPM_RETURNCMD,
				x, y,
				0,
				m_wnd,
				nullptr));
	if (command)
		handle_context_menu(command);
	return true;
}


LRESULT debugview_info::view_proc(UINT message, WPARAM wparam, LPARAM lparam)
{
	// handle a few messages
	switch (message)
	{
	// paint: redraw the last bitmap
	case WM_PAINT:
		{
			PAINTSTRUCT pstruct;
			HDC const dc = BeginPaint(m_wnd, &pstruct);
			draw_contents(dc);
			EndPaint(m_wnd, &pstruct);
			break;
		}

	// keydown: handle debugger keys
	case WM_SYSKEYDOWN:
		if (wparam != VK_F10)
			return DefWindowProc(m_wnd, message, wparam, lparam);
		[[fallthrough]];
	case WM_KEYDOWN:
		{
			if (m_owner.handle_key(wparam, lparam))
			{
				m_owner.set_ignore_char_lparam(lparam);
			}
			else
			{
				switch (wparam)
				{
				case VK_UP:
					m_view->process_char(DCH_UP);
					m_owner.set_ignore_char_lparam(lparam);
					break;

				case VK_DOWN:
					m_view->process_char(DCH_DOWN);
					m_owner.set_ignore_char_lparam(lparam);
					break;

				case VK_LEFT:
					if (GetAsyncKeyState(VK_CONTROL) & 0x8000)
						m_view->process_char(DCH_CTRLLEFT);
					else
						m_view->process_char(DCH_LEFT);
					m_owner.set_ignore_char_lparam(lparam);
					break;

				case VK_RIGHT:
					if (GetAsyncKeyState(VK_CONTROL) & 0x8000)
						m_view->process_char(DCH_CTRLRIGHT);
					else
						m_view->process_char(DCH_RIGHT);
					m_owner.set_ignore_char_lparam(lparam);
					break;

				case VK_PRIOR:
					m_view->process_char(DCH_PUP);
					m_owner.set_ignore_char_lparam(lparam);
					break;

				case VK_NEXT:
					m_view->process_char(DCH_PDOWN);
					m_owner.set_ignore_char_lparam(lparam);
					break;

				case VK_HOME:
					if (GetAsyncKeyState(VK_CONTROL) & 0x8000)
						m_view->process_char(DCH_CTRLHOME);
					else
						m_view->process_char(DCH_HOME);
					m_owner.set_ignore_char_lparam(lparam);
					break;

				case VK_END:
					if (GetAsyncKeyState(VK_CONTROL) & 0x8000)
						m_view->process_char(DCH_CTRLEND);
					else
						m_view->process_char(DCH_END);
					m_owner.set_ignore_char_lparam(lparam);
					break;

				case VK_ESCAPE:
					m_owner.set_default_focus();
					m_owner.set_ignore_char_lparam(lparam);
					break;

				case VK_TAB:
					if (GetAsyncKeyState(VK_SHIFT) & 0x8000)
						m_owner.prev_view(this);
					else
						m_owner.next_view(this);
					m_owner.set_ignore_char_lparam(lparam);
					break;
				}
			}
			break;
		}

	// char: ignore chars associated with keys we've handled
	case WM_CHAR:
		if (m_owner.check_ignore_char_lparam(lparam))
		{
			if (waiting_for_debugger() || !seq_pressed())
			{
				if (wparam >= 32 && wparam < 127)
				{
					if (m_view->cursor_supported())
						m_view->set_cursor_visible(true);
					m_view->process_char(wparam);
				}
				else
				{
					return DefWindowProc(m_wnd, message, wparam, lparam);
				}
			}
		}
		break;

	// gaining focus
	case WM_SETFOCUS:
		if (m_view->cursor_supported())
			m_view->set_cursor_visible(true);
		break;

	// losing focus
	case WM_KILLFOCUS:
		if (m_view->cursor_supported())
			m_view->set_cursor_visible(false);
		break;

	// mouse click
	case WM_LBUTTONDOWN:
	case WM_MBUTTONDOWN:
		{
			debug_view_xy const topleft = m_view->visible_position();
			debug_view_xy const visiblesize = m_view->visible_size();
			debug_view_xy newpos;
			newpos.x = std::max(std::min<int>(topleft.x + GET_X_LPARAM(lparam) / metrics().debug_font_width(), topleft.x + visiblesize.x - 1), 0);
			newpos.y = std::max(std::min<int>(topleft.y + GET_Y_LPARAM(lparam) / metrics().debug_font_height(), topleft.y + visiblesize.y - 1), 0);
			m_view->process_click((message == WM_LBUTTONDOWN) ? DCK_LEFT_CLICK : DCK_MIDDLE_CLICK, newpos);
			SetFocus(m_wnd);
			break;
		}

	// right click
	case WM_RBUTTONDOWN:
		if (m_view->cursor_supported())
		{
			debug_view_xy const topleft = m_view->visible_position();
			debug_view_xy const visiblesize = m_view->visible_size();
			debug_view_xy newpos;
			newpos.x = std::max(std::min<int>(topleft.x + GET_X_LPARAM(lparam) / metrics().debug_font_width(), topleft.x + visiblesize.x - 1), 0);
			newpos.y = std::max(std::min<int>(topleft.y + GET_Y_LPARAM(lparam) / metrics().debug_font_height(), topleft.y + visiblesize.y - 1), 0);
			m_view->set_cursor_position(newpos);
			m_view->set_cursor_visible(true);
		}
		return DefWindowProc(m_wnd, message, wparam, lparam);

	// horizontal scroll
	case WM_HSCROLL:
		{
			debug_view_xy topleft = m_view->visible_position();
			topleft.x = process_scroll(LOWORD(wparam), (HWND)lparam);
			m_view->set_visible_position(topleft);
			machine().debug_view().flush_osd_updates();
			break;
		}

	// vertical scroll
	case WM_VSCROLL:
		{
			debug_view_xy topleft = m_view->visible_position();
			topleft.y = process_scroll(LOWORD(wparam), (HWND)lparam);
			m_view->set_visible_position(topleft);
			machine().debug_view().flush_osd_updates();
			break;
		}

	case WM_CONTEXTMENU:
		if (!process_context_menu(GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam)))
			return DefWindowProc(m_wnd, message, wparam, lparam);
		break;

	// everything else: defaults
	default:
		return DefWindowProc(m_wnd, message, wparam, lparam);
	}

	return 0;
}


void debugview_info::static_update(debug_view &view, void *osdprivate)
{
	auto *const info = (debugview_info *)osdprivate;
	assert(info->m_view == &view);
	info->update();
}


LRESULT CALLBACK debugview_info::static_view_proc(HWND wnd, UINT message, WPARAM wparam, LPARAM lparam)
{
	if (message == WM_CREATE)
	{
		// set the info pointer
		CREATESTRUCT const *const createinfo = (CREATESTRUCT *)lparam;
		SetWindowLongPtr(wnd, GWLP_USERDATA, (LONG_PTR)createinfo->lpCreateParams);
		return 0;
	}

	auto *const info = (debugview_info *)(uintptr_t)GetWindowLongPtr(wnd, GWLP_USERDATA);
	if (info == nullptr)
		return DefWindowProc(wnd, message, wparam, lparam);

	assert((info->m_wnd == wnd) || (info->m_wnd == nullptr));
	return info->view_proc(message, wparam, lparam);
}


void debugview_info::register_window_class()
{
	if (!s_window_class_registered)
	{
		WNDCLASS wc = { 0 };

		// initialize the description of the window class
		wc.lpszClassName    = TEXT("MAMEDebugView");
		wc.hInstance        = GetModuleHandleUni();
		wc.lpfnWndProc      = &debugview_info::static_view_proc;
		wc.hCursor          = LoadCursor(nullptr, IDC_ARROW);
		wc.hIcon            = LoadIcon(wc.hInstance, MAKEINTRESOURCE(2));
		wc.lpszMenuName     = nullptr;
		wc.hbrBackground    = nullptr;
		wc.style            = 0;
		wc.cbClsExtra       = 0;
		wc.cbWndExtra       = 0;

		UnregisterClass(wc.lpszClassName, wc.hInstance);

		// register the class; fail if we can't
		if (!RegisterClass(&wc))
			fatalerror("Unable to register debug view class\n");

		s_window_class_registered = true;
	}
}
