/* Extended Module Player
 * Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr
 *
 * This file is part of the Extended Module Player and is distributed
 * under the terms of the GNU General Public License. See the COPYING
 * file for more information.
 */

#include "common.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef HAVE_SIGNAL_H
#include <signal.h>
#endif
#ifdef HAVE_SYS_TIME_H
#include <sys/time.h>
#endif
#if defined(XMP_AMIGA)
#include <proto/timer.h>
#include <time.h>
#include "main_amiga.h"
#endif
#if defined(__WATCOMC__)
#include <time.h>
#endif
#ifdef HAVE_SYS_TYPES_H
#include <sys/types.h>
#endif
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <stdarg.h>
#if defined(HAVE_GETOPT_H) && defined(HAVE_GETOPT_LONG)
#include <getopt.h>
#else
#include "getopt_long.h"
#endif
#include <errno.h>

#include "sound.h"
#include "xmp_version.h"

#ifdef _WIN32
#include <windows.h>
#endif

static const struct sound_driver *sound;
static unsigned int foreground_in, foreground_out;
static int refresh_status;

#ifdef HAVE_SIGNAL_H
static void cleanup(int sig)
{
	signal(SIGTERM, SIG_DFL);
	signal(SIGINT, SIG_DFL);
#ifdef SIGQUIT
	signal(SIGQUIT, SIG_DFL);
#endif
	signal(SIGFPE, SIG_DFL);
	signal(SIGSEGV, SIG_DFL);

	/* Don't deinit from signal handler
	 * (https://github.com/cmatsuoka/xmp-cli/issues/5)
	 * sound->deinit();
	 */

	reset_tty();

	signal(sig, SIG_DFL);
#ifdef HAVE_KILL
	kill(getpid(), sig);
#endif
}
#endif

#ifdef SIGTSTP
static void sigtstp_handler(int n)
{
	report("\n");
	signal(SIGTSTP, SIG_DFL);
#ifdef HAVE_KILL
	kill(getpid(), SIGTSTP);
#endif
}

static void sigcont_handler(int sig)
{
#ifdef HAVE_TERMIOS_H
	unsigned int old_in = foreground_in;

	foreground_in  = tcgetpgrp(STDIN_FILENO)  == getpgrp();
	foreground_out = tcgetpgrp(STDERR_FILENO) == getpgrp();

	if (old_in != foreground_in) {
		/* Only call if it was not already prepared */
		set_tty();
	}
#else
	foreground_in = foreground_out = 1;
	#if defined(XMP_AMIGA)
	set_tty();	/* amiga version -- sets stdin to raw mode. */
	#endif
#endif

	if (sig != 0)
		refresh_status = 1;

	signal(SIGCONT, sigcont_handler);
	signal(SIGTSTP, sigtstp_handler);
}
#endif

static void show_info(int what, const struct xmp_module_info *mi, int mode)
{
	report("\r%78.78s\n", " ");
	switch (what) {
	case '?':
		info_help();
		break;
	case 'i':
		info_ins_smp(mi);
		break;
	case 'I':
		info_instruments(mi);
		break;
	case 'S':
		info_samples(mi);
		break;
	case 'c':
		info_comment(mi);
		break;
	case 'm':
		info_mod(mi, mode);
		break;
	}
}

static void shuffle(int argc, char **argv)
{
	int i, j;
	char *x;

	for (i = 1; i < argc; i++) {
		j = 1 + rand() % (argc - 1);
		x = argv[i];
		argv[i] = argv[j];
		argv[j] = x;
	}
}

static void check_pause(xmp_context xc, struct control *ctl,
	const struct xmp_module_info *mi, const struct xmp_frame_info *fi, int verbose)
{
	if (ctl->pause) {
		sound->pause();
		if (verbose) {
			info_frame(mi, fi, ctl, 1);
		}
		while (ctl->pause) {
			delay_ms(100);
			read_command(xc, mi, ctl);
			if (ctl->display) {
				if (verbose) {
					int mode = xmp_get_player(xc, XMP_PLAYER_MODE);
					show_info(ctl->display, mi, mode);
					info_frame(mi, fi, ctl, 1);
				}
				ctl->display = 0;
			}
		}
		sound->resume();
	}
}

static void get_mixer_type(int t, struct options *opt, char *buf, size_t size)
{
	const char *fmt;

	fmt = opt->format & XMP_FORMAT_MONO ? "mono" : "stereo";

	switch (t) {
	case XMP_MIXER_STANDARD: {
		const char *itp;
		switch (opt->interp) {
		case XMP_INTERP_NEAREST:
			itp = "non-interpolated";
			break;
		case XMP_INTERP_LINEAR:
			itp = "linear interpolated";
			break;
		case XMP_INTERP_SPLINE:
			itp = "cubic spline interpolated";
			break;
		default:
			itp = "unknown interpolation";
		}
		snprintf(buf, size, "%s %s mixer", fmt, itp);
		break; }
	case XMP_MIXER_A500:
		snprintf(buf, size, "Amiga 500 %s mixer", fmt);
		break;
	case XMP_MIXER_A500F:
		snprintf(buf, size, "Amiga 500 %s mixer (led filter: on)", fmt);
		break;
	default:
		snprintf(buf, size, "unknown %s mixer", fmt);
	}
}

static void load_error(const char *name, const char *filename, int val)
{
	const char *msg;

	val = -val;
	switch (val) {
	case XMP_ERROR_FORMAT:
		msg = "Unrecognized file format";
		break;
	case XMP_ERROR_DEPACK:
		msg = "Error depacking file";
		break;
	case XMP_ERROR_LOAD:
		msg = "Error loading module";
		break;
	case XMP_ERROR_SYSTEM:
		msg = strerror(errno);
		break;
	default:
		msg = "Unknown error";
	}

	fprintf(stderr, "%s: %s: %s\n", name, filename, msg);
}


int main(int argc, char **argv)
{
	xmp_context xc;
	struct xmp_module_info mi;
	struct xmp_frame_info fi;
	struct options opt, save_opt;
	struct control control;
	int i;
	int first;
	int skipprev;
	FILE *f = NULL;
	int val, lf_flag;
	int flags;
	int played;
#if defined(_WIN32)
	setvbuf(stderr, NULL, _IONBF, 0);
	srand(GetTickCount());
#elif defined(__WATCOMC__)
	srand(time(NULL));
#elif defined(__amigaos4__)
	struct TimeVal tv;
	amiga_inittimer();
	amiga_getsystime(&tv);
	srand(tv.Microseconds);
#elif defined(XMP_AMIGA)
	struct timeval tv;
	amiga_inittimer();
	amiga_getsystime(&tv);
	srand(tv.tv_micro);
#else
	struct timeval tv;
	gettimeofday(&tv, NULL);
	srand(tv.tv_usec);
#endif

	memset(&opt, 0, sizeof (struct options));
	memset(&control, 0, sizeof (struct control));

	/* set defaults */
	opt.verbose = 1;
	opt.rate = 44100;
	opt.mix = -1;
	opt.defpan = 50;
	opt.numvoices = 128;
	opt.driver_id = NULL;
	opt.interp = XMP_INTERP_SPLINE;
	opt.dsp = XMP_DSP_LOWPASS;
	opt.player_mode = XMP_MODE_AUTO;
	opt.amplify = 1;
	for (i = 0; i < XMP_MAX_CHANNELS; i++) {
		opt.mute[i] = -1; /* Use module default mute/unmute state */
	}

	/* read configuration file */
	if (!opt.norc) {
		read_config(&opt);
	}

	get_options(argc, argv, &opt);

	if (!opt.probeonly && optind >= argc) {
		fprintf(stderr, "%s: no modules to play\n"
			"Use `%s --help' for more information.\n",
			argv[0], argv[0]);
		exit(EXIT_FAILURE);
	}

	if (opt.interp < 0) {
		fprintf(stderr, "%s: unknown interpolation type\n"
			"Use `%s --help' for more information.\n",
			argv[0], argv[0]);
		exit(EXIT_FAILURE);
	}

	if (opt.player_mode < 0) {
		fprintf(stderr, "%s: unknown player mode\n"
			"Use `%s --help' for more information.\n",
			argv[0], argv[0]);
		exit(EXIT_FAILURE);
	}

	if (opt.silent) {
		opt.driver_id = "null";
	}

	sound = select_sound_driver(&opt);

	if (sound == NULL) {
		fprintf(stderr, "%s: can't initialize sound", argv[0]);
		if (opt.driver_id != NULL) {
			fprintf(stderr, " (driver = %s)", opt.driver_id);
		}
		fprintf(stderr, "\n");

		if (f != NULL) {
			fclose(f);
		}
		exit(EXIT_FAILURE);
	}

	if (opt.verbose > 0) {
		report("Extended Module Player " VERSION "\n"
			"Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr\n");

		report("Using %s\n", sound->description());

		report("Mixer set to %d Hz, %dbit, %s%s%s%s\n", opt.rate,
		    get_bits_from_format(&opt),
		    get_signed_from_format(&opt) ? "signed " : "unsigned ",
		    opt.interp == XMP_INTERP_LINEAR ? "linear interpolated " :
		    opt.interp == XMP_INTERP_SPLINE ? "cubic spline interpolated " : "",
		    get_channels_from_format(&opt) == 1 ? "mono" : "stereo",
		    opt.dsp & XMP_DSP_LOWPASS ? "" : " (no filter)");

		report("Press 'h' for help\n\n");
	}

	if (opt.probeonly) {
		exit(EXIT_SUCCESS);
	}

#ifdef HAVE_SIGNAL_H
	signal(SIGTERM, cleanup);
	signal(SIGINT, cleanup);
	signal(SIGFPE, cleanup);
	signal(SIGSEGV, cleanup);
#ifdef SIGQUIT
	signal(SIGQUIT, cleanup);
#endif
#ifdef SIGTSTP
	signal(SIGCONT, sigcont_handler);
	signal(SIGTSTP, sigtstp_handler);
	sigcont_handler(0);
#endif
#endif

	xc = xmp_create_context();
	if (xc == NULL) {
		perror(argv[0]);
		exit(EXIT_FAILURE);
	}

	skipprev = 0;

	if (opt.ins_path) {
		xmp_set_instrument_path(xc, opt.ins_path);
	}

	lf_flag = 0;
	memcpy(&save_opt, &opt, sizeof (struct options));

	control.loop = opt.loop;
	control.explore = opt.explore;
	control.amiga_mixer = opt.amiga_mixer;
	first = optind;

    play_all:
	if (opt.random) {
		shuffle(argc - optind + 1, &argv[optind - 1]);
	}

	for (played = 0; optind < argc; optind++) {
		memcpy(&opt, &save_opt, sizeof (struct options));

		if (opt.verbose > 0) {
			if (lf_flag)
				report("\n");
			lf_flag = 1;
			report("Loading %s (%d of %d)\n",
				argv[optind], optind - first + 1, argc - first);
		}

		/* these must be set before loading the module */
		xmp_set_player(xc, XMP_PLAYER_DEFPAN, opt.defpan);

		/* load module */

		val = xmp_load_module(xc, argv[optind]);

		if (val < 0) {
			load_error(argv[0], argv[optind], val);

			if (skipprev) {
				optind -= 2;
				if (optind < (first - 1)) {
					optind += 2;
					skipprev = 0;
				}
			}
			continue;
		}

		xmp_get_module_info(xc, &mi);
		if (!opt.norc) {
			read_modconf(&opt, mi.md5);
		}


		/* these must be set before xmp_start_player() */
		xmp_set_player(xc, XMP_PLAYER_VOICES, opt.numvoices);

		/* check sequence */

		if (opt.sequence) {
			if (opt.sequence < mi.num_sequences) {
				if (mi.seq_data[opt.sequence].duration > 0) {
					opt.start = mi.seq_data[opt.sequence].entry_point;
				}
			} else {
				fprintf(stderr, "%s: sequence %d doesn't exist in this module\n",
					argv[0], opt.sequence);
				if (skipprev) {
					optind -= 2;
					if (optind < first) {
						optind += 2;
					}
				}
				continue;
			}
		}
		control.sequence = opt.sequence;

		skipprev = 0;
		control.time = 0.0;

		/* Play module */

		if (xmp_start_player(xc, opt.rate, opt.format) == 0) {
			xmp_set_player(xc, XMP_PLAYER_INTERP, opt.interp);
			xmp_set_player(xc, XMP_PLAYER_DSP, opt.dsp);
			if (opt.player_mode != XMP_MODE_AUTO) {
				xmp_set_player(xc, XMP_PLAYER_MODE,
							opt.player_mode);
			}

			played = 1;

			xmp_set_player(xc, XMP_PLAYER_AMP, opt.amplify);

			if (opt.mix >= 0) {
				xmp_set_player(xc, XMP_PLAYER_MIX, opt.mix);
			}

			if (opt.reverse) {
				int mix;
				mix = xmp_get_player(xc, XMP_PLAYER_MIX);
				xmp_set_player(xc, XMP_PLAYER_MIX, -mix);
			}

			xmp_set_position(xc, opt.start);

			/* Mute channels */

			for (i = 0; i < XMP_MAX_CHANNELS; i++) {
				/* Only mute/unmute if an option has been
				 * provided explicitly (to avoid unmuting
				 * disabled channels in IT et al.) */
				if (opt.mute[i] >= 0) {
					xmp_channel_mute(xc, i, opt.mute[i]);
				}
			}

			/* Set player flags */

#define set_flag(x,y,z) do { \
  if ((y) > 0) (x) |= (z); \
  else if ((y) < 0) (x) &= ~ (z); \
} while (0)

			flags = xmp_get_player(xc, XMP_PLAYER_CFLAGS);
			set_flag(flags, opt.vblank, XMP_FLAGS_VBLANK);
			set_flag(flags, opt.fx9bug, XMP_FLAGS_FX9BUG);
			set_flag(flags, opt.fixloop, XMP_FLAGS_FIXLOOP);
			set_flag(flags, control.amiga_mixer, XMP_FLAGS_A500);
			xmp_set_player(xc, XMP_PLAYER_CFLAGS, flags);

			/* Show module data */

			if (opt.verbose > 0) {
				int mode = xmp_get_player(xc, XMP_PLAYER_MODE);
				info_mod(&mi, mode);
			}

			if (opt.verbose > 1) {
				info_instruments(&mi);
			}

			if (opt.show_comment) {
				if (mi.comment != NULL) {
					report("\n");
					info_comment(&mi);
				}
			}

			/* Play sequence */

			refresh_status = 1;
			info_frame_init();
			fi.loop_count = 0;
    play_sequence:
			while (!opt.info && xmp_play_frame(xc) == 0) {
				int old_loop = fi.loop_count;

				xmp_get_frame_info(xc, &fi);
				control.mixer_type = xmp_get_player(
						xc, XMP_PLAYER_MIXER_TYPE);

				/* Check loop */

				if (old_loop != fi.loop_count) {
					if (control.loop == 1 ||
					    (opt.loop_time > 0 &&
					     control.time < opt.loop_time)) {
						info_message("Loop sequence %d", control.sequence);
					} else {
						break;
					}
				}

#ifdef SIGTSTP
				sigcont_handler(0);
#else
				foreground_in = foreground_out = 1;
#endif
				if (foreground_out && opt.verbose > 0) {
					info_frame(&mi, &fi, &control, refresh_status);
					refresh_status = 0;
				}

				control.time += 1.0 * fi.frame_time / 1000;

				sound->play(fi.buffer, fi.buffer_size);

				if (foreground_in && !opt.nocmd) {
					read_command(xc, &mi, &control);

					if (control.display) {
						int mode = xmp_get_player(xc,
							XMP_PLAYER_MODE);
						show_info(control.display, &mi,
									mode);
						control.display = 0;
						refresh_status = 1;
					}

					switch (control.cur_info) {
					case 'X': {
						char buf[80];
						get_mixer_type(control.mixer_type, &opt, buf, 80);
						info_message("Mixer type: %s", buf);
						control.cur_info = 0;
						break; }
					case 'Z':
						info_message("Current sequence: %d (start at position %02X)", control.sequence, mi.seq_data[control.sequence].entry_point);
						control.cur_info = 0;
						break;
					}
				}

				/* Check maximum replay time */

				if (opt.max_time > 0 &&
				    control.time > opt.max_time) {
					break;
				}

				check_pause(xc, &control, &mi, &fi,
							opt.verbose);

				opt.start = 0;
			}

			/* Subsong explorer */

			if (control.explore && control.skip == 0) {
				control.sequence++;
				if (control.sequence < mi.num_sequences) {
					xmp_set_position(xc, mi.seq_data
						[control.sequence].entry_point);
					info_message("Playing sequence %d",
						control.sequence);
					goto play_sequence;
				}
			}

			xmp_end_player(xc);
		}

		xmp_release_module(xc);

		if (!opt.info) {
			report("\n");
		}

		if (control.skip == -1) {
			optind -= optind > first ? 2 : 1;
			skipprev = 1;
		} else if (control.skip == -2) {
			goto end;
		}
		control.skip = 0;
	}

	if (control.loop == 2 && played) {
		info_message("Restart module list");
		optind = first;
		goto play_all;
	}

	sound->flush();

    end:
	xmp_free_context(xc);

	if (foreground_in)
		reset_tty();

	sound->deinit();

	return 0;
}
