//
// diskselector.cpp
//
// Floppy disk selector GUI
// by James Hammons
// © 2014-2018 Underground Software
//
// JLH = James Hammons <jlhamm@acm.org>
//
// WHO  WHEN        WHAT
// ---  ----------  -----------------------------------------------------------
// JLH  10/13/2013  Created this file
//
// STILL TO DO:
//
// - Fix bug where hovering on scroll image causes it to fly across the screen
//   [DONE]
//

#include "diskselector.h"
#include <dirent.h>
#include <algorithm>
#include <string>
#include <vector>
#include "crc32.h"
#include "fileio.h"
#include "floppydrive.h"
#include "font10pt.h"
#include "gui.h"
#include "log.h"
#include "settings.h"
#include "video.h"

// Icons, in GIMP "C" format
#include "gfx/scroll-left.c"
#include "gfx/scroll-right.c"


struct Bitmap {
	unsigned int width;
	unsigned int height;
	unsigned int bytesPerPixel;					// 3:RGB, 4:RGBA
	unsigned char pixelData[];
};

enum { DSS_SHOWING, DSS_HIDING, DSS_SHOWN, DSS_HIDDEN, DSS_LSB_SHOWING, DSS_LSB_SHOWN, DSS_LSB_HIDING, DSS_RSB_SHOWING, DSS_RSB_SHOWN, DSS_RSB_HIDING, DSS_TEXT_SCROLLING };

#define DS_WIDTH			402
#define DS_HEIGHT			322
#define SCROLL_HOT_WIDTH	48
#define DS_XPOS	((VIRTUAL_SCREEN_WIDTH - DS_WIDTH) / 2)
#define DS_YPOS	((VIRTUAL_SCREEN_HEIGHT - DS_HEIGHT) / 2)


static bool entered = false;
static int driveNumber;
static int diskSelectorState = DSS_HIDDEN;
static int diskSelected = -1;
static int lastDiskSelected = -1;
static int numColumns;
static int colStart = 0;
static int dxLeft = 0;
static int dxRight = 0;
static int rsbPos = DS_WIDTH;
static int lsbPos = -40;
static int textScrollCount = 0;
static bool refresh = false;

/*
So, how this will work for multiple columns, where the number of columns is greater than 3, is to have an arrow button pop up on the left or right hand side (putting the mouse on the left or right side of the disk selector activates (shows) the button, if such a move can be made. Button hides when the mouse moves out of the hot zone or when it has no more effect.
*/


// We make provision for sets of 32 or less...
/*
The way the manifests are laid out, we make the assumption that the boot disk of a set is always listed first.  Therefore, image[0] will always be the boot disk.
*/
struct DiskSet
{
	uint8_t num;			// # of disks in this set
	std::string name;		// The name of this disk set
//	std::string fullPath;	// The path to the containing folder
	std::string image[32];	// List of disk images in this set
	std::string imgName[32];// List of human readable names of disk images
	uint32_t crc[32];		// List of CRC32s of the disk images in the set
	uint32_t crcFound[32];	// List of CRC32s actually discovered on filesystem

	DiskSet(): num(0) {}
};


//
// Struct to hold filenames & full paths to same
//
struct FileStruct
{
	std::string image;
	std::string fullPath;
	DiskSet diskSet;

//	FileStruct(): diskSet(NULL) {}
//	~FileStruct() { if (diskSet != NULL) delete diskSet; }

	// Functor, to presumably make the std::sort go faster
	bool operator()(const FileStruct & a, const FileStruct & b) const
	{
		return (strcasecmp(a.image.c_str(), b.image.c_str()) < 0 ? true : false);
	}
};


static SDL_Texture * window = NULL;
static uint32_t windowPixels[DS_WIDTH * DS_HEIGHT];
SDL_Texture * scrollLeftIcon = NULL;
SDL_Texture * scrollRightIcon = NULL;
bool DiskSelector::showWindow = false;
std::vector<FileStruct> fsList;
std::vector<FileStruct> hdList;


void DiskSelector::Init(SDL_Renderer * renderer)
{
	window = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ABGR8888,
		SDL_TEXTUREACCESS_TARGET, DS_WIDTH, DS_HEIGHT);

	if (!window)
	{
		WriteLog("GUI (DiskSelector): Could not create window!\n");
		return;
	}

	if (SDL_SetTextureBlendMode(window, SDL_BLENDMODE_BLEND) == -1)
		WriteLog("GUI (DiskSelector): Could not set blend mode for window.\n");

	scrollLeftIcon  = GUI::CreateTexture(renderer, &scroll_left);
	scrollRightIcon = GUI::CreateTexture(renderer, &scroll_right);

	for(uint32_t i=0; i<DS_WIDTH*DS_HEIGHT; i++)
		windowPixels[i] = 0xEF007F00;

	SDL_UpdateTexture(window, NULL, windowPixels, DS_WIDTH * sizeof(Uint32));
	FindDisks();
	FindHardDisks();
	DrawFilenames(renderer);
}


//
// Find all disks images top level call
//
void DiskSelector::FindDisks(void)
{
	fsList.clear();
	FindDisks(settings.disksPath);
	std::sort(fsList.begin(), fsList.end(), FileStruct());
	// Calculate the number of columns in the file selector...
	numColumns = (int)ceilf((float)fsList.size() / 27.0f);
	WriteLog("GUI (DiskSelector)::FindDisks(): # of columns is %i (%i files)\n", numColumns, fsList.size());
}


/*
OK, so the way that you can determine if a file is a directory in a cross-
platform way is to do an opendir() call on a discovered filename.  If it
returns NULL, then it's a regular file and not a directory.  Though I think the
Linux method is more elegant.  :-P
*/
//
// Find all disks images within path (recursive call does depth first search)
//
void DiskSelector::FindDisks(const char * path)
{
	DIR * dir = opendir(path);

	if (!dir)
	{
		WriteLog("GUI (DiskSelector)::FindDisks: Could not open directory \"%s\%!\n", path);
		return;
	}

	dirent * ent;

	while ((ent = readdir(dir)) != NULL)
	{
		char buf[0x10000];
		sprintf(buf, "%s/%s", path, ent->d_name);

		// Cross-platform way to test if it's a directory...
		DIR * test = opendir(buf);

//		if ((ent->d_type == DT_REG) && HasLegalExtension(ent->d_name))
		if (test == NULL)
		{
		 	if (HasLegalExtension(ent->d_name))
		 	{
				FileStruct fs;
				fs.image = ent->d_name;
				fs.fullPath = buf;
				fsList.push_back(fs);
			}
		}
//		else if (ent->d_type == DT_DIR)
		else
		{
			// Make sure we close the thing, since it's a bona-fide dir!
			closedir(test);

			// Only recurse if the directory is not one of the special ones...
			if ((strcmp(ent->d_name, "..") != 0)
				&& (strcmp(ent->d_name, ".") != 0))
			{
				// Check to see if this is a special directory with a manifest
				char buf2[0x10000];
				sprintf(buf2, "%s/manifest.txt", buf);
				FILE * fp = fopen(buf2, "r");

				// No manifest means it's just a regular directory...
				if (fp == NULL)
					FindDisks(buf);
				else
				{
					// Read the manifest and all that good stuff
					FileStruct fs;
					ReadManifest(fp, &fs.diskSet);
					fclose(fp);

					// Finally, check that the stuff in the manifest is
					// actually in the directory...
					if (CheckManifest(buf, &fs.diskSet) == true)
					{
						fs.fullPath = buf;
						fs.image = fs.diskSet.name;
						fsList.push_back(fs);
					}
					else
						WriteLog("Manifest for '%s' failed check phase.\n", fs.diskSet.name.c_str());
#if 0
					printf("Name found: \"%s\" (%d)\nDisks:\n", fs.diskSet.name.c_str(), fs.diskSet.num);
					for(int i=0; i<fs.diskSet.num; i++)
						printf("%s (CRC: %08X)\n", fs.diskSet.image[i].c_str(), fs.diskSet.crc[i]);
#endif

				}
			}
		}
	}

	closedir(dir);
}


void DiskSelector::ReadManifest(FILE * fp, DiskSet * ds)
{
	char line[0x10000];
	int disksFound = 0;
	int lineNo = 0;

	while (!feof(fp))
	{
		fgets(line, 0x10000, fp);
		lineNo++;

		if ((line[0] == '#') || (line[0] == '\n'))
			; // Do nothing with comments or blank lines...
		else
		{
			char buf[1024];
			char crcbuf[16];
			char altName[1024];

			if (strncmp(line, "diskset", 7) == 0)
			{
				sscanf(line, "diskset=\"%[^\"]\"", buf);
				ds->name = buf;
			}
			else if (strncmp(line, "disks", 5) == 0)
			{
				sscanf(line, "disks=%hhd", &ds->num);
			}
			else if (strncmp(line, "disk", 4) == 0)
			{
				int n = sscanf(line, "disk=%s %s (%s)", buf, crcbuf, altName);

				if ((n == 2) || (n == 3))
				{
					ds->image[disksFound] = buf;
					ds->crc[disksFound] = strtoul(crcbuf, NULL, 16);
					disksFound++;

					if (n == 3)
						ds->imgName[disksFound] = altName;
					else
					{
						// Find the file's extension, if any
						char * ext = strrchr(buf, '.');

						// Kill the disk extension, if it exists
						if (ext != NULL)
							*ext = 0;

						ds->imgName[disksFound] = buf;
					}
				}
				else
					WriteLog("Malformed disk descriptor in manifest at line %d\n", lineNo);
			}
		}
	}

	if (disksFound != ds->num)
		WriteLog("Found only %d entries in manifest, expected %hhd\n", disksFound, ds->num);
}


bool DiskSelector::CheckManifest(const char * path, DiskSet * ds)
{
	uint8_t found = 0;

	for(int i=0; i<ds->num; i++)
	{
		std::string filename = path;
		filename += "/";
		filename += ds->image[i];
		uint32_t size;
		uint8_t * buf = ReadFile(filename.c_str(), &size);

		if (buf != NULL)
		{
			ds->crcFound[i] = CRC32(buf, size);
			free(buf);
			found++;

			if (ds->crc[i] != ds->crcFound[i])
			{
				WriteLog("Warning: Bad CRC32 for '%s'. Expected: %08X, found: %08X\n", ds->image[i], ds->crc[i], ds->crcFound[i]);
			}
		}
	}

	return (found == ds->num ? true : false);
}


bool DiskSelector::HasLegalExtension(const char * name)
{
	// Find the file's extension, if any
	const char * ext = strrchr(name, '.');

	// No extension, so fuggetaboutit
	if (ext == NULL)
		return false;

	// Otherwise, look for a legal extension
	// We should be smarter than this, and look at headers & file sizes instead
	if ((strcasecmp(ext, ".dsk") == 0)
		|| (strcasecmp(ext, ".do") == 0)
		|| (strcasecmp(ext, ".po") == 0)
		|| (strcasecmp(ext, ".woz") == 0))
		return true;

	return false;
}


//
// Find all disks images top level call
//
void DiskSelector::FindHardDisks(void)
{
	hdList.clear();
	FindHardDisks(settings.disksPath);
	std::sort(hdList.begin(), hdList.end(), FileStruct());
	WriteLog("GUI (DiskSelector)::FindHardDisks(): # of HDs is %i\n", hdList.size());
}


//
// Find all hard disk images within path (recursive call does depth first search)
//
void DiskSelector::FindHardDisks(const char * path)
{
	DIR * dir = opendir(path);

	if (!dir)
	{
		WriteLog("GUI (DiskSelector)::FindHardDisks: Could not open directory \"%s\%!\n", path);
		return;
	}

	dirent * ent;

	while ((ent = readdir(dir)) != NULL)
	{
		char buf[0x10000];
		sprintf(buf, "%s/%s", path, ent->d_name);

		// Cross-platform way to test if it's a directory (test = NULL -> file)
		DIR * test = opendir(buf);

		if (test == NULL)
		{
			const char * ext = strrchr(ent->d_name, '.');

			if ((ext != NULL)
				&& ((strcasecmp(ext, ".2mg") == 0)
					|| (strcasecmp(ext, ".hdv") == 0)))
			{
				FileStruct fs;
				fs.image = ent->d_name;
				fs.fullPath = buf;
				hdList.push_back(fs);
			}
		}
		else
		{
			// Make sure we close the thing, since it's a bona-fide dir!
			closedir(test);

			// Only recurse if the directory is not one of the special ones...
			if ((strcmp(ent->d_name, "..") != 0)
				&& (strcmp(ent->d_name, ".") != 0))
			{
				FindHardDisks(buf);
			}
		}
	}

	closedir(dir);
}


void DiskSelector::DrawFilenames(SDL_Renderer * renderer)
{
	if (SDL_SetRenderTarget(renderer, window) < 0)
	{
		WriteLog("GUI: Could not set Render Target to overlay... (%s)\n", SDL_GetError());
		return;
	}

	// 3 columns of 16 chars apiece (with 8X16 font), 18 rows
	// 3 columns of 18 chars apiece (with 7X12 font), 24 rows
	// 3 columns of 21 chars apiece (with 6X11 font), 27 rows

	unsigned int count = 0;
	unsigned int fsStart = colStart * 27;
	int offset = 0;

	// Draw partial columns (for scrolling left/right)
	// [could probably combine these...]
	if (textScrollCount < 0)
	{
		int partialColStart = (colStart - 1) * 27;
		offset = -1 * textScrollCount;

		for(unsigned int y=0; y<27; y++)
		{
			for(unsigned int i=22+textScrollCount, x=0; i<21; i++, x++)
			{
				if (i >= fsList[partialColStart + y].image.length())
					break;

				GUI::DrawCharacter(renderer, x + 1, y + 1, fsList[partialColStart + y].image[i], false);
			}
		}
	}
	else if (textScrollCount > 0)
	{
		offset = 22 - textScrollCount;

		for(unsigned int y=0; y<27; y++)
		{
			for(unsigned int i=textScrollCount, x=0; i<21; i++, x++)
			{
				if (i >= fsList[fsStart + y].image.length())
					break;

				GUI::DrawCharacter(renderer, x + 1, y + 1, fsList[fsStart + y].image[i], false);
			}
		}

		fsStart += 27;
	}

	while (fsStart < fsList.size())
	{
//		int currentX = (count / 18) * 17;
//		int currentY = (count % 18);
//		int currentX = (count / 24) * 19;
//		int currentY = (count % 24);
		int currentX = (count / 27) * 22;
		int currentY = (count % 27);

//		for(unsigned int i=0; i<16; i++)
//		for(unsigned int i=0; i<18; i++)
		for(unsigned int i=0; i<21; i++)
		{
			if (i >= fsList[fsStart].image.length())
				break;

			bool invert = (diskSelected == (int)fsStart ? true : false);
			GUI::DrawCharacter(renderer, currentX + i + 1 + offset, currentY + 1, fsList[fsStart].image[i], invert);
		}

		count++;
		fsStart++;

//		if (count >= (18 * 3))
//		if (count >= (24 * 3))
		if (count >= (27 * 3))
			break;
	}

	// If a disk is selected, show it on the top line in inverse video
	if (diskSelected > -1)
	{
		for(unsigned int i=0; i<65; i++)
		{
			if (i >= fsList[diskSelected].image.length())
				break;

			GUI::DrawCharacter(renderer, i + 1, 0, fsList[diskSelected].image[i], true);
		}
	}

	// Set render target back to default
	SDL_SetRenderTarget(renderer, NULL);
}


void DiskSelector::ShowWindow(int drive)
{
	diskSelectorState = DSS_SHOWN;
	entered = false;
	showWindow = true;
	driveNumber = drive;
}


void DiskSelector::HideWindow(void)
{
	diskSelectorState = DSS_HIDDEN;
	dxLeft = 0;
	dxRight = 0;
	rsbPos = DS_WIDTH;
	lsbPos = -40;
	showWindow = false;
	refresh = true;
}


void DiskSelector::MouseDown(int32_t x, int32_t y, uint32_t buttons)
{
	if (!showWindow || !entered)
		return;

	if ((diskSelectorState == DSS_LSB_SHOWING) || (diskSelectorState == DSS_LSB_SHOWN))
	{
		colStart--;
		textScrollCount = 21;

		if (colStart == 0)
		{
			diskSelectorState = DSS_LSB_HIDING;
			dxLeft = -8;
		}

		return;
	}

	if ((diskSelectorState == DSS_RSB_SHOWING) || (diskSelectorState == DSS_RSB_SHOWN))
	{
		colStart++;
		textScrollCount = -21;

		if ((colStart + 3) == numColumns)
		{
			diskSelectorState = DSS_RSB_HIDING;
			dxRight = 8;
		}

		return;
	}

	if (diskSelected != -1)
	{
		floppyDrive[0].LoadImage(fsList[diskSelected].fullPath.c_str(), driveNumber);
	}

	showWindow = false;
}


void DiskSelector::MouseUp(int32_t x, int32_t y, uint32_t buttons)
{
	if (!showWindow)
		return;

}


void DiskSelector::MouseMove(int32_t x, int32_t y, uint32_t buttons)
{
	if (!showWindow)
		return;

	// Check to see if DS has been hovered yet, and, if so, set a flag to show
	// that it has
	if (!entered && ((x >= DS_XPOS) && (x <= (DS_XPOS + DS_WIDTH))
		&& (y >= DS_YPOS) && (y <= (DS_YPOS + DS_HEIGHT))))
		entered = true;

	// Check to see if the DS, since being hovered, is now no longer being
	// hovered
//N.B.: Should probably make like a 1/2 to 1 second timeout to allow for overshooting the edge of the thing, maybe have the window fade out gradually and let it come back if you enter before it leaves...
	if (entered && ((x < DS_XPOS) || (x > (DS_XPOS + DS_WIDTH))
		|| (y < DS_YPOS) || (y > (DS_YPOS + DS_HEIGHT))))
	{
		diskSelectorState = DSS_HIDDEN;
		dxLeft = 0;
		dxRight = 0;
		rsbPos = DS_WIDTH;
		lsbPos = -40;
		showWindow = false;
		refresh = true;
		return;
	}

	// Bail out if the DS hasn't been entered yet
	if (!entered)
		return;

/*
states:
+-----+---------------------+-----+
|     |                     |     |
|     |                     |     |
+-----+---------------------+-----+
 ^           ^                ^
 |           |                x is here and state is DSS_SHOWN
 |           x is here and state is DSS_LSB_SHOWING or DSS_RSB_SHOWING
 x is here and state is DSS_SHOWN

*/
	if (x < (DS_XPOS + SCROLL_HOT_WIDTH))
	{
		if ((colStart > 0) && (diskSelectorState == DSS_SHOWN))
		{
			diskSelectorState = DSS_LSB_SHOWING;
			dxLeft = 8;
		}
	}
	else if (x > (DS_XPOS + DS_WIDTH - SCROLL_HOT_WIDTH))
	{
		if (((colStart + 3) < numColumns) && (diskSelectorState == DSS_SHOWN))
		{
			diskSelectorState = DSS_RSB_SHOWING;
			dxRight = -8;
		}
	}
	else
	{
		// Handle the excluded middle  :-P
		if ((diskSelectorState == DSS_LSB_SHOWING)
			|| (diskSelectorState == DSS_LSB_SHOWN))
		{
			diskSelectorState = DSS_LSB_HIDING;
			dxLeft = -8;
		}
		else if ((diskSelectorState == DSS_RSB_SHOWING)
			|| (diskSelectorState == DSS_RSB_SHOWN))
		{
			diskSelectorState = DSS_RSB_HIDING;
			dxRight = 8;
		}
	}

	// The -1 terms move the origin to the upper left corner (from 1 in, and 1
	// down)
	int xChar = ((x - DS_XPOS) / FONT_WIDTH) - 1;
	int yChar = ((y - DS_YPOS) / FONT_HEIGHT) - 1;
	diskSelected = ((xChar / 22) * 27) + yChar + (colStart * 27);

	if ((yChar < 0) || (yChar >= 27)
		|| (diskSelected >= (int)fsList.size())
		|| (diskSelectorState == DSS_LSB_SHOWING)
		|| (diskSelectorState == DSS_LSB_SHOWN)
		|| (diskSelectorState == DSS_RSB_SHOWING)
		|| (diskSelectorState == DSS_RSB_SHOWN))
		diskSelected = -1;

	if (diskSelected != lastDiskSelected)
	{
		HandleSelection(sdlRenderer);
		lastDiskSelected = diskSelected;
	}
}


void DiskSelector::HandleGUIState(void)
{
	lsbPos += dxLeft;
	rsbPos += dxRight;

	if ((lsbPos > (SCROLL_HOT_WIDTH - 40)) && (diskSelectorState == DSS_LSB_SHOWING))
	{
		diskSelectorState = DSS_LSB_SHOWN;
		lsbPos = SCROLL_HOT_WIDTH - 40;
		dxLeft = 0;
	}
	else if ((lsbPos < -40) && (diskSelectorState == DSS_LSB_HIDING))
	{
		diskSelectorState = DSS_SHOWN;
		lsbPos = -40;
		dxLeft = 0;
	}
	else if ((rsbPos < (DS_WIDTH - SCROLL_HOT_WIDTH)) && (diskSelectorState == DSS_RSB_SHOWING))
	{
		diskSelectorState = DSS_RSB_SHOWN;
		rsbPos = DS_WIDTH - SCROLL_HOT_WIDTH;
		dxRight = 0;
	}
	else if ((rsbPos > DS_WIDTH) && (diskSelectorState == DSS_RSB_HIDING))
	{
		diskSelectorState = DSS_SHOWN;
		rsbPos = DS_WIDTH;
		dxRight = 0;
	}

	if (textScrollCount < 0)
	{
		textScrollCount += 2;

		if (textScrollCount > 0)
		{
			textScrollCount = 0;
			refresh = true;
		}
	}
	else if (textScrollCount > 0)
	{
		textScrollCount -= 2;

		if (textScrollCount < 0)
		{
			textScrollCount = 0;
			refresh = true;
		}
	}
}


void DiskSelector::HandleSelection(SDL_Renderer * renderer)
{
	SDL_UpdateTexture(window, NULL, windowPixels, DS_WIDTH * sizeof(Uint32));
	DrawFilenames(renderer);
	refresh = false;
}


void DiskSelector::Render(SDL_Renderer * renderer)
{
	if (!(window && showWindow))
		return;

	HandleGUIState();

	if (((diskSelectorState != DSS_LSB_SHOWN)
		&& (diskSelectorState != DSS_RSB_SHOWN)
		&& (diskSelectorState != DSS_SHOWN))
		|| (textScrollCount != 0) || refresh)
		HandleSelection(renderer);

	// Render scroll arrows (need to figure out why no alpha!)
	SDL_SetRenderTarget(renderer, window);
	SDL_Rect dst2 = { 0, ((DS_HEIGHT - 40) / 2), 40, 40 };
	dst2.x = lsbPos;
	SDL_RenderCopy(renderer, scrollLeftIcon, NULL, &dst2);
	SDL_Rect dst3 = { 0, ((DS_HEIGHT - 40) / 2), 40, 40 };
	dst3.x = rsbPos;
	SDL_RenderCopy(renderer, scrollRightIcon, NULL, &dst3);
	SDL_SetRenderTarget(renderer, NULL);

	SDL_Rect dst = { DS_XPOS, DS_YPOS, DS_WIDTH, DS_HEIGHT };
	SDL_RenderCopy(renderer, window, NULL, &dst);
}

