/*
 * ptouch-print — CLI for libptouch
 *
 * Author: knb
 * Email: knb@artif.org
 */

#include "libptouch.h"

#include <errno.h>
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>

static void usage(const char *argv0)
{
	fprintf(stderr,
		"Usage: %s [options]\n"
		"  -f, --file PATH     入力ファイル\n"
		"  -w, --width DOTS    1bit ラスター時: 幅（ドット）\n"
		"  -H, --height DOTS   1bit ラスター時: 高さ（ドット）\n"
		"  -t, --threshold N  しきい値 0–255（既定 %u、PNG/SVG）\n"
		"      --trim-right[=DOTS] 右側空白を削減（省略時は左余白、失敗時 0）\n"
		"  -n, --dry-run       読み込みと check_raster のみ（USB なし）\n"
		"  -v, --verbose       印刷前情報と印刷後ステータスを標準出力\n"
		"  -p, --pid HEX       USB 製品 ID（既定: P900W の 0x2085）。例: P750W 0x2062、P710BT 0x20af\n"
		"  -S, --status        ステータスを表示して終了\n"
		"  -V, --version       バージョンを表示して終了\n"
		"  -h, --help          このヘルプ\n"
		"\n"
		"PNG は画像サイズを使用（-w/-H 不要）。\n"
		"SVG は現在テープ幅に自動フィット（USB 必須）。\n"
		"1bit packed ラスター（行優先）の場合は -f -w -H が必須。\n"
		"--status のときは -f は不要（他オプションは無視されます）。\n",
		argv0, (unsigned)LIBPTOUCH_PNG_DEFAULT_THRESHOLD);
}

/** パスが .png で終わる、または先頭 8 バイトが PNG シグネチャ */
static int input_is_png(const char *path)
{
	const char *dot = strrchr(path, '.');
	if (dot && strcasecmp(dot, ".png") == 0)
		return 1;

	FILE *fp = fopen(path, "rb");
	if (!fp)
		return 0;
	unsigned char sig[8];
	size_t n = fread(sig, 1, sizeof(sig), fp);
	fclose(fp);
	if (n != sizeof(sig))
		return 0;
	static const unsigned char png_magic[8] = { 137, 80,  78,  71,
						    13,  10, 26, 10 };
	return memcmp(sig, png_magic, sizeof(png_magic)) == 0;
}

static int input_is_svg(const char *path)
{
	const char *dot = strrchr(path, '.');
	if (dot && strcasecmp(dot, ".svg") == 0)
		return 1;
	return 0;
}

static int read_file(const char *path, uint8_t **out, size_t *out_len)
{
	FILE *fp = fopen(path, "rb");
	if (!fp) {
		fprintf(stderr, "open %s: %s\n", path, strerror(errno));
		return -1;
	}
	if (fseek(fp, 0, SEEK_END) != 0) {
		fprintf(stderr, "fseek: %s\n", strerror(errno));
		fclose(fp);
		return -1;
	}
	long sz = ftell(fp);
	if (sz < 0) {
		fprintf(stderr, "ftell: %s\n", strerror(errno));
		fclose(fp);
		return -1;
	}
	rewind(fp);
	uint8_t *buf = malloc((size_t)sz);
	if (!buf) {
		fprintf(stderr, "malloc failed\n");
		fclose(fp);
		return -1;
	}
	size_t n = fread(buf, 1, (size_t)sz, fp);
	fclose(fp);
	if (n != (size_t)sz) {
		fprintf(stderr, "short read\n");
		free(buf);
		return -1;
	}
	*out = buf;
	*out_len = n;
	return 0;
}

static void verbose_print_pre_print_info(libptouch_ctx *ctx,
					 const libptouch_raster_params_t *params,
					 size_t data_len)
{
	if (!ctx || !params)
		return;

	printf("=== verbose: pre-print info ===\n");
	printf("raster bytes: %zu\n", data_len);
	printf("raster size: %ux%u dots (length x width)\n",
	       (unsigned)params->width_dots, (unsigned)params->height_dots);
	printf("margin_mm: %u\n", (unsigned)params->margin_mm);

	libptouch_media_info_t mi;
	if (libptouch_get_current_media_info(ctx, &mi) == LIBPTOUCH_OK) {
		printf("media width code: 0x%02X\n", (unsigned)mi.media_width_code);
		printf("media kind code: 0x%02X\n", (unsigned)mi.media_kind_code);
		printf("tape width: %.1f mm\n", mi.tape_width_mm);
		printf("printable dots: %u\n", (unsigned)mi.printable_dots);
		printf("left/right margins: %u/%u dots\n",
		       (unsigned)mi.left_margin_dots, (unsigned)mi.right_margin_dots);
		printf("print/feed dpi: %.1f/%.1f\n", mi.print_dpi, mi.feed_dpi);
	} else {
		printf("media info: unavailable (%s)\n", libptouch_strerror(ctx));
	}

	uint8_t st[LIBPTOUCH_STATUS_LENGTH];
	if (libptouch_get_status(ctx, st) == LIBPTOUCH_OK) {
		libptouch_status_fprint(stdout, st);
	} else {
		printf("status: unavailable (%s)\n", libptouch_strerror(ctx));
	}
}

static void verbose_print_post_print_status(libptouch_ctx *ctx, const char *label)
{
	if (!ctx)
		return;
	printf("=== verbose: post-print status (%s) ===\n", label);
	/*
	 * 印刷直後は phase change (status[18] != 0x00) が続くことがあるため、
	 * 短時間ポーリングで最終状態まで追跡する。
	 */
	const int max_polls = 12; /* ~2.4s */
	const useconds_t poll_interval_us = 200000;
	for (int i = 0; i < max_polls; i++) {
		uint8_t st[LIBPTOUCH_STATUS_LENGTH];
		if (libptouch_get_status(ctx, st) != LIBPTOUCH_OK) {
			printf("status poll %d/%d: unavailable (%s)\n", i + 1,
			       max_polls, libptouch_strerror(ctx));
		} else {
			printf("status poll %d/%d:\n", i + 1, max_polls);
			libptouch_status_fprint(stdout, st);
			if (st[18] == 0x00u) {
				printf("post-print polling settled: print end.\n");
				return;
			}
		}
		usleep(poll_interval_us);
	}
	printf("post-print polling timeout: phase did not settle.\n");
}

int main(int argc, char **argv)
{
	const char *file = NULL;
	unsigned width = 0, height = 0;
	unsigned threshold = LIBPTOUCH_PNG_DEFAULT_THRESHOLD;
	unsigned usb_pid_arg = 0;
	int dry_run = 0;
	int verbose = 0;
	int trim_right_enabled = 0;
	int trim_right_auto = 0;
	unsigned trim_right_dots = 0;
	int has_threshold = 0;
	int want_status = 0;
	static struct option longopts[] = {
		{ "width", required_argument, NULL, 'w' },
		{ "height", required_argument, NULL, 'H' },
		{ "file", required_argument, NULL, 'f' },
		{ "threshold", required_argument, NULL, 't' },
		{ "trim-right", optional_argument, NULL, 'r' },
		{ "dry-run", no_argument, NULL, 'n' },
		{ "verbose", no_argument, NULL, 'v' },
		{ "pid", required_argument, NULL, 'p' },
		{ "status", no_argument, NULL, 'S' },
		{ "version", no_argument, NULL, 'V' },
		{ "help", no_argument, NULL, 'h' },
		{ NULL, 0, NULL, 0 },
	};

	int c;
	while ((c = getopt_long(argc, argv, "w:H:f:t:r::p:nvhSV", longopts, NULL)) !=
	       -1) {
		switch (c) {
		case 'w':
			width = (unsigned)strtoul(optarg, NULL, 10);
			break;
		case 'H':
			height = (unsigned)strtoul(optarg, NULL, 10);
			break;
		case 'f':
			file = optarg;
			break;
		case 't':
			threshold = (unsigned)strtoul(optarg, NULL, 10);
			if (threshold > 255u) {
				fprintf(stderr, "-t must be 0..255\n");
				return 2;
			}
			has_threshold = 1;
			break;
		case 'n':
			dry_run = 1;
			break;
		case 'v':
			verbose = 1;
			break;
		case 'r':
			trim_right_enabled = 1;
			if (!optarg) {
				trim_right_auto = 1;
				trim_right_dots = 0;
				break;
			}
			trim_right_auto = 0;
			trim_right_dots = (unsigned)strtoul(optarg, NULL, 10);
			if (trim_right_dots > 0xFFFFu) {
				fprintf(stderr, "--trim-right must be 0..65535\n");
				return 2;
			}
			break;
		case 'p':
			usb_pid_arg = (unsigned)strtoul(optarg, NULL, 0);
			if (usb_pid_arg == 0u || usb_pid_arg > 0xFFFFu) {
				fprintf(stderr, "-p/--pid must be 1..0xFFFF\n");
				return 2;
			}
			break;
		case 'S':
			want_status = 1;
			break;
		case 'V':
			printf("ptouch-print %s\n", LIBPTOUCH_VERSION_STRING);
			return 0;
		case 'h':
			usage(argv[0]);
			return 0;
		default:
			usage(argv[0]);
			return 2;
		}
	}

	if (want_status) {
		if (file || width || height || dry_run || has_threshold)
			fprintf(stderr,
				"warning: options other than --status are ignored\n");

		libptouch_ctx *sctx = libptouch_create();
		if (!sctx) {
			fprintf(stderr, "libptouch_create failed\n");
			return 1;
		}
		libptouch_err_t se =
		    usb_pid_arg != 0
			? libptouch_open_usb_vid_pid(
			      sctx, LIBPTOUCH_USB_VID_BROTHER,
			      (uint16_t)usb_pid_arg)
			: libptouch_open_usb(sctx);
		if (se != LIBPTOUCH_OK) {
			fprintf(stderr, "open_usb: %s\n", libptouch_strerror(sctx));
			libptouch_destroy(sctx);
			return 1;
		}
		/* 印刷直後などは内部処理中で最初のステータス応答が不安定なことがあるため、
		 * 少し待ってからステータスを取りに行く。 */
		usleep(300000); /* 300ms */

		uint8_t st[LIBPTOUCH_STATUS_LENGTH];
		se = libptouch_get_status(sctx, st);
		if (se != LIBPTOUCH_OK) {
			fprintf(stderr, "get_status: %s\n", libptouch_strerror(sctx));
			libptouch_close(sctx);
			libptouch_destroy(sctx);
			return 1;
		}
		libptouch_status_fprint(stdout, st);
		libptouch_close(sctx);
		libptouch_destroy(sctx);
		return 0;
	}

	if (!file) {
		fprintf(stderr, "-f is required (or use --status)\n");
		usage(argv[0]);
		return 2;
	}

	int png = input_is_png(file);
	int svg = input_is_svg(file);
	if (png || svg) {
		if (width != 0 || height != 0)
			fprintf(stderr,
				"warning: -w/-H ignored for image inputs\n");
	} else {
		if (width == 0 || height == 0) {
			fprintf(stderr,
				"1bit raster requires -w and -H (or use a PNG/SVG file)\n");
			usage(argv[0]);
			return 2;
		}
		if (has_threshold)
			fprintf(stderr,
				"warning: -t applies to PNG/SVG only (ignored)\n");
	}

	libptouch_ctx *ctx = libptouch_create();
	if (!ctx) {
		fprintf(stderr, "libptouch_create failed\n");
		return 1;
	}

	uint8_t *data = NULL;
	size_t data_len = 0;
	libptouch_raster_params_t params = { 0, 0, 0 };
	libptouch_err_t e;
	int usb_opened = 0;
	int data_from_lib = 0;

	if (png) {
		libptouch_png_options_t opt = { .threshold = (uint8_t)threshold };
		e = libptouch_png_file_to_raster(ctx, file,
						 has_threshold ? &opt : NULL,
						 &data, &data_len, &params);
		if (e != LIBPTOUCH_OK) {
			fprintf(stderr, "png_file_to_raster: %s\n",
				libptouch_strerror(ctx));
			libptouch_destroy(ctx);
			return 1;
		}
		data_from_lib = 1;
	} else if (svg) {
		e = usb_pid_arg != 0
			? libptouch_open_usb_vid_pid(ctx, LIBPTOUCH_USB_VID_BROTHER,
						     (uint16_t)usb_pid_arg)
			: libptouch_open_usb(ctx);
		if (e != LIBPTOUCH_OK) {
			fprintf(stderr, "open_usb: %s\n", libptouch_strerror(ctx));
			libptouch_destroy(ctx);
			return 1;
		}
		usb_opened = 1;
		libptouch_svg_options_t opt = { .threshold = (uint8_t)threshold };
		e = libptouch_svg_file_to_raster_fit_current_tape(
			ctx, file, has_threshold ? &opt : NULL, &data, &data_len,
			&params);
		if (e != LIBPTOUCH_OK) {
			fprintf(stderr, "svg_file_to_raster_fit_current_tape: %s\n",
				libptouch_strerror(ctx));
			libptouch_close(ctx);
			libptouch_destroy(ctx);
			return 1;
		}
		data_from_lib = 1;
	} else {
		if (read_file(file, &data, &data_len) != 0) {
			libptouch_destroy(ctx);
			return 1;
		}
		params.width_dots = width;
		params.height_dots = height;
		params.margin_mm = 0;
	}

	if (trim_right_enabled) {
		uint16_t pad = (uint16_t)trim_right_dots;
		if (trim_right_auto) {
			pad = 0u;
			if (!usb_opened) {
				e = usb_pid_arg != 0
					? libptouch_open_usb_vid_pid(
					      ctx, LIBPTOUCH_USB_VID_BROTHER,
					      (uint16_t)usb_pid_arg)
					: libptouch_open_usb(ctx);
				if (e == LIBPTOUCH_OK)
					usb_opened = 1;
			}
			if (usb_opened) {
				libptouch_media_info_t mi;
				if (libptouch_get_current_media_info(ctx, &mi) ==
				    LIBPTOUCH_OK) {
					pad = mi.left_margin_dots;
				}
			}
		}

		uint8_t *trimmed = NULL;
		size_t trimmed_len = 0;
		libptouch_raster_params_t trimmed_params = { 0, 0, 0 };
		e = libptouch_trim_right_blank_columns(
			ctx, data, data_len, &params, pad, &trimmed, &trimmed_len,
			&trimmed_params);
		if (e != LIBPTOUCH_OK) {
			fprintf(stderr, "trim_right_blank_columns: %s\n",
				libptouch_strerror(ctx));
			libptouch_destroy(ctx);
			if (data_from_lib)
				libptouch_free_raster(data);
			else
				free(data);
			return 1;
		}
		if (data_from_lib)
			libptouch_free_raster(data);
		else
			free(data);
		data = trimmed;
		data_len = trimmed_len;
		params = trimmed_params;
		data_from_lib = 1;
	}

	e = libptouch_check_raster(ctx, data, data_len, &params);
	if (e != LIBPTOUCH_OK) {
		fprintf(stderr, "check_raster: %s\n",
			libptouch_strerror(ctx));
		libptouch_destroy(ctx);
		if (data_from_lib)
			libptouch_free_raster(data);
		else
			free(data);
		return 1;
	}

	if (dry_run) {
		printf("dry-run OK: %zu bytes, src %ux%u dots (print lengthxwidth %ux%u)\n",
		       data_len,
		       (unsigned)params.width_dots,
		       (unsigned)params.height_dots,
		       (unsigned)params.width_dots,
		       (unsigned)params.height_dots);
		if (usb_opened)
			libptouch_close(ctx);
		libptouch_destroy(ctx);
		if (data_from_lib)
			libptouch_free_raster(data);
		else
			free(data);
		return 0;
	}

	if (!usb_opened) {
		e = usb_pid_arg != 0
			? libptouch_open_usb_vid_pid(ctx, LIBPTOUCH_USB_VID_BROTHER,
						     (uint16_t)usb_pid_arg)
			: libptouch_open_usb(ctx);
		if (e != LIBPTOUCH_OK) {
			fprintf(stderr, "open_usb: %s\n", libptouch_strerror(ctx));
			libptouch_destroy(ctx);
			if (data_from_lib)
				libptouch_free_raster(data);
			else
				free(data);
			return 1;
		}
	}

	if (verbose)
		verbose_print_pre_print_info(ctx, &params, data_len);

	e = libptouch_print_raster(ctx, data, data_len, &params);
	if (e != LIBPTOUCH_OK) {
		if (verbose)
			verbose_print_post_print_status(ctx, "print failed");
		fprintf(stderr, "print_raster: %s\n",
			libptouch_strerror(ctx));
		libptouch_close(ctx);
		libptouch_destroy(ctx);
		if (data_from_lib)
			libptouch_free_raster(data);
		else
			free(data);
		return 1;
	}

	if (verbose)
		verbose_print_post_print_status(ctx, "print success");

	libptouch_close(ctx);
	libptouch_destroy(ctx);
	if (data_from_lib)
		libptouch_free_raster(data);
	else
		free(data);
	return 0;
}
