/**
 * FreeRDP: A Remote Desktop Protocol Implementation
 * Print Virtual Channel - CUPS driver
 *
 * Copyright 2010-2011 Vic Lee
 * Copyright 2015 Thincast Technologies GmbH
 * Copyright 2015 DI (FH) Martin Haimberger <martin.haimberger@thincast.com>
 * Copyright 2016 Armin Novak <armin.novak@gmail.com>
 *
 * 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 <winpr/assert.h>

#include <freerdp/config.h>
#include <freerdp/utils/helpers.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

#include <time.h>
#include <cups/cups.h>

#include <winpr/crt.h>
#include <winpr/file.h>
#include <winpr/string.h>

#include <freerdp/channels/rdpdr.h>

#include <freerdp/client/printer.h>

#include <freerdp/channels/log.h>
#define TAG CHANNELS_TAG("printer.client.cups")

#if defined(__APPLE__)
#include <errno.h>
#include <sys/sysctl.h>

static bool is_mac_os_sonoma_or_later(void)
{
	char str[256] = WINPR_C_ARRAY_INIT;
	size_t size = sizeof(str);

	errno = 0;
	int ret = sysctlbyname("kern.osrelease", str, &size, nullptr, 0);
	if (ret != 0)
	{
		char buffer[256] = WINPR_C_ARRAY_INIT;
		WLog_WARN(TAG, "sysctlbyname('kern.osrelease') failed with %s [%d]",
		          winpr_strerror(errno, buffer, sizeof(buffer)), errno);
		return false;
	}

	int major = 0;
	int minor = 0;
	int patch = 0;

	// NOLINTNEXTLINE(cert-err34-c)
	const int rc = sscanf(str, "%d.%d.%d", &major, &minor, &patch);
	if (rc != 3)
	{
		WLog_WARN(TAG, "could not match '%s' to format '%d.%d.%d'", str, major, minor, patch);
		return false;
	}

	if (major < 23)
		return false;
	return true;
}
#endif

typedef struct
{
	rdpPrinterDriver driver;

	size_t id_sequence;
	size_t references;
} rdpCupsPrinterDriver;

typedef struct
{
	rdpPrintJob printjob;

	http_t* printjob_object;
	int printjob_id;
} rdpCupsPrintJob;

typedef struct
{
	rdpPrinter printer;

	rdpCupsPrintJob* printjob;
} rdpCupsPrinter;

WINPR_ATTR_MALLOC(free, 1)
static char* printer_cups_get_printjob_name(size_t id)
{
	struct tm tres = WINPR_C_ARRAY_INIT;
	const time_t tt = time(nullptr);
	const struct tm* t = localtime_r(&tt, &tres);

	char* str = nullptr;
	size_t len = 0;
	const int rc =
	    winpr_asprintf(&str, &len, "%s Print %04d-%02d-%02d %02d-%02d-%02d - Job %" PRIuz,
	                   freerdp_getApplicationDetailsString(), t->tm_year + 1900, t->tm_mon + 1,
	                   t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, id);
	if (rc <= 0)
	{
		free(str);
		return nullptr;
	}

	return str;
}

static bool http_status_ok(http_status_t status)
{
	switch (status)
	{
		case HTTP_OK:
		case HTTP_CONTINUE:
			return true;
		default:
			return false;
	}
}

static UINT write_printjob(rdpPrintJob* printjob, const BYTE* data, size_t size)
{
	rdpCupsPrintJob* cups_printjob = (rdpCupsPrintJob*)printjob;

	WINPR_ASSERT(cups_printjob);

	http_status_t rc =
	    cupsWriteRequestData(cups_printjob->printjob_object, (const char*)data, size);
	if (!http_status_ok(rc))
		WLog_WARN(TAG, "cupsWriteRequestData returned %s", httpStatus(rc));

	return CHANNEL_RC_OK;
}
/**
 * Function description
 *
 * @return 0 on success, otherwise a Win32 error code
 */
static UINT printer_cups_write_printjob(rdpPrintJob* printjob, const BYTE* data, size_t size)
{
	return write_printjob(printjob, data, size);
}

static void printer_cups_close_printjob(rdpPrintJob* printjob)
{
	rdpCupsPrintJob* cups_printjob = (rdpCupsPrintJob*)printjob;
	rdpCupsPrinter* cups_printer = nullptr;

	WINPR_ASSERT(cups_printjob);

	ipp_status_t rc = cupsFinishDocument(cups_printjob->printjob_object, printjob->printer->name);
	if (rc != IPP_OK)
		WLog_WARN(TAG, "cupsFinishDocument returned %s", ippErrorString(rc));

	cups_printjob->printjob_id = 0;
	httpClose(cups_printjob->printjob_object);

	cups_printer = (rdpCupsPrinter*)printjob->printer;
	WINPR_ASSERT(cups_printer);

	cups_printer->printjob = nullptr;
	free(cups_printjob);
}

static rdpPrintJob* printer_cups_create_printjob(rdpPrinter* printer, UINT32 id)
{
	rdpCupsPrinter* cups_printer = (rdpCupsPrinter*)printer;
	rdpCupsPrintJob* cups_printjob = nullptr;

	WINPR_ASSERT(cups_printer);

	if (cups_printer->printjob != nullptr)
	{
		WLog_WARN(TAG, "printjob [printer '%s'] already existing, abort!", printer->name);
		return nullptr;
	}

	cups_printjob = (rdpCupsPrintJob*)calloc(1, sizeof(rdpCupsPrintJob));
	if (!cups_printjob)
		return nullptr;

	cups_printjob->printjob.id = id;
	cups_printjob->printjob.printer = printer;

	cups_printjob->printjob.Write = printer_cups_write_printjob;
	cups_printjob->printjob.Close = printer_cups_close_printjob;

	{
		cups_printjob->printjob_object = httpConnect2(cupsServer(), ippPort(), nullptr, AF_UNSPEC,
		                                              HTTP_ENCRYPT_IF_REQUESTED, 1, 10000, nullptr);

		if (!cups_printjob->printjob_object)
		{
			WLog_WARN(TAG, "httpConnect2 failed for '%s:%d", cupsServer(), ippPort());
			free(cups_printjob);
			return nullptr;
		}

		char* jobTitle = printer_cups_get_printjob_name(cups_printjob->printjob.id);
		if (!jobTitle)
		{
			httpClose(cups_printjob->printjob_object);
			free(cups_printjob);
			return nullptr;
		}

		cups_printjob->printjob_id =
		    cupsCreateJob(cups_printjob->printjob_object, printer->name, jobTitle, 0, nullptr);

		if (!cups_printjob->printjob_id)
		{
			WLog_WARN(TAG, "cupsCreateJob failed for printer '%s', driver '%s'", printer->name,
			          printer->driver);
			httpClose(cups_printjob->printjob_object);
			free(cups_printjob);
			free(jobTitle);
			return nullptr;
		}

		http_status_t rc =
		    cupsStartDocument(cups_printjob->printjob_object, printer->name,
		                      cups_printjob->printjob_id, jobTitle, CUPS_FORMAT_AUTO, 1);
		free(jobTitle);
		if (!http_status_ok(rc))
			WLog_WARN(TAG, "cupsStartDocument [printer '%s', driver '%s'] returned %s",
			          printer->name, printer->driver, httpStatus(rc));
	}

	cups_printer->printjob = cups_printjob;

	return &cups_printjob->printjob;
}

static rdpPrintJob* printer_cups_find_printjob(rdpPrinter* printer, UINT32 id)
{
	rdpCupsPrinter* cups_printer = (rdpCupsPrinter*)printer;

	WINPR_ASSERT(cups_printer);

	if (cups_printer->printjob == nullptr)
		return nullptr;
	if (cups_printer->printjob->printjob.id != id)
		return nullptr;

	return &cups_printer->printjob->printjob;
}

static void printer_cups_free_printer(rdpPrinter* printer)
{
	rdpCupsPrinter* cups_printer = (rdpCupsPrinter*)printer;

	WINPR_ASSERT(cups_printer);

	if (cups_printer->printjob)
	{
		WINPR_ASSERT(cups_printer->printjob->printjob.Close);
		cups_printer->printjob->printjob.Close(&cups_printer->printjob->printjob);
	}

	if (printer->backend)
	{
		WINPR_ASSERT(printer->backend->ReleaseRef);
		printer->backend->ReleaseRef(printer->backend);
	}
	free(printer->name);
	free(printer->driver);
	free(printer);
}

static void printer_cups_add_ref_printer(rdpPrinter* printer)
{
	if (printer)
		printer->references++;
}

static void printer_cups_release_ref_printer(rdpPrinter* printer)
{
	if (!printer)
		return;
	if (printer->references <= 1)
		printer_cups_free_printer(printer);
	else
		printer->references--;
}

static rdpPrinter* printer_cups_new_printer(rdpCupsPrinterDriver* cups_driver, const char* name,
                                            const char* driverName, BOOL is_default)
{
	rdpCupsPrinter* cups_printer = nullptr;

	cups_printer = (rdpCupsPrinter*)calloc(1, sizeof(rdpCupsPrinter));
	if (!cups_printer)
		return nullptr;

	cups_printer->printer.backend = &cups_driver->driver;

	cups_printer->printer.id = cups_driver->id_sequence++;
	cups_printer->printer.name = _strdup(name);
	if (!cups_printer->printer.name)
		goto fail;

	if (driverName)
		cups_printer->printer.driver = _strdup(driverName);
	else
	{
		const char* dname = "MS Publisher Imagesetter";
#if defined(__APPLE__)
		if (is_mac_os_sonoma_or_later())
			dname = "Microsoft Print to PDF";
#endif
		cups_printer->printer.driver = _strdup(dname);
	}
	if (!cups_printer->printer.driver)
		goto fail;

	cups_printer->printer.is_default = is_default;

	cups_printer->printer.CreatePrintJob = printer_cups_create_printjob;
	cups_printer->printer.FindPrintJob = printer_cups_find_printjob;
	cups_printer->printer.AddRef = printer_cups_add_ref_printer;
	cups_printer->printer.ReleaseRef = printer_cups_release_ref_printer;

	WINPR_ASSERT(cups_printer->printer.AddRef);
	cups_printer->printer.AddRef(&cups_printer->printer);

	WINPR_ASSERT(cups_printer->printer.backend->AddRef);
	cups_printer->printer.backend->AddRef(cups_printer->printer.backend);

	return &cups_printer->printer;

fail:
	printer_cups_free_printer(&cups_printer->printer);
	return nullptr;
}

static void printer_cups_release_enum_printers(rdpPrinter** printers)
{
	rdpPrinter** cur = printers;

	while ((cur != nullptr) && ((*cur) != nullptr))
	{
		if ((*cur)->ReleaseRef)
			(*cur)->ReleaseRef(*cur);
		cur++;
	}
	free((void*)printers);
}

static rdpPrinter** printer_cups_enum_printers(rdpPrinterDriver* driver)
{
	rdpPrinter** printers = nullptr;
	int num_printers = 0;
	cups_dest_t* dests = nullptr;
	BOOL haveDefault = FALSE;
	const int num_dests = cupsGetDests(&dests);

	WINPR_ASSERT(driver);
	if (num_dests >= 0)
		printers = (rdpPrinter**)calloc((size_t)num_dests + 1, sizeof(rdpPrinter*));
	if (!printers)
		return nullptr;

	for (size_t i = 0; i < (size_t)num_dests; i++)
	{
		const cups_dest_t* dest = &dests[i];
		if (dest->instance == nullptr)
		{
			rdpPrinter* current = printer_cups_new_printer((rdpCupsPrinterDriver*)driver,
			                                               dest->name, nullptr, dest->is_default);
			if (!current)
			{
				printer_cups_release_enum_printers(printers);
				printers = nullptr;
				break;
			}

			if (current->is_default)
				haveDefault = TRUE;

			printers[num_printers++] = current;
		}
	}
	cupsFreeDests(num_dests, dests);

	if (!haveDefault && (num_dests > 0) && printers)
	{
		if (printers[0])
			printers[0]->is_default = TRUE;
	}

	return printers;
}

static rdpPrinter* printer_cups_get_printer(rdpPrinterDriver* driver, const char* name,
                                            const char* driverName, BOOL isDefault)
{
	rdpCupsPrinterDriver* cups_driver = (rdpCupsPrinterDriver*)driver;

	WINPR_ASSERT(cups_driver);
	return printer_cups_new_printer(cups_driver, name, driverName, isDefault);
}

static void printer_cups_add_ref_driver(rdpPrinterDriver* driver)
{
	rdpCupsPrinterDriver* cups_driver = (rdpCupsPrinterDriver*)driver;
	if (cups_driver)
		cups_driver->references++;
}

/* Singleton */
static rdpCupsPrinterDriver* uniq_cups_driver = nullptr;

static void printer_cups_release_ref_driver(rdpPrinterDriver* driver)
{
	rdpCupsPrinterDriver* cups_driver = (rdpCupsPrinterDriver*)driver;

	WINPR_ASSERT(cups_driver);

	if (cups_driver->references <= 1)
	{
		if (uniq_cups_driver == cups_driver)
			uniq_cups_driver = nullptr;
		free(cups_driver);
	}
	else
		cups_driver->references--;
}

FREERDP_ENTRY_POINT(UINT VCAPITYPE cups_freerdp_printer_client_subsystem_entry(void* arg))
{
	rdpPrinterDriver** ppPrinter = (rdpPrinterDriver**)arg;
	if (!ppPrinter)
		return ERROR_INVALID_PARAMETER;

	if (!uniq_cups_driver)
	{
		uniq_cups_driver = (rdpCupsPrinterDriver*)calloc(1, sizeof(rdpCupsPrinterDriver));

		if (!uniq_cups_driver)
			return ERROR_OUTOFMEMORY;

		uniq_cups_driver->driver.EnumPrinters = printer_cups_enum_printers;
		uniq_cups_driver->driver.ReleaseEnumPrinters = printer_cups_release_enum_printers;
		uniq_cups_driver->driver.GetPrinter = printer_cups_get_printer;

		uniq_cups_driver->driver.AddRef = printer_cups_add_ref_driver;
		uniq_cups_driver->driver.ReleaseRef = printer_cups_release_ref_driver;

		uniq_cups_driver->id_sequence = 1;
	}

	WINPR_ASSERT(uniq_cups_driver->driver.AddRef);
	uniq_cups_driver->driver.AddRef(&uniq_cups_driver->driver);

	*ppPrinter = &uniq_cups_driver->driver;
	return CHANNEL_RC_OK;
}
