/* OpenCP Module Player
 * copyright (c) 1994-'10 Niklas Beisert <nbeisert@physik.tu-muenchen.de>
 * copyright (c) 2004-'26 Stian Skjelstad <stian.skjelstad@gmail.com>
 *
 * Wavetable devices system
 *
 * 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., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * revision history: (please note changes here)
 *  -nb980510   Niklas Beisert <nbeisert@physik.tu-muenchen.de>
 *    -first release
 *  -kb980717   Tammo Hinrichs <opencp@gmx.net>
 *    -changed INI reading of driver symbols to _dllinfo lookup
 */

#include "config.h"
#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "types.h"
#include "boot/plinkman.h"
#include "boot/psetting.h"
#include "dev/deviwave.h"
#include "dev/mcp.h"
#include "dev/mix.h"
#include "dev/player.h"
#include "filesel/dirdb.h"
#include "filesel/filesystem.h"
#include "filesel/filesystem-drive.h"
#include "filesel/filesystem-file-dev.h"
#include "filesel/filesystem-setup.h"
#include "filesel/mdb.h"
#include "filesel/pfilesel.h"
#include "stuff/compat.h"
#include "stuff/err.h"
#include "stuff/imsrtns.h"
#include "stuff/poutput.h"

#include "freq.c"
#include "smpman.c"

struct mcpDriverListEntry_t
{
	char name[32];
	const struct mcpDriver_t *driver; /* can be NULL if driver is not found */
	int detected;
	int probed;
	int disabled;
};

static struct mcpDriverListEntry_t *mcpDriverList;
static int                          mcpDriverListEntries;
static int                          mcpDriverListNone;

const struct mcpDriver_t *mcpDriver;
const struct mcpDevAPI_t *mcpDevAPI;

static struct mcpAPI_t _mcpAPI =
{
	0, /* MixMaxRate */
	0, /* MixProcRate */
	mcpGetFreq6848,
	mcpGetFreq8363,
	mcpGetNote6848,
	mcpGetNote8363,
	mcpReduceSamples,
};

const struct mcpAPI_t *mcpAPI = &_mcpAPI;

static int deviwaveDriverListInsert (int insertat, const char *name, int length)
{
	int i;
	int disabled = 0;

	if (name[0] == '-')
	{
		disabled = 1;
		name++;
		length--;
	}

	if (!length)
	{
		return errOk;
	}

	assert ((insertat >= 0) && (insertat <= mcpDriverListEntries));
	/* soft assert for name/length, since it comes from userspace */
	for (i = 0; i < mcpDriverListEntries; i++)
	{
		if ((strlen (mcpDriverList[i].name) == length) &&
		    !strncasecmp (mcpDriverList[i].name, name, length))
		{
			fprintf (stderr, "deviwaveDriverListInsert: duplicate entry found\n");
			return errOk;
		}
	}

	/* grow the mcpDriverList */
	{
		struct mcpDriverListEntry_t *temp;
		temp = realloc (mcpDriverList, sizeof (mcpDriverList[0]) * (mcpDriverListEntries + 1));
		if (!temp)
		{
			fprintf (stderr, "deviwaveDriverListInsert: realloc() failed\n");
			return errAllocMem;
		}
		mcpDriverList = temp;
	}
	memmove (mcpDriverList + insertat + 1, mcpDriverList + insertat, sizeof (mcpDriverList[0]) * (mcpDriverListEntries - insertat));
	mcpDriverListEntries++;
	snprintf (mcpDriverList[insertat].name, sizeof (mcpDriverList[insertat].name),
		  "%.*s", length, name);
	mcpDriverList[insertat].driver = 0;
	mcpDriverList[insertat].detected = 0;
	mcpDriverList[insertat].probed = 0;
	mcpDriverList[insertat].disabled = disabled;

	if ((length == 8) && !strncasecmp (name, "devwNone", 8))
	{
		mcpDriverListNone = insertat;
	} else if (mcpDriverListNone <= insertat)
	{
		mcpDriverListNone = 0;
	}

	return errOk;
}

static int deviwavePreInit (const struct configAPI_t *configAPI)
{
	const char *str, *next;
	/* this is ran before plugins are initialized */

	mcpDriverListNone = -1;

	str = configAPI->GetProfileString2 (configAPI->SoundSec, "sound", "wavetabledevices", "devwNone");
	if (!strlen(str))
	{
		return errOk;
	}

	for (; *str; str = next)
	{
		int length, err;

		next = strpbrk (str, " \t\r\n");
		if (next)
		{
			length = next - str;
			next++;
		} else {
			length = strlen (str);
			next = str + length;
		}

		if ((err = deviwaveDriverListInsert (mcpDriverListEntries, str, length)))
		{
			return err;
		}
	}

	return errOk;
}

void mcpRegisterDriver (const struct mcpDriver_t *driver)
{
	int i;

	/* Locate the slot in the list from ocp.ini */
	for (i=0; i < mcpDriverListEntries; i++)
	{
		if (!strcmp (mcpDriverList[i].name, driver->name))
		{
			break;
		}
	}

	/* new driver that is not listed in ocp.ini? allocate a slot just before devwNone */
	if (i == mcpDriverListEntries)
	{
		i = mcpDriverListNone >= 0 ? mcpDriverListNone : mcpDriverListEntries;
		if (deviwaveDriverListInsert (mcpDriverListNone >= 0 ? mcpDriverListNone : mcpDriverListEntries, driver->name, strlen (driver->name)))
		{
			/* failure */
			return;
		}
	}

	if (mcpDriverList[i].driver)
	{
		fprintf (stderr, "mcpRegisterDriver: warning, driver %s already registered\n", driver->name);
		return;
	}

	mcpDriverList[i].driver = driver;
}

static const char *dots (const char *src)
{
	static char buf[34];
	int l = strlen (src);
	if (l > 32)
	{
		l = 32;
	}
	snprintf (buf, sizeof (buf), "%.*s%.*s", l, src, 32 - l, "................................");
	return buf;
}

static struct ocpfile_t *setup_devw;
static void setup_devw_run (void **token, const struct DevInterfaceAPI_t *API);

static int deviwaveLateInit (struct PluginInitAPI_t *API)
{
	const char *def;
	int i, playrate;

	setup_devw = API->dev_file_create (
		API->dmSetup->basedir,
		"devw.dev",
		"Select wavetable driver",
		"",
		0, /* token */
		0, /* Init */
		setup_devw_run,
		0, /* Close */
		0  /* Destructor */
	);
	API->filesystem_setup_register_file (setup_devw);

	playrate=API->configAPI->GetProfileInt("commandline_s", "r", API->configAPI->GetProfileInt2(API->configAPI->SoundSec, "sound", "mixrate", 44100, 10), 10);
	if (playrate<66)
	{
		if (playrate%11)
			playrate*=1000;
		else
			playrate=playrate*11025/11;
	}
	_mcpAPI.MixMaxRate  = playrate;
	_mcpAPI.MixProcRate = API->configAPI->GetProfileInt2(API->configAPI->SoundSec, "sound", "mixprocrate", 1536000, 10);

	fprintf (stderr, "wavetabledevices:\n");

	/* Do we have a specific device specified on the command-line ? */
	def=API->configAPI->GetProfileString("commandline_s", "w", "");
	if (strlen(def))
	{
		for (i=0; i < mcpDriverListEntries; i++)
		{
			if (!strcasecmp (def, mcpDriverList[i].name))
			{
				if (mcpDriverList[i].driver)
				{
					mcpDriverList[i].detected = mcpDriverList[i].driver->Detect (mcpDriverList[i].driver);
					mcpDriverList[i].probed = 1;
					if (mcpDriverList[i].detected)
					{
						mcpDevAPI = mcpDriverList[i].driver->Open (mcpDriverList[i].driver, API->configAPI, mixAPI);
						if (mcpDevAPI)
						{
							fprintf (stderr, " %-8s: %s (selected due to -sw commandline)\n", mcpDriverList[i].name, dots(""));
							mcpDriver = mcpDriverList[i].driver;
							return errOk;
						}
					}
				}
				break;
			}
		}
		fprintf (stderr, "Unable to find/initialize driver specificed with -sp\n");
	}

	/* Do the regular auto-detection */
	for (i=0; i < mcpDriverListEntries; i++)
	{
		if (!mcpDriverList[i].driver)
		{
			fprintf (stderr, " %-8s: %s (driver not found)\n", mcpDriverList[i].name, dots(""));
			continue;
		}
		if (mcpDriverList[i].probed)
		{
			fprintf (stderr, " %-8s: %s (already probed)\n", mcpDriverList[i].name, dots(mcpDriverList[i].driver->description));
			continue;
		}

		mcpDriverList[i].detected = mcpDriverList[i].driver->Detect (mcpDriverList[i].driver);
		mcpDriverList[i].probed = 1;
		if (mcpDriverList[i].detected)
		{
			mcpDevAPI = mcpDriverList[i].driver->Open (mcpDriverList[i].driver, API->configAPI, mixAPI);
			if (mcpDevAPI)
			{
				fprintf (stderr, " %-8s: %s (detected)\n", mcpDriverList[i].name, dots(mcpDriverList[i].driver->description));
				mcpDriver = mcpDriverList[i].driver;
				for (i++ ;i < mcpDriverListEntries; i++)
				{
					if (mcpDriverList[i].driver)
					{
						fprintf (stderr, " %-8s: %s (skipped)\n", mcpDriverList[i].name, dots(mcpDriverList[i].driver->description));
					} else {
						fprintf (stderr, " %-8s: %s (driver not found)\n", mcpDriverList[i].name, dots(""));
					}
				}
				return errOk;
			}
			fprintf (stderr, " %-8s: %s (not detected)\n", mcpDriverList[i].name, dots(mcpDriverList[i].driver->description));
		}
	}

	/* no driver enabled yet, soft error only */
	return errOk;
}

static void deviwavePreClose (struct PluginCloseAPI_t *API)
{
	int i;

	if (setup_devw)
	{
		API->filesystem_setup_unregister_file (setup_devw);
		setup_devw->unref (setup_devw);
		setup_devw = 0;
	}

	if (!mcpDriver)
	{
		return;
	}
	for (i = 0; i < mcpDriverListEntries; i++)
	{
		if (mcpDriverList[i].driver == mcpDriver)
		{
			mcpDriverList[i].driver->Close (mcpDriverList[i].driver);
			mcpDriver = 0;
			mcpDevAPI = 0;
			return;
		}
	}
}

void mcpUnregisterDriver (const struct mcpDriver_t *driver)
{
	int i;
	for (i=0; i < mcpDriverListEntries; i++)
	{
		if (mcpDriverList[i].driver == driver)
		{
			/* shutdown driver if active */
			if (driver == mcpDriver)
			{
				mcpDriverList[i].driver->Close (driver);
				mcpDriver = 0;
				mcpDevAPI = 0;
			}
			mcpDriverList[i].driver = 0;
			return;
		}
	}
	fprintf (stderr, "mcpUnregisterDriver: warning, driver %s not registered\n", driver->name);
}

static void deviwaveLateClose (void)
{
	int i;
	for (i = 0; i < mcpDriverListEntries; i++)
	{
		if (mcpDriverList[i].driver)
		{
			fprintf (stderr, "deviwaveLateClose: warning, driver %s still registered\n", mcpDriverList[i].driver->name);
		}
	}

	free (mcpDriverList);
	mcpDriverList = 0;
	mcpDriverListEntries = 0;
	mcpDriverListNone = -1;
}

static void setup_devw_draw (const struct DevInterfaceAPI_t *API, const char *title, int dsel)
{
	unsigned int mlHeight;
	unsigned int mlTop;
	unsigned int mlLeft;
	unsigned int mlWidth;

	unsigned int i, skip, half, dot, fit;

	/* SETUP the framesize */
	if (mcpDriverListEntries < 3)
	{
		mlHeight = 10;
	} else {
		mlHeight = mcpDriverListEntries + 7;
		if (mlHeight > (plScrHeight - 2))
		{
			mlHeight = plScrHeight - 2;
		}
	}
	fit = mlHeight - 7;
	mlTop = (plScrHeight - mlHeight) / 2;

	mlWidth = 70;
	mlLeft = (plScrWidth - mlWidth) / 2;

	half = fit / 2;
	if (mcpDriverListEntries <= fit)
	{ /* all entries can fit */
		skip = 0;
		dot = 0;
	} else if (dsel < half)
	{ /* we are in the top part */
		skip = 0;
		dot = 3;
	} else if (dsel >= (mcpDriverListEntries - half))
	{ /* we are at the bottom part */
		skip = mcpDriverListEntries - fit;
		dot = fit + 2;
	} else {
		skip = dsel - half;
		dot = skip * (fit) / (mcpDriverListEntries - (fit)) + 3;
	}

	API->console->DisplayFrame (mlTop++, mlLeft++, mlHeight, mlWidth, DIALOG_COLOR_FRAME, title, dot, 2, mlHeight - 4);
	mlWidth -= 2;
	mlHeight -= 2;

	API->console->DisplayPrintf (mlTop++, mlLeft, 0x07, mlWidth, " Available wavetable drivers, and their priority in the autodection:");

	mlTop++; // 2: horizontal bar

	for (i = 2; i < (mlHeight-3); i++)
	{
		int index = i - 2 + skip;
		int color;
		const char *msg;

		if (index >= mcpDriverListEntries)
		{
			mlTop++;
			continue;
		}

		if (!mcpDriverList[index].driver)
		{
			color = 12;
			msg = "(driver not found)";
		} else if (mcpDriverList[index].driver == mcpDriver)
		{
			color = 10;
			msg="(active)";
		} else if (mcpDriverList[index].disabled)
		{
			color = 1;
			msg = "(disabled)";
		} else if (mcpDriverList[index].probed && !mcpDriverList[index].detected)
		{
			color = 1;
			msg = "(detection failed)";
		} else {
			color = 7;
			msg = "";
		}

		API->console->DisplayPrintf (mlTop++, mlLeft,
			(((dsel == index)?7:0) << 4) |
			 ((dsel == index)?0:7),
			mlWidth, "%-.3d %.*o%.8s: %s %.*o%.18s",
			index + 1,
			(dsel == index)?0:3,
			mcpDriverList[index].name,
			dots(mcpDriverList[index].driver?mcpDriverList[index].driver->description:""),
			color,
			msg
		);
	}

	mlTop++; // Horizontal bar
	API->console->DisplayPrintf (mlTop++, mlLeft, 0x0f, mlWidth,
		" <\x18>%0.7o/%0.15o<\x19>%0.7o: Navigate  "
		"%0.15o<+>%0.7o/%0.15o<->%0.7o: Change priority  "
		"%0.15o<ESC>%0.7o close dialog"
	);

	if ((dsel >= mcpDriverListEntries) || (mcpDriver && (mcpDriverList[dsel].driver == mcpDriver)))
	{
		mlTop++;
	} else if ((!mcpDriverList[dsel].driver) && (!mcpDriverList[dsel].disabled))
	{
		API->console->DisplayPrintf (mlTop++, mlLeft, 0x0f, mlWidth,
			" <d>%0.7o: disable driver  "
			"%0.15o<DEL>%0.7o: delete entry"
		);
	} else if ((!mcpDriverList[dsel].driver) && (mcpDriverList[dsel].disabled))
	{
		API->console->DisplayPrintf (mlTop++, mlLeft, 0x0f, mlWidth,
			" <e>%0.7o: enable driver  "
			"%0.15o<DEL>%0.7o: delete entry"
		);
	} else if (mcpDriverList[dsel].disabled)
	{
		API->console->DisplayPrintf (mlTop++, mlLeft, 0x0f, mlWidth,
			" <e>%0.7o: enable driver"
		);
	} else {
		API->console->DisplayPrintf (mlTop++, mlLeft, 0x0f, mlWidth,
			" <ENTER>%0.7o: activate driver  "
			"%0.15o<d>%0.7o: disable driver"
		);
	}
}

static void devw_save_devices (const struct DevInterfaceAPI_t *API)
{
	int len = 1;
	int i;
	char *tmp;
	for (i=0; i < mcpDriverListEntries; i++)
	{
		len += (i?1:0) + (mcpDriverList[i].disabled?1:0) + strlen (mcpDriverList[i].name);
	}
	tmp = calloc (1, len);
	if (!tmp)
	{
		fprintf (stderr, "devw_save_devices: calloc() failed\n");
		return;
	}
	for (i=0; i < mcpDriverListEntries; i++)
	{
		if (i) strcat (tmp, " ");
		if (mcpDriverList[i].disabled) strcat (tmp, "-");
		strcat (tmp, mcpDriverList[i].name);
	}
	API->configAPI->SetProfileString (API->configAPI->SoundSec, "wavetabledevices", tmp);
	free (tmp);
}

static void setup_devw_run (void **token, const struct DevInterfaceAPI_t *API)
{
	int dsel = 0;
	while (1)
	{
		API->fsDraw();
		setup_devw_draw (API, "Wavetable plugins", dsel);
		while (API->console->KeyboardHit())
		{
			int key = API->console->KeyboardGetChar();
			switch (key)
			{
				case KEY_DOWN:
					if (dsel + 1 < mcpDriverListEntries)
					{
						dsel++;
					}
					break;
				case KEY_UP:
					if (dsel > 0)
					{
						dsel--;
					}
					break;
				case KEY_HOME:
					dsel = 0;
					break;
				case KEY_END:
					dsel = mcpDriverListEntries ? mcpDriverListEntries - 1 : 0;
					break;
				case KEY_EXIT:
				case KEY_ESC:
					devw_save_devices (API);
					API->configAPI->StoreConfig();
					return;
				case _KEY_ENTER:
					if ((dsel < mcpDriverListEntries) &&
					    mcpDriverList[dsel].driver &&
					    (mcpDriverList[dsel].driver != mcpDriver) &&
					    (!mcpDriverList[dsel].disabled) &&
					    (!(mcpDriverList[dsel].probed && !mcpDriverList[dsel].detected)))
					{
						API->console->Driver->consoleRestore();
						if (mcpDriver)
						{
							mcpDriver->Close (mcpDriver);
							mcpDriver=0;
						}
						if (!mcpDriverList[dsel].probed)
						{
							mcpDriverList[dsel].detected = mcpDriverList[dsel].driver->Detect (mcpDriverList[dsel].driver);
							mcpDriverList[dsel].probed = 1;
						}
						if (mcpDriverList[dsel].detected)
						{
							mcpDevAPI = mcpDriverList[dsel].driver->Open (mcpDriverList[dsel].driver, API->configAPI, mixAPI);
							if (mcpDevAPI)
							{
								mcpDriver = mcpDriverList[dsel].driver;
							}
						}
						API->console->Driver->consoleSave();
					}
					break;
				case '+':
					if (dsel)
					{
						struct mcpDriverListEntry_t temp;
						temp                  = mcpDriverList[dsel-1];
						mcpDriverList[dsel-1] = mcpDriverList[dsel];
						mcpDriverList[dsel]   = temp;
						dsel--;
					}
					break;
				case '-':
					if ((mcpDriverListEntries >= 2) &&
					    (dsel < (mcpDriverListEntries-1)))
					{
						struct mcpDriverListEntry_t temp;
						temp                  = mcpDriverList[dsel+1];
						mcpDriverList[dsel+1] = mcpDriverList[dsel];
						mcpDriverList[dsel]   = temp;
						dsel++;
					}
					break;
				case 'd':
				case 'D':
					if ((dsel < mcpDriverListEntries) &&
					    (!(mcpDriverList[dsel].driver && (mcpDriverList[dsel].driver == mcpDriver))) &&
					    (!mcpDriverList[dsel].disabled) &&
					    (!(mcpDriverList[dsel].probed && !mcpDriverList[dsel].detected)))
					{
						mcpDriverList[dsel].disabled = 1;
					}
					break;
				case 'e':
				case 'E':
					if ((dsel < mcpDriverListEntries) &&
					    mcpDriverList[dsel].disabled)
					{
						mcpDriverList[dsel].disabled = 0;
					}
					break;
				case KEY_DELETE:
					if ((dsel < mcpDriverListEntries) &&
					    !mcpDriverList[dsel].driver)
					{
						memmove (mcpDriverList + dsel, mcpDriverList + dsel + 1, sizeof (mcpDriverList[0]) * (mcpDriverListEntries - dsel - 1));
						mcpDriverListEntries--;
					}
					if (dsel >= mcpDriverListEntries)
					{
						dsel = mcpDriverListEntries ? mcpDriverListEntries - 1 : 0;
					}
					break;
				default:
					break;
			}
		}
		API->console->FrameLock();
	}
}

DLLEXTINFO_CORE_PREFIX struct linkinfostruct dllextinfo =
{
	.name = "mcpbase",
	.desc = "OpenCP Wavetable Devices System (c) 1994-'26 Niklas Beisert, Tammo Hinrichs, Stian Skjelstad",
	.ver = DLLVERSION,
	.PreInit = deviwavePreInit,
	.LateInit = deviwaveLateInit,
	.PreClose = deviwavePreClose,
	.LateClose = deviwaveLateClose,
	.sortindex = 30
};
