// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "ui/platform/win/ui_window_win.h"

#include "ui/inactive_press.h"
#include "ui/platform/win/ui_window_title_win.h"
#include "base/platform/base_platform_info.h"
#include "styles/palette.h"

#include <QtCore/QAbstractNativeEventFilter>
#include <QtGui/QWindow>
#include <QtWidgets/QStyleFactory>
#include <QtWidgets/QApplication>
#include <qpa/qplatformnativeinterface.h>

#include <dwmapi.h>
#include <uxtheme.h>

Q_DECLARE_METATYPE(QMargins);

namespace Ui {
namespace Platform {
namespace {

bool IsCompositionEnabled() {
	auto result = BOOL(FALSE);
	const auto success = (DwmIsCompositionEnabled(&result) == S_OK);
	return success && result;
}

} // namespace

class WindowHelper::NativeFilter final : public QAbstractNativeEventFilter {
public:
	void registerWindow(HWND handle, not_null<WindowHelper*> helper);
	void unregisterWindow(HWND handle);

	bool nativeEventFilter(
		const QByteArray &eventType,
		void *message,
		long *result) override;

private:
	base::flat_map<HWND, not_null<WindowHelper*>> _windowByHandle;

};

void WindowHelper::NativeFilter::registerWindow(
		HWND handle,
		not_null<WindowHelper*> helper) {
	_windowByHandle.emplace(handle, helper);
}

void WindowHelper::NativeFilter::unregisterWindow(HWND handle) {
	_windowByHandle.remove(handle);
}

bool WindowHelper::NativeFilter::nativeEventFilter(
		const QByteArray &eventType,
		void *message,
		long *result) {
	const auto msg = static_cast<MSG*>(message);
	const auto i = _windowByHandle.find(msg->hwnd);
	return (i != end(_windowByHandle))
		? i->second->handleNativeEvent(
			msg->message,
			msg->wParam,
			msg->lParam,
			static_cast<LRESULT*>(result))
		: false;
}

WindowHelper::WindowHelper(not_null<RpWidget*> window)
: _window(window)
, _handle(GetWindowHandle(_window))
, _title(Ui::CreateChild<TitleWidget>(_window.get()))
, _body(Ui::CreateChild<RpWidget>(_window.get()))
, _shadow(_window, st::windowShadowFg->c) {
	Expects(_handle != nullptr);

	GetNativeFilter()->registerWindow(_handle, this);
	init();
}

WindowHelper::~WindowHelper() {
	GetNativeFilter()->unregisterWindow(_handle);
}

not_null<RpWidget*> WindowHelper::body() {
	return _body;
}

void WindowHelper::setTitle(const QString &title) {
	_title->setText(title);
	_window->setWindowTitle(title);
}

void WindowHelper::setTitleStyle(const style::WindowTitle &st) {
	_title->setStyle(st);
}

void WindowHelper::setMinimumSize(QSize size) {
	_window->setMinimumSize(size.width(), _title->height() + size.height());
}

void WindowHelper::setFixedSize(QSize size) {
	_window->setFixedSize(size.width(), _title->height() + size.height());
	_title->setResizeEnabled(false);
	_shadow.setResizeEnabled(false);
}

void WindowHelper::setGeometry(QRect rect) {
	_window->setGeometry(rect.marginsAdded({ 0, _title->height(), 0, 0 }));
}

void WindowHelper::init() {
	style::PaletteChanged(
	) | rpl::start_with_next([=] {
		_shadow.setColor(st::windowShadowFg->c);
	}, _window->lifetime());

	rpl::combine(
		_window->sizeValue(),
		_title->heightValue()
	) | rpl::start_with_next([=](QSize size, int titleHeight) {
		_body->setGeometry(
			0,
			titleHeight,
			size.width(),
			size.height() - titleHeight);
	}, _body->lifetime());

	updateMargins();

	if (!::Platform::IsWindows8OrGreater()) {
		SetWindowTheme(_handle, L" ", L" ");
		QApplication::setStyle(QStyleFactory::create("Windows"));
	}

	_menu = GetSystemMenu(_handle, FALSE);
	updateSystemMenu();

	const auto handleStateChanged = [=](Qt::WindowState state) {
		updateSystemMenu(state);
		if (fixedSize() && (state & Qt::WindowMaximized)) {
			crl::on_main(_window.get(), [=] {
				_window->setWindowState(
					_window->windowState() & ~Qt::WindowMaximized);
			});
		}
	};
	Ui::Connect(
		_window->windowHandle(),
		&QWindow::windowStateChanged,
		handleStateChanged);
}

bool WindowHelper::handleNativeEvent(
		UINT msg,
		WPARAM wParam,
		LPARAM lParam,
		LRESULT *result) {
	switch (msg) {

	case WM_ACTIVATE: {
		if (LOWORD(wParam) == WA_CLICKACTIVE) {
			Ui::MarkInactivePress(_window, true);
		}
		if (LOWORD(wParam) != WA_INACTIVE) {
			_shadow.update(WindowShadow::Change::Activate);
		} else {
			_shadow.update(WindowShadow::Change::Deactivate);
		}
		_window->update();
	} return false;

	case WM_NCPAINT: {
		if (::Platform::IsWindows8OrGreater()) {
			return false;
		}
		if (result) *result = 0;
	} return true;

	case WM_NCCALCSIZE: {
		WINDOWPLACEMENT wp;
		wp.length = sizeof(WINDOWPLACEMENT);
		if (GetWindowPlacement(_handle, &wp)
			&& (wp.showCmd == SW_SHOWMAXIMIZED)) {
			const auto params = (LPNCCALCSIZE_PARAMS)lParam;
			const auto r = (wParam == TRUE)
				? &params->rgrc[0]
				: (LPRECT)lParam;
			const auto hMonitor = MonitorFromPoint(
				{ (r->left + r->right) / 2, (r->top + r->bottom) / 2 },
				MONITOR_DEFAULTTONEAREST);
			if (hMonitor) {
				MONITORINFO mi;
				mi.cbSize = sizeof(mi);
				if (GetMonitorInfo(hMonitor, &mi)) {
					*r = mi.rcWork;
				}
			}
		}
		if (result) *result = 0;
		return true;
	}

	case WM_NCLBUTTONDBLCLK:
	case WM_NCMBUTTONDBLCLK:
	case WM_NCRBUTTONDBLCLK:
	case WM_NCXBUTTONDBLCLK: {
		if (!fixedSize()) {
			return false;
		}
		if (result) *result = 0;
	} return true;

	case WM_NCACTIVATE: {
		if (IsCompositionEnabled()) {
			const auto res = DefWindowProc(_handle, msg, wParam, -1);
			if (result) *result = res;
		} else {
			// Thanks https://github.com/melak47/BorderlessWindow
			if (result) *result = 1;
		}
	} return true;

	case WM_WINDOWPOSCHANGING:
	case WM_WINDOWPOSCHANGED: {
		WINDOWPLACEMENT wp;
		wp.length = sizeof(WINDOWPLACEMENT);
		if (GetWindowPlacement(_handle, &wp)
			&& (wp.showCmd == SW_SHOWMAXIMIZED
				|| wp.showCmd == SW_SHOWMINIMIZED)) {
			_shadow.update(WindowShadow::Change::Hidden);
		} else {
			_shadow.update(
				WindowShadow::Change::Moved | WindowShadow::Change::Resized,
				(WINDOWPOS*)lParam);
		}
	} return false;

	case WM_SIZE: {
		if (wParam == SIZE_MAXIMIZED
			|| wParam == SIZE_RESTORED
			|| wParam == SIZE_MINIMIZED) {
			if (wParam != SIZE_RESTORED
				|| _window->windowState() != Qt::WindowNoState) {
				Qt::WindowState state = Qt::WindowNoState;
				if (wParam == SIZE_MAXIMIZED) {
					state = Qt::WindowMaximized;
				} else if (wParam == SIZE_MINIMIZED) {
					state = Qt::WindowMinimized;
				}
				emit _window->windowHandle()->windowStateChanged(state);
			}
			updateMargins();
			const auto changes = (wParam == SIZE_MINIMIZED
				|| wParam == SIZE_MAXIMIZED)
				? WindowShadow::Change::Hidden
				: (WindowShadow::Change::Resized
					| WindowShadow::Change::Shown);
			_shadow.update(changes);
		}
	} return false;

	case WM_SHOWWINDOW: {
		const auto style = GetWindowLong(_handle, GWL_STYLE);
		const auto changes = WindowShadow::Change::Resized
			| ((wParam && !(style & (WS_MAXIMIZE | WS_MINIMIZE)))
				? WindowShadow::Change::Shown
				: WindowShadow::Change::Hidden);
		_shadow.update(changes);
	} return false;

	case WM_MOVE: {
		_shadow.update(WindowShadow::Change::Moved);
	} return false;

	case WM_NCHITTEST: {
		if (!result) {
			return false;
		}

		const auto p = MAKEPOINTS(lParam);
		auto r = RECT();
		GetWindowRect(_handle, &r);
		const auto mapped = QPoint(
			p.x - r.left + _marginsDelta.left(),
			p.y - r.top + _marginsDelta.top());
		if (!_window->rect().contains(mapped)) {
			*result = HTTRANSPARENT;
		} else if (!_title->geometry().contains(mapped)) {
			*result = HTCLIENT;
		} else switch (_title->hitTest(_title->pos() + mapped)) {
		case HitTestResult::Client:
		case HitTestResult::SysButton:   *result = HTCLIENT; break;
		case HitTestResult::Caption:     *result = HTCAPTION; break;
		case HitTestResult::Top:         *result = HTTOP; break;
		case HitTestResult::TopRight:    *result = HTTOPRIGHT; break;
		case HitTestResult::Right:       *result = HTRIGHT; break;
		case HitTestResult::BottomRight: *result = HTBOTTOMRIGHT; break;
		case HitTestResult::Bottom:      *result = HTBOTTOM; break;
		case HitTestResult::BottomLeft:  *result = HTBOTTOMLEFT; break;
		case HitTestResult::Left:        *result = HTLEFT; break;
		case HitTestResult::TopLeft:     *result = HTTOPLEFT; break;
		case HitTestResult::None:
		default:                         *result = HTTRANSPARENT; break;
		};
	} return true;

	case WM_NCRBUTTONUP: {
		SendMessage(_handle, WM_SYSCOMMAND, SC_MOUSEMENU, lParam);
	} return true;

	case WM_SYSCOMMAND: {
		if (wParam == SC_MOUSEMENU && !fixedSize()) {
			POINTS p = MAKEPOINTS(lParam);
			updateSystemMenu(_window->windowHandle()->windowState());
			TrackPopupMenu(
				_menu,
				TPM_LEFTALIGN | TPM_TOPALIGN | TPM_LEFTBUTTON,
				p.x,
				p.y,
				0,
				_handle,
				0);
		}
	} return false;

	case WM_COMMAND: {
		if (HIWORD(wParam)) {
			return false;
		}
		const auto command = LOWORD(wParam);
		switch (command) {
		case SC_CLOSE:
			_window->close();
			return true;
		case SC_MINIMIZE:
			_window->setWindowState(
				_window->windowState() | Qt::WindowMinimized);
			return true;
		case SC_MAXIMIZE:
			if (!fixedSize()) {
				_window->setWindowState(Qt::WindowMaximized);
			}
			return true;
		case SC_RESTORE:
			_window->setWindowState(Qt::WindowNoState);
			return true;
		}
	} return true;

	}
	return false;
}

bool WindowHelper::fixedSize() const {
	return _window->minimumSize() == _window->maximumSize();
}

void WindowHelper::updateMargins() {
	if (_updatingMargins) return;

	_updatingMargins = true;
	const auto guard = gsl::finally([&] { _updatingMargins = false; });

	RECT r, a;

	GetClientRect(_handle, &r);
	a = r;

	const auto style = GetWindowLong(_handle, GWL_STYLE);
	const auto styleEx = GetWindowLong(_handle, GWL_EXSTYLE);
	AdjustWindowRectEx(&a, style, false, styleEx);
	auto margins = QMargins(
		a.left - r.left,
		a.top - r.top,
		r.right - a.right,
		r.bottom - a.bottom);
	if (style & WS_MAXIMIZE) {
		RECT w, m;
		GetWindowRect(_handle , &w);
		m = w;

		HMONITOR hMonitor = MonitorFromRect(&w, MONITOR_DEFAULTTONEAREST);
		if (hMonitor) {
			MONITORINFO mi;
			mi.cbSize = sizeof(mi);
			GetMonitorInfo(hMonitor, &mi);
			m = mi.rcWork;
		}

		_marginsDelta = QMargins(
			w.left - m.left,
			w.top - m.top,
			m.right - w.right,
			m.bottom - w.bottom);

		margins.setLeft(margins.left() - _marginsDelta.left());
		margins.setRight(margins.right() - _marginsDelta.right());
		margins.setBottom(margins.bottom() - _marginsDelta.bottom());
		margins.setTop(margins.top() - _marginsDelta.top());
	} else if (!_marginsDelta.isNull()) {
		RECT w;
		GetWindowRect(_handle, &w);
		SetWindowPos(
			_handle,
			0,
			0,
			0,
			w.right - w.left - _marginsDelta.left() - _marginsDelta.right(),
			w.bottom - w.top - _marginsDelta.top() - _marginsDelta.bottom(),
			(SWP_NOMOVE
				| SWP_NOSENDCHANGING
				| SWP_NOZORDER
				| SWP_NOACTIVATE
				| SWP_NOREPOSITION));
		_marginsDelta = QMargins();
	}

	if (const auto native = QGuiApplication::platformNativeInterface()) {
		native->setWindowProperty(
			_window->windowHandle()->handle(),
			"WindowsCustomMargins",
			QVariant::fromValue<QMargins>(margins));
	}
}

void WindowHelper::updateSystemMenu() {
	updateSystemMenu(_window->windowHandle()->windowState());
}

void WindowHelper::updateSystemMenu(Qt::WindowState state) {
	if (!_menu) {
		return;
	}

	const auto menuToDisable = (state == Qt::WindowMaximized)
		? SC_MAXIMIZE
		: (state == Qt::WindowMinimized)
		? SC_MINIMIZE
		: SC_RESTORE;
	const auto itemCount = GetMenuItemCount(_menu);
	for (int i = 0; i < itemCount; ++i) {
		MENUITEMINFO itemInfo = { 0 };
		itemInfo.cbSize = sizeof(itemInfo);
		itemInfo.fMask = MIIM_TYPE | MIIM_STATE | MIIM_ID;
		if (!GetMenuItemInfo(_menu, i, TRUE, &itemInfo)) {
			break;
		}
		if (itemInfo.fType & MFT_SEPARATOR) {
			continue;
		} else if (!itemInfo.wID || itemInfo.wID == SC_CLOSE) {
			continue;
		}
		UINT fOldState = itemInfo.fState;
		UINT fState = itemInfo.fState & ~(MFS_DISABLED | MFS_DEFAULT);
		if (itemInfo.wID == menuToDisable
			|| (itemInfo.wID != SC_MINIMIZE
				&& itemInfo.wID != SC_MAXIMIZE
				&& itemInfo.wID != SC_RESTORE)) {
			fState |= MFS_DISABLED;
		}
		itemInfo.fMask = MIIM_STATE;
		itemInfo.fState = fState;
		if (!SetMenuItemInfo(_menu, i, TRUE, &itemInfo)) {
			break;
		}
	}
}

not_null<WindowHelper::NativeFilter*> WindowHelper::GetNativeFilter() {
	Expects(QCoreApplication::instance() != nullptr);

	static const auto GlobalFilter = [&] {
		const auto application = QCoreApplication::instance();
		const auto filter = Ui::CreateChild<NativeFilter>(application);
		application->installNativeEventFilter(filter);
		return filter;
	}();
	return GlobalFilter;
}

HWND GetWindowHandle(not_null<RpWidget*> widget) {
	widget->window()->createWinId();

	const auto window = widget->window()->windowHandle();
	const auto native = QGuiApplication::platformNativeInterface();
	Assert(window != nullptr);
	Assert(native != nullptr);

	return static_cast<HWND>(native->nativeResourceForWindow(
		QByteArrayLiteral("handle"),
		window));
}

std::unique_ptr<BasicWindowHelper> CreateWindowHelper(
		not_null<RpWidget*> window) {
	return std::make_unique<WindowHelper>(window);
}

} // namespace Platform
} // namespace Ui
