/* Copyright 2017-2021 m-privacy GmbH
 *
 * This is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this software; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
 * USA.
 */

#include <winsock2.h>
#include <windows.h>

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <poppler.h>
#include <cairo.h>
#include <cairo-win32.h>
#include <stdlib.h>
#include <stdio.h>

#include <assert.h>

#include <rfb/LogWriter.h>
#include <FL/fl_utf8.h>
#include "pdfprinter.h"

static rfb::LogWriter vlog("pdfprinter");

static void log_hresult_and_lasterror(HRESULT hr) {
	DWORD gle = GetLastError();
	vlog.error("PrintDlgEx returned HRESULT 0x%08X, GetLastError() = %lu\n", hr, (unsigned long)gle);

	// Format last error message
	if (gle != 0) {
		LPVOID msg = NULL;
		FormatMessageA(
			FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
			NULL, gle, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&msg, 0, NULL);
		if (msg) {
			vlog.error("GetLastError text: %s\n", (char*)msg);
			LocalFree(msg);
		}
	}

	// If hr is a FACILITY_WIN32 error, try to extract text
	DWORD win32err = HRESULT_CODE(hr);
	if (win32err != 0) {
		LPVOID msg2 = NULL;
		FormatMessageA(
			FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
			NULL, win32err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&msg2, 0, NULL);
		if (msg2) {
			vlog.error("HRESULT mapped text: %s\n", (char*)msg2);
			LocalFree(msg2);
		}
	}
}

PRINTDLGEX* get_print_dlg(int num_pages) {
	HWND ownerWnd = FindWindow("tightgateviewer", NULL);
	if (!ownerWnd) {
		vlog.error("FindWindow(\"tightgateviewer\", NULL) failed to find window. Returning NULL. Printing possibly won't work on Windows 11");
		return NULL;
	}

	PRINTDLGEX* pdx = (PRINTDLGEX*) calloc(1, sizeof(PRINTDLGEX));
	if (!pdx) {
		vlog.error("calloc PRINTDLGEX failed");
		return NULL;
	}

	// Always allocate at least one page range as a workaround for PrintDlgEx requirements
	PRINTPAGERANGE* range = (PRINTPAGERANGE*)GlobalAlloc(GPTR, sizeof(PRINTPAGERANGE));
	if (!range) {
		vlog.error("GlobalAlloc PRINTPAGERANGE failed");
		free(pdx);
		return NULL;
	}

	// Setup minimal valid page range
	range[0].nFromPage = 1;
	range[0].nToPage = num_pages > 0 ? num_pages : 1;

	pdx->lStructSize = sizeof(PRINTDLGEX);
	pdx->hwndOwner = ownerWnd;
	pdx->hDevMode = NULL;
	pdx->hDevNames = NULL;

	pdx->lpPageRanges = range;
	pdx->nPageRanges = 1;
	pdx->nMaxPageRanges = 1;

	//	pdx->Flags = PD_RETURNDC | PD_NOSELECTION | PD_HIDEPRINTTOFILE;
	pdx->Flags = PD_RETURNDC
		| PD_NOSELECTION
		| PD_COLLATE
		| PD_USEDEVMODECOPIESANDCOLLATE
		| PD_DISABLEPRINTTOFILE;
		//		| PD_HIDEPRINTTOFILE;
	pdx->nCopies = 1;
	pdx->nStartPage = START_PAGE_GENERAL;
	pdx->nMinPage = 1;
	pdx->nMaxPage = num_pages > 0 ? num_pages : 1;
	pdx->Flags2 = 0;
	pdx->ExclusionFlags = 0;
	pdx->lpCallback = NULL;

	vlog.debug("Calling PrintDlgEx with lStructSize=%u hwndOwner=%p Flags=0x%08x nMinPage=%u nMaxPage=%u",
		(unsigned)pdx->lStructSize, (void*)pdx->hwndOwner, (unsigned)pdx->Flags,
		(unsigned)pdx->nMinPage, (unsigned)pdx->nMaxPage);

	SetLastError(0);
	HRESULT hr = PrintDlgEx(pdx);

	if (FAILED(hr)) {
		log_hresult_and_lasterror(hr);
		if (pdx->lpPageRanges)
			GlobalFree(pdx->lpPageRanges);
		free(pdx);
		return NULL;
	}

	vlog.debug("PrintDlgEx returned nPageRanges=%u lpPageRanges=%p", pdx->nPageRanges, (void*)pdx->lpPageRanges);

	if (pdx->nPageRanges == 0 || pdx->lpPageRanges == NULL) {
		vlog.debug("No page ranges selected — will print all pages from %u to %u", pdx->nMinPage, pdx->nMaxPage);
	}

	if (pdx->dwResultAction != PD_RESULT_PRINT) {
		vlog.debug("PrintDlgEx dwResultAction = %u (user canceled or other)", (unsigned)pdx->dwResultAction);
		if (pdx->hDC) {
			vlog.debug("PrintDlgEx returned an HDC even though not printing: hDC=%p", (void*)pdx->hDC);
		}
		if (pdx->lpPageRanges)
			GlobalFree(pdx->lpPageRanges);
		free(pdx);
		return NULL;
	}

	vlog.debug("PrintDlgEx succeeded. hDevMode=%p hDevNames=%p hDC=%p nPageRanges=%u lpPageRanges=%p",
		(void*)pdx->hDevMode, (void*)pdx->hDevNames, (void*)pdx->hDC,
		(unsigned)pdx->nPageRanges, (void*)pdx->lpPageRanges);

	for (DWORD i = 0; i < pdx->nPageRanges; ++i) {
		vlog.debug(" pageRange[%u] from=%u to=%u", (unsigned)i,
			(unsigned)pdx->lpPageRanges[i].nFromPage, (unsigned)pdx->lpPageRanges[i].nToPage);
	}

	// Caller must call DeleteDC(pdx->hDC), free pdx, and free hDevMode/hDevNames if present
	return pdx;
}

char* get_uri_from_path(const char* _path, GError* error) {
	vlog.info("get_uri_from_path()\n");

	size_t src_len = strlen(_path);
	char path[src_len * 2];
	fl_utf8froma(path, src_len * 2, _path, src_len);

	char* absolute;
	if (g_path_is_absolute(path)) {
		absolute = g_strdup (path);
	} else {
		gchar* dir = g_get_current_dir ();
		absolute = g_build_path (dir, path, (gchar *) 0);
	}
	char* uri;
	uri = g_filename_to_uri (absolute, NULL, &error);
	return uri;
}

void scale_page(PopplerPage* page, HDC hdc) {
	/* Inspired from poppler/utils/pdftocairo-win32.cc - if there
	   are scale problems, take a look at the original code */
	double source_w, source_h;
	poppler_page_get_size (page, &source_w, &source_h);

	// Transform the hdc scale to points to be consistent with other cairo backends
	int x_dpi = GetDeviceCaps (hdc, LOGPIXELSX);
	int y_dpi = GetDeviceCaps (hdc, LOGPIXELSY);
	int x_off = GetDeviceCaps (hdc, PHYSICALOFFSETX);
	int y_off = GetDeviceCaps (hdc, PHYSICALOFFSETY);

	double printable_w = GetDeviceCaps (hdc, PHYSICALWIDTH)*72.0/x_dpi;
	double printable_h = GetDeviceCaps (hdc, PHYSICALHEIGHT)*72.0/y_dpi;

	const double target_scale = min((printable_w / source_w), (printable_h/source_h));
	XFORM xform;
	xform.eM11 = x_dpi/72.0 * target_scale;
	xform.eM12 = 0;
	xform.eM21 = 0;
	xform.eM22 = y_dpi/72.0 * target_scale;
	xform.eDx = -x_off;
	xform.eDy = -y_off;

	if(!SetGraphicsMode (hdc, GM_ADVANCED)) {
		vlog.error("scale_page(): I could not SetGraphicsMode()\n");
	}
	if (!SetWorldTransform (hdc, &xform)) {
		vlog.error("scale_page(): I could not SetWorldTransform()\n");
	}
}

/**
 * do_print_cairo_raster_gdi
 *
 * Alternative printing function replacing the previous Cairo Win32 printing
 * surface with an explicit raster-based pipeline.
 *
 * The original do_print() function used cairo_win32_printing_surface_create()
 * and showed occasional clipping of content for PDF's that were originally
 * in landscape form.
 *
 * The underlying cause was not fully determined, but it appeared related
 * to how the Windows GDI/EMF printing pipeline defines and enforces the
 * effective drawable region during output.
 *
 * The original do_print() function used cairo_win32_printing_surface_create()
 * and, in some cases, exhibited occasional clipping of content when printing
 * PDFs that were originally in landscape orientation.
 *
 * Pipeline:
 *   Poppler PDF rendering → Cairo image surface → GDI bitmap output
 *
 * Each page is rendered independently, removing dependency on the
 * Win32 printing surface and its clipping behaviour.
 *
 * Trade-offs:
 *   - raster output instead of vector printing
 *   - increased memory usage at higher resolutions
 *   - quality depends on chosen rendering DPI
 */

int do_print_cairo_raster_gdi(PRINTDLGEX* print_dlg, PopplerDocument* document) {
	int num_pages = poppler_document_get_n_pages(document);

	vlog.verbose("%s: image-surface pipeline num_pages=%d", __func__, num_pages);

	HDC hdc = print_dlg->hDC;

	int dpi_x = GetDeviceCaps(hdc, LOGPIXELSX);
	int dpi_y = GetDeviceCaps(hdc, LOGPIXELSY);

	vlog.verbose("%s: Printer DPI: %d x %d", __func__, dpi_x, dpi_y);

	int phys_w = GetDeviceCaps(hdc, HORZRES);
	int phys_h = GetDeviceCaps(hdc, VERTRES);

	vlog.verbose("%s: Device resolution: %d x %d pixels", __func__, phys_w, phys_h);

	double page_w_pt = 595.0;
	double page_h_pt = 842.0;

	// Convert PDF points → device pixels
	double scale_x = dpi_x / 72.0;
	double scale_y = dpi_y / 72.0;

	int img_w = (int)(page_w_pt * scale_x);
	int img_h = (int)(page_h_pt * scale_y);

	vlog.verbose("%s: Render target size: %d x %d pixels", __func__, img_w, img_h);

	for (int i = 0; i < num_pages; ++i) {

		vlog.verbose("%s: ---- Page %d ----", __func__, i + 1);

		PopplerPage* page = poppler_document_get_page(document, i);
		if (!page) {
			vlog.error("%s: Failed to load page %d", __func__, i);
			continue;
		}

		// Create OFFSCREEN Cairo image surface
		cairo_surface_t* img_surface =
			cairo_image_surface_create(CAIRO_FORMAT_ARGB32, img_w, img_h);

		cairo_t* cr = cairo_create(img_surface);

		if (!cr) {
			vlog.error("%s: Failed to create cairo image context", __func__);
			g_object_unref(page);
			continue;
		}

		// White background (important for printing)
		cairo_set_source_rgb(cr, 1, 1, 1);
		cairo_paint(cr);

		// Apply scaling ONLY here (no GDI transforms)
		cairo_scale(cr, scale_x, scale_y);

		vlog.verbose("%s: Applied cairo_scale %.4f x %.4f", __func__, scale_x, scale_y);

		poppler_page_render_for_printing(page, cr);

		cairo_destroy(cr);

		// Convert Cairo surface → raw bitmap
		unsigned char* data = cairo_image_surface_get_data(img_surface);
		int stride = cairo_image_surface_get_stride(img_surface);

		BITMAPINFO bmi = {0};
		bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
		bmi.bmiHeader.biWidth = img_w;
		bmi.bmiHeader.biHeight = -img_h; // top-down bitmap
		bmi.bmiHeader.biPlanes = 1;
		bmi.bmiHeader.biBitCount = 32;
		bmi.bmiHeader.biCompression = BI_RGB;

		vlog.verbose("%s: StartPage %d", __func__, i + 1);

		if (StartPage(hdc) <= 0) {
			vlog.error("%s: StartPage failed", __func__);
			cairo_surface_destroy(img_surface);
			g_object_unref(page);
			break;
		}

		// Draw full-page bitmap
		int result = StretchDIBits(
			hdc,
			0, 0, phys_w, phys_h,
			0, 0, img_w, img_h,
			data,
			&bmi,
			DIB_RGB_COLORS,
			SRCCOPY
		);

		if (result == GDI_ERROR) {
			vlog.error("%s: StretchDIBits failed", __func__);
		}

		EndPage(hdc);

		vlog.verbose("%s: EndPage %d", __func__, i + 1);

		cairo_surface_destroy(img_surface);
		g_object_unref(page);
	}

	return 0;
}

int do_print(PRINTDLGEX* print_dlg, PopplerDocument* document) {
	int num_pages = poppler_document_get_n_pages(document);

	cairo_surface_t* surface = cairo_win32_printing_surface_create(print_dlg->hDC);
	if (cairo_surface_status(surface)) {
		vlog.error("Failed to create printing surface: %s", cairo_status_to_string(cairo_surface_status(surface)));
		return -1;
	}

	cairo_t* cr = cairo_create(surface);
	if (cairo_status(cr)) {
		vlog.error("Failed to create cairo context: %s", cairo_status_to_string(cairo_status(cr)));
		cairo_surface_destroy(surface);
		return -1;
	}

	if (print_dlg->nPageRanges == 0 || print_dlg->lpPageRanges == NULL) {
		// User selected "All" — print everything
		vlog.debug("No page range selected, printing ALL pages");
		for (int page_num = 0; page_num < num_pages; ++page_num) {
			PopplerPage* page = poppler_document_get_page(document, page_num);
			if (!page) {
				vlog.error("Could not open page %d", page_num);
				cairo_destroy(cr);
				cairo_surface_destroy(surface);
				return 1;
			}

			scale_page(page, print_dlg->hDC);
			if (StartPage(print_dlg->hDC) <= 0) {
				vlog.error("StartPage failed for page %d", page_num + 1);
				g_object_unref(page);
				cairo_destroy(cr);
				cairo_surface_destroy(surface);
				return -1;
			}

			cairo_save(cr);
			poppler_page_render_for_printing(page, cr);
			cairo_restore(cr);
			cairo_surface_show_page(surface);
			g_object_unref(page);

			if (EndPage(print_dlg->hDC) <= 0) {
				vlog.error("EndPage failed for page %d", page_num + 1);
				cairo_destroy(cr);
				cairo_surface_destroy(surface);
				return -1;
			}
		}
	} else {
		// User selected specific ranges
		for (DWORD i = 0; i < print_dlg->nPageRanges; ++i) {
			int from = print_dlg->lpPageRanges[i].nFromPage;
			int to   = print_dlg->lpPageRanges[i].nToPage;
			vlog.verbose("Printing range: %d to %d", from, to);

			for (int page_num = from - 1; page_num < to; ++page_num) {
				PopplerPage* page = poppler_document_get_page(document, page_num);
				if (!page) {
					vlog.error("Could not open page %d", page_num);
					cairo_destroy(cr);
					cairo_surface_destroy(surface);
					return 1;
				}

				scale_page(page, print_dlg->hDC);
				if (StartPage(print_dlg->hDC) <= 0) {
					vlog.error("StartPage failed for page %d", page_num + 1);
					g_object_unref(page);
					cairo_destroy(cr);
					cairo_surface_destroy(surface);
					return -1;
				}

				cairo_save(cr);
				poppler_page_render_for_printing(page, cr);
				cairo_restore(cr);
				cairo_surface_show_page(surface);
				g_object_unref(page);

				if (EndPage(print_dlg->hDC) <= 0) {
					vlog.error("EndPage failed for page %d", page_num + 1);
					cairo_destroy(cr);
					cairo_surface_destroy(surface);
					return -1;
				}
			}
		}
	}

	cairo_destroy(cr);
	cairo_surface_finish(surface);
	cairo_surface_destroy(surface);
	return 0;
}

int windows_print_pdf(char* path) {
	GError* error = NULL;

	char* uri = get_uri_from_path(path, error);
	if (!uri) {
		vlog.error("I couldn't convert path to uri: %s\n", error->message);
		return -1;
	}

	PopplerDocument* document = poppler_document_new_from_file (uri, NULL, &error);
	if (!document) {
		vlog.error("Poppler couldn't open document '%s': %s\n", uri, error->message);
		return -1;
	}
	int num_pages = poppler_document_get_n_pages (document);

	PRINTDLGEX* print_dlg = get_print_dlg(num_pages);
	if (!print_dlg) {
		g_object_unref (document);
		return -1;
	}
	vlog.debug("PrintDlgEx returned hDC=%p, dwResultAction=%u", (void*) print_dlg->hDC, (unsigned) print_dlg->dwResultAction);

	DOCINFO doc_info = {0};
	doc_info.cbSize = sizeof(DOCINFO);
	const char* final_slash = strrchr(uri, '\\');
	doc_info.lpszDocName = final_slash ? final_slash + 1 : uri;

	int startdoc_res = StartDoc(print_dlg->hDC, &doc_info);
	vlog.verbose("StartDoc returned: %d", startdoc_res);
	if (startdoc_res <= 0) {
		vlog.error("StartDoc failed, aborting print");
		g_object_unref(document);
		free(print_dlg);
		return -1;
	}

	do_print_cairo_raster_gdi(print_dlg, document);

	int enddoc_res = EndDoc(print_dlg->hDC);
	vlog.verbose("EndDoc returned: %d", enddoc_res);
	if (enddoc_res <= 0) {
		vlog.error("EndDoc failed");
		g_object_unref(document);
		free(print_dlg);
		return -1;
	}

	GdiFlush();

	g_object_unref (document);
	free(print_dlg);

	return 0;
}
