/*
 *   This file is part of open_agb_firm
 *   Copyright (C) 2021 derrek, profi200
 *
 *   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 3 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, see <http://www.gnu.org/licenses/>.
 */

#include <stdlib.h>
#include <string.h>
#include "types.h"
#include "error_codes.h"
#include "fs.h"
#include "util.h"
#include "arm11/drivers/hid.h"
#include "arm11/fmt.h"
#include "drivers/gfx.h"


#define MAX_DIR_ENTRIES  (510u)
#define DIR_READ_BLOCKS  (10u)
#define SCREEN_COLS      (53u - 1) // - 1 because the console inserts a newline after the last line otherwise.
#define SCREEN_ROWS      (24u)


typedef struct
{
	u8 type;       // 0 = file, 1 = dir
	char str[256];
} DirListEnt;

typedef struct
{
	u32 num;
	DirListEnt entries[MAX_DIR_ENTRIES];
	DirListEnt *ptrs[MAX_DIR_ENTRIES];
} DirList;



int dlistCompare(const void *a, const void *b)
{
	const DirListEnt *const entA = *(DirListEnt**)a;
	const DirListEnt *const entB = *(DirListEnt**)b;

	if(entA->type != entB->type) return (int)entB->type - entA->type;

	const char *strA = entA->str;
	const char *strB = entB->str;
	int res = *strA - *strB;
	while(*strA != '\0' && *strB != '\0' && res == 0) res = *++strA - *++strB;

	return res;
}

static Result scanDir(const char *const path, DirList *const dList, const char *const filter)
{
	FILINFO *const fi = (FILINFO*)malloc(sizeof(FILINFO) * DIR_READ_BLOCKS);
	if(fi == NULL) return RES_OUT_OF_MEM;

	dList->num = 0;

	Result res;
	DHandle dh;
	if((res = fOpenDir(&dh, path)) == RES_OK)
	{
		u32 read;
		u32 dListPos = 0;
		const u32 filterLen = strlen(filter);
		do
		{
			if((res = fReadDir(dh, fi, DIR_READ_BLOCKS, &read)) != RES_OK) break;
			if(dListPos + read > MAX_DIR_ENTRIES) break;

			for(u32 i = 0; i < read; i++)
			{
				const u8 isDir = (fi[i].fattrib & AM_DIR ? 1u : 0u);
				if(isDir == 0) // File
				{
					const u32 entLen = strlen(fi[i].fname);
					if(entLen <= filterLen || strcmp(filter, fi[i].fname + entLen - filterLen) != 0)
						continue;
				}

				dList->entries[dListPos].type = isDir;
				safeStrcpy(dList->entries[dListPos].str, fi[i].fname, 256);
				dList->ptrs[dListPos] = &dList->entries[dListPos];
				dListPos++;
			}
		} while(read == DIR_READ_BLOCKS);
		dList->num = dListPos;

		fCloseDir(dh);
	}

	free(fi);

	qsort(dList->ptrs, dList->num, sizeof(DirListEnt*), dlistCompare);

	return res;
}

static void showDirList(const DirList *const dList, u32 start)
{
	// Clear screen.
	ee_printf("\x1b[2J");

	const u32 listLength = (dList->num - start > SCREEN_ROWS ? start + SCREEN_ROWS : dList->num);
	for(u32 i = start; i < listLength; i++)
	{
		const char *const printStr =
			(dList->ptrs[i]->type == 0 ? "\x1b[%lu;H\x1b[37m %.51s" : "\x1b[%lu;H\x1b[33m %.51s");

		ee_printf(printStr, i - start, dList->ptrs[i]->str);
	}
}

Result browseFiles(const char *const basePath, char selected[512])
{
	if(basePath == NULL || selected == NULL) return RES_INVALID_ARG;
	// TODO: Check if the base path is empty.

	char *curDir = (char*)malloc(512);
	if(curDir == NULL) return RES_OUT_OF_MEM;
	safeStrcpy(curDir, basePath, 512);

	DirList *const dList = (DirList*)malloc(sizeof(DirList));
	if(dList == NULL) return RES_OUT_OF_MEM;

	Result res;
	if((res = scanDir(curDir, dList, ".gba")) != RES_OK) goto end;
	showDirList(dList, 0);

	s32 cursorPos = 0; // Within the entire list.
	u32 windowPos = 0; // Window start position within the list.
	s32 oldCursorPos = 0;
	while(1)
	{
		ee_printf("\x1b[%lu;H ", oldCursorPos - windowPos);      // Clear old cursor.
		ee_printf("\x1b[%lu;H\x1b[37m>", cursorPos - windowPos); // Draw cursor.

		u32 kDown;
		do
		{
			GFX_waitForVBlank0();

			hidScanInput();
			if(hidGetExtraKeys(0) & (KEY_POWER_HELD | KEY_POWER)) goto end;
			kDown = hidKeysDown();
		} while(kDown == 0);

		const u32 num = dList->num;
		if(num != 0)
		{
			oldCursorPos = cursorPos;
			if(kDown & KEY_DRIGHT)
			{
				cursorPos += SCREEN_ROWS;
				if((u32)cursorPos > num) cursorPos = num - 1;
			}
			if(kDown & KEY_DLEFT)
			{
				cursorPos -= SCREEN_ROWS;
				if(cursorPos < -1) cursorPos = 0;
			}
			if(kDown & KEY_DUP)    cursorPos -= 1;
			if(kDown & KEY_DDOWN)  cursorPos += 1;
		}

		if(cursorPos < 0)              cursorPos = num - 1; // Wrap to end of list.
		if((u32)cursorPos > (num - 1)) cursorPos = 0;       // Wrap to start of list.

		if((u32)cursorPos < windowPos)
		{
			windowPos = cursorPos;
			showDirList(dList, windowPos);
		}
		if((u32)cursorPos >= windowPos + SCREEN_ROWS)
		{
			windowPos = cursorPos - (SCREEN_ROWS - 1);
			showDirList(dList, windowPos);
		}

		if(kDown & (KEY_A | KEY_B))
		{
			u32 pathLen = strlen(curDir);

			if(kDown & KEY_A && num != 0)
			{
				// TODO: !!! Insecure !!!
				if(curDir[pathLen - 1] != '/') curDir[pathLen++] = '/';
				safeStrcpy(curDir + pathLen, dList->ptrs[cursorPos]->str, 256);

				if(dList->ptrs[cursorPos]->type == 0)
				{
					safeStrcpy(selected, curDir, 512);
					break;
				}
			}
			if(kDown & KEY_B)
			{
				char *tmpPathPtr = curDir + pathLen;
				while(*--tmpPathPtr != '/');
				if(*(tmpPathPtr - 1) == ':') tmpPathPtr++;
				*tmpPathPtr = '\0';
			}

			if((res = scanDir(curDir, dList, ".gba")) != RES_OK) break;
			cursorPos = 0;
			windowPos = 0;
			showDirList(dList, 0);
		}
	}

end:
	free(dList);
	free(curDir);

	// Clear screen.
	ee_printf("\x1b[2J");

	return res;
}
