#include <errno.h>
#include <libbluray/bluray.h>
#include <libbluray/filesystem.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>

/* macros */
#define ENCRYPTED_BYTES_TO_READ 6144

/* function declarations */
static int block_device_exists(const char *path);
static int copy_dir(BLURAY *bluray, const char *path);
static int copy_file(BLURAY *bluray, const char *src, const char *dst);
static int copy_main_title(BLURAY *bluray, const char *dst);
static int file_exists(const char *path, struct stat *info);
static int init(int argc, char **argv, BLURAY **bluray, int *only_main_title);
static BLURAY *open_bluray(const char *device, const char *keyfile, const int copy_titles);
static void print_default_usage(void);
static void print_help(void);
static void print_only_main_title_usage(void);
static void print_version(void);
static int regular_file_exists(const char *path);
static char *search_keyfile(void);
static void stop(int sig);

/* variables */
int running = 1;

/* function implementations */
int
block_device_exists(const char *path)
{
	int exist;
	struct stat info;

	exist = 0;

	if (file_exists(path, &info)) {
		if (S_ISBLK(info.st_mode))
			exist = 1;
		else
			fprintf(stderr, BIN ": %s isn't a block device.\n", path);
	}

	return exist;
}

int
copy_dir(BLURAY *bluray, const char *path)
{
	int all_good, read;
	struct bd_dir_s *dir;
	BD_DIRENT *dirent;
	char *new_path;

	all_good = 1;
	read = 0;

	dir = bd_open_dir(bluray, path);
	if (dir == NULL) {
		fprintf(stderr, BIN ": Can't open Blu-ray dir %s.\n", path);
		return 0;
	}

	if (strcmp(path, "") != 0 &&
	    mkdir(path, 0755) == -1 && errno != EEXIST) {
		fprintf(stderr, BIN ": Can't create destination dir %s.\n", path);
		return 0;
	}
	
	dirent = malloc(sizeof(BD_DIRENT));
	if (dirent == NULL) {
		fputs(BIN ": Can't allocate memory for BD_DIRENT.\n", stderr);
		dir->close(dir);
		return 0;
	}

	do {
		read = dir->read(dir, dirent);
		if (read == -1) {
			break;
			/*
			 [libbluray-devel] UDF dir read does not return 1 on EOF
			 https://mailman.videolan.org/pipermail/libbluray-devel/2023-September/003297.html
			 https://code.videolan.org/videolan/libbluray/-/blob/master/src/libbluray/disc/udf_fs.c#L110
			 https://code.videolan.org/videolan/libudfread/-/blob/master/src/udfread.c#L1375
			fprintf(stderr, BIN ": Can't read Blu-ray dir %s.\n", path);
			return 0;
			*/
		}

		new_path = malloc(sizeof(char) * (strlen(path) + strlen(dirent->d_name) + 2));
		if (new_path == NULL) {
			fputs(BIN ": Can't allocate memory for new_path.\n", stderr);
			all_good = 0;
			break;
		}
		strcpy(new_path, path);
		if (strcmp(path, "") != 0)
			strcat(new_path, "/");
		strcat(new_path, dirent->d_name);

		if (strchr(dirent->d_name, '.') == NULL) {
			if (!copy_dir(bluray, new_path))
				all_good = 0;
		} else {
			if (!copy_file(bluray, new_path, new_path))
				all_good = 0;
		}

		free(new_path);
	} while (running && read == 0);
	if (!running)
		all_good = 0;

	free(dirent);

	dir->close(dir);

	return all_good;
}

int
copy_file(BLURAY *bluray, const char *src, const char *dst)
{
	uint8_t bytes[ENCRYPTED_BYTES_TO_READ];
	FILE *dst_file;
	int64_t read;
	struct bd_file_s *src_file;
	int success;
	size_t written;

	success  = 1;

	src_file = bd_open_file_dec(bluray, src);
	if (src_file == NULL) {
		fprintf(stderr, BIN ": Can't open Blu-ray file %s.\n", src);
		return 0;
	}
	dst_file = dst == NULL ? stdout : fopen(dst, "w");
	if (dst_file == NULL) {
		fprintf(stderr, BIN ": Can't open destination file %s.\n", dst);
		src_file->close(src_file);
		return 0;
	}

	while (running && (read = src_file->read(src_file, bytes, ENCRYPTED_BYTES_TO_READ)) && read > 0) {
		written = fwrite(bytes, 1, read, dst_file);
		if (written != read) {
			success = 0;
			if (dst_file == stdout)
				fputs(BIN ": Can't write to standard output.\n", stderr);
			else
				fprintf(stderr, BIN ": Destination write error on %s.\n", dst);
			break;
		}
	}
	if (read < 0) {
		success = 0;
		fprintf(stderr, BIN ": Blu-ray read error on %s.\n", src);
	}
	if (!running)
		success = 0;

	if (dst_file != stdout)
		fclose(dst_file);
	src_file->close(src_file);

	return success;
}

int
copy_main_title(BLURAY *bluray, const char *dst)
{
	unsigned char bytes[ENCRYPTED_BYTES_TO_READ];
	FILE *dst_file;
	BLURAY_TITLE_INFO *info;
	int read, success, title;
	size_t written;

	success = 1;

	title = bd_get_main_title(bluray);
	if (title == -1) {
		fputs(BIN ": Can't get main Blu-ray title.\n", stderr);
		return 0;
	}
	if (bd_select_title(bluray, title) == 0) {
		fprintf(stderr, BIN ": Can't select Blu-ray title %i.\n", title);
		return 0;
	}
	info = bd_get_title_info(bluray, title, 1);
	if (info == NULL) {
		fprintf(stderr, BIN ": Can't get Blu-ray title %i info.\n", title);
		return 0;
	}
	dst_file = dst == NULL ? stdout : fopen(dst, "w");
	if (dst_file == NULL) {
		fprintf(stderr, BIN ": Can't open destination file %s.\n", dst);
		bd_free_title_info(info);
		return 0;
	}

	while (running && (read = bd_read(bluray, bytes, ENCRYPTED_BYTES_TO_READ)) && read > 0) {
		written = fwrite(bytes, 1, read, dst_file);
		if (written != read) {
			success = 0;
			if (dst_file == stdout)
				fputs(BIN ": Can't write to standard output.\n", stderr);
			else
				fprintf(stderr, BIN ": Destination write error on %s.\n", dst);
			break;
		}
	}
	if (read < 0) {
		success = 0;
		fprintf(stderr, BIN ": Blu-ray read error on title %i.\n", title);
	}
	if (!running)
		success = 0;

	if (dst_file != stdout)
		fclose(dst_file);
	bd_free_title_info(info);

	return success;
}

int
file_exists(const char *path, struct stat *info)
{
	int exist;

	exist = 0;

	if (stat(path, info) == 0)
		exist = 1;
	 else
		fprintf(stderr, BIN ": Can't open %s.\n", path);

	return exist;
}

int
init(int argc, char **argv, BLURAY **bluray, int *only_main_title)
{
	char *device, *keyfile;
	int free_keyfile, i, param_num;

	device           = "/dev/sr0";
	free_keyfile     = 0;
	keyfile          = NULL;
	*only_main_title = 0;

	for (i = 1; i < argc; i++) {
		if (!strcmp(argv[i], "--")) {
			i++;
			break;

		} else if (!strcmp(argv[i], "-d") || !strcmp(argv[i], "--device")) {
			if (i == argc - 1) {
				fprintf(stderr, BIN ": After \"%s\" write the Blu-ray device file path.\n", argv[i]);
				goto exit;
			}
			device = argv[++i];

		} else if (!strncmp(argv[i], "-d", 2)) {
			device = argv[i] + 2;

		} else if (!strncmp(argv[i], "--device=", 9)) {
			device = argv[i] + 9;

		} else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) {
			print_help();
			goto exit;

		} else if (!strcmp(argv[i], "-k") || !strcmp(argv[i], "--keydb")) {
			if (i == argc - 1) {
				fprintf(stderr, BIN ": After \"%s\" write the keys database file path.\n", argv[i]);
				goto exit;
			}
			keyfile = argv[++i];

		} else if (!strncmp(argv[i], "-k", 2)) {
			keyfile = argv[i] + 2;

		} else if (!strncmp(argv[i], "--keydb=", 8)) {
			keyfile = argv[i] + 8;

		} else if (!strcmp(argv[i], "-m") || !strcmp(argv[i], "--main")) {
			*only_main_title = 1;

		} else if (!strcmp(argv[i], "-v") || !strcmp(argv[i], "--version")) {
			print_version();
			goto exit;

		} else if (!strncmp(argv[i], "-", 1)) {
			fprintf(stderr, BIN ": Unknown \"%s\" option.\n", argv[i]);
			goto exit;

		} else {
			break;

		}
	}

	param_num = argc - i;
	if (*only_main_title && (param_num < 0 || param_num > 1)) {
		print_only_main_title_usage();
		goto exit;
	} else if (param_num < 0 || param_num > 2) {
		print_default_usage();
		goto exit;
	}

	if (!block_device_exists(device))
		goto exit;

	if (keyfile == NULL) {
		keyfile = search_keyfile();
		if (keyfile == NULL) {
			fputs(BIN ": Can't find keys database file.\n", stderr);
			goto exit;
		} else {
			free_keyfile = 1;
		}

	} else if (!regular_file_exists(keyfile)) {
		goto exit;
	}

	*bluray = open_bluray(device, keyfile, *only_main_title);

exit:
	if (free_keyfile)
		free(keyfile);
	if (*bluray == NULL)
		exit(1);

	return i;
}

int
main(int argc, char *argv[])
{
	BLURAY *bluray;
	int first_param, only_main_title, param_num, success;

	first_param = init(argc, argv, &bluray, &only_main_title);
	param_num   = argc - first_param;
	success     = 0;

	signal(SIGINT, stop);
	signal(SIGTERM, stop);

	switch (param_num) {
	case 0:
		if (only_main_title)
			success = copy_main_title(bluray, NULL);
		else
			success = copy_dir(bluray, "");
		break;
	case 1:
		if (only_main_title)
			success = copy_main_title(bluray, argv[first_param]);
		else
			success = copy_file(bluray, argv[first_param], NULL);
		break;
	case 2:
		success = copy_file(bluray, argv[first_param], argv[first_param + 1]);
		break;
	}

	bd_close(bluray);

	return !success;
}

BLURAY *
open_bluray(const char *device, const char *keyfile, const int copy_titles)
{
	BLURAY *bluray;
	const BLURAY_DISC_INFO *info;

	bluray = bd_open(device, keyfile);
	if (bluray == NULL) {
		fprintf(stderr, BIN ": Can't open device %s.\n", device);
		return NULL;
	}

	info = bd_get_disc_info(bluray);
	if (info == NULL) {
		fputs(BIN ": Can't get disc info.\n", stderr);
		bd_close(bluray);
		return NULL;
	}

	if (info->aacs_detected == 1 && info->libaacs_detected != 1) {
		fputs(BIN ": To decode an AACS encrypted disc install libaacs.\n", stderr);
		bd_close(bluray);
		return NULL;
	}
	if (info->aacs_detected == 1 && info->aacs_handled != 1) {
		fputs(BIN ": Unsupported AACS encoding.\n", stderr);
		bd_close(bluray);
		return NULL;
	}

	if (info->bdplus_detected == 1 && info->libbdplus_detected != 1) {
		fputs(BIN ": To decode a BD+ encrypted disc install libbdplus.\n", stderr);
		bd_close(bluray);
		return NULL;
	}
	if (info->bdplus_detected == 1 && info->bdplus_handled != 1) {
		fputs(BIN ": Unsupported BD+ encoding.\n", stderr);
		bd_close(bluray);
		return NULL;
	}

	if ((copy_titles == 1 || info->bdplus_detected == 1) &&
	    bd_get_titles(bluray, TITLES_RELEVANT, 0) == 0) {
		fputs(BIN ": Can't get Blu-ray titles.\n", stderr);
		bd_close(bluray);
		return NULL;
	}

	return bluray;
}

void
print_default_usage(void)
{
	fputs("Usage: " BIN " [-d device] [-k keyfile] [FILE] [DEST]\n", stdout);
}

void
print_help(void)
{
	print_default_usage();
	fputs( \
"\n" \
"Without FILE, the whole disc will be copied to the current directory.\n" \
"FILE must be a file path relative to the disc root.\n" \
"\n" \
"Without DEST, FILE will be sent to standard output.\n" \
"DEST path must include file name.\n" \
"\n" \
	, stdout);
	print_only_main_title_usage();
	fputs( \
"\n" \
"Without DEST, the main disc title will be sent to standard output.\n" \
"DEST path must include file name.\n" \
"\n" \
"\"" BIN " -h\" or \"--help\" prints this help text.\n" \
"\"" BIN " -v\" or \"--version\" prints version and license information.\n" \
"\n" \
BIN " exits with 0 only after a successful copy,\n" \
"otherwise it exits with 1.\n" \
	, stdout);
}

void
print_only_main_title_usage(void)
{
	fputs("Usage: " BIN " [-d device] [-k keyfile] -m [DEST]\n", stdout);
}

void
print_version(void)
{
	fputs( \
BIN " " VERSION "\n" \
"Copyright (C) 2023 Matteo Bini\n" \
"License: GPLv3+ <http://www.gnu.org/licenses/gpl.html>.\n" \
"This is free software: you are free to change and redistribute it.\n" \
"There is NO WARRANTY, to the extent permitted by law.\n" \
"\n" \
"Written by Matteo Bini.\n" \
	, stdout);
}

int
regular_file_exists(const char *path)
{
	int exist;
	struct stat info;

	exist = 0;

	if (file_exists(path, &info)) {
		if (S_ISREG(info.st_mode))
			exist = 1;
		else
			fprintf(stderr, BIN ": %s isn't a regular file.\n", path);
	}

	return exist;
}

char *
search_keyfile(void)
{
	char *home, *name;
	size_t i, len;
	static char *path;
	char relpath[24];

	len = 0;
	home = getenv("HOME");
	strcpy(relpath, "/.config/aacs/KEYDB.cfg");

	if (home == NULL)
		strcpy(relpath, "KEYDB.cfg");
	else
		len += strlen(home);
	len += strlen(relpath);
	path = malloc(len + 1);
	for (i = 0; i != len; i++)
		path[i] = '\0';
	if (home != NULL)
		strcpy(path, home);
	strcat(path, relpath);

	if (regular_file_exists(path))
		return path;

	name = strrchr(path, '/');
	if (name == NULL)
		name = path;
	else
		name++;

	strcpy(name, "keydb.cfg");
	if (regular_file_exists(path))
		return path;

	strcpy(name, "KeyDB.cfg");
	if (regular_file_exists(path))
		return path;

	strcpy(name, "KEYDB.CFG");
	if (regular_file_exists(path))
		return path;

	free(path);
	path = NULL;
	return path;
}

void
stop(int sig)
{
	running = 0;
}
