/**
 * mice - 3DS Music Player
 * Copyright (C) 2025 sillyangel
 *
 * This program comes with ABSOLUTELY NO WARRANTY and is free software. You are
 * welcome to redistribute it under certain conditions; for details see the
 * LICENSE file.
 */

#include <3ds.h>
#include <dirent.h>
#include <stdbool.h>
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "all.h"
#include "error.h"
#include "file.h"
#include "main.h"
#include "metadata.h"
#include "playback.h"
#include "gui.h"

volatile bool runThreads = true;

/* Log message buffer for GUI display */
#define MAX_LOG_MESSAGES 100
static char* logMessages[MAX_LOG_MESSAGES] = {0};
static int logMessageCount = 0;
static int logScroll = 0;

static void addLogMessage(const char* msg) {
	if (logMessageCount >= MAX_LOG_MESSAGES) {
		/* Remove oldest message */
		free(logMessages[0]);
		memmove(logMessages, logMessages + 1, (MAX_LOG_MESSAGES - 1) * sizeof(char*));
		logMessageCount--;
	}
	logMessages[logMessageCount++] = strdup(msg);
}

/**
 * Prints the current key mappings (removed - not needed for GUI)
 */
/* Controls are now implied by the GUI interface */

/**
 * Allows the playback thread to return any error messages that it may
 * encounter.
 *
 * \param	infoIn	Struct containing addresses of the event, the error code,
 *					and an optional error string.
 */
void playbackWatchdog(void* infoIn)
{
	struct watchdogInfo* info = infoIn;

	while(runThreads)
	{
		svcWaitSynchronization(*info->errInfo->failEvent, U64_MAX);
		svcClearEvent(*info->errInfo->failEvent);

		if(*info->errInfo->error > 0)
		{
			char errorMsg[256];
			snprintf(errorMsg, sizeof(errorMsg), "Error %d: %s", 
				*info->errInfo->error, mice_strerror(*info->errInfo->error));
			addLogMessage(errorMsg);
		}
		else if (*info->errInfo->error == -1)
		{
			/* Used to signify that playback has stopped.
			 * Not technically an error. Don't spam logs.
			 */
			/* addLogMessage("Stopped"); */
		}
	}

	return;
}

/**
 * Stop the currently playing file (if there is one) and play another file.
 *
 * \param	ep_file			File to play.
 * \param	playbackInfo	Information that the playback thread requires to
 *							play file.
 */
static int changeFile(const char* ep_file, struct playbackInfo_t* playbackInfo)
{
	s32 prio;
	static Thread thread = NULL;

	if(ep_file != NULL && getFileType(ep_file) == FILE_TYPE_ERROR)
	{
		*playbackInfo->errInfo->error = errno;
		svcSignalEvent(*playbackInfo->errInfo->failEvent);
		return -1;
	}

	/**
	 * If music is playing, stop it. Only one playback thread should be playing
	 * at any time.
	 */
	if(thread != NULL)
	{
		/* Tell the thread to stop playback before we join it */
		stopPlayback();

		threadJoin(thread, U64_MAX);
		threadFree(thread);
		thread = NULL;
	}

	/* If file is NULL, then only thread termination was requested. */
	if(ep_file == NULL || playbackInfo == NULL)
		return 0;
	
	//playbackInfo->file = strdup(ep_file);
	if (memccpy(playbackInfo->file, ep_file, '\0', sizeof(playbackInfo->file)) == NULL)
	{
		addLogMessage("Error: File path too long");
		return -1;
	}

	/* Remove "Playing:" message that was causing display issues */
	/* printf("Playing: %s\n", playbackInfo->file); */
	playbackInfo->samples_total = 0;
	playbackInfo->samples_played = 0;
	playbackInfo->samples_per_second = 0;

	svcGetThreadPriority(&prio, CUR_THREAD_HANDLE);
	thread = threadCreate(playFile, playbackInfo, 32 * 1024, prio - 1, -2, false);

	return 0;
}

static int cmpstringp(const void *p1, const void *p2)
{
	/* The actual arguments to this function are "pointers to
	   pointers to char", but strcmp(3) arguments are "pointers
	   to char", hence the following cast plus dereference */

	return strcasecmp(* (char * const *) p1, * (char * const *) p2);
}

/* Check filename extension against supported audio types. Case-insensitive. */
static bool isMusicFilename(const char *name)
{
	if(name == NULL)
		return false;

	const char *ext = strrchr(name, '.');
	if(ext == NULL || *(ext + 1) == '\0')
		return false;

	ext++; /* skip dot */
	char lext[16];
	size_t i = 0;
	for(; i < sizeof(lext)-1 && ext[i]; i++)
		lext[i] = tolower((unsigned char)ext[i]);
	lext[i] = '\0';

	const char *allowed[] = {"mp3","wav","flac","ogg","opus","sid","m4a","aac", NULL};
	for(int j = 0; allowed[j] != NULL; j++)
		if(strcmp(lext, allowed[j]) == 0)
			return true;

	return false;
}

/* Build a list of music filenames (basenames) in the given directory. Caller must free array and each string. */
static int buildMusicFileListInDir(const char *dirpath, char ***outFiles)
{
	DIR *dp;
	struct dirent *ep;
	char **files = NULL;
	int fileNum = 0;

	if((dp = opendir(dirpath)) == NULL)
		return -1;

	while((ep = readdir(dp)) != NULL)
	{
		if(ep->d_type == DT_DIR)
			continue;

		if(!isMusicFilename(ep->d_name))
			continue;

		files = realloc(files, (fileNum + 1) * sizeof(char*));
		files[fileNum] = strdup(ep->d_name);
		fileNum++;
	}

	if(fileNum > 0)
		qsort(files, fileNum, sizeof(char*), cmpstringp);

	closedir(dp);
	*outFiles = files;
	return fileNum;
}

/* Play next file in the same folder as the currently-playing file. Returns 0 on success, -1 on failure or no-next. */
static int playNextFromPath(struct playbackInfo_t *playbackInfo)
{
	if(playbackInfo == NULL || playbackInfo->file[0] == '\0')
		return -1;

	char fullcpy[PATH_MAX];
	strncpy(fullcpy, playbackInfo->file, sizeof(fullcpy));
	fullcpy[sizeof(fullcpy)-1] = '\0';

	/* find last slash */
	char *slash = strrchr(fullcpy, '/');
	char dirpath[PATH_MAX];
	char *basename = NULL;

	if(slash == NULL)
	{
		/* No directory component; assume current dir */
		if(getcwd(dirpath, sizeof(dirpath)) == NULL)
			return -1;
		basename = fullcpy;
	}
	else
	{
		size_t dirlen = slash - fullcpy;
		if(dirlen >= sizeof(dirpath))
			return -1;
		memcpy(dirpath, fullcpy, dirlen);
		dirpath[dirlen] = '\0';
		basename = slash + 1;
	}

	char **files = NULL;
	int count = buildMusicFileListInDir(dirpath, &files);
	if(count <= 0)
		return -1;

	int index = -1;
	for(int i = 0; i < count; i++)
	{
		if(strcmp(files[i], basename) == 0)
		{
			index = i;
			break;
		}
	}

	int result = -1;
	if(index >= 0 && index + 1 < count)
	{
		char nextpath[PATH_MAX];
		snprintf(nextpath, sizeof(nextpath), "%s/%s", dirpath, files[index+1]);
		result = changeFile(nextpath, playbackInfo);
	}

	for(int i = 0; i < count; i++)
		free(files[i]);
	free(files);

	return result;
}

/**
 * Store the list of files and folders in current directory to an array.
 */
static int getDir(struct dirList_t* dirList)
{
	DIR				*dp;
	struct dirent	*ep;
	int				fileNum = 0;
	int				dirNum = 0;
	char*			wd = getcwd(NULL, 0);

	if(wd == NULL)
		goto out;

	/* Clear strings */
	for(int i = 0; i < dirList->dirNum; i++)
		free(dirList->directories[i]);

	for(int i = 0; i < dirList->fileNum; i++)
		free(dirList->files[i]);

	free(dirList->currentDir);

	if((dirList->currentDir = strdup(wd)) == NULL)
		addLogMessage("Memory allocation failure");

	if((dp = opendir(wd)) == NULL)
		goto out;

	while((ep = readdir(dp)) != NULL)
	{
		if(ep->d_type == DT_DIR)
		{
			/* Add more space for another pointer to a dirent struct */
			dirList->directories = realloc(dirList->directories, (dirNum + 1) * sizeof(char*));

			if((dirList->directories[dirNum] = strdup(ep->d_name)) == NULL)
				addLogMessage("Memory allocation failure");

			dirNum++;
			continue;
		}

		/* Add more space for another pointer to a dirent struct */
		dirList->files = realloc(dirList->files, (fileNum + 1) * sizeof(char*));

		if((dirList->files[fileNum] = strdup(ep->d_name)) == NULL)
			addLogMessage("Memory allocation failure");

		fileNum++;
	}

	qsort(&dirList->files[0], fileNum, sizeof(char *), cmpstringp);
	qsort(&dirList->directories[0], dirNum, sizeof(char *), cmpstringp);

	dirList->dirNum = dirNum;
	dirList->fileNum = fileNum;

	if(closedir(dp) != 0)
		goto out;

out:
	free(wd);
	return fileNum + dirNum;
}

/**
 * Build file list for GUI display.
 * Creates a combined list of directories and files for rendering.
 */
static void buildFileListForGUI(struct dirList_t dirList, const char*** outList, int* outCount, int from)
{
	static const char* combinedList[512];
	static char entryBuffer[512][256];
	int index = 0;
	
	/* Add parent directory option */
	if(from == 0)
	{
		snprintf(entryBuffer[index], sizeof(entryBuffer[index]), "../");
		combinedList[index] = entryBuffer[index];
		index++;
	}
	
	/* Add all directories */
	for(int i = 0; i < dirList.dirNum && index < 512; i++)
	{
		snprintf(entryBuffer[index], sizeof(entryBuffer[index]), "%s/", dirList.directories[i]);
		combinedList[index] = entryBuffer[index];
		index++;
	}
	
	/* Add all files */
	for(int i = 0; i < dirList.fileNum && index < 512; i++)
	{
		snprintf(entryBuffer[index], sizeof(entryBuffer[index]), "%s", dirList.files[i]);
		combinedList[index] = entryBuffer[index];
		index++;
	}
	
	*outList = combinedList;
	*outCount = index;
}

/**
 * Dummy function kept for compatibility (no longer used with GUI)
 */
static int listDir(int from __attribute__((unused)), 
                   int max __attribute__((unused)), 
                   int select __attribute__((unused)), 
                   struct dirList_t dirList __attribute__((unused)))
{
	/* This function is no longer used with GUI rendering */
	return 0;
}

/**
 * Get number of files in current working folder
 *
 * \return	Number of files in current working folder, -1 on failure with
 *			errno set.
 */
int getNumberFiles(void)
{
	DIR				*dp;
	struct dirent	*ep;
	int				ret = 0;

	if((dp = opendir(".")) == NULL)
		goto err;

	while((ep = readdir(dp)) != NULL)
		ret++;

	closedir(dp);

out:
	return ret;

err:
	ret = -1;
	goto out;
}

int main(int argc __attribute__((unused)), char **argv __attribute__((unused)))
{
	int			fileMax;
	int			fileNum = 0;
	int			from = 0;
	Thread			watchdogThread __attribute__((unused));
	Handle			playbackFailEvent;
	struct watchdogInfo	watchdogInfoIn;
	struct errInfo_t	errInfo;
	struct playbackInfo_t	playbackInfo = { 0 };
	volatile int		error = 0;
	struct dirList_t	dirList = { 0 };
	struct metadata_t	currentMetadata = { 0 };

	/* ignore key release of L/R if L+R or L+down was pressed */
	bool keyLComboPressed = false;
	bool keyRComboPressed = false;

	/* Initialize GUI system */
	if(guiInit() != 0)
	{
		return -1;
	}

	svcCreateEvent(&playbackFailEvent, RESET_ONESHOT);
	errInfo.error = &error;
	errInfo.failEvent = &playbackFailEvent;

	watchdogInfoIn.screen = NULL; /* No longer using console */
	watchdogInfoIn.errInfo = &errInfo;
	watchdogThread = threadCreate(playbackWatchdog,
			&watchdogInfoIn, 4 * 1024, 0x20, -2, true);

	playbackInfo.errInfo = &errInfo;

	/* position of parent folder in parent directory */
	int prevPosition[MAX_DIRECTORIES] = {0};
	int prevFrom[MAX_DIRECTORIES] = {0};
	int oldFileNum, oldFrom;

	chdir(DEFAULT_DIR);
	chdir("MUSIC");

	/* TODO: Not actually possible to get less than 0 */
	if(getDir(&dirList) < 0)
	{
		addLogMessage("Unable to obtain directory information");
		goto err;
	}

	fileMax = getNumberFiles();

	/**
	 * This allows for music to continue playing through the headphones whilst
	 * the 3DS is closed.
	 */
	aptSetSleepAllowed(false);

	while(aptMainLoop())
	{
		u32			kDown;
		u32			kHeld;
		u32         kUp;
		static u64	mill = 0;

		/* Begin GUI frame */
		guiBeginFrame();
		guiClearTopScreen();
		guiClearBottomScreen();

		hidScanInput();
		kDown = hidKeysDown();
		kHeld = hidKeysHeld();
		kUp = hidKeysUp();

		/* Exit mice */
		if(kDown & KEY_START)
			break;

#ifdef DEBUG
		/* Debug info logged if needed */
#endif
		if(kDown)
			mill = osGetTime();

		if(kHeld & KEY_L)
		{
			/* Pause/Play */
			if(kDown & (KEY_R | KEY_UP))
			{
				if(isPlaying() == false)
					continue;

				togglePlayback();

				keyLComboPressed = true;
				// distinguish between L+R and L+Up
				if (KEY_R & kDown) {
					keyRComboPressed = true;
				}
				continue;
			}

			/* Show controls - no longer needed with GUI */
			if(kDown & KEY_LEFT)
			{
				keyLComboPressed = true;
				continue;
			}
		}
		// if R is pressed first
		if ((kHeld & KEY_R) && (kDown & KEY_L))
		{
				if(isPlaying() == false)
				continue;

			togglePlayback();

			keyLComboPressed = true;
			keyRComboPressed = true;
			continue;
		}

		if((kDown & KEY_UP ||
					((kHeld & KEY_UP) && (osGetTime() - mill > 500))) &&
				fileNum > 0)
		{
			fileNum--;

			// one line taken up by cwd, other by ../
			if(fileMax - fileNum > MAX_LIST-2 && from != 0)
				from--;
		}

		if((kDown & KEY_DOWN ||
					((kHeld & KEY_DOWN) && (osGetTime() - mill > 500))) &&
				fileNum < fileMax)
		{
			fileNum++;

			if(fileNum >= MAX_LIST && fileMax - fileNum >= 0 &&
					from < fileMax - MAX_LIST)
				from++;
		}

		if((kDown & KEY_LEFT ||
					((kHeld & KEY_LEFT) && (osGetTime() - mill > 500))) &&
				fileNum > 0)
		{
			int skip = MAX_LIST / 2;

			if(fileNum < skip)
				skip = fileNum;

			fileNum -= skip;

			// one line taken up by cwd, other by ../
			if(fileMax - fileNum > MAX_LIST-2 && from != 0)
			{
				from -= skip;
					if(from < 0)
					from = 0;
			}
		}

		if((kDown & KEY_RIGHT ||
					((kHeld & KEY_RIGHT) && (osGetTime() - mill > 500))) &&
				fileNum < fileMax)
		{
			int skip = fileMax - fileNum;

			if(skip > MAX_LIST / 2)
				skip = MAX_LIST / 2;

			fileNum += skip;

			if(fileNum >= MAX_LIST && fileMax - fileNum >= 0 &&
					from < fileMax - MAX_LIST)
			{
				from += skip;
				if(from > fileMax - MAX_LIST)
					from = fileMax - MAX_LIST;
			}
		}

		/*
		 * Pressing B goes up a folder, as well as pressing A or R when ".."
		 * is selected.
		 */
		if((kDown & KEY_B) ||
				((kDown & KEY_A) && (from == 0 && fileNum == 0)))
		{
			chdir("..");
			fileMax = getDir(&dirList);

			fileNum = prevPosition[0];
			from = prevFrom[0];
			for (int i=0; i<MAX_DIRECTORIES-1; i++) {
				prevPosition[i] = prevPosition[i+1];
				prevFrom[i] = prevFrom[i+1];
			}
			/* default to first entry */
			prevPosition[MAX_DIRECTORIES-1] = 0;
			prevFrom[MAX_DIRECTORIES-1] = 0;

			continue;
		}

		if(kDown & KEY_A)
		{
			if(dirList.dirNum >= fileNum)
			{
				chdir(dirList.directories[fileNum - 1]);
				fileMax = getDir(&dirList);

				oldFileNum = fileNum;
				oldFrom = from;
				fileNum = 0;
				from = 0;

				/* save old position in folder */
				for (int i=MAX_DIRECTORIES-1; i>0; i--) {
					prevPosition[i] = prevPosition[i-1];
					prevFrom[i] = prevFrom[i-1];
				}
				prevPosition[0] = oldFileNum;
				prevFrom[0] = oldFrom;
				continue;
			}

			if(dirList.dirNum < fileNum)
			{
				/* Extract and display metadata */
				char fullPath[512];
				snprintf(fullPath, sizeof(fullPath), "%s", dirList.files[fileNum - dirList.dirNum - 1]);
				extractMetadata(fullPath, &currentMetadata);
				guiDisplayMetadata(&currentMetadata, dirList.files[fileNum - dirList.dirNum - 1]);

				changeFile(dirList.files[fileNum - dirList.dirNum - 1], &playbackInfo);
				error = 0;
				continue;
			}
		}

		// ignore R release if key combo with R used
		bool keyRActivation = false;
		if (kUp & KEY_R) {
			if (!keyRComboPressed) {
				keyRActivation = true;
			}
			keyRComboPressed = false;
		}
		bool goToNextFile = (kDown & KEY_ZR) || keyRActivation;
		// check that next entry is a file
		if (goToNextFile && fileNum < fileMax && dirList.dirNum < fileNum+1) {
			fileNum += 1;
			if(fileNum >= MAX_LIST && fileMax - fileNum >= 0 &&
					from < fileMax - MAX_LIST)
				from++;
			
			/* Extract and display metadata */
			char fullPath[512];
			snprintf(fullPath, sizeof(fullPath), "%s", dirList.files[fileNum - dirList.dirNum - 1]);
			extractMetadata(fullPath, &currentMetadata);
			guiDisplayMetadata(&currentMetadata, dirList.files[fileNum - dirList.dirNum - 1]);
			
			changeFile(dirList.files[fileNum - dirList.dirNum - 1], &playbackInfo);
			error = 0;
			continue;
		}
		// ignore L release if key combo with L used
		bool keyLActivation = false;
		if (kUp & KEY_L) {
			if (!keyLComboPressed) {
				keyLActivation = true;
			}
			keyLComboPressed = false;
		}
		bool goToPrevFile = (kDown & KEY_ZL) || keyLActivation;
		// don't go to ../ and check that previous entry is a file
		if (goToPrevFile && fileNum > 1 && dirList.dirNum < fileNum-1) {
			fileNum -= 1;
			if(fileMax - fileNum > MAX_LIST-2 && from != 0)
				from--;
			
			/* Extract and display metadata */
			char fullPath[512];
			snprintf(fullPath, sizeof(fullPath), "%s", dirList.files[fileNum - dirList.dirNum - 1]);
			extractMetadata(fullPath, &currentMetadata);
			guiDisplayMetadata(&currentMetadata, dirList.files[fileNum - dirList.dirNum - 1]);
			
			changeFile(dirList.files[fileNum - dirList.dirNum - 1], &playbackInfo);
			error = 0;
			continue;
		}

		// play next song automatically
		if (error == -1) {
			// don't try to play folders
			if (fileNum >= fileMax || dirList.dirNum >= fileNum) {
				error = 0;
				continue;
			}
			fileNum += 1;
			
			/* Extract and display metadata */
			char fullPath[512];
			snprintf(fullPath, sizeof(fullPath), "%s", dirList.files[fileNum - dirList.dirNum - 1]);
			extractMetadata(fullPath, &currentMetadata);
			guiDisplayMetadata(&currentMetadata, dirList.files[fileNum - dirList.dirNum - 1]);
			
			changeFile(dirList.files[fileNum - dirList.dirNum - 1], &playbackInfo);
			error = 0;
			continue;
		}

		/* Render GUI elements */
		const char** fileList;
		int fileListCount;
		buildFileListForGUI(dirList, &fileList, &fileListCount, 0);
		
		/* Display metadata if we have any */
		if(currentMetadata.title[0] || currentMetadata.artist[0] || currentMetadata.album[0])
		{
		const char* currentFile = (fileNum > 0 && fileNum <= dirList.dirNum + dirList.fileNum) ?
			(fileNum > dirList.dirNum ? dirList.files[fileNum - dirList.dirNum - 1] : "..") : "";
		guiDisplayMetadata(&currentMetadata, currentFile);
		}
		
		/* Calculate scroll position to keep selection visible (13 lines visible with path) */
		int scroll = from;
		if(fileNum < scroll)
			scroll = fileNum;
		else if(fileNum >= scroll + 13)
			scroll = fileNum - 12;
		
		/* Display current directory path */
		char currentPath[256];
		if(getcwd(currentPath, sizeof(currentPath)))
		{
			guiDisplayCurrentPath(currentPath);
		}
		
		/* Display file list on bottom screen */
		guiDisplayFileList(fileList, fileListCount, fileNum, scroll);
		
		/* Display logs on top screen */
		guiDisplayLog((const char**)logMessages, logMessageCount, logScroll);
		
		/* Display playback status and progress bar */
		if(playbackInfo.samples_per_second > 0)
		{
			float position = (float)playbackInfo.samples_played / playbackInfo.samples_per_second;
			float duration = (float)playbackInfo.samples_total / playbackInfo.samples_per_second;
			guiDisplayProgressBar(position, duration);
			guiDisplayPlaybackStatus(isPlaying(), isPaused(), position, duration);
		}
		
		/* Display version */
		guiDisplayVersion(MICE_VERSION);
		
		/* End GUI frame */
		guiEndFrame();
	}

out:
	addLogMessage("Exiting...");
	runThreads = false;
	clearMetadata(&currentMetadata);
	svcSignalEvent(playbackFailEvent);
	changeFile(NULL, &playbackInfo);

	/* Cleanup GUI */
	guiExit();
	
	/* Cleanup log messages */
	for(int i = 0; i < logMessageCount; i++)
		free(logMessages[i]);
	
	return 0;

err:
	addLogMessage("A fatal error occurred. Press START to exit.");

	while(true)
	{
		u32 kDown;

		hidScanInput();
		kDown = hidKeysDown();

		if(kDown & KEY_START)
			break;
	}

	goto out;
}
