/***************************************************************************
 *   Copyright (C) 2007 by Arep                                            *
 *   Support is provided through the forums at                             *
 *   http://wii.console-tribe.com                                          *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
 ***************************************************************************/

#include "misc.h"
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
#include "disc.h"
#include "dumper.h"
#include "unscrambler.h"
#include "xbox_ref/xbox_ref_log.h"
#include "xbox_ref_bridge.h"
#include "redump_dat.h"
#include "native_report.h"

#ifndef WIN32
#include "linux_rawio.h"
#endif

#ifdef WIN32
#include <sys/stat.h>
#include <io.h>
#include <windows.h>
#else
#include <sys/stat.h>
#include <unistd.h>
#endif

#define printf xbox_ref_printf
#define fprintf xbox_ref_log_fprintf

#define USECS_PER_SEC	1000000


static const char *friidump_requested_output_target(void);

typedef struct {
	bool active;
	bool dump_attempted;
	bool seed_ok;
	bool seed_applicable;
	bool have_seed_duration;
	double seed_duration;
	bool dump_ok;
	bool stop_attempted;
	bool stop_ok;
	char model[256];
	char disc_type[64];
	char game_id[64];
	char title[256];
	char region[128];
	char output[512];
	u_int32_t command;
	int method;
	u_int32_t sectors;
	u_int32_t source_sectors;
	u_int32_t expected_output_sectors;
	bool have_source_sectors;
	bool have_expected_output_sectors;
	u_int32_t fail_sector;
	u_int32_t hlds_type;
	u_int32_t cache_base;
	u_int32_t mem_blocks;
	const char *profile;
} friidump_validation_summary;

static friidump_validation_summary g_validation_summary;
static redump_verify_result g_redump_result;
static bool g_redump_attempted = false;
static bool g_operation_duration_override_valid = false;
static double g_operation_duration_override = 0.0;
static char g_executable_dir[1024];
static friidump_native_report g_native_report;
static bool g_native_report_dump_started = false;
static bool g_native_report_resumed = false;
static volatile sig_atomic_t g_native_cancel_requested = 0;

static void friidump_summary_copy(char *dst, size_t dst_size, const char *src) {
	if (!dst || dst_size == 0)
		return;
	if (!src)
		src = "";
	snprintf(dst, dst_size, "%s", src);
}

static void friidump_summary_reset(void) {
	memset(&g_validation_summary, 0, sizeof(g_validation_summary));
	g_validation_summary.fail_sector = 0xFFFFFFFFU;
	g_validation_summary.seed_applicable = true;
	g_operation_duration_override_valid = false;
	g_operation_duration_override = 0.0;
}

static void friidump_format_hms(double seconds, char *buf, size_t buf_size) {
	long total, hours, minutes, secs;
	if (!buf || buf_size == 0)
		return;
	if (seconds < 0.0)
		seconds = 0.0;
	total = (long) (seconds + 0.5);
	hours = total / 3600;
	minutes = (total / 60) % 60;
	secs = total % 60;
	snprintf(buf, buf_size, "%02ld:%02ld:%02ld", hours, minutes, secs);
}


static bool friidump_capture_seed_diagnostic (disc *d) {
	disc_seed_diagnostic diag;
	char cdb_text[3 * 12 + 1];
	char note[768];
	int i;
	int used;

	memset (&diag, 0, sizeof (diag));
	if (!disc_get_seed_diagnostic (d, &diag))
		return false;

	cdb_text[0] = '\0';
	used = 0;
	for (i = 0; i < diag.cdb_length && i < 12; i++) {
		int wrote = snprintf (cdb_text + used, sizeof (cdb_text) - (size_t) used,
			"%s%02X", i ? " " : "", diag.cdb[i]);
		if (wrote < 0 || wrote >= (int) (sizeof (cdb_text) - (size_t) used))
			break;
		used += wrote;
	}

	fprintf (stderr,
		"\nSeed diagnostic final failure:\n"
		"  Block.............: %u/20\n"
		"  Sectors...........: %u..%u\n"
		"  Retry.............: %d\n"
		"  Stage.............: %s\n"
		"  Detail............: %s\n"
		"  Transport result..: %d\n"
		"  OS error..........: %d\n"
		"  SCSI status.......: 0x%02X\n"
		"  Sense.............: %02X/%02X/%02X\n"
		"  CDB...............: %s\n",
		diag.seed_block_index + 1,
		diag.sector_start, diag.sector_end,
		diag.retry,
		diag.stage[0] ? diag.stage : "unknown",
		diag.detail[0] ? diag.detail : "none",
		diag.transport_result,
		diag.os_error,
		diag.scsi_status & 0xff,
		diag.sense_key & 0xff,
		diag.asc & 0xff,
		diag.ascq & 0xff,
		cdb_text[0] ? cdb_text : "none");

	snprintf (note, sizeof (note),
		"Seed diagnostic: block %u/20, sectors %u..%u, retry %d, stage %s, detail %s, transport_result %d, os_error %d, scsi_status 0x%02X, sense %02X/%02X/%02X, CDB [%s].",
		diag.seed_block_index + 1,
		diag.sector_start, diag.sector_end,
		diag.retry,
		diag.stage[0] ? diag.stage : "unknown",
		diag.detail[0] ? diag.detail : "none",
		diag.transport_result,
		diag.os_error,
		diag.scsi_status & 0xff,
		diag.sense_key & 0xff,
		diag.asc & 0xff,
		diag.ascq & 0xff,
		cdb_text[0] ? cdb_text : "none");
	if (friidump_native_report_is_enabled (&g_native_report))
		friidump_native_report_add_note (&g_native_report, note);
	return true;
}

#ifndef WIN32
static bool friidump_linux_vendor_rawio_required(
    disc *d,
    bool xbox_output_requested,
    bool drive_supported,
    bool probe_requested) {
    int method;

    if (!d)
        return false;

    if (probe_requested)
        return true;

    if (xbox_output_requested)
        return disc_is_xbox_unlock_drive(d);

    method = (int) disc_get_method(d);
    return drive_supported && method >= 0 && method <= 9;
}

static void friidump_linux_record_rawio_failure(
    disc *d,
    bool xbox_output_requested,
    const char *note) {
    bool stop_ok;

    if (friidump_native_report_is_enabled(&g_native_report)) {
        friidump_native_report_set_seed(
            &g_native_report,
            false,
            xbox_output_requested ? "not_applicable" : "not_attempted",
            false,
            0.0);
        friidump_native_report_set_dump(
            &g_native_report,
            false,
            "not_attempted",
            false,
            0,
            false,
            0,
            false,
            0.0,
            NULL,
            false,
            0,
            NULL);
        friidump_native_report_set_outcome(&g_native_report, "other", "fail");
        friidump_native_report_add_note(&g_native_report, note);
    }

    stop_ok = disc_stop_unit(d, false);
    fprintf(stderr,
        "Issuing STOP UNIT / spin-down after Linux raw-I/O preflight failure... %s\n",
        stop_ok ? "OK" : "Failed");

    if (g_validation_summary.active) {
        g_validation_summary.stop_attempted = true;
        g_validation_summary.stop_ok = stop_ok;
    }

    if (friidump_native_report_is_enabled(&g_native_report)) {
        friidump_native_report_add_note(
            &g_native_report,
            stop_ok
                ? "STOP UNIT after Linux raw-I/O preflight failure succeeded."
                : "STOP UNIT after Linux raw-I/O preflight failure failed.");
    }
}

static bool friidump_linux_rawio_preflight(
    disc *d,
    bool xbox_output_requested,
    bool drive_supported,
    bool probe_requested) {
    friidump_linux_rawio_state state;
    unsigned long long effective_mask = 0;
    char executable_path[1024];
    char note[1536];
    const char *display_path;

    if (!friidump_linux_vendor_rawio_required(
            d, xbox_output_requested, drive_supported, probe_requested))
        return true;

    executable_path[0] = '\0';
    if (!friidump_linux_executable_path(
            executable_path, sizeof(executable_path))) {
        const char *fallback_dir = g_executable_dir[0] ? g_executable_dir : ".";
        const char *suffix = "friidump";
        size_t used = strlen(fallback_dir);
        size_t suffix_len = strlen(suffix);

        if (used >= sizeof(executable_path))
            used = sizeof(executable_path) - 1;
        memcpy(executable_path, fallback_dir, used);
        executable_path[used] = '\0';

        if (used > 0 && executable_path[used - 1] != '/' &&
                used + 1 < sizeof(executable_path)) {
            executable_path[used++] = '/';
            executable_path[used] = '\0';
        }

        if (suffix_len > sizeof(executable_path) - used - 1)
            suffix_len = sizeof(executable_path) - used - 1;
        memcpy(executable_path + used, suffix, suffix_len);
        executable_path[used + suffix_len] = '\0';
    }
    display_path = executable_path;

    if (geteuid() == 0) {
        fprintf(stderr,
            "\nERROR: FriiDump refuses this Linux vendor-command path while running as root.\n"
            "  Run FriiDump as your normal user and grant only CAP_SYS_RAWIO to the\n"
            "  exact validated executable. Do not use sudo to run the whole program.\n"
            "  Exact executable: %s\n"
            "  Install: sudo setcap cap_sys_rawio=ep '%s'\n"
            "  Verify:  getcap '%s'\n"
            "  Rebuilding or replacing the executable clears file capabilities.\n\n",
            display_path, display_path, display_path);
        snprintf(
            note,
            sizeof(note),
            "Linux raw-I/O preflight failed: FriiDump was running as root. The supported least-privilege configuration is a normal user process with cap_sys_rawio=ep on the exact validated executable (%s).",
            display_path);
        friidump_linux_record_rawio_failure(d, xbox_output_requested, note);
        return false;
    }

    state = friidump_linux_rawio_effective(&effective_mask);
    if (state == FRIIDUMP_LINUX_RAWIO_PRESENT) {
        fprintf(stderr,
            "Linux raw-I/O preflight: PASS (CAP_SYS_RAWIO effective; CapEff=0x%016llX).\n",
            effective_mask);
        return true;
    }

    fprintf(stderr,
        "\nERROR: Linux vendor-command authorization is unavailable.\n"
        "  This operation uses vendor-specific SCSI commands and requires effective\n"
        "  CAP_SYS_RAWIO. Device access through the cdrom group is necessary but is\n"
        "  not sufficient for commands such as HLDS 0xE7 memory reads.\n"
        "  Exact executable: %s\n"
        "  Install: sudo setcap cap_sys_rawio=ep '%s'\n"
        "  Verify:  getcap '%s'\n"
        "  Expected: %s cap_sys_rawio=ep\n"
        "  Do not run the entire FriiDump process as root. Rebuilding or replacing\n"
        "  the executable clears file capabilities, so reapply and verify afterward.\n\n",
        display_path, display_path, display_path, display_path);

    snprintf(
        note,
        sizeof(note),
        "Linux raw-I/O preflight failed: effective CAP_SYS_RAWIO was %s for executable %s (CapEff=0x%016llX). Apply cap_sys_rawio=ep to the exact validated executable; do not run FriiDump as root. Rebuilds clear file capabilities.",
        state == FRIIDUMP_LINUX_RAWIO_MISSING ? "missing" : "unavailable/undetectable",
        display_path,
        effective_mask);
    friidump_linux_record_rawio_failure(d, xbox_output_requested, note);
    return false;
}
#endif

static void friidump_summary_begin(disc *d) {
	const char *target;
	if (!d || disc_get_hlds_e7_type(d) == 0)
		return;
	friidump_summary_reset();
	g_validation_summary.active = true;
	friidump_summary_copy(g_validation_summary.model, sizeof(g_validation_summary.model), disc_get_drive_model_string(d));
	g_validation_summary.command = disc_get_command(d);
	g_validation_summary.method = disc_get_method(d);
	g_validation_summary.hlds_type = disc_get_hlds_e7_type(d);
	g_validation_summary.profile = disc_get_hlds_e7_profile_name(d);
	g_validation_summary.cache_base = disc_get_hlds_e7_cache_base(d);
	g_validation_summary.mem_blocks = disc_get_hlds_e7_mem_blocks(d);
	target = friidump_requested_output_target();
	friidump_summary_copy(g_validation_summary.output, sizeof(g_validation_summary.output), target ? target : "(none)");
}

static void friidump_print_validation_summary(double duration, bool have_duration) {
	double mib_completed, mib_per_hour;
	bool have_completed_measurement;
	const char *seed_status;
	const char *dump_status;
	if (!g_validation_summary.active)
		return;
	seed_status = g_validation_summary.seed_applicable
		? (g_validation_summary.seed_ok ? "OK" : "FAILED/NOT REACHED")
		: "N/A (Xbox reference auth path)";
	dump_status = "NOT ATTEMPTED";
	if (g_validation_summary.dump_attempted) {
		if (g_validation_summary.dump_ok)
			dump_status = "OK";
		else if (g_native_cancel_requested && g_native_report.have_byte_count &&
		         g_native_report.byte_count > 0)
			dump_status = "CANCELLED (PARTIAL)";
		else if (g_native_cancel_requested)
			dump_status = "CANCELLED";
		else if (g_native_report.have_byte_count && g_native_report.byte_count > 0)
			dump_status = "FAILED (PARTIAL)";
		else
			dump_status = "FAILED";
	}
	have_completed_measurement =
		g_native_report.dump_attempted &&
		g_native_report.have_byte_count &&
		!g_native_report_resumed;
	mib_completed = have_completed_measurement
		? (double) g_native_report.byte_count / 1024.0 / 1024.0
		: 0.0;
	mib_per_hour = (have_completed_measurement && have_duration && duration > 0.0)
		? (mib_completed / duration * 3600.0)
		: 0.0;
	fprintf(stderr,
		"\nHLDS 0xE7 validation summary:\n"
		"  Model.............: %s\n"
		"  Profile...........: %s\n"
		"  Command/method....: %u / %d\n"
		"  Cache base........: 0x%08x\n"
		"  Memory windows....: %u\n"
		"  Disc type.........: %s\n"
		"  Game/Media ID.....: %s\n"
		"  Title.............: %s\n",
		g_validation_summary.model[0] ? g_validation_summary.model : "(unknown)",
		g_validation_summary.profile ? g_validation_summary.profile : "(unknown)",
		g_validation_summary.command, g_validation_summary.method,
		g_validation_summary.cache_base, g_validation_summary.mem_blocks,
		g_validation_summary.disc_type[0] ? g_validation_summary.disc_type : "(unknown)",
		g_validation_summary.game_id[0] ? g_validation_summary.game_id : "(unknown)",
		g_validation_summary.title[0] ? g_validation_summary.title : "(unknown)");
	if (g_validation_summary.region[0])
		fprintf(stderr, "  Region............: %s\n", g_validation_summary.region);
	fprintf(stderr,
		"  Output............: %s\n"
		"  Seed read.........: %s\n"
		"  Dump status.......: %s\n"
		"  STOP UNIT.........: %s\n",
		g_validation_summary.output[0] ? g_validation_summary.output : "(none)",
		seed_status,
		dump_status,
		g_validation_summary.stop_attempted ? (g_validation_summary.stop_ok ? "OK" : "FAILED") : "NOT ATTEMPTED");
	if (g_validation_summary.have_seed_duration) {
		char seed_buf[32];
		friidump_format_hms(g_validation_summary.seed_duration, seed_buf, sizeof(seed_buf));
		fprintf(stderr, "  Seed elapsed......: %s (%.2f seconds)\n", seed_buf, g_validation_summary.seed_duration);
	}
	if (g_validation_summary.dump_attempted && !g_validation_summary.dump_ok &&
	    g_validation_summary.fail_sector != 0xFFFFFFFFU) {
		if (g_native_cancel_requested)
			fprintf(stderr, "  Cancelled at......: sector %u\n", g_validation_summary.fail_sector);
		else
			fprintf(stderr, "  Failure sector....: %u..%u\n", g_validation_summary.fail_sector, g_validation_summary.fail_sector + 15);
	}
	if (g_validation_summary.have_source_sectors)
		fprintf(stderr, "  Source sectors....: %u\n", g_validation_summary.source_sectors);
	if (g_validation_summary.have_expected_output_sectors)
		fprintf(stderr, "  Expected output...: %u sectors\n", g_validation_summary.expected_output_sectors);
	else if (g_validation_summary.sectors)
		fprintf(stderr, "  Expected sectors..: %u\n", g_validation_summary.sectors);
	if (g_native_report.have_sector_count)
		fprintf(stderr, g_native_report_resumed
		        ? "  Output sectors....: %llu (includes prior resumed data)\n"
		        : "  Completed sectors.: %llu\n",
		        (unsigned long long) g_native_report.sector_count);
	if (g_native_report.have_byte_count)
		fprintf(stderr, g_native_report_resumed
		        ? "  Output bytes......: %llu (%.2f MiB; includes prior resumed data)\n"
		        : "  Completed bytes...: %llu (%.2f MiB)\n",
		        (unsigned long long) g_native_report.byte_count,
		        (double) g_native_report.byte_count / 1024.0 / 1024.0);
	if (have_duration)
		fprintf(stderr, "  Duration..........: %.2f seconds\n", duration);
	if (have_duration && mib_per_hour > 0.0)
		fprintf(stderr, "  Observed average..: %.2f MiB/h over %.2f MiB completed output\n",
		        mib_per_hour, mib_completed);
	else if (g_native_report_resumed)
		fprintf(stderr, "  Observed average..: N/A (resumed output; measurement scope unknown)\n");
	if (g_validation_summary.dump_ok) {
		if (g_redump_attempted) {
			fprintf(stderr, "  Redump verify.....: %s\n", redump_verify_overall_string(&g_redump_result));
			if (g_redump_result.status == REDUMP_VERIFY_MATCH)
				fprintf(stderr, "  Redump title......: %s\n", g_redump_result.game_name);
		} else {
			fprintf(stderr, "  Redump verify.....: NOT RUN\n");
		}
	}
}


/* Name of package */
#define PACKAGE "friidump"

/* Define to the address where bug reports for this package should be sent. */
#define PACKAGE_BUGREPORT "arep@no.net"

/* Define to the full name of this package. */
#define PACKAGE_NAME "FriiDump"

/* Define to the version of this package. */
#define PACKAGE_VERSION "0.5.3.16-pf1-candidate21"


#ifdef WIN32

#include "getopt-win32.h"


#else
#include <sys/time.h>
#include <getopt.h>
#endif


/* Struct for program options */
struct {
	char *device;
	bool autodump;
	bool gui;
	char *raw_in;
	char *raw_out;
	char *iso_out;
	char *xiso_out;
	bool iso_requested;
	bool xiso_requested;
	bool xbox_filename_override;
	bool xbox_auto_filename;
	bool resume;
	int dump_method;
	u_int32_t command;
	u_int32_t start_sector;
	u_int32_t sectors_no;
	u_int32_t speed;
	u_int32_t disctype;
	u_int32_t sec_disc;
	u_int32_t sec_mem;
	bool no_hashing;
	bool no_unscrambling;
	bool no_flushing;
	bool stop_unit;
	bool allmethods;
	bool hlds_e7_scan;
	bool hlds_e7_subcmd_sweep;
	bool hlds_e7_memrange_sweep;
	char *hlds_e7_scan_log;
	char *hlds_e7_scan_dump_prefix;
	char *hlds_profile_report;
	char *redump_dat_dir;
	char *redump_report;
	bool no_redump_verify;
	bool xgd1_layout_probe;
	char *xgd1_layout_probe_report;
	bool xgd1_raw_id_probe;
	char *xgd1_raw_id_probe_report;
	char *native_report_json;
	char *native_report_dir;
	char *firmware_modified_note;
} options;


/* Struct for progress data */
typedef struct {
	struct timeval start_time;
	struct timeval end_time;
	double mb_total;
	double mb_total_real;
	u_int32_t sectors_skipped;
} progstats;


static char friidump_drive_letter_from_device(const char *device) {
	if (!device || !device[0]) return 0;
	if (device[0] && device[1] == ':') return device[0];
	if (device[0] == '\\' && device[1] == '\\' && device[2] == '.' && device[3] == '\\' && device[4] && device[5] == ':') return device[4];
	return device[0];
}


static const char *friidump_requested_output_target(void) {
	if (options.xgd1_raw_id_probe_report && options.xgd1_raw_id_probe_report[0]) return options.xgd1_raw_id_probe_report;
	if (options.xgd1_layout_probe_report && options.xgd1_layout_probe_report[0]) return options.xgd1_layout_probe_report;
	if (options.iso_out && options.iso_out[0]) return options.iso_out;
	if (options.xiso_out && options.xiso_out[0]) return options.xiso_out;
	if (options.raw_out && options.raw_out[0]) return options.raw_out;
	return NULL;
}


static void friidump_json_string(FILE *f, const char *s) {
	const unsigned char *p;
	fputc('"', f);
	if (s) {
		for (p = (const unsigned char *) s; *p; p++) {
			switch (*p) {
				case '\\': fputs("\\\\", f); break;
				case '"': fputs("\\\"", f); break;
				case '\b': fputs("\\b", f); break;
				case '\f': fputs("\\f", f); break;
				case '\n': fputs("\\n", f); break;
				case '\r': fputs("\\r", f); break;
				case '\t': fputs("\\t", f); break;
				default:
					if (*p < 0x20) {
						char tmp[8];
						snprintf(tmp, sizeof(tmp), "\\u%04x", (unsigned int) *p);
						fputs(tmp, f);
					} else {
						fputc(*p, f);
					}
					break;
			}
		}
	}
	fputc('"', f);
}

static void friidump_json_kv_string(FILE *f, const char *key, const char *value, bool comma) {
	fputs("  ", f);
	friidump_json_string(f, key);
	fputs(": ", f);
	friidump_json_string(f, value ? value : "");
	fputs(comma ? ",\n" : "\n", f);
}

static void friidump_json_kv_u32_hex(FILE *f, const char *key, u_int32_t value, bool comma) {
	char tmp[32];
	snprintf(tmp, sizeof(tmp), "0x%08x", value);
	friidump_json_kv_string(f, key, value ? tmp : "", comma);
}

static void friidump_json_kv_int(FILE *f, const char *key, int value, bool comma) {
	char tmp[32];
	fputs("  ", f);
	friidump_json_string(f, key);
	snprintf(tmp, sizeof(tmp), ": %d%s\n", value, comma ? "," : "");
	fputs(tmp, f);
}

static void friidump_json_kv_bool(FILE *f, const char *key, bool value, bool comma) {
	fputs("  ", f);
	friidump_json_string(f, key);
	fputs(value ? ": true" : ": false", f);
	fputs(comma ? ",\n" : "\n", f);
}

static bool friidump_write_hlds_profile_report(disc *d, const char *path) {
	FILE *f;
	if (!d || !path || !path[0])
		return false;
	f = fopen(path, "wb");
	if (!f) {
		fprintf(stderr, "WARNING: could not write HLDS profile report: %s\n", path);
		return false;
	}
	fputs("{\n", f);
	friidump_json_kv_string(f, "schema", "friidump_hlds_profile_report_v1", true);
	friidump_json_kv_string(f, "drive_model", disc_get_drive_model_string(d), true);
	friidump_json_kv_string(f, "profile_name", disc_get_hlds_e7_profile_name(d), true);
	friidump_json_kv_string(f, "support_tier", disc_get_hlds_e7_support_tier(d), true);
	friidump_json_kv_string(f, "family", disc_get_hlds_e7_family(d), true);
	friidump_json_kv_string(f, "tokens", disc_get_hlds_e7_tokens(d), true);
	friidump_json_kv_string(f, "stage5b_record_id", disc_get_hlds_e7_record_id(d), true);
	friidump_json_kv_u32_hex(f, "static_e7_cdb_base", disc_get_hlds_e7_static_cdb_base(d), true);
	friidump_json_kv_u32_hex(f, "static_e7_gate", disc_get_hlds_e7_static_gate(d), true);
	friidump_json_kv_int(f, "runtime_e7_type", (int) disc_get_hlds_e7_type(d), true);
	friidump_json_kv_u32_hex(f, "runtime_cache_base", disc_get_hlds_e7_cache_base(d), true);
	friidump_json_kv_int(f, "runtime_mem_windows", (int) disc_get_hlds_e7_mem_blocks(d), true);
	friidump_json_kv_int(f, "preferred_method", disc_get_hlds_e7_preferred_method(d), true);
	friidump_json_kv_int(f, "selected_method", (int) disc_get_method(d), true);
	friidump_json_kv_int(f, "selected_command", (int) disc_get_command(d), true);
	friidump_json_kv_bool(f, "live_safe_from_static_only", false, true);
	friidump_json_kv_string(f, "safety_note", "Static firmware addresses are evidence only; FriiDump does not use them as host-side commands and does not emit flash/write/update CDBs.", true);
	friidump_json_kv_string(f, "notes", disc_get_hlds_e7_notes(d), false);
	fputs("}\n", f);
	fclose(f);
	return true;
}


static void friidump_retarget_log_from_options(void) {
	const char *target = friidump_requested_output_target();
	if (target) {
		xbox_ref_log_retarget(target);
	} else if (options.device && options.device[0]) {
		xbox_ref_log_open_for_target(NULL, friidump_drive_letter_from_device(options.device));
	}
}


void progress_for_guis (bool start, u_int32_t sectors_done, u_int32_t total_sectors, progstats *stats) {
	int perc;
	double elapsed, mb_done, mb_done_real, mb_hour, seconds_left;
	struct timeval now;
	time_t eta;
	struct tm etatm;
	char buf[50];
	
	if (start) {
		gettimeofday (&(stats -> start_time), NULL);
		stats -> mb_total = (double) total_sectors * 2064 / 1024 / 1024;
		stats -> mb_total_real = (double) (total_sectors - sectors_done) * 2064 / 1024 / 1024;
		stats -> sectors_skipped = sectors_done;
	} else {
		perc = (int) (100.0 * sectors_done / total_sectors);
		gettimeofday (&now, NULL);
		elapsed = difftime (now.tv_sec, (stats -> start_time).tv_sec);
		mb_done = (double) sectors_done * 2064 / 1024 / 1024;
		mb_done_real = (double) (sectors_done - stats -> sectors_skipped) * 2064 / 1024 / 1024;
		mb_hour = mb_done_real / elapsed * 60 * 60;
		seconds_left = stats -> mb_total_real / mb_hour * 60 * 60;
		eta = (time_t) ((stats -> start_time).tv_sec + seconds_left);
		if (localtime_r (&eta, &etatm))
			strftime (buf, 50, "%d/%m/%Y %H:%M:%S", &etatm);
		else
			sprintf (buf, "N/A");

		/* This is the only thing we print to stdout, so that other programs can easily capture and parse our output */
		fprintf (stdout, "%d%%|%u/%u sectors|%.2lf/%.0lf MB|%.0lf/%.0lf seconds|%.2lf MB/h|%s\n",
			 perc, sectors_done, total_sectors, mb_done, stats -> mb_total, elapsed, seconds_left, mb_hour, buf);
		fflush (stdout);
	}

	/* Save return time, in case this will be the last call */
	gettimeofday (&(stats -> end_time), NULL);

	return;
}


void progress (bool start, u_int32_t sectors_done, u_int32_t total_sectors, progstats *stats) {
	int perc, i;
	double elapsed, mb_done, mb_done_real, mb_hour, seconds_left;
	struct timeval now;
	time_t eta;
	struct tm etatm;
	char buf[50];
	
	if (start) {
		gettimeofday (&(stats -> start_time), NULL);
		stats -> mb_total = (double) total_sectors * 2064 / 1024 / 1024;
		stats -> mb_total_real = (double) (total_sectors - sectors_done) * 2064 / 1024 / 1024;
		stats -> sectors_skipped = sectors_done;
	} else {
		perc = (int) (100.0 * sectors_done / total_sectors);
		gettimeofday (&now, NULL);
		elapsed = difftime (now.tv_sec, (stats -> start_time).tv_sec);
		mb_done = (double) sectors_done * 2064 / 1024 / 1024;
		mb_done_real = (double) (sectors_done - stats -> sectors_skipped) * 2064 / 1024 / 1024;
		mb_hour = mb_done_real / elapsed * 60 * 60;
		seconds_left = stats -> mb_total_real / mb_hour * 60 * 60;
		eta = (time_t) ((stats -> start_time).tv_sec + seconds_left);
		if (localtime_r (&eta, &etatm))
			strftime (buf, 50, "%d/%m/%Y %H:%M:%S", &etatm);
		else
			sprintf (buf, "N/A");

		fprintf (stdout, "\r%3d%% ", perc);
		fprintf (stdout, "|");
		for (i = 0; i < 100 / 3; i++) {
			if (i == perc / 3)
				fprintf (stdout, "*");
			else
				fprintf (stdout, "-");
		}
		fprintf (stdout, "| ");
		fprintf (stdout, "%.2lf MB/h, ETA: %s", mb_hour, buf);
		fflush (stdout);
	}

	if (sectors_done == total_sectors)
		printf ("\n");

	/* Save return time, in case this will be the last call */
	gettimeofday (&(stats -> end_time), NULL);

	return;
}



void welcome (void) {
	/* Welcome text */
	fprintf (stderr,
		"FriiDump " PACKAGE_VERSION " - Copyright (C) 2007 Arep\n"
		"This software comes with ABSOLUTELY NO WARRANTY.\n"
		"This is free software, and you are welcome to redistribute it\n"
		"under certain conditions; see COPYING for details.\n"
		"\n"
		"Official support forum: http://wii.console-tribe.com\n"
		"\n"
		"Forum for this UNOFFICIAL VERSION: http://forum.redump.org\n"
		"\n"
		);
	fflush (stderr);

	return;
}



static void friidump_init_executable_dir(const char *argv0) {
    char path[1024];
    size_t length = 0;
    char *slash;
    char *backslash;
    char *separator;

    g_executable_dir[0] = '\0';
    path[0] = '\0';

#ifdef WIN32
    {
        DWORD result = GetModuleFileNameA(NULL, path, (DWORD)sizeof(path));
        if (result > 0 && result < sizeof(path)) {
            path[result] = '\0';
            length = (size_t)result;
        }
    }
#else
#if defined(__linux__)
    {
        ssize_t result = readlink("/proc/self/exe", path, sizeof(path) - 1);
        if (result > 0 && (size_t)result < sizeof(path)) {
            path[result] = '\0';
            length = (size_t)result;
        }
    }
#endif
    if (length == 0 && argv0 && argv0[0] &&
        (strchr(argv0, '/') || strchr(argv0, '\\'))) {
        char *resolved = realpath(argv0, path);
        if (resolved)
            length = strlen(path);
    }
#endif

    if (length == 0)
        return;

    slash = strrchr(path, '/');
    backslash = strrchr(path, '\\');
    separator = slash;
    if (backslash && (!separator || backslash > separator))
        separator = backslash;

    if (!separator)
        return;

    *separator = '\0';
    if (path[0])
        snprintf(g_executable_dir, sizeof(g_executable_dir), "%s", path);
}

static const char *friidump_redump_dat_basename(disc_type type_id) {
    switch (type_id) {
        case DISC_TYPE_GAMECUBE:
            return "Nintendo - GameCube.dat";
        case DISC_TYPE_WII:
        case DISC_TYPE_WII_DL:
            return "Nintendo - Wii.dat";
        case DISC_TYPE_XBOX:
            return "Microsoft - Xbox.dat";
        default:
            return NULL;
    }
}

static uint64_t friidump_file_size(const char *path) {
    if (!path || !path[0])
        return 0;
#ifdef WIN32
    {
        struct _stat64 st;
        if (_stat64(path, &st) == 0)
            return (uint64_t)st.st_size;
    }
#else
    {
        struct stat st;
        if (stat(path, &st) == 0)
            return (uint64_t)st.st_size;
    }
#endif
    return 0;
}

static bool friidump_sync_report_file(FILE *f) {
    if (!f || fflush(f) != 0)
        return false;
#ifdef WIN32
    return _commit(_fileno(f)) == 0;
#else
    return fsync(fileno(f)) == 0;
#endif
}

static bool friidump_atomic_replace(const char *temporary_path, const char *final_path) {
    if (!temporary_path || !final_path)
        return false;
#ifdef WIN32
    return MoveFileExA(temporary_path, final_path,
                       MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0;
#else
    return rename(temporary_path, final_path) == 0;
#endif
}

static void friidump_json_write_string_value(FILE *f, const char *prefix,
                                             const char *value, const char *suffix) {
    fputs(prefix, f);
    friidump_json_string(f, value ? value : "");
    fputs(suffix, f);
}

static void friidump_write_redump_report(const redump_verify_result *result,
                                         const char *output_path,
                                         uint64_t output_size,
                                         const char *crc32,
                                         const char *md5,
                                         const char *sha1,
                                         const char *sha256,
                                         const char *hash_source,
                                         const char *representation_note) {
    FILE *f;
    char *temporary_path;
    char line[256];
    size_t temporary_path_size;
    bool write_ok;

    if (!options.redump_report || !options.redump_report[0] || !result)
        return;

    temporary_path_size = strlen(options.redump_report) + 5;
    temporary_path = (char *)malloc(temporary_path_size);
    if (!temporary_path) {
        fprintf(stderr, "Redump report.......: memory allocation failed\n");
        return;
    }
    snprintf(temporary_path, temporary_path_size, "%s.tmp", options.redump_report);
    remove(temporary_path);

    f = fopen(temporary_path, "wb");
    if (!f) {
        fprintf(stderr, "Redump report.......: could not write temporary file %s\n", temporary_path);
        free(temporary_path);
        return;
    }

    /* Use fputs/fputc rather than the logged fprintf wrapper. Report contents
     * belong in the JSON artifact, not as a malformed partial mirror in the
     * human-readable run log. */
    fputs("{\n", f);
    fputs("  \"schema\": 2,\n", f);
    friidump_json_write_string_value(f, "  \"producer\": ", "friidump-" PACKAGE_VERSION, ",\n");
    friidump_json_write_string_value(f, "  \"status\": ", redump_verify_status_string(result->status), ",\n");
    friidump_json_write_string_value(f, "  \"overall\": ", redump_verify_overall_string(result), ",\n");
    friidump_json_write_string_value(f, "  \"confidence\": ", redump_verify_confidence_string(result), ",\n");
    friidump_json_write_string_value(f, "  \"output\": ", output_path ? output_path : "", ",\n");
    snprintf(line, sizeof(line), "  \"output_size\": %llu,\n", (unsigned long long)output_size);
    fputs(line, f);
    friidump_json_write_string_value(f, "  \"crc32\": ", crc32 ? crc32 : "", ",\n");
    friidump_json_write_string_value(f, "  \"md5\": ", md5 ? md5 : "", ",\n");
    friidump_json_write_string_value(f, "  \"sha1\": ", sha1 ? sha1 : "", ",\n");
    friidump_json_write_string_value(f, "  \"sha256\": ", sha256 ? sha256 : "", ",\n");
    friidump_json_write_string_value(f, "  \"hash_source\": ", hash_source ? hash_source : "", ",\n");
    friidump_json_write_string_value(f, "  \"representation_note\": ", representation_note ? representation_note : "", ",\n");
    friidump_json_write_string_value(f, "  \"dat_path\": ", result->dat_path, ",\n");
    snprintf(line, sizeof(line), "  \"entries_scanned\": %lu,\n", result->entries_scanned);
    fputs(line, f);
    snprintf(line, sizeof(line), "  \"exact_matches\": %lu,\n", result->exact_matches);
    fputs(line, f);
    friidump_json_write_string_value(f, "  \"match_kind\": ", redump_verify_match_kind_string(result), ",\n");
    friidump_json_write_string_value(f, "  \"game_name\": ", result->game_name, ",\n");
    friidump_json_write_string_value(f, "  \"rom_name\": ", result->rom_name, ",\n");

    fputs("  \"field_match_counts\": {\n", f);
    snprintf(line, sizeof(line), "    \"size\": %lu,\n", result->size_matches); fputs(line, f);
    snprintf(line, sizeof(line), "    \"crc32\": %lu,\n", result->crc32_matches); fputs(line, f);
    snprintf(line, sizeof(line), "    \"md5\": %lu,\n", result->md5_matches); fputs(line, f);
    snprintf(line, sizeof(line), "    \"sha1\": %lu\n", result->sha1_matches); fputs(line, f);
    fputs("  },\n", f);

    fputs("  \"expected\": {\n", f);
    friidump_json_write_string_value(f, "    \"size\": ", result->expected_size, ",\n");
    friidump_json_write_string_value(f, "    \"crc32\": ", result->expected_crc32, ",\n");
    friidump_json_write_string_value(f, "    \"md5\": ", result->expected_md5, ",\n");
    friidump_json_write_string_value(f, "    \"sha1\": ", result->expected_sha1, "\n");
    fputs("  },\n", f);

    fputs("  \"evidence\": {\n", f);
    friidump_json_write_string_value(f, "    \"size\": ", redump_field_status_string(result->size_status), ",\n");
    friidump_json_write_string_value(f, "    \"crc32\": ", redump_field_status_string(result->crc32_status), ",\n");
    friidump_json_write_string_value(f, "    \"md5\": ", redump_field_status_string(result->md5_status), ",\n");
    friidump_json_write_string_value(f, "    \"sha1\": ", redump_field_status_string(result->sha1_status), "\n");
    fputs("  },\n", f);
    friidump_json_write_string_value(f, "  \"detail\": ", result->detail, "\n");
    fputs("}\n", f);

    write_ok = !ferror(f) && friidump_sync_report_file(f);
    if (fclose(f) != 0)
        write_ok = false;

    if (!write_ok) {
        remove(temporary_path);
        fprintf(stderr, "Redump report.......: write/flush failed; final report was not replaced\n");
        free(temporary_path);
        return;
    }

    if (!friidump_atomic_replace(temporary_path, options.redump_report)) {
        fprintf(stderr, "Redump report.......: could not replace %s; complete temporary report retained at %s\n",
                options.redump_report, temporary_path);
        free(temporary_path);
        return;
    }

    fprintf(stderr, "Redump report.......: %s (atomic write)\n", options.redump_report);
    free(temporary_path);
}

static void friidump_print_redump_field(const char *label, redump_field_status status) {
    fprintf(stderr, "%-20s %s\n", label, redump_field_status_string(status));
}

static void friidump_verify_redump_values(disc_type type_id,
                                            const char *output_path,
                                            uint64_t output_size,
                                            const char *crc32,
                                            const char *md5,
                                            const char *sha1,
                                            const char *sha256,
                                            const char *hash_source) {
    const char *basename;
    const char *dat_dir;
    const char *representation_note;
    char dat_path[1024];

    redump_verify_result_init(&g_redump_result);
    g_redump_attempted = false;
    representation_note = (type_id == DISC_TYPE_XBOX)
        ? "XGD1 acquisition success and exact Redump hash identity are separate claims; unresolved zero-filled pregame/postgame content may prevent an exact match."
        : "";

    if (options.no_redump_verify) {
        fprintf(stderr, "Redump verification: DISABLED (--no-redump-verify)\n");
        return;
    }
    if (options.no_hashing) {
        fprintf(stderr, "Redump verification: SKIPPED (hashing disabled)\n");
        return;
    }
    if (!output_path || !output_path[0]) {
        fprintf(stderr, "Redump verification: SKIPPED (final output path unavailable)\n");
        return;
    }
    if (!crc32 || !crc32[0] || !md5 || !md5[0] || !sha1 || !sha1[0]) {
        fprintf(stderr, "Redump verification: SKIPPED (finalized CRC32/MD5/SHA-1 evidence incomplete)\n");
        return;
    }

    basename = friidump_redump_dat_basename(type_id);
    if (!basename) {
        fprintf(stderr, "Redump verification: SKIPPED (no DAT mapping for this disc type)\n");
        return;
    }

    dat_dir = (options.redump_dat_dir && options.redump_dat_dir[0])
        ? options.redump_dat_dir
        : NULL;
    redump_resolve_dat_path(dat_dir, g_executable_dir, basename,
                            dat_path, sizeof(dat_path));
    if (output_size == 0)
        output_size = friidump_file_size(output_path);

    g_redump_attempted = true;
    redump_verify_dat_file(dat_path, output_size, crc32, md5, sha1, &g_redump_result);

    fprintf(stderr, "\nRedump verification\n");
    fprintf(stderr, "------------------------------------------------------------\n");
    fprintf(stderr, "%-20s %s\n", "DAT", g_redump_result.dat_path);
    fprintf(stderr, "%-20s %s\n", "Hash source", hash_source ? hash_source : "Finalized output hashes");
    fprintf(stderr, "%-20s %lu\n", "Entries scanned", g_redump_result.entries_scanned);

    if (g_redump_result.status == REDUMP_VERIFY_MATCH) {
        fprintf(stderr, "%-20s %s\n", "Matched entry", g_redump_result.game_name);
        fprintf(stderr, "%-20s %s\n", "ROM", g_redump_result.rom_name);
        friidump_print_redump_field("Image size", g_redump_result.size_status);
        friidump_print_redump_field("CRC32", g_redump_result.crc32_status);
        friidump_print_redump_field("MD5", g_redump_result.md5_status);
        friidump_print_redump_field("SHA-1", g_redump_result.sha1_status);
        if (g_redump_result.exact_matches > 1)
            fprintf(stderr, "%-20s %lu exact entries\n", "Duplicate matches", g_redump_result.exact_matches);
    } else if (g_redump_result.candidate_available) {
        fprintf(stderr, "%-20s %s\n", "Closest candidate", g_redump_result.game_name);
        fprintf(stderr, "%-20s %s\n", "ROM", g_redump_result.rom_name);
        friidump_print_redump_field("Image size", g_redump_result.size_status);
        friidump_print_redump_field("CRC32", g_redump_result.crc32_status);
        friidump_print_redump_field("MD5", g_redump_result.md5_status);
        friidump_print_redump_field("SHA-1", g_redump_result.sha1_status);
    } else {
        fprintf(stderr, "%-20s %s\n", "Closest candidate", "None (no hash-correlated entry)");
        fprintf(stderr, "%-20s %lu entries\n", "Size matches", g_redump_result.size_matches);
        fprintf(stderr, "%-20s %lu entries\n", "CRC32 matches", g_redump_result.crc32_matches);
        fprintf(stderr, "%-20s %lu entries\n", "MD5 matches", g_redump_result.md5_matches);
        fprintf(stderr, "%-20s %lu entries\n", "SHA-1 matches", g_redump_result.sha1_matches);
    }

    fprintf(stderr, "%-20s %s\n", "Overall", redump_verify_overall_string(&g_redump_result));
    fprintf(stderr, "%-20s %s\n", "Confidence", redump_verify_confidence_string(&g_redump_result));
    if (g_redump_result.status != REDUMP_VERIFY_MATCH)
        fprintf(stderr, "%-20s %s\n", "Detail", g_redump_result.detail);
    if (type_id == DISC_TYPE_XBOX)
        fprintf(stderr, "%-20s %s\n", "Representation note", representation_note);

    friidump_write_redump_report(&g_redump_result, output_path, output_size,
                                 crc32, md5, sha1, sha256, hash_source,
                                 representation_note);
}

static void friidump_verify_redump_iso(dumper *dmp, disc_type type_id) {
    const char *output_path;

    if (!dmp)
        return;

    output_path = options.iso_out;
    friidump_verify_redump_values(type_id,
                                  output_path,
                                  friidump_file_size(output_path),
                                  dumper_get_iso_crc32(dmp),
                                  dumper_get_iso_md5(dmp),
                                  dumper_get_iso_sha1(dmp),
                                  dumper_get_iso_sha2(dmp),
                                  "FriiDump finalized ISO multihash");
}

static bool friidump_uses_windows_xbox_reference_path(disc *d, disc_type type_id) {
#ifdef WIN32
    return type_id == DISC_TYPE_XBOX && d && disc_is_xbox_challenge_drive(d);
#else
    (void) d;
    (void) type_id;
    return false;
#endif
}

static void friidump_verify_redump_xbox_reference(dumper *dmp) {
    xbox_ref_dump_result result;

    xbox_ref_dump_result_init(&result);
    if (!dumper_get_xbox_reference_result(dmp, &result)) {
        fprintf(stderr, "Redump verification: SKIPPED (Xbox reference result handoff unavailable)\n");
        return;
    }
    if (result.mode != '1') {
        fprintf(stderr, "Redump verification: SKIPPED (Xbox XISO is not a full Redump disc image)\n");
        return;
    }
    if (!result.dump_success) {
        fprintf(stderr, "Redump verification: SKIPPED (Xbox reference dump did not complete)\n");
        return;
    }
    if (!result.hashes_complete) {
        fprintf(stderr, "Redump verification: SKIPPED (Xbox finalized full-file hashes incomplete)\n");
        return;
    }

    if (g_validation_summary.active) {
        if (result.output_path[0])
            friidump_summary_copy(g_validation_summary.output, sizeof(g_validation_summary.output), result.output_path);
        if (result.title[0])
            friidump_summary_copy(g_validation_summary.title, sizeof(g_validation_summary.title), result.title);
        if (result.media_id[0])
            friidump_summary_copy(g_validation_summary.game_id, sizeof(g_validation_summary.game_id), result.media_id);
        if (result.output_sectors)
            g_validation_summary.sectors = result.output_sectors;
        g_validation_summary.seed_applicable = false;
        g_validation_summary.have_seed_duration = false;
    }
    if (result.elapsed_seconds > 0.0) {
        g_operation_duration_override_valid = true;
        g_operation_duration_override = result.elapsed_seconds;
    }

    friidump_verify_redump_values(DISC_TYPE_XBOX,
                                  result.output_path,
                                  result.output_size,
                                  result.crc32,
                                  result.md5,
                                  result.sha1,
                                  result.sha256,
                                  "Xbox finalized full-file hashes");
}


static void friidump_native_signal_handler(int signal_number) {
    (void) signal_number;
    g_native_cancel_requested = 1;
}

static bool friidump_native_cancel_callback(void *unused) {
    (void) unused;
    return g_native_cancel_requested != 0;
}

static const char *friidump_native_platform(disc_type type_id) {
    switch (type_id) {
        case DISC_TYPE_GAMECUBE:
            return "gamecube";
        case DISC_TYPE_WII:
        case DISC_TYPE_WII_DL:
            return "wii";
        case DISC_TYPE_XBOX:
            return "xbox";
        case DISC_TYPE_DVD:
            return "dvd";
        default:
            return "unknown";
    }
}

static bool friidump_native_report_operation_supported(void) {
    return options.device &&
           !options.raw_in &&
           !options.allmethods &&
           !options.stop_unit &&
           !options.hlds_e7_scan &&
           !options.hlds_e7_subcmd_sweep &&
           !options.hlds_e7_memrange_sweep &&
           !options.xgd1_layout_probe &&
           !options.xgd1_raw_id_probe &&
           !(options.raw_out && options.iso_requested) &&
           (options.autodump || options.raw_out ||
            options.iso_requested || options.xiso_requested);
}

static bool friidump_native_report_option_requested(void) {
    return options.native_report_json ||
           options.native_report_dir ||
           options.firmware_modified_note;
}

static void friidump_native_configure(void) {
    if (!friidump_native_report_operation_supported())
        return;

    friidump_native_report_enable_default(&g_native_report);

    if (options.native_report_json)
        friidump_native_report_enable_path(
            &g_native_report, options.native_report_json);
    if (options.native_report_dir)
        friidump_native_report_enable_dir(
            &g_native_report, options.native_report_dir);

#ifdef FRIIDUMP_BUILD_COMMIT
    friidump_native_report_set_build_commit(&g_native_report, FRIIDUMP_BUILD_COMMIT);
#endif

    if (options.firmware_modified_note)
        friidump_native_report_set_firmware_modified(
            &g_native_report, true, options.firmware_modified_note);

    if (signal(SIGINT, friidump_native_signal_handler) == SIG_ERR) {
        fprintf(stderr, "WARNING: could not install the native-report Ctrl+C handler.\n");
    }
}

static void friidump_native_set_drive(disc *d) {
    if (!friidump_native_report_is_enabled(&g_native_report) || !d)
        return;

    friidump_native_report_set_drive(
        &g_native_report,
        disc_get_drive_vendor(d),
        disc_get_drive_product_id(d),
        disc_get_drive_firmware_revision(d),
        "ATAPI",
        disc_get_device(d));
}

static void friidump_native_set_profile(disc *d, bool xbox_output_requested) {
    (void) xbox_output_requested;

    if (!friidump_native_report_is_enabled(&g_native_report) || !d)
        return;

    friidump_native_report_set_profile(
        &g_native_report,
        disc_get_hlds_e7_support_tier(d),
        disc_get_hlds_e7_static_cdb_base(d),
        disc_get_hlds_e7_static_gate(d));
}

static void friidump_native_set_preflight_failure(int media_rc,
                                                   int sense_key,
                                                   int asc,
                                                   int ascq) {
    char note[256];

    if (!friidump_native_report_is_enabled(&g_native_report))
        return;

    friidump_native_report_set_seed(
        &g_native_report, false, "not_attempted", false, 0.0);
    friidump_native_report_set_dump(
        &g_native_report, false, "not_attempted",
        false, 0, false, 0, false, 0.0,
        NULL, false, 0, NULL);
    friidump_native_report_set_reference(
        &g_native_report, NULL, "not_applicable", NULL);

    if (media_rc == 0) {
        friidump_native_report_set_outcome(
            &g_native_report, "diagnostic_no_media", "not_applicable");
        snprintf(note, sizeof(note),
                 "Media preflight found no readable disc (sense %02X/%02X/%02X).",
                 sense_key & 0xff, asc & 0xff, ascq & 0xff);
    } else {
        friidump_native_report_set_outcome(
            &g_native_report, "other", "fail");
        snprintf(note, sizeof(note),
                 "Drive or media did not become ready (sense %02X/%02X/%02X).",
                 sense_key & 0xff, asc & 0xff, ascq & 0xff);
    }
    friidump_native_report_add_note(&g_native_report, note);
}

static void friidump_native_set_media(disc_type type_id,
                                      const char *title,
                                      const char *region,
                                      const char *disc_id) {
    if (!friidump_native_report_is_enabled(&g_native_report))
        return;
    friidump_native_report_set_media(
        &g_native_report,
        friidump_native_platform(type_id),
        title,
        region,
        disc_id);
}

static void friidump_native_add_measurement_scope(const char *scope) {
    char note[128];
    if (!friidump_native_report_is_enabled(&g_native_report) || !scope)
        return;
    snprintf(note, sizeof(note), "Measurement scope: %s.", scope);
    friidump_native_report_add_note(&g_native_report, note);
}

static void friidump_native_capture_output(dumper *dmp,
                                           disc_type type_id,
                                           bool xiso_mode,
                                           bool success,
                                           bool cancelled,
                                           u_int32_t failure_sector,
                                           u_int32_t expected_sectors) {
    xbox_ref_dump_result xbox_result;
    bool have_xbox_result;
    const char *path;
    const char *crc32;
    const char *md5;
    const char *sha1;
    const char *sha256;
    uint64_t bytes;
    uint64_t sectors;
    uint64_t sector_size;
    bool partial;
    const char *failure_stage;

    if (!friidump_native_report_is_enabled(&g_native_report) || !dmp)
        return;

    g_native_report_dump_started = true;
    g_native_report_resumed = dumper_get_start_sector(dmp) > 0;

    xbox_ref_dump_result_init(&xbox_result);
    have_xbox_result = dumper_get_xbox_reference_result(dmp, &xbox_result);
    path = NULL;
    crc32 = md5 = sha1 = sha256 = NULL;
    bytes = 0;
    sectors = 0;
    sector_size = 2048;

    if (have_xbox_result) {
        if (xbox_result.cancelled)
            cancelled = true;
        path = xbox_result.output_path[0] ? xbox_result.output_path : NULL;
        bytes = xbox_result.output_size;
        sectors = xbox_result.output_sectors;
        if (g_validation_summary.active) {
            if (xbox_result.output_path[0])
                friidump_summary_copy(
                    g_validation_summary.output,
                    sizeof(g_validation_summary.output),
                    xbox_result.output_path);
            if (xbox_result.title[0])
                friidump_summary_copy(
                    g_validation_summary.title,
                    sizeof(g_validation_summary.title),
                    xbox_result.title);
            if (xbox_result.media_id[0])
                friidump_summary_copy(
                    g_validation_summary.game_id,
                    sizeof(g_validation_summary.game_id),
                    xbox_result.media_id);
            if (xbox_result.region[0])
                friidump_summary_copy(
                    g_validation_summary.region,
                    sizeof(g_validation_summary.region),
                    xbox_result.region);
            if (expected_sectors > 0) {
                g_validation_summary.source_sectors = expected_sectors;
                g_validation_summary.have_source_sectors = true;
            }
            if (xbox_result.output_sectors > 0) {
                g_validation_summary.expected_output_sectors =
                    xbox_result.output_sectors;
                g_validation_summary.have_expected_output_sectors = true;
            }
            g_validation_summary.seed_applicable = false;
            g_validation_summary.have_seed_duration = false;
        }
        if (xbox_result.hashes_complete) {
            crc32 = xbox_result.crc32;
            md5 = xbox_result.md5;
            sha1 = xbox_result.sha1;
            sha256 = xbox_result.sha256;
        }
        if (xbox_result.title[0] || xbox_result.media_id[0] || xbox_result.region[0]) {
            friidump_native_report_set_media(
                &g_native_report,
                "xbox",
                xbox_result.title[0] ? xbox_result.title :
                    (g_native_report.have_title ? g_native_report.title : NULL),
                xbox_result.region[0] ? xbox_result.region : NULL,
                xbox_result.media_id[0] ? xbox_result.media_id :
                    (g_native_report.have_disc_id ? g_native_report.disc_id : NULL));
        }
        if (xbox_result.have_game_region) {
            char region_note[96];
            snprintf(region_note, sizeof(region_note),
                     "Xbox XBE GameRegion mask: 0x%08x.",
                     xbox_result.game_region);
            friidump_native_report_add_note(&g_native_report, region_note);
        }
        if (xbox_result.elapsed_seconds > 0.0) {
            g_native_report.have_dump_duration = true;
            g_native_report.dump_duration_seconds = xbox_result.elapsed_seconds;
        }
    } else if (xiso_mode) {
        path = (options.xiso_out && options.xiso_out[0]) ? options.xiso_out : NULL;
        if (path)
            bytes = friidump_file_size(path);
        sectors = bytes / 2048;
        if (!options.no_hashing) {
            crc32 = dumper_get_xiso_crc32(dmp);
            md5 = dumper_get_xiso_md5(dmp);
            sha1 = dumper_get_xiso_sha1(dmp);
            sha256 = dumper_get_xiso_sha2(dmp);
        }
    } else if (options.iso_out && options.iso_out[0]) {
        path = options.iso_out;
        bytes = friidump_file_size(path);
        sectors = bytes / 2048;
        if (!options.no_hashing) {
            crc32 = dumper_get_iso_crc32(dmp);
            md5 = dumper_get_iso_md5(dmp);
            sha1 = dumper_get_iso_sha1(dmp);
            sha256 = dumper_get_iso_sha2(dmp);
        }
    } else if (options.raw_out && options.raw_out[0]) {
        path = options.raw_out;
        sector_size = 2064;
        bytes = friidump_file_size(path);
        sectors = bytes / sector_size;
        if (!options.no_hashing) {
            crc32 = dumper_get_raw_crc32(dmp);
            md5 = dumper_get_raw_md5(dmp);
            sha1 = dumper_get_raw_sha1(dmp);
            sha256 = dumper_get_raw_sha2(dmp);
        }
    }

    if (path && (!success || bytes == 0)) {
        uint64_t observed_bytes = friidump_file_size(path);
        if (observed_bytes > 0 || !success)
            bytes = observed_bytes;
        if (!success)
            sectors = bytes / sector_size;
    }

    if (success && sectors == 0 && type_id != DISC_TYPE_XBOX) {
        sectors = expected_sectors;
        bytes = sectors * sector_size;
    }

    if (g_native_report_resumed) {
        /* FriiDump's streaming hashes cover only bytes produced by this
         * invocation when a file is resumed, not the complete output file. */
        crc32 = md5 = sha1 = sha256 = NULL;
    }

    partial = !success && (bytes > 0 ||
        (failure_sector != 0 && failure_sector != 0xFFFFFFFFU));
    failure_stage = NULL;
    if (!success) {
        if (cancelled)
            failure_stage = "user_cancelled";
        else
            failure_stage = (failure_sector == 0xFFFFFFFFU)
                ? "xbox_reference_dump"
                : "raw_sector_read_or_output_write";
    }

    friidump_native_report_set_dump(
        &g_native_report,
        true,
        success ? "pass" : (partial ? "partial" : "fail"),
        success || sectors > 0,
        sectors,
        success || bytes > 0,
        bytes,
        g_native_report.have_dump_duration,
        g_native_report.dump_duration_seconds,
        failure_stage,
        !success && failure_sector != 0xFFFFFFFFU,
        failure_sector,
        path);

    friidump_native_report_set_hashes(
        &g_native_report, crc32, md5, sha1, sha256);

    if (path) {
        friidump_native_report_add_artifact(
            &g_native_report,
            "dump_output",
            path,
            true,
            bytes,
            sha256);
    }

    if (success)
        friidump_native_report_set_outcome(
            &g_native_report, "full_dump", "pass");
    else if (partial)
        friidump_native_report_set_outcome(
            &g_native_report, "partial_dump", "partial");
    else if (cancelled)
        friidump_native_report_set_outcome(
            &g_native_report, "other", "fail");
    else
        friidump_native_report_set_outcome(
            &g_native_report, "full_dump", "fail");

    if (cancelled)
        friidump_native_report_add_note(
            &g_native_report,
            "The invocation was cancelled by the user; only completed output bytes are represented.");

    if (g_native_report_resumed) {
        friidump_native_add_measurement_scope("unknown");
        friidump_native_report_add_note(
            &g_native_report,
            "Resumed output: duration is not used for throughput because the output byte count includes data from an earlier invocation.");
        friidump_native_report_add_note(
            &g_native_report,
            "Resumed output: streaming hashes were suppressed because they do not describe the complete output file.");
    } else if (!success && partial) {
        friidump_native_add_measurement_scope("partial_progress");
    } else if (type_id == DISC_TYPE_XBOX || xiso_mode) {
        friidump_native_add_measurement_scope("assembled_output");
        friidump_native_report_add_note(
            &g_native_report,
            "Xbox output throughput describes the produced logical or assembled output and is not directly comparable to a full optical-payload read.");
    } else if (success) {
        friidump_native_add_measurement_scope("full_optical_payload");
    } else {
        friidump_native_add_measurement_scope("unknown");
    }

    if (options.no_hashing)
        friidump_native_report_add_note(
            &g_native_report,
            "Output hashing was disabled for this invocation.");
}

static bool friidump_elapsed_from_stats(const progstats *stats, double *duration) {
    double value;
    suseconds_t usecs;

    if (!stats || !duration || stats->start_time.tv_sec == 0 ||
        stats->end_time.tv_sec == 0)
        return false;

    value = (double) (stats->end_time.tv_sec - stats->start_time.tv_sec);
    if (stats->end_time.tv_usec >= stats->start_time.tv_usec) {
        usecs = stats->end_time.tv_usec - stats->start_time.tv_usec;
    } else {
        value -= 1.0;
        usecs = USECS_PER_SEC + stats->end_time.tv_usec - stats->start_time.tv_usec;
    }
    value += (double) usecs / (double) USECS_PER_SEC;
    if (value < 0.0)
        return false;
    *duration = value;
    return true;
}

static void friidump_native_finalize_reference(void) {
    if (!friidump_native_report_is_enabled(&g_native_report))
        return;

    if (strcmp(g_native_report.run_result, "not_applicable") == 0) {
        friidump_native_report_set_reference(
            &g_native_report, NULL, "not_applicable", NULL);
    } else if (g_redump_attempted) {
        if (g_redump_result.status == REDUMP_VERIFY_MATCH) {
            friidump_native_report_set_reference(
                &g_native_report,
                "redump",
                "match",
                g_redump_result.rom_name[0]
                    ? g_redump_result.rom_name
                    : g_redump_result.game_name);
        } else if (g_redump_result.status == REDUMP_VERIFY_NO_MATCH) {
            friidump_native_report_set_reference(
                &g_native_report, "redump", "mismatch", NULL);
        } else {
            friidump_native_report_set_reference(
                &g_native_report, "redump", "not_checked", NULL);
        }
    } else if (options.xiso_requested) {
        friidump_native_report_set_reference(
            &g_native_report, NULL, "not_applicable", NULL);
    } else {
        friidump_native_report_set_reference(
            &g_native_report, NULL, "not_checked", NULL);
    }
}

static void friidump_native_publish(double duration, bool have_duration) {
    char path[FRIIDUMP_REPORT_PATH_MAX];
    char error_text[256];
    const char *name_basis;

    if (!friidump_native_report_is_enabled(&g_native_report))
        return;

    name_basis = xbox_ref_log_path();
    if (!name_basis)
        name_basis = friidump_requested_output_target();
    if (!name_basis)
        name_basis = "friidump.log";
    friidump_native_report_set_name_basis(&g_native_report, name_basis);

    if (g_native_report_dump_started && !g_native_report_resumed &&
        !g_native_report.have_dump_duration && have_duration) {
        g_native_report.have_dump_duration = true;
        g_native_report.dump_duration_seconds = duration;
    }

    friidump_native_finalize_reference();

    if (friidump_native_report_write(
            &g_native_report,
            path,
            sizeof(path),
            error_text,
            sizeof(error_text))) {
        fprintf(stderr, "Native report.......: %s (atomic write)\n", path);
    } else {
        fprintf(stderr, "WARNING: native report was not written: %s\n", error_text);
    }
}

void help (void) {
	/* 80 cols guide:
	 *      |-------------------------------------------------------------------------------|
	 */
	fprintf (stderr, "\n"
		"Available command line options:\n"
		"\n"
		" -h, --help			Show this help\n"
		" -a, --autodump			Dump the disc to an ISO file with an\n"
		"				automatically-generated name, resuming the dump\n"
		"				if possible\n"
		" -g, --gui			Use more verbose output that can be easily\n"
		"				parsed by a GUI frontend\n"
		" -d, --device <device>		Dump disc from device <device>\n"
		" -p, --stop			Instruct device to stop disc rotation\n"
		" -D, --dvd			Force standard DVD-ROM mode for any drive\n"
		"				(equivalent to -T 3; uses FriiDump original\n"
		"				DVD/raw/ISO paths, not Xbox mode)\n"
		" -c, --command <nr>		Force memory dump command:\n"
		"				0 - vanilla 2064\n"
		"				1 - vanilla 2384\n"
		"				2 - Hitachi\n"
		"				3 - Lite-On\n"
		"				4 - Renesas\n"
		" -x, --speed <x>		Set streaming speed (1, 24, 32, 64, etc.,\n"
		"				where 1 = 150 KiB/s and so on)\n"
		" -T, --type <nr>		Force disc type:\n"
		"				0 - GameCube\n"
		"				1 - Wii\n"
		"				2 - Wii_DL\n"
		"				3 - DVD\n"
		"				4 - Xbox/XGD 2048-byte-sector mode\n"
		"				    Native profiles: GDR-8050L and GDR-3120L.\n"
		"				    Other drives keep normal FriiDump behavior\n"
		"				    unless Xbox mode is explicitly forced.\n"
		" -S, --size <sectors>		Force disc size\n"
		" -r, --raw <file>		Output to file <file> in raw format (2064-byte\n"
		"				sectors)\n"
		" -i, --iso[=<file>]		Output to file <file> in ISO format (2048-byte\n"
		"				sectors). For Xbox/GDR-8050L, omitting <file>\n"
		"				derives Title[MediaID].iso from the XBE/DMI;\n"
		"				providing <file> is an explicit override. For\n"
		"				Xbox/XGD this reconstructs the redump-style\n"
		"				XGD1 layout and writes .pfi.bin, .dmi.bin, and\n"
		"				.redump.json metadata when possible\n"
		" -X, --xiso[=<file>]	Output Xbox/XGD game partition as XISO (.xiso).\n"
		"				For Xbox/GDR-8050L, omitting <file> derives\n"
		"				Title[MediaID].xiso from the XBE/DMI; providing\n"
		"				<file> is an explicit override. Attempts to read\n"
		"				the 32-sector game lead-in from drive-readable\n"
		"				sectors and zero-fills only unreadable sectors.\n"
		" -u, --unscramble <file>	Convert (unscramble) raw image contained in\n"
		"				<file> to ISO format\n"
		" -H, --nohash			Do not compute CRC32/MD5/SHA-1/SHA-256 hashes\n"
		"				for generated files\n"
		" -s, --resume			Resume partial dump\n"
		"				-  General  -----------------------------------\n"
		" -0, --method0[=<req>,<exp>]	Use dumping method 0 (Optional argument\n"
		"				specifies how many sectors to request from disc\n"
		"				and read from cache at a time. Values should be\n"
		"				separated with a comma. Default 16,16)\n"
		"				-  Non-Streaming  -----------------------------\n"
		" -1, --method1[=<req>,<exp>]	Use dumping method 1 (Default 16,16)\n"
		" -2, --method2[=<req>,<exp>]	Use dumping method 2 (Default 16,16)\n"
		" -3, --method3[=<req>,<exp>]	Use dumping method 3 (Default 16,16)\n"
		"				-  Streaming  ---------------------------------\n"
		" -4, --method4[=<req>,<exp>]	Use dumping method 4 (Default 27,27)\n"
		" -5, --method5[=<req>,<exp>]	Use dumping method 5 (Default 27,27)\n"
		" -6, --method6[=<req>,<exp>]	Use dumping method 6 (Default 27,27)\n"
		"				-  Hitachi  -----------------------------------\n"
		" -7, --method7			Use dumping method 7 (Read and dump 5 blocks\n"
		"				at a time, using streaming read)\n"
		" -8, --method8			Use dumping method 8 (Read and dump 5 blocks\n"
		"				at a time, using streaming read, using DMA)\n"
		" -9, --method9			Use dumping method 9 (Read and dump 5 blocks\n"
		"				at a time, using streaming read, using DMA and\n"
		"				some speed tricks)\n"
		"     --hlds-e7-scan		Probe HLDS HIT 0xE7 cache/memdump bases only;\n"
		"				writes JSON and does not crack seeds or dump data\n"
		"     --hlds-e7-subcmd-sweep	Probe HIT 0xE7 subcommands/address candidates only;\n"
		"				writes JSON and does not crack seeds or dump data\n"
		"     --hlds-e7-memrange-sweep	Sweep wider HIT 0xE7 subcmd 0x01 address ranges;\n"
		"				writes JSON and does not crack seeds or dump data\n"
		"     --scan-log <file>		JSON output path for --hlds-e7-scan\n"
		"     --scan-dump-prefix <prefix>	Optional raw 0xE7 window dump prefix for --hlds-e7-scan\n"
		"     --hlds-profile-report <file> Write selected HLDS profile/evidence JSON\n"
		"     --redump-dat-dir <dir> Directory containing canonical Redump DAT files\n"
		"                              (default: bundled executable data, installed share data, then current directory)\n"
		"                              Environment override: FRIIDUMP_REDUMP_DAT_DIR\n"
		"     --redump-report <file>   Write atomic Redump evidence JSON\n"
		"     --no-redump-verify       Disable automatic post-dump DAT verification\n"
		"     --xgd1-layout-probe <file> Read-only locked/unlocked XGD1 boundary probe;\n"
		"                              writes atomic JSON and does not create an ISO\n"
		"     --xgd1-raw-id-probe <file> Modified-firmware cache-flushed, block-aligned raw-ID probe;\n"
		"                              maps logical LBAs to decoded physical sector IDs\n"
		"                              Native .friidump.json reports are created by default\n"
		"                              beside the final log using its filename\n"
		"     --report-json <file>     Override the complete native report pathname\n"
		"     --report-dir <directory> Override only the report destination directory\n"
		"                              (may be combined with --report-json)\n"
		"     --firmware-modified <note> Mark firmware modified; omission assumes stock\n"
		" -A, --allmethods		Try all known command/method combinations until\n"
		"				one works. Reopens the drive for each command so\n"
		"				command-specific vendor handlers are rebound.\n"
#ifdef DEBUG
		" -n, --donottunscramble		Do not try unscrambling to check EDC. Only\n"
		"				useful for testing the raw performance of the\n"
		"				different methods\n"
		" -f, --donottflush		Do not call fflush() after every fwrite()\n"
#endif
	);
#ifndef WIN32
	fprintf (stderr,
		"\nLinux raw-I/O requirement:\n"
		"  GC/Wii memory-dump methods and Xbox vendor-unlock paths require effective\n"
		"  CAP_SYS_RAWIO. Apply cap_sys_rawio=ep to the exact validated executable;\n"
		"  do not run FriiDump as root. Rebuilds and replacements clear capabilities.\n"
		"  See docs/LINUX.md and validation/friidump-linux-rawio-capability.sh.\n");
#endif

	return;
}


bool optparse (int argc, char **argv) {
	bool out;
	char *result = NULL;
	int c;
	int option_index = 0;
	static struct option long_options[] = {
		{"help", 0, 0, 'h'},	//0 - no_argument
		{"autodump", 0, 0, 'a'},
		{"gui", 0, 0, 'g'},
		{"device", 1, 0, 'd'},	//1 - required_argument
		{"raw", 1, 0, 'r'},
		{"iso", 2, 0, 'i'},
		{"xiso", 2, 0, 'X'},
		{"unscramble", 1, 0, 'u'},
		{"nohash", 0, 0, 'H'},
		{"resume", 0, 0, 's'},
		{"method0", 2, 0, '0'},	//2 - optional_argument
		{"method1", 2, 0, '1'},
		{"method2", 2, 0, '2'},
		{"method3", 2, 0, '3'},
		{"method4", 2, 0, '4'},
		{"method5", 2, 0, '5'},
		{"method6", 2, 0, '6'},
		{"method7", 0, 0, '7'},
		{"method8", 0, 0, '8'},
		{"method9", 0, 0, '9'},
		{"stop", 0, 0, 'p'},
		{"dvd", 0, 0, 'D'},
		{"command", 1, 0, 'c'},
		{"startsector", 1, 0, 't'},
		{"size", 1, 0, 'S'},
		{"speed", 1, 0, 'x'},
		{"type", 1, 0, 'T'},
		{"allmethods", 0, 0, 'A'},
		{"hlds-e7-scan", 0, 0, 1000},
		{"hlds-e7-subcmd-sweep", 0, 0, 1003},
		{"hlds-e7-memrange-sweep", 0, 0, 1004},
		{"scan-log", 1, 0, 1001},
		{"scan-dump-prefix", 1, 0, 1002},
		{"hlds-profile-report", 1, 0, 1005},
		{"redump-dat-dir", 1, 0, 1006},
		{"redump-report", 1, 0, 1007},
		{"no-redump-verify", 0, 0, 1008},
		{"xgd1-layout-probe", 1, 0, 1009},
		{"xgd1-raw-id-probe", 1, 0, 1010},
		{"report-json", 1, 0, 1011},
		{"report-dir", 1, 0, 1012},
		{"firmware-modified", 1, 0, 1013},
#ifdef DEBUG
		/* We don't want newbies to generate and put into circulation bad dumps, so this options are disabled for releases */
		{"donottunscramble", 0, 0, 'n'},
		{"donottflush", 0, 0, 'f'},
#endif
		{0, 0, 0, 0}
	};

	if (argc == 1) {
		help ();
		exit (1);
	}
	
	/* Init options to default values */
	options.device = NULL;
	options.autodump = false;
	options.gui = false;
	options.raw_in = NULL;
	options.raw_out = NULL;
	options.iso_out = NULL;
	options.xiso_out = NULL;
	options.iso_requested = false;
	options.xiso_requested = false;
	options.xbox_filename_override = false;
	options.xbox_auto_filename = false;
	options.no_hashing = false;
	options.resume = false;
	options.dump_method = -1;
	options.command = -1;
	options.start_sector = -1;
	options.sectors_no = -1;
	options.speed = -1;
	options.disctype = -1;
	options.sec_disc = -1;
	options.sec_mem = -1;
	options.no_unscrambling = false;
	options.no_flushing = false;
	options.stop_unit = false;
	options.allmethods = false;
	options.hlds_e7_scan = false;
	options.hlds_e7_subcmd_sweep = false;
	options.hlds_e7_memrange_sweep = false;
	options.hlds_e7_scan_log = NULL;
	options.hlds_e7_scan_dump_prefix = NULL;
	options.hlds_profile_report = NULL;
	options.redump_dat_dir = NULL;
	options.redump_report = NULL;
	options.no_redump_verify = false;
	options.xgd1_layout_probe = false;
	options.xgd1_layout_probe_report = NULL;
	options.xgd1_raw_id_probe = false;
	options.xgd1_raw_id_probe_report = NULL;
	options.native_report_json = NULL;
	options.native_report_dir = NULL;
	options.firmware_modified_note = NULL;

	do {
#ifdef DEBUG
		c = getopt_long (argc, argv, "hpagd:r:i::X::u:Hs0::1::2::3::4::5::6::789Dc:t:S:x:T:Anf", long_options, &option_index);
#else
		c = getopt_long (argc, argv, "hpagd:r:i::X::u:Hs0::1::2::3::4::5::6::789Dc:t:S:x:T:A", long_options, &option_index);
#endif

		switch (c) {
			case 'h':
				help ();
				exit (1);
				break;
			case 'p':
				options.stop_unit = true;
				break;
			case 'a':
				options.autodump = true;
				options.resume = true;
				break;
			case 'g':
				options.gui = true;
				break;
			case 'd':
				my_strdup (options.device, optarg);
				break;
			case 'r':
				my_strdup (options.raw_out, optarg);
				break;
			case 'i':
				options.iso_requested = true;
				/* Preserve the old `-i file.iso` syntax even though Xbox now also
				 * supports bare `-i` for XBE/DMI-derived names. */
				if (!optarg && optind < argc && argv[optind] && argv[optind][0] != '-')
					optarg = argv[optind++];
				if (optarg) {
					my_strdup (options.iso_out, optarg);
					options.xbox_filename_override = true;
				} else {
					options.xbox_auto_filename = true;
				}
				break;
			case 'X':
				options.xiso_requested = true;
				/* Preserve the old `-X file.xiso` syntax while allowing bare `-X`. */
				if (!optarg && optind < argc && argv[optind] && argv[optind][0] != '-')
					optarg = argv[optind++];
				if (optarg) {
					my_strdup (options.xiso_out, optarg);
					options.xbox_filename_override = true;
				} else {
					options.xbox_auto_filename = true;
				}
				break;
			case 'u':
				my_strdup (options.raw_in, optarg);
				break;
			case 'H':
				options.no_hashing = true;
				break;
			case 's':
				options.resume = true;
				break;
			case '0':
			case '1':
			case '2':
			case '3':
			case '4':
			case '5':
			case '6':
				options.dump_method = c - '0';
				if (optarg) {
					result = strtok(optarg, ",");
					result = strtok(NULL, ",");
					options.sec_disc = atol(strpbrk(optarg,"1234567890"));
					if (result) options.sec_mem = atol(result);
					else {
						help ();
						exit (1);
					}
				}
				break;
			case '7':
			case '8':
			case '9':
				options.dump_method = c - '0';
				break;
			case 'D':
				options.disctype = DISC_TYPE_DVD;
				unscrambler_set_disctype (DISC_TYPE_DVD);
				break;
			case 'c':
				options.command = atol (optarg);
				if (options.command > 4) {
					help ();
					exit (1);
				};
				break;
			case 't':
				options.start_sector = atol (optarg);
				break;
			case 'S':
				options.sectors_no = atol (optarg);
				break;
			case 'x':
				options.speed = atol (optarg);
				break;
			case 'T':
				options.disctype = atol (optarg);
				if (options.disctype > 4) {
					help ();
					exit (1);
				};
				if (options.disctype <= DISC_TYPE_DVD)
					unscrambler_set_disctype (options.disctype);
				break;
			case 'A':
				options.allmethods = true;
				options.resume = true;
				break;
#ifdef DEBUG
			case 'n':
				options.no_unscrambling = true;
				break;
			case 'f':
				options.no_flushing = true;
				break;
#endif
			case 1000:
				options.hlds_e7_scan = true;
				break;
			case 1003:
				options.hlds_e7_subcmd_sweep = true;
				break;
			case 1004:
				options.hlds_e7_memrange_sweep = true;
				break;
			case 1001:
				my_strdup (options.hlds_e7_scan_log, optarg);
				break;
			case 1002:
				my_strdup (options.hlds_e7_scan_dump_prefix, optarg);
				break;
			case 1005:
				my_strdup (options.hlds_profile_report, optarg);
				break;
			case 1006:
				my_strdup (options.redump_dat_dir, optarg);
				break;
			case 1007:
				my_strdup (options.redump_report, optarg);
				break;
			case 1008:
				options.no_redump_verify = true;
				break;
			case 1009:
				options.xgd1_layout_probe = true;
				my_strdup (options.xgd1_layout_probe_report, optarg);
				options.disctype = DISC_TYPE_XBOX;
				break;
			case 1010:
				options.xgd1_raw_id_probe = true;
				my_strdup (options.xgd1_raw_id_probe_report, optarg);
				options.disctype = DISC_TYPE_XBOX;
				break;
			case 1011:
				my_strdup (options.native_report_json, optarg);
				break;
			case 1012:
				my_strdup (options.native_report_dir, optarg);
				break;
			case 1013:
				my_strdup (options.firmware_modified_note, optarg);
				break;
			case -1:
				break;
			default:
// 				fprintf (stderr, "?? getopt returned character code 0%o ??\n", c);
				exit (7);
				break;
		}
	} while (c != -1);

	if (optind < argc) {
		/* Command-line arguments remaining. Ignore them, warning the user. */
		fprintf (stderr, "WARNING: Extra parameters ignored\n");
	}

	if (options.xgd1_layout_probe || options.xgd1_raw_id_probe)
		options.disctype = DISC_TYPE_XBOX;

	/* Sanity checks... */
	out = false;
	if (!options.device && !options.raw_in) {
		fprintf (stderr, "No operation specified. Please use the -d or -u options.\n");
	} else if (options.raw_in && options.raw_out) {
		fprintf (stderr,
			"Are you sure you want to convert a raw image to another raw image? ;)\n"
			"Take a look at the -i and -a options!\n"
		);
	} else if (options.autodump && (options.raw_out || options.iso_requested || options.xiso_requested)) {
		fprintf (stderr, "The -r, -i and -X options cannot be used together with -a.\n");
	} else if (options.xiso_requested && (options.raw_out || options.iso_requested)) {
		fprintf (stderr, "The -X/--xiso option is a separate Xbox output mode and cannot be combined with -r or -i.\n");
	} else if ((options.xgd1_layout_probe || options.xgd1_raw_id_probe) &&
	           (options.autodump || options.raw_in || options.raw_out ||
	            options.iso_requested || options.xiso_requested ||
	            options.allmethods || options.hlds_e7_scan ||
	            options.hlds_e7_subcmd_sweep || options.hlds_e7_memrange_sweep ||
	            (options.xgd1_layout_probe && options.xgd1_raw_id_probe))) {
		fprintf (stderr, "XGD1 probe modes are mutually exclusive read-only diagnostics and cannot be combined with dump, conversion, all-methods, or HLDS 0xE7 probe options.\n");
	} else if ((options.native_report_json && !options.native_report_json[0]) ||
	           (options.native_report_dir && !options.native_report_dir[0])) {
		fprintf (stderr, "Native report file and directory arguments must not be empty.\n");
	} else if ((options.native_report_json && strlen(options.native_report_json) >= FRIIDUMP_REPORT_PATH_MAX) ||
	           (options.native_report_dir && strlen(options.native_report_dir) >= FRIIDUMP_REPORT_PATH_MAX)) {
		fprintf (stderr, "Native report path is too long.\n");
	} else if (options.firmware_modified_note &&
	           !options.firmware_modified_note[0]) {
		fprintf (stderr, "--firmware-modified requires a non-empty explanation.\n");
	} else if (options.firmware_modified_note &&
	           strlen(options.firmware_modified_note) > 1000) {
		fprintf (stderr, "--firmware-modified note exceeds the 1000-byte report limit.\n");
	} else if (friidump_native_report_option_requested() &&
	           !friidump_native_report_operation_supported()) {
		fprintf (stderr, "Native compatibility report options require one normal device dump invocation with exactly one primary output and cannot be combined with conversion, all-methods, stop-only, or research probe modes.\n");
	} else if (options.device &&
	           !options.allmethods && !options.stop_unit &&
	           !options.hlds_e7_scan && !options.hlds_e7_subcmd_sweep &&
	           !options.hlds_e7_memrange_sweep && !options.xgd1_layout_probe &&
	           !options.xgd1_raw_id_probe &&
	           options.raw_out && options.iso_requested) {
		fprintf (stderr, "Native compatibility reports are created by default and require exactly one primary output; do not combine -r and -i.\n");
	} else {
		/* Specified options seem to make sense */
		out = true;
	}
		
	return (out);
}

int dologic (disc *d, progstats *stats) {
	disc_type type_id;
	char *type, *game_id, *region, *maker_id, *maker, *version, *title, tmp[0x03E0 + 4 + 1];
	bool drive_supported;
	bool xbox_forced;
	bool xbox_output_requested;
	bool dump_attempted;
	int out;
	dumper *dmp;
	u_int32_t current_sector = 0;
	
	xbox_forced = (options.disctype == DISC_TYPE_XBOX);
	xbox_output_requested = xbox_forced || options.xiso_requested || options.xgd1_layout_probe || options.xgd1_raw_id_probe;
	dump_attempted = false;
	out = false;
	g_native_cancel_requested = 0;
	
	

				if (options.stop_unit) { //stop rotation, if requested
					fprintf (stderr, "Issuing STOP command... %s\n", (disc_stop_unit (d, false)) ? "OK" : "Failed");
					exit (1);
				}
				else disc_stop_unit(d, true); //else start rotation

				drive_supported = disc_get_drive_support_status (d);
				fprintf (stderr,
					"\n"
					"Drive information:\n"
					"----------------------------------------------------------------------\n"
					"Drive model........: %s\n"
					"Supported..........: %s\n", disc_get_drive_model_string (d), drive_supported ? "Yes" : "No"
				);

					if (xbox_output_requested && !disc_is_xbox_unlock_drive (d)) {
						if (friidump_native_report_is_enabled(&g_native_report))
							friidump_native_report_add_note(&g_native_report, "The selected drive does not expose a supported Xbox unlock profile; dumping was not attempted.");
						fprintf (stderr,
							"Xbox/XGD output is limited to the supported Xbox unlock profiles "
							"currently wired into this branch: GDR-8050L, GDR-3120L, "
							"and known Samsung/Kreon-style vendor-unlock drives.\n"
							"Refusing to fall back to FriiDump GC/Wii methods for Xbox mode on this drive.\n");
						return false;
					}

					/* Xbox/XGD (-T 4 or -X) is a direct MMC/SCSI READ(10) path.
					 * Do not let the detected GC/Wii vendor memdump method (for example
					 * Hitachi command 2 / method 9 on GDR-8050L) drive sector reads. */
					if (xbox_output_requested && options.dump_method == -1)
						options.dump_method = 10;

					init_range(d, options.sec_disc, options.sec_mem);

					if (!(disc_set_read_method (d, options.dump_method))) {
						if (friidump_native_report_is_enabled(&g_native_report))
							friidump_native_report_add_note(&g_native_report, "FriiDump could not select the requested read method; dumping was not attempted.");
						return false;
					}

					if (xbox_output_requested && disc_get_method(d) == 10) {
						fprintf (stderr, "Command............: Xbox direct MMC/SCSI path\n");
						fprintf (stderr, "Method.............: 10 (Xbox READ(10))\n");
					} else {
						if (options.command!=-1) fprintf (stderr, 
							"Command............: %d (forced)\n", disc_get_command(d));
						else fprintf (stderr, 
							"Command............: %d\n", disc_get_command(d));
						if (disc_get_def_method(d)!=disc_get_method(d)) fprintf (stderr, 
							"Method.............: %d (forced)\n", disc_get_method(d));
						else fprintf (stderr, 
							"Method.............: %d\n", disc_get_method(d));
						if (disc_get_hlds_e7_type (d) != 0) {
							fprintf (stderr, "HLDS 0xE7 profile..: %s\n", disc_get_hlds_e7_profile_name (d));
							fprintf (stderr, "HLDS support tier..: %s\n", disc_get_hlds_e7_support_tier (d));
							fprintf (stderr, "HLDS family........: %s\n", disc_get_hlds_e7_family (d));
							fprintf (stderr, "HLDS E7 tokens.....: %s\n", disc_get_hlds_e7_tokens (d));
							fprintf (stderr, "HLDS Stage5B row...: %s\n", disc_get_hlds_e7_record_id (d));
							fprintf (stderr, "Static CDB/gate....: 0x%03x / 0x%08x\n", disc_get_hlds_e7_static_cdb_base (d), disc_get_hlds_e7_static_gate (d));
							fprintf (stderr, "Cache base.........: 0x%08x\n", disc_get_hlds_e7_cache_base (d));
							fprintf (stderr, "Memory windows.....: %u\n", disc_get_hlds_e7_mem_blocks (d));
							if (disc_get_hlds_e7_notes (d) && disc_get_hlds_e7_notes (d)[0])
								fprintf (stderr, "HLDS notes.........: %s\n", disc_get_hlds_e7_notes (d));
						}
					}
					if (options.hlds_profile_report) {
						if (friidump_write_hlds_profile_report(d, options.hlds_profile_report))
							fprintf(stderr, "HLDS profile report: %s\n", options.hlds_profile_report);
					}
					options.dump_method=disc_get_method(d);
					friidump_summary_begin(d);
					friidump_native_set_profile(d, xbox_output_requested);
#ifndef WIN32
					if (!friidump_linux_rawio_preflight(
							d,
							xbox_output_requested,
							drive_supported,
							options.hlds_e7_scan ||
							options.hlds_e7_subcmd_sweep ||
							options.hlds_e7_memrange_sweep))
						return false;
#endif
				if ((options.dump_method==0) 
				|| (options.dump_method==1) || (options.dump_method==2) || (options.dump_method==3)
				|| (options.dump_method==4) || (options.dump_method==5) || (options.dump_method==6)
				){
					fprintf (stderr, 
					"Requested sectors..: %d\n", disc_get_sec_disc(d));
					fprintf (stderr, 
					"Expected sectors...: %d\n", disc_get_sec_mem(d));
				}

				fprintf (stderr, "\nPress Ctrl+C at any time to terminate\n");

				//set speed for 1st time
				if (options.speed != -1) disc_set_speed(d, options.speed * 177);
				if (options.speed != -1) disc_set_streaming_speed(d, options.speed * 177);
//				disc_set_speed(d, 0xffff);

				/* Windows may attach filesystem/autoplay polling to odd GC/Wii discs and
				 * steal the drive during the HLDS 0xE7 seed phase, especially on Type1
				 * GCC-4160N/GCC-4240N.  Reuse the same volume guard mechanism that the
				 * Xbox path already uses, but apply it before disc_init()/seed reads for
				 * all HLDS 0xE7 GC/Wii profiles.  Failure is warning-only because Explorer
				 * may already have a transient handle; the read path itself remains the
				 * authority. */
				if (!xbox_output_requested && disc_get_hlds_e7_type (d) != 0) {
					if (disc_get_hlds_e7_type (d) == 44 || disc_get_hlds_e7_type (d) == 45) {
						fprintf (stderr,
							"\nGDR-8050L modified-firmware warning:\n"
							"  This GC/Wii 0xE7 path assumes a cross-flashed or modified GDR-8050L firmware with 0xE7 memdump support added.\n"
							"  Stock GDR-8050L firmware is still supported for Xbox ripping, but it is not expected to dump GC/Wii discs through this path.\n");
					}
#ifdef WIN32
					fprintf (stderr,
						"\nWindows AutoPlay warning:\n"
						"  HLDS 0xE7 GC/Wii seed reads are sensitive to Windows polling.\n"
						"  Disable AutoPlay for this drive/media and close File Explorer or any \"insert a disc\" dialogs before dumping.\n"
						"  FriiDump will try to lock the volume, but AutoPlay can still interfere before or during seed retrieval.\n");
					fprintf (stderr, "Applying Windows volume lock guard for HLDS 0xE7 seed reads... ");
#else
					fprintf (stderr,
						"\nLinux media-polling warning:\n"
						"  HLDS 0xE7 GC/Wii seed reads can be disturbed by desktop automount and media polling.\n"
						"  Unmount the disc and close file managers or media players before dumping.\n");
#endif
					disc_refresh_volume (d);
					{
						int volume_lock_result = disc_lock_volume (d);
#ifdef WIN32
						if (volume_lock_result < 0)
							fprintf (stderr, "Warning: failed; disable AutoPlay, close File Explorer/AutoPlay dialogs for this drive, then rerun.\n");
						else
							fprintf (stderr, "OK\n");
#else
						if (volume_lock_result > 0)
							fprintf (stderr, "Linux exclusive optical-device lock: unavailable; continuing without one.\n");
						else if (volume_lock_result < 0)
							fprintf (stderr, "Linux exclusive optical-device lock: failed; continuing without one.\n");
						else
							fprintf (stderr, "Linux exclusive optical-device lock: acquired.\n");
#endif
					}
				}

				if (options.xgd1_layout_probe || options.xgd1_raw_id_probe) {
#ifdef WIN32
					int media_rc;
					int media_sense_key;
					int media_asc;
					int media_ascq;
					int probe_status;
					bool stop_ok;
					const char *probe_name;

					probe_name = options.xgd1_raw_id_probe ? "XGD1 raw-sector ID" : "XGD1 logical-boundary";

					if (!disc_is_xbox_challenge_drive (d)) {
						fprintf (stderr,
						         "%s probe currently supports only the GDR-8050L challenge-handshake profile.\n",
						         probe_name);
						return false;
					}

					if (options.xgd1_raw_id_probe &&
					    !(disc_get_hlds_e7_type (d) == 44 ||
					      disc_get_hlds_e7_type (d) == 45 ||
					      disc_get_hlds_e7_type (d) == 442 ||
					      disc_get_hlds_e7_type (d) == 443 ||
					      disc_get_hlds_e7_type (d) == 445)) {
						fprintf (stderr,
						         "--xgd1-raw-id-probe requires the modified GDR-8050L HIT 0xE7 memdump profile.\n");
						return false;
					}

					fprintf (stderr, "\nChecking for ready Xbox media before %s probing... ", probe_name);
					media_rc = disc_media_preflight (d, 15000, &media_sense_key, &media_asc, &media_ascq);
					if (media_rc <= 0) {
						fprintf (stderr,
						         "Failed (sense %02X/%02X/%02X). Insert the disc, wait for spin-up, close AutoPlay/File Explorer, and retry.\n",
						         media_sense_key, media_asc, media_ascq);
						return false;
					}
					fprintf (stderr, "OK\n");

					gettimeofday (&(stats -> start_time), NULL);
					if (options.xgd1_raw_id_probe)
						probe_status = xbox_ref_xgd1_raw_id_probe_with_handle (
								disc_get_native_handle (d), disc_get_device (d), options.xgd1_raw_id_probe_report);
					else
						probe_status = xbox_ref_xgd1_layout_probe_with_handle (
								disc_get_native_handle (d), disc_get_device (d), options.xgd1_layout_probe_report);

					fprintf (stderr, "Issuing STOP UNIT / spin-down after %s probe... ", probe_name);
					stop_ok = disc_stop_unit (d, false);
					fprintf (stderr, "%s\n", stop_ok ? "OK" : "Failed");
					gettimeofday (&(stats -> end_time), NULL);
					fprintf (stderr, "%s probe status: %s\n", probe_name, probe_status == 0 ? "OK" : "FAILED");
					friidump_summary_reset();
					return probe_status == 0;
#else
					fprintf (stderr, "XGD1 probe modes are available only in the Windows build.\n");
					return false;
#endif
				}

				if (options.hlds_e7_scan) {
					out = disc_hlds_e7_scan (d, options.hlds_e7_scan_log, options.hlds_e7_scan_dump_prefix);
					fprintf (stderr, "Issuing STOP UNIT / spin-down after HLDS 0xE7 scan... ");
					fprintf (stderr, "%s\n", disc_stop_unit (d, false) ? "OK" : "Failed");
					return out;
				}

				if (options.hlds_e7_subcmd_sweep) {
					out = disc_hlds_e7_subcmd_sweep (d, options.hlds_e7_scan_log, options.hlds_e7_scan_dump_prefix);
					fprintf (stderr, "Issuing STOP UNIT / spin-down after HLDS 0xE7 subcommand sweep... ");
					fprintf (stderr, "%s\n", disc_stop_unit (d, false) ? "OK" : "Failed");
					return out;
				}

				if (options.hlds_e7_memrange_sweep) {
					out = disc_hlds_e7_memrange_sweep (d, options.hlds_e7_scan_log, options.hlds_e7_scan_dump_prefix);
					fprintf (stderr, "Issuing STOP UNIT / spin-down after HLDS 0xE7 memory-range sweep... ");
					fprintf (stderr, "%s\n", disc_stop_unit (d, false) ? "OK" : "Failed");
					return out;
				}

				{
					int media_rc, media_sense_key, media_asc, media_ascq;
					fprintf (stderr, "\nChecking for ready media before disc seed retrieval... ");
					media_rc = disc_media_preflight (d, 15000, &media_sense_key, &media_asc, &media_ascq);
					if (media_rc <= 0) {
						friidump_native_set_preflight_failure(
							media_rc, media_sense_key, media_asc, media_ascq);
						if (media_rc == 0)
							fprintf (stderr, "No readable disc present (sense %02X/%02X/%02X). Insert a disc, wait for spin-up, and retry.\n", media_sense_key, media_asc, media_ascq);
						else
							fprintf (stderr, "Drive/media not ready (sense %02X/%02X/%02X). Wait for spin-up, close AutoPlay/File Explorer, and retry.\n", media_sense_key, media_asc, media_ascq);
						return false;
					}
					fprintf (stderr, "OK\n");

					struct timeval seed_start, seed_end;
					double seed_elapsed;
					char seed_elapsed_buf[32];

					if (xbox_output_requested)
						fprintf (stderr, "\nInitializing Xbox/XGD disc state... ");
					else
						fprintf (stderr, "\nRetrieving disc seeds, this might take a while... ");

					gettimeofday(&seed_start, NULL);
					if (!disc_init (d, options.disctype, options.sectors_no)) {
						gettimeofday(&seed_end, NULL);
						seed_elapsed = (double)(seed_end.tv_sec - seed_start.tv_sec) +
							(double)(seed_end.tv_usec - seed_start.tv_usec) / (double)USECS_PER_SEC;
						if (seed_elapsed < 0.0) seed_elapsed = 0.0;
						friidump_format_hms(seed_elapsed, seed_elapsed_buf, sizeof(seed_elapsed_buf));
						if (!xbox_output_requested)
							fprintf (stderr, "[Elapsed:%s] ", seed_elapsed_buf);
						if (g_validation_summary.active) {
							g_validation_summary.have_seed_duration = true;
							g_validation_summary.seed_duration = seed_elapsed;
						}
						if (friidump_native_report_is_enabled(&g_native_report)) {
							if (g_native_cancel_requested) {
								friidump_native_report_set_seed(&g_native_report, true, "fail", true, seed_elapsed);
								friidump_native_report_set_outcome(&g_native_report, "other", "fail");
								friidump_native_report_add_note(&g_native_report, "The invocation was cancelled by the user during media initialization or seed retrieval.");
							} else if (xbox_output_requested) {
								friidump_native_report_set_seed(&g_native_report, false, "not_applicable", false, 0.0);
								friidump_native_report_set_outcome(&g_native_report, "other", "fail");
								friidump_native_report_add_note(&g_native_report, "Xbox/media initialization failed before dumping.");
							} else {
								friidump_native_report_set_seed(&g_native_report, true, "fail", true, seed_elapsed);
								friidump_native_report_set_outcome(&g_native_report, "seed_only", "fail");
								friidump_native_report_add_note(&g_native_report, "Disc initialization or seed retrieval failed.");
							}
							friidump_native_report_set_dump(&g_native_report, false, "not_attempted", false, 0, false, 0, false, 0.0, NULL, false, 0, NULL);
						}
						if (!xbox_output_requested) {
							bool stop_ok;
							friidump_capture_seed_diagnostic (d);
							stop_ok = disc_stop_unit (d, false);
							fprintf (stderr, "\nIssuing STOP UNIT / spin-down after seed failure... %s\n", stop_ok ? "OK" : "Failed");
							if (g_validation_summary.active) {
								g_validation_summary.stop_attempted = true;
								g_validation_summary.stop_ok = stop_ok;
							}
							if (friidump_native_report_is_enabled(&g_native_report))
								friidump_native_report_add_note(&g_native_report,
									stop_ok ? "STOP UNIT after seed failure succeeded." : "STOP UNIT after seed failure failed.");
						}
						fprintf (stderr, "Failed\n");
						out = false;
					} else {
						gettimeofday(&seed_end, NULL);
						seed_elapsed = (double)(seed_end.tv_sec - seed_start.tv_sec) +
							(double)(seed_end.tv_usec - seed_start.tv_usec) / (double)USECS_PER_SEC;
						if (seed_elapsed < 0.0) seed_elapsed = 0.0;
						friidump_format_hms(seed_elapsed, seed_elapsed_buf, sizeof(seed_elapsed_buf));
						if (!xbox_output_requested)
							fprintf (stderr, "[Elapsed:%s] ", seed_elapsed_buf);
						fprintf (stderr, "OK\n");
						if (g_validation_summary.active) {
							g_validation_summary.seed_ok = true;
							g_validation_summary.have_seed_duration = true;
							g_validation_summary.seed_duration = seed_elapsed;
						}
					disc_get_type (d, &type_id, &type);
					disc_get_gameid (d, &game_id);
					disc_get_region (d, NULL, &region);
					disc_get_maker (d, &maker_id, &maker);
					disc_get_version (d, NULL, &version);
					disc_get_title (d, &title);
					if (g_validation_summary.active) {
						friidump_summary_copy(g_validation_summary.disc_type, sizeof(g_validation_summary.disc_type), type);
						friidump_summary_copy(g_validation_summary.game_id, sizeof(g_validation_summary.game_id), game_id);
						friidump_summary_copy(g_validation_summary.title, sizeof(g_validation_summary.title), title);
						g_validation_summary.sectors = disc_get_sectors_no(d);
					}
					friidump_native_set_media(type_id, title,
							type_id == DISC_TYPE_XBOX ? NULL : region, game_id);
					if (friidump_native_report_is_enabled(&g_native_report)) {
						if (type_id == DISC_TYPE_GAMECUBE || type_id == DISC_TYPE_WII || type_id == DISC_TYPE_WII_DL)
							friidump_native_report_set_seed(&g_native_report, true, "pass", true, seed_elapsed);
						else
							friidump_native_report_set_seed(&g_native_report, false, "not_applicable", false, 0.0);
					}
					if (g_native_cancel_requested) {
						friidump_native_report_set_dump(&g_native_report, false, "not_attempted", false, 0, false, 0, false, 0.0, NULL, false, 0, NULL);
						friidump_native_report_set_outcome(&g_native_report, "other", "fail");
						friidump_native_report_add_note(&g_native_report, "The invocation was cancelled by the user before dumping began.");
						return false;
					}
					fprintf (stderr, 
						"\n"
						"Disc information:\n"
						"----------------------------------------------------------------------\n");

					if (options.disctype!=-1) fprintf (stderr, 
						"Disc type..........: %s (forced)\n", type);
					else fprintf (stderr, 
						"Disc type..........: %s\n", type);
					if (options.sectors_no!=-1) fprintf (stderr, 
						"Disc size..........: %d (forced)\n", disc_get_sectors_no(d));
					else fprintf (stderr, 
						"Disc size..........: %d\n", disc_get_sectors_no(d));

					if (disc_get_layerbreak(d)>0 && type_id==DISC_TYPE_DVD) fprintf (stderr, 
						"Layer break........: %d\n", disc_get_layerbreak(d));

					if ((type_id==DISC_TYPE_GAMECUBE) || (type_id==DISC_TYPE_WII) || (type_id==DISC_TYPE_WII_DL)) fprintf (stderr, 
						"Game ID............: %s\n"
						"Region.............: %s\n"
						"Maker..............: %s - %s\n"
						"Version............: %s\n"
						"Game title.........: %s\n", game_id, region, maker_id, maker, version, title
					);

					if (type_id == DISC_TYPE_WII || type_id == DISC_TYPE_WII_DL)
						fprintf (stderr, "Contains update....: %s\n" , disc_get_update (d) ? "Yes" : "No");
					fprintf (stderr, "\n");
					
					disc_set_unscrambling (d, !options.no_unscrambling);

					if (type_id <= DISC_TYPE_DVD)
						unscrambler_set_disctype (type_id);

					if (options.autodump) {
						snprintf (tmp, sizeof (tmp), "%s.iso", title);
						my_strdup (options.iso_out, tmp);
						options.iso_requested = true;
					}

					/* Xbox/GDR-8050L supports default XBE/DMI-derived names.
					 * Use an empty-string placeholder so the copied reference path can
					 * derive Title[MediaID].iso/.xiso after the first unlock/XBE probe. */
					if (type_id == DISC_TYPE_XBOX && options.iso_requested && !options.iso_out)
						my_strdup (options.iso_out, "");
					if (type_id == DISC_TYPE_XBOX && options.xiso_requested && !options.xiso_out)
						my_strdup (options.xiso_out, "");

					//set speed 2nd time after rotation is started and some sectors read
					if (options.speed != -1) disc_set_streaming_speed(d, options.speed * 177);
					if (options.speed != -1) disc_set_speed(d, options.speed * 177);

					/* If at least an output file was specified, proceed dumping, otherwise stop here */
					if (options.xiso_requested) {
						if (type_id != DISC_TYPE_XBOX) {
							fprintf (stderr, "Xbox XISO output requires Xbox/XGD disc type. Use -T 4 or an Xbox-capable drive/disc.\n");
							out = false;
						} else {
							fprintf (stderr, options.xiso_out && options.xiso_out[0] ? "Writing to file \"%s\" in Xbox XISO format\n\n" : "Writing Xbox XISO using XBE-derived filename (override with -X <file>)\n\n", (options.xiso_out && options.xiso_out[0]) ? options.xiso_out : "");

							dmp = dumper_new (d);
							if (friidump_native_report_is_enabled(&g_native_report))
								dumper_set_cancel_callback(dmp, friidump_native_cancel_callback, NULL);
							dumper_set_hashing (dmp, !options.no_hashing);
							dumper_set_flushing (dmp, !options.no_flushing);

							if (!dumper_set_xiso_output_file (dmp, options.xiso_out, options.resume)) {
								fprintf (stderr, "Cannot setup Xbox XISO output file\n");
							} else if (!dumper_prepare_xiso (dmp)) {
								fprintf (stderr, "Cannot prepare Xbox XISO dumper");
							} else {
								if (options.gui)
									dumper_set_progress_callback (dmp, (progress_func) progress_for_guis, stats);
								else
									dumper_set_progress_callback (dmp, (progress_func) progress, stats);

								dump_attempted = true;
								if (g_validation_summary.active)
									g_validation_summary.dump_attempted = true;
								out = dumper_dump_xiso (dmp, &current_sector);
								gettimeofday(&(stats -> end_time), NULL);
								if (out) {
									fprintf (stderr, "Xbox XISO dump completed successfully!\n");
									if (!options.no_hashing && !friidump_uses_windows_xbox_reference_path(d, type_id))
										fprintf (stderr,
										"XISO image hashes:\n"
										"CRC32...: %s\n"
										"MD5.....: %s\n"
										"SHA-1...: %s\n"
										"SHA-256.: %s\n",
										dumper_get_xiso_crc32 (dmp), dumper_get_xiso_md5 (dmp),
										dumper_get_xiso_sha1 (dmp), dumper_get_xiso_sha2 (dmp)
									);
								out = true;
								if (g_validation_summary.active)
									g_validation_summary.dump_ok = true;
								} else {
									if (dumper_was_cancelled(dmp))
										fprintf (stderr, "\nXbox XISO dump cancelled at output sector: %u\n", current_sector);
									else
										fprintf (stderr, "\nXbox XISO dump failed at output sector: %u\n", current_sector);
									out = false;
									if (g_validation_summary.active) {
										g_validation_summary.dump_ok = false;
										g_validation_summary.fail_sector = current_sector;
									}
								}
							}

							if (dump_attempted)
								friidump_native_capture_output(dmp, type_id, true, out, dumper_was_cancelled(dmp), current_sector, disc_get_sectors_no(d));
							else if (friidump_native_report_is_enabled(&g_native_report)) {
								friidump_native_report_set_outcome(&g_native_report, "other", "fail");
								friidump_native_report_add_note(&g_native_report, "Xbox XISO output could not be prepared; dumping was not attempted.");
							}
							dmp = dumper_destroy (dmp);
						}
						} else if (options.raw_out || options.iso_requested) {
							if (type_id == DISC_TYPE_XBOX && options.raw_out)
								fprintf (stderr, "Xbox/XGD output does not support -r/raw together with the redump-style ISO path. Use -i or -X.\n");
							else if (options.raw_out)
								fprintf (stderr, "Writing to file \"%s\" in raw format\n", options.raw_out);
							if (options.iso_out) {
								if (type_id == DISC_TYPE_XBOX)
									fprintf (stderr, options.iso_out && options.iso_out[0] ? "Writing to file \"%s\" in Xbox/XGD redump-style ISO format\n" : "Writing Xbox/XGD redump-style ISO using XBE-derived filename (override with -i <file>)\n", (options.iso_out && options.iso_out[0]) ? options.iso_out : "");
								else
									fprintf (stderr, "Writing to file \"%s\" in ISO format\n", options.iso_out);
							}
							fprintf (stderr, "\n");

						dmp = dumper_new (d);
						if (friidump_native_report_is_enabled(&g_native_report))
							dumper_set_cancel_callback(dmp, friidump_native_cancel_callback, NULL);

						dumper_set_hashing (dmp, !options.no_hashing);
						dumper_set_flushing (dmp, !options.no_flushing);

						if (!dumper_set_raw_output_file (dmp, options.raw_out, options.resume)) {
							fprintf (stderr, "Cannot setup raw output file\n");
						} else if (!dumper_set_iso_output_file (dmp, options.iso_out, options.resume)) {
							fprintf (stderr, "Cannot setup ISO output file\n");
						} else if (!dumper_prepare (dmp)) {
							fprintf (stderr, "Cannot prepare dumper");
						} else {
//	 						fprintf (stderr, "Starting dump process from sector %u...\n", dmp -> start_sector);
//		 					opdd.start_sector = options.start_sector;

							if (options.gui)
								dumper_set_progress_callback (dmp, (progress_func) progress_for_guis, stats);
							else
								dumper_set_progress_callback (dmp, (progress_func) progress, stats);

							dump_attempted = true;
							if (g_validation_summary.active)
								g_validation_summary.dump_attempted = true;
							out = dumper_dump (dmp, &current_sector);
							gettimeofday(&(stats -> end_time), NULL);
							if (out) {
								fprintf (stderr, "Dump completed successfully!\n");
								if (!options.no_hashing && options.raw_out)
									fprintf (stderr,
										"Raw image hashes:\n"
										"CRC32...: %s\n"
										//"MD4.....: %s\n"
										"MD5.....: %s\n"
										"SHA-1...: %s\n"
										"SHA-256.: %s\n"
										/*"ED2K....: %s\n"*/,
										dumper_get_raw_crc32 (dmp), /*dumper_get_raw_md4 (dmp),*/ dumper_get_raw_md5 (dmp),
										dumper_get_raw_sha1 (dmp), dumper_get_raw_sha2 (dmp)/*, dumper_get_raw_ed2k (dmp)*/
									);
								if (!options.no_hashing && options.iso_out && !friidump_uses_windows_xbox_reference_path(d, type_id))
									fprintf (stderr,
										"ISO image hashes:\n"
										"CRC32...: %s\n"
										//"MD4.....: %s\n"
										"MD5.....: %s\n"
										"SHA-1...: %s\n"
										"SHA-256.: %s\n"
										/*"ED2K....: %s\n"*/,
										dumper_get_iso_crc32 (dmp), /*dumper_get_iso_md4 (dmp),*/ dumper_get_iso_md5 (dmp),
										dumper_get_iso_sha1 (dmp), dumper_get_iso_sha2 (dmp)/*, dumper_get_iso_ed2k (dmp)*/
									);

								if (type_id == DISC_TYPE_XBOX && disc_is_xbox_challenge_drive (d) && options.iso_requested)
									friidump_verify_redump_xbox_reference(dmp);
								else if (options.iso_out)
									friidump_verify_redump_iso(dmp, type_id);

								out = true;
								if (g_validation_summary.active)
									g_validation_summary.dump_ok = true;
							} else {
								if (g_validation_summary.active) {
									g_validation_summary.dump_ok = false;
									g_validation_summary.fail_sector = current_sector;
								}
								if (dumper_was_cancelled(dmp))
									fprintf (stderr, "\nDump cancelled at sector: %u\n", current_sector);
								else if (current_sector == 0xFFFFFFFFU)
									fprintf (stderr, "\nXbox reference dumper failed; see the Xbox-specific message above.\n");
								else
									fprintf (stderr, "\nDump failed at sectors: %u..%u\n", current_sector, current_sector+15);
								out = false;
								//disc_stop_unit (d, 0);
							}
						}

						if (dump_attempted)
							friidump_native_capture_output(dmp, type_id, false, out, dumper_was_cancelled(dmp), current_sector, disc_get_sectors_no(d));
						else if (friidump_native_report_is_enabled(&g_native_report)) {
							friidump_native_report_set_outcome(&g_native_report, "other", "fail");
							friidump_native_report_add_note(&g_native_report, "Dump output could not be prepared; dumping was not attempted.");
						}
						dmp = dumper_destroy (dmp);
					} else {
						fprintf (stderr, "No output file for dumping specified, please take a look at the -i, -r, -X and -a options\n");
					}
				}
			}
	if (dump_attempted) {
		bool stop_ok = disc_stop_unit (d, false);
		fprintf (stderr, "\nIssuing STOP UNIT / spin-down after dump attempt... %s\n", stop_ok ? "OK" : "Failed");
		if (g_validation_summary.active) {
			g_validation_summary.stop_attempted = true;
			g_validation_summary.stop_ok = stop_ok;
		}
	}

	return out;
}


static int try_all_methods (progstats *stats) {
	u_int32_t saved_command;
	int saved_method;
	u_int32_t command;
	int method;
	int out;
	disc *d;

	saved_command = options.command;
	saved_method = options.dump_method;
	out = false;

	fprintf (stderr, "Trying all command/method combinations... This will take a LOOOONG time and generate an insanely long console output :p\n");

	for (command = 0; command <= 4 && !out; command++) {
		for (method = 0; method <= 10 && !out; method++) {
			options.command = command;
			options.dump_method = method;
			memset (stats, 0, sizeof (*stats));

			fprintf (stderr, "\nTrying with command %u, method %d\n", command, method);
			fprintf (stderr, "Initializing DVD drive... ");

			d = disc_new (options.device, options.command);
			if (!d) {
				fprintf (stderr, "Failed\n");
#ifndef WIN32
				fprintf (stderr,
					"Probably you do not have access to the DVD device. Ask the administrator\n"
					"to add you to the proper device-access group. Do not run FriiDump as root.\n"
				);
#endif
				continue;
			}

			fprintf (stderr, "OK\n");
			out = dologic (d, stats);
			d = disc_destroy (d);

			if (out)
				fprintf (stderr, "Command %u and method %d combination worked!\n", command, method);
		}
	}

	if (!out) {
		options.command = saved_command;
		options.dump_method = saved_method;
	}

	return out;
}

int main (int argc, char *argv[]) {
	disc *d;
	progstats stats;
	double duration;
	bool have_duration;
	int out, ret;
	unscrambler *u;
	unscrambler_progress_func pfunc;
	u_int32_t current_sector;

	/* First of all... */
	drop_euid ();
	friidump_native_report_init(&g_native_report, PACKAGE_VERSION);
	friidump_init_executable_dir((argc > 0) ? argv[0] : NULL);
	xbox_ref_log_open_for_target(NULL, 0);
	
	welcome ();

	memset (&stats, 0, sizeof (stats));
	d = NULL;
	out = false;
	ret = EXIT_FAILURE;
	if (optparse (argc, argv)) {
		friidump_native_configure();
		friidump_retarget_log_from_options();
		if (options.device) {
			if (options.allmethods) {
				out = try_all_methods (&stats);
			} else {
				/* Dump DVD to file */
				fprintf (stderr, "Initializing DVD drive... ");

				if (!(d = disc_new (options.device, options.command))) {
					fprintf (stderr, "Failed\n");
#ifndef WIN32
					fprintf (stderr,
						"Probably you do not have access to the DVD device. Ask the administrator\n"
						"to add you to the proper device-access group. Do not run FriiDump as root.\n"
					);
#endif
				} else {
					fprintf (stderr, "OK\n");
					friidump_native_set_drive(d);
					out = dologic (d, &stats);
					d = disc_destroy (d);
				}
			}
		} else if (options.raw_in) {
			/* Convert raw image to ISO format */
			u = unscrambler_new ();
			
			if (options.gui)
				pfunc = (unscrambler_progress_func) progress_for_guis;
			else
				pfunc = (unscrambler_progress_func) progress;

			if ((out = unscrambler_unscramble_file (u, options.raw_in, options.iso_out, pfunc, &stats, &current_sector)))
				fprintf (stderr, "Unscrambling completed successfully!\n");
			else
				fprintf (stderr, "\nUnscrambling failed at sectors: %u..%u\n", current_sector, current_sector+15);

			u = unscrambler_destroy (u);
		} else {
			MY_ASSERT (0);
		}

		duration = 0.0;
		have_duration = friidump_elapsed_from_stats(&stats, &duration);
		if (g_operation_duration_override_valid) {
			duration = g_operation_duration_override;
			have_duration = true;
		}

		if (out) {
			if (have_duration)
				fprintf (stderr, "Operation took %.2f seconds\n", duration);
			friidump_print_validation_summary(duration, have_duration);
			ret = EXIT_SUCCESS;
		} else {
			friidump_print_validation_summary(duration, have_duration);
			ret = EXIT_FAILURE;
		}

		friidump_native_publish(duration, have_duration);

		my_free (options.device);
		my_free (options.iso_out);
		my_free (options.xiso_out);
		my_free (options.raw_out);
		my_free (options.raw_in);
		my_free (options.hlds_profile_report);
		my_free (options.xgd1_layout_probe_report);
		my_free (options.xgd1_raw_id_probe_report);
		my_free (options.native_report_json);
		my_free (options.native_report_dir);
		my_free (options.firmware_modified_note);
	}

	if (xbox_ref_log_path())
		fprintf (stderr, "Log file complete: %s\n", xbox_ref_log_path());
	xbox_ref_log_close();

	return (ret);
}
