/*
 * libptouch — USB (libusb): open/close, bulk transfers
 *
 * Author: knb
 * Email: knb@artif.org
 */

#include "libptouch_internal.h"

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

/**
 * バルク IN/OUT を両方持つインタフェースを探して claim する。
 * USB Printer クラス (bInterfaceClass==7) を優先する。
 */
static int claim_interface_with_bulk(libusb_device_handle *h, int *out_iface,
				     uint8_t *out_ep, uint8_t *in_ep)
{
	libusb_device *dev = libusb_get_device(h);
	struct libusb_config_descriptor *cfg = NULL;
	int r = libusb_get_active_config_descriptor(dev, &cfg);
	if (r != 0 || !cfg)
		return -1;

	for (int pass = 0; pass < 2; pass++) {
		for (int i = 0; i < cfg->bNumInterfaces; i++) {
			const struct libusb_interface_descriptor *alt =
			    &cfg->interface[i].altsetting[0];
			if (pass == 0 &&
			    alt->bInterfaceClass != LIBUSB_CLASS_PRINTER)
				continue;

			uint8_t ep_out = 0, ep_in = 0;
			for (uint8_t k = 0; k < alt->bNumEndpoints; k++) {
				const struct libusb_endpoint_descriptor *ep =
				    &alt->endpoint[k];
				if ((ep->bmAttributes &
				     LIBUSB_TRANSFER_TYPE_MASK) ==
				    LIBUSB_TRANSFER_TYPE_BULK) {
					if (ep->bEndpointAddress &
					    LIBUSB_ENDPOINT_IN)
						ep_in = ep->bEndpointAddress;
					else
						ep_out = ep->bEndpointAddress;
				}
			}
			if (!ep_out || !ep_in)
				continue;
			r = libusb_claim_interface(h, i);
			if (r == 0) {
				*out_iface = i;
				*out_ep = ep_out;
				*in_ep = ep_in;
				libusb_free_config_descriptor(cfg);
				return 0;
			}
		}
	}
	libusb_free_config_descriptor(cfg);
	return -1;
}

libptouch_err_t libptouch_open_usb_vid_pid(libptouch_ctx *ctx, uint16_t vid,
					   uint16_t pid)
{
	if (!ctx) {
		return LIBPTOUCH_ERR_ARG;
	}
	if (ctx->usb_handle) {
		ptouch_set_error(ctx, LIBPTOUCH_ERR_ARG, "USB already open");
		return LIBPTOUCH_ERR_ARG;
	}

	int r = libusb_init(&ctx->usb_ctx);
	if (r != LIBUSB_SUCCESS) {
		ptouch_set_error_usb(ctx, r, "libusb_init");
		ctx->usb_ctx = NULL;
		return LIBPTOUCH_ERR_USB;
	}

	libusb_device_handle *h =
	    libusb_open_device_with_vid_pid(ctx->usb_ctx, vid, pid);
	if (!h) {
		libusb_exit(ctx->usb_ctx);
		ctx->usb_ctx = NULL;
		char buf[160];
		snprintf(buf, sizeof(buf),
			 "no USB device 0x%04x:0x%04x (check cable, power, udev)",
			 (unsigned)vid, (unsigned)pid);
		ptouch_set_error(ctx, LIBPTOUCH_ERR_NOT_FOUND, buf);
		return LIBPTOUCH_ERR_NOT_FOUND;
	}
	ctx->usb_handle = h;

#if defined(LIBUSB_API_VERSION) && (LIBUSB_API_VERSION >= 0x01000106)
	(void)libusb_set_auto_detach_kernel_driver(h, 1);
#endif

	if (claim_interface_with_bulk(h, &ctx->claimed_interface,
				      &ctx->bulk_out_ep, &ctx->bulk_in_ep) != 0) {
		ptouch_set_error(ctx, LIBPTOUCH_ERR_USB,
			  "no bulk IN/OUT interface (permissions or driver?)");
		libusb_close(h);
		ctx->usb_handle = NULL;
		libusb_exit(ctx->usb_ctx);
		ctx->usb_ctx = NULL;
		return LIBPTOUCH_ERR_USB;
	}

	(void)libusb_clear_halt(h, ctx->bulk_out_ep);
	(void)libusb_clear_halt(h, ctx->bulk_in_ep);

	ctx->usb_pid = pid;
	ctx->usb_open = 1;
	ptouch_set_error(ctx, LIBPTOUCH_OK, "");
	return LIBPTOUCH_OK;
}

libptouch_err_t libptouch_open_usb(libptouch_ctx *ctx)
{
	return libptouch_open_usb_vid_pid(ctx, LIBPTOUCH_USB_VID_BROTHER,
					  LIBPTOUCH_USB_PID_PTP900W);
}

void libptouch_close(libptouch_ctx *ctx)
{
	if (!ctx)
		return;
	if (ctx->usb_handle) {
		if (ctx->claimed_interface >= 0) {
			(void)libusb_release_interface(ctx->usb_handle,
						       ctx->claimed_interface);
			ctx->claimed_interface = -1;
		}
		libusb_close(ctx->usb_handle);
		ctx->usb_handle = NULL;
	}
	if (ctx->usb_ctx) {
		libusb_exit(ctx->usb_ctx);
		ctx->usb_ctx = NULL;
	}
	ctx->bulk_out_ep = 0;
	ctx->bulk_in_ep = 0;
	ctx->usb_pid = 0;
	ctx->usb_open = 0;
}

int ptouch_bulk_out(libusb_device_handle *h, uint8_t ep, const uint8_t *data,
		    int len, unsigned int timeout_ms)
{
	int tr = 0;
	int r = libusb_bulk_transfer(h, ep, (unsigned char *)data, len, &tr,
				     (int)timeout_ms);
	if (r != 0)
		return r;
	if (tr != len)
		return LIBUSB_ERROR_IO;
	return 0;
}

int ptouch_bulk_in_exact(libusb_device_handle *h, uint8_t ep, uint8_t *buf,
			 int len, unsigned int timeout_ms)
{
	int got = 0;
	while (got < len) {
		int tr = 0;
		int r = libusb_bulk_transfer(h, ep, buf + got, len - got, &tr,
					     (int)timeout_ms);
		if (r != 0)
			return r;
		if (tr == 0)
			return LIBUSB_ERROR_IO;
		got += tr;
	}
	return 0;
}

libptouch_err_t ptouch_bulk_send_job(libptouch_ctx *ctx, const uint8_t *buf,
				     size_t len, const char *what)
{
	int tr = 0;
	int r = libusb_bulk_transfer(ctx->usb_handle, ctx->bulk_out_ep,
				     (unsigned char *)buf, (int)len, &tr,
				     120000);
	if (r != 0) {
		ptouch_set_error_usb(ctx, r, what);
		return LIBPTOUCH_ERR_USB;
	}
	if ((size_t)tr != len) {
		char msg[96];
		snprintf(msg, sizeof(msg), "%s: short write %d/%zu", what, tr,
			 len);
		ptouch_set_error(ctx, LIBPTOUCH_ERR_USB, msg);
		return LIBPTOUCH_ERR_USB;
	}
	return LIBPTOUCH_OK;
}
