/*
 * libptouch — raster layout, transpose, P-touch print job (ESC/P bulk)
 *
 * Author: knb
 * Email: knb@artif.org
 */

#include "libptouch_internal.h"
#include "libptouch_layout.h"

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

static void pack_line(uint8_t *line, size_t line_bytes, unsigned head_dots,
		      const uint8_t *row, uint32_t width_dots, uint16_t left_dots,
		      uint16_t print_dots)
{
	memset(line, 0, line_bytes);
	if (width_dots > (uint32_t)print_dots)
		return;
	uint32_t start = (uint32_t)left_dots +
			 ((uint32_t)print_dots - width_dots) / 2u;
	for (uint32_t x = 0; x < width_dots; x++) {
		uint32_t ubyte = x / 8u;
		uint32_t ubit = 7u - (x % 8u);
		if (((row[ubyte] >> ubit) & 1u) == 0)
			continue;
		uint32_t dot = start + x;
		if (dot >= (uint32_t)head_dots)
			break;
		uint32_t b = dot / 8u;
		uint32_t bi = 7u - (dot % 8u);
		line[b] |= (uint8_t)(1u << bi);
	}
}

static uint8_t *transpose_raster_alloc(const uint8_t *in, uint32_t W, uint32_t H,
				      uint32_t *outW, uint32_t *outH)
{
	*outW = H;
	*outH = W;
	size_t old_rb = (W + 7u) / 8u;
	size_t new_rb = (H + 7u) / 8u;
	uint8_t *out = (uint8_t *)calloc(new_rb * W, 1);
	if (!out)
		return NULL;
	for (uint32_t y = 0; y < H; y++) {
		for (uint32_t x = 0; x < W; x++) {
			size_t ob = (size_t)y * old_rb + x / 8u;
			int bit = (int)((in[ob] >> (7u - x % 8u)) & 1u);
			if (!bit)
				continue;
			uint32_t nx = y;
			uint32_t ny = x;
			size_t nb = (size_t)ny * new_rb + nx / 8u;
			out[nb] |= (uint8_t)(1u << (7u - nx % 8u));
		}
	}
	return out;
}

libptouch_err_t libptouch_print_raster(libptouch_ctx *ctx,
				       const uint8_t *data, size_t data_len,
				       const libptouch_raster_params_t *params)
{
	libptouch_err_t v =
	    libptouch_check_raster(ctx, data, data_len, params);
	if (v != LIBPTOUCH_OK)
		return v;

	if (!ctx->usb_open || !ctx->bulk_out_ep) {
		ptouch_set_error(ctx, LIBPTOUCH_ERR_IO, "not connected");
		return LIBPTOUCH_ERR_IO;
	}

	uint8_t st[LIBPTOUCH_STATUS_LENGTH];
	v = libptouch_get_status(ctx, st);
	if (v != LIBPTOUCH_OK)
		return v;

	const ptouch_printer_profile_t *prof =
	    ptouch_layout_resolve_profile(ctx->usb_pid, st[4]);
	if (!prof) {
		ptouch_set_error(ctx, LIBPTOUCH_ERR_UNSUPPORTED,
				 "no layout profile for this printer");
		return LIBPTOUCH_ERR_UNSUPPORTED;
	}

	uint8_t media_kind = st[11];
	uint8_t media_w = st[10];
	uint16_t left_dots, print_dots, right_dots;
	v = ptouch_layout_from_status(prof, media_kind, media_w, &left_dots,
				      &print_dots, &right_dots);
	if (v != LIBPTOUCH_OK) {
		ptouch_set_error(ctx, LIBPTOUCH_ERR_UNSUPPORTED,
			  "tape width/layout not supported for this media "
			  "(check status media bytes)");
		return LIBPTOUCH_ERR_UNSUPPORTED;
	}
	(void)right_dots;

	unsigned head_dots = prof->head_width_dots;
	size_t line_payload = (size_t)((head_dots + 7u) / 8u);
	size_t gf_packet = 3u + line_payload;

	uint32_t wd = params->width_dots;
	uint32_t ht = params->height_dots;
	uint8_t *transposed = transpose_raster_alloc(data, wd, ht, &wd, &ht);
	if (!transposed) {
		ptouch_set_error(ctx, LIBPTOUCH_ERR_NOMEM,
			  "transpose raster: out of memory");
		return LIBPTOUCH_ERR_NOMEM;
	}
	const uint8_t *src = transposed;

	if (wd > (uint32_t)print_dots) {
		char buf[160];
		snprintf(buf, sizeof(buf),
			 "image width %u dots > printable %u for loaded tape",
			 (unsigned)wd, (unsigned)print_dots);
		ptouch_set_error(ctx, LIBPTOUCH_ERR_ARG, buf);
		free(transposed);
		return LIBPTOUCH_ERR_ARG;
	}

	double margin_dpi = prof->margin_feed_dpi;
	unsigned margin_max = prof->margin_feed_max_dots;
	unsigned margin_dots = 14u;
	if (params->margin_mm > 0) {
		margin_dots = (unsigned)((double)params->margin_mm * margin_dpi /
						 25.4 +
					 0.5);
		if (margin_dots < 14u)
			margin_dots = 14u;
		if (margin_dots > margin_max)
			margin_dots = margin_max;
	}

	uint32_t lines = ht;
	uint8_t n5 = (uint8_t)(lines & 0xFFu);
	uint8_t n6 = (uint8_t)((lines >> 8) & 0xFFu);
	uint8_t n7 = (uint8_t)((lines >> 16) & 0xFFu);
	uint8_t n8 = (uint8_t)((lines >> 24) & 0xFFu);

	uint8_t n2_paper = 0x09u;
	if (media_kind == 0x03u)
		n2_paper = 0x00u;
	else if (media_kind == 0x11u)
		n2_paper = 0x11u;
	else if (media_kind == 0x17u)
		n2_paper = 0x17u;
	else if (media_kind == 0x13u)
		n2_paper = 0x13u;

	uint8_t head[256];
	size_t pos = 0;
	memset(head + pos, 0, 200);
	pos += 200u;
	static const uint8_t esc_at[] = { 0x1B, 0x40 };
	memcpy(head + pos, esc_at, sizeof(esc_at));
	pos += sizeof(esc_at);
	static const uint8_t raster_mode[] = { 0x1B, 0x69, 0x61, 0x01 };
	memcpy(head + pos, raster_mode, sizeof(raster_mode));
	pos += sizeof(raster_mode);

	uint8_t esc_iz[] = { 0x1B, 0x69, 0x7A, 0x0Eu, n2_paper, media_w,
			     0x00u, n5,    n6,	  n7,	 n8,	0x02u, 0x00u };
	memcpy(head + pos, esc_iz, sizeof(esc_iz));
	pos += sizeof(esc_iz);

	static const uint8_t esc_im[] = { 0x1B, 0x69, 0x4D, 0x40 };
	memcpy(head + pos, esc_im, sizeof(esc_im));
	pos += sizeof(esc_im);
	static const uint8_t esc_ia[] = { 0x1B, 0x69, 0x41, 0x01 };
	memcpy(head + pos, esc_ia, sizeof(esc_ia));
	pos += sizeof(esc_ia);
	static const uint8_t esc_ik[] = { 0x1B, 0x69, 0x4B, 0x0C };
	memcpy(head + pos, esc_ik, sizeof(esc_ik));
	pos += sizeof(esc_ik);

	uint8_t esc_id[] = {
		0x1B, 0x69, 0x64,
		(uint8_t)(margin_dots & 0xFFu),
		(uint8_t)((margin_dots >> 8) & 0xFFu)
	};
	memcpy(head + pos, esc_id, sizeof(esc_id));
	pos += sizeof(esc_id);

	static const uint8_t mode_m[] = { 0x4D, 0x00 };
	memcpy(head + pos, mode_m, sizeof(mode_m));
	pos += sizeof(mode_m);

	v = ptouch_bulk_send_job(ctx, head, pos, "print preamble");
	if (v != LIBPTOUCH_OK) {
		free(transposed);
		return v;
	}

	size_t row_b = ((size_t)wd + 7u) / 8u;
	static const uint8_t g_hdr[] = { 0x47, 0x46, 0x00 };

	uint8_t *gbuf = (uint8_t *)malloc(gf_packet);
	if (!gbuf) {
		ptouch_set_error(ctx, LIBPTOUCH_ERR_NOMEM,
				 "raster line buffer");
		free(transposed);
		return LIBPTOUCH_ERR_NOMEM;
	}

	for (uint32_t y = 0; y < lines; y++) {
		const uint8_t *row = src + (size_t)y * row_b;
		pack_line(gbuf + 3, line_payload, head_dots, row, wd, left_dots,
			  print_dots);
		memcpy(gbuf, g_hdr, sizeof(g_hdr));
		v = ptouch_bulk_send_job(ctx, gbuf, gf_packet, "raster line");
		if (v != LIBPTOUCH_OK) {
			free(gbuf);
			free(transposed);
			return v;
		}
	}
	free(gbuf);

	static const uint8_t print_end[] = { 0x1A };
	v = ptouch_bulk_send_job(ctx, print_end, sizeof(print_end), "print end");
	if (v != LIBPTOUCH_OK) {
		free(transposed);
		return v;
	}

	uint8_t sink[64];
	int tr = 0;
	(void)libusb_bulk_transfer(ctx->usb_handle, ctx->bulk_in_ep, sink,
				   (int)sizeof(sink), &tr, 3000);
	(void)tr;

	free(transposed);
	ptouch_set_error(ctx, LIBPTOUCH_OK, "");
	return LIBPTOUCH_OK;
}
