// Emacs style mode select   -*- C++ -*-
//-----------------------------------------------------------------------------
//
// $Id: 15e69cbabfcf2da9c6f8ab4b90cbd369915d7e1a $
//
// Copyright (C) 1993-1996 by id Software, Inc.
// Copyright (C) 2006-2025 by The Odamex Team.
//
// 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.
//
// DESCRIPTION:
//	Boot GUI for WAD selection.
//
//-----------------------------------------------------------------------------

#include "odamex.h"

#include "gui_boot.h"

#include <algorithm>

#include "FL/Fl.H"
#include "FL/Fl_Box.H"
#include "FL/Fl_Button.H"
#include "FL/Fl_Check_Browser.H"
#include "FL/Fl_Check_Button.H"
#include "FL/Fl_Hold_Browser.H"
#include "FL/Fl_Native_File_Chooser.H"
#include "FL/Fl_Return_Button.H"
#include "FL/Fl_Tabs.H"
#include "FL/Fl_Window.H"
#include "FL/fl_ask.H"

#include "gui_common.h"

#include "i_system.h"
#include "w_ident.h"

// -- Externals --

void CL_QuitCommand();
EXTERN_CVAR(i_skipbootwin);
EXTERN_CVAR(waddirs);

// ---------------

typedef std::vector<scannedIWAD_t> scannedIWADs_t;
typedef std::vector<std::pair<scannedPWAD_t, bool>> scannedPWADs_t;
typedef std::vector<scannedPWAD_t*> scannedPWADPtrs_t;

// display strings for options tab and their corresponding command line arguments
const std::vector<std::pair<std::string, std::string> > OPTIONS_LIST = {
	{"No Monsters", "-nomonsters"},
	{"Fast Monsters", "-fast"},
	{"Respawn Monsters", "-respawn"},
	{"Pistol Start", "-pistolstart"},
	{"Spawn Multiplayer Things", "-coop-things"}
};

/**
 * @brief Find the PWAD pointer in the scanned WAD array.
 */
static auto FindScanned(const std::vector<std::pair<std::string, int>>& filterIndices, const scannedPWADs_t& pwads, const scannedPWAD_t* pwad)
{
	int i = 0;
	for (const auto& [name, index] : filterIndices)
	{
		if (name == pwad->filename)
		{
			return std::make_pair(i, index);
		}
		i++;
	}
	i = 0;
	for (const auto& [scannedPWAD, _] : pwads)
	{
		if (&scannedPWAD == pwad)
		{
			return std::make_pair(-1, i);
		}
		i++;
	}
	return std::make_pair(-1, -1);
}

/**
 * @brief Add a scanned PWAD from the pointer list - unless there's a dupe.
 */
static void AddSelected(scannedPWADPtrs_t& mut, scannedPWAD_t* pwad)
{
	scannedPWADPtrs_t::iterator it = std::find(mut.begin(), mut.end(), pwad);
	if (it == mut.end())
	{
		mut.push_back(pwad);
	}
}

/**
 * @brief Erase a scanned PWAD from the pointer list.
 */
static void EraseSelected(scannedPWADPtrs_t& mut, scannedPWAD_t* pwad)
{
	scannedPWADPtrs_t::iterator it = std::find(mut.begin(), mut.end(), pwad);
	if (it != mut.end())
	{
		mut.erase(it);
	}
}

const scannedIWAD_t* g_SelectedIWAD;
scannedWADs_t g_SelectedWADs;

constexpr int WINDOW_WIDTH = 425;
constexpr int WINDOW_HEIGHT = 240;

class BootWindow : public Fl_Window
{
	Fl_Group* m_tabIWAD;
	Fl_Group* m_tabPWADs;
	std::string m_genWADDirs;
	scannedIWADs_t m_IWADs;
	scannedPWADs_t m_PWADs;
	scannedPWADPtrs_t m_selectedPWADs;
	Fl_Hold_Browser* m_IWADBrowser;
	Fl_Check_Browser* m_PWADSelectBrowser;
	Fl_Hold_Browser* m_PWADOrderBrowser;
	Fl_Check_Browser* m_gameOptionsBrowser;
	StringTokens m_WADDirs;
	Fl_Hold_Browser* m_WADDirList;
	Fl_Input* m_searchPWADs;
	std::vector<std::pair<std::string, int>> m_filterIndices;

  public:
	BootWindow(int X, int Y, int W, int H, const char* L)
	    : Fl_Window(X, Y, W, H, L), m_genWADDirs(), m_IWADs(), m_PWADs(), m_selectedPWADs(), m_WADDirs(), m_filterIndices()
	{
		{
			Fl_Tabs* tabs = new Fl_Tabs(0, 0, 425, 200);
			{
				tabs->callback(tabsCB, static_cast<void*>(this));
				tabs->when(FL_WHEN_CHANGED);
				m_tabIWAD = new Fl_Group(0, 25, 425, 175, "Game Select");
				{
					Fl_Box* logo = new Fl_Box(10, 35, 115, 155);
					logo->image(GUIRes::icon_odamex_128());
					logo->align(Fl_Align(512));
				} // Fl_Box* logo
				{
					m_IWADBrowser = new Fl_Hold_Browser(135, 35, 280, 155);
					m_IWADBrowser->callback(BootWindow::doPlayCB, static_cast<void*>(this));
					m_IWADBrowser->when(FL_WHEN_ENTER_KEY);
				} // Fl_Browser* m_IWADBrowser
				m_tabIWAD->end();
			} // Fl_Group* tabIWAD
			{
				m_tabPWADs = new Fl_Group(0, 25, 425, 175, "PWAD Select");
				{
					m_searchPWADs = new Fl_Input(36, 35, 154, 20);
					m_searchPWADs->label("@search");
					m_searchPWADs->callback(BootWindow::doSearchCB, static_cast<void*>(this));
					m_searchPWADs->when(FL_WHEN_CHANGED);
				} // Fl_Input* m_searchPWADs
				{
					m_PWADSelectBrowser = new Fl_Check_Browser(10, 65, 183, 135);
					m_PWADSelectBrowser->callback(BootWindow::scanCheckedPWADsCB,
					                              static_cast<void*>(this));
					m_PWADSelectBrowser->when(FL_WHEN_CHANGED);
				} // Fl_Check_Browser* m_PWADSelectBrowser
				{
					m_PWADOrderBrowser = new Fl_Hold_Browser(203, 65, 182, 125);
				} // Fl_Hold_Browser* m_PWADOrderBrowser
				{
					Fl_Box* o = new Fl_Box(203, 35, 182, 20, "Change Load Order");
				} // Fl_Box* o
				{
					Fl_Button* doWADUp = new Fl_Button(395, 90, 20, 20, "@2<<");
					doWADUp->callback(BootWindow::doWADUpCB, static_cast<void*>(this));
				} // Fl_Button* doWADUp
				{
					Fl_Button* doWADDown = new Fl_Button(395, 115, 20, 20, "@2>>");
					doWADDown->callback(BootWindow::doWADDownCB,
					                    static_cast<void*>(this));
				} // Fl_Button* doWADDown
				{
					Fl_Button* doWADRemove = new Fl_Button(395, 140, 20, 20, "@1+");
					doWADRemove->callback(BootWindow::doWADRemoveCB,
					                      static_cast<void*>(this));
				} // Fl_Button* doWADRemove
				m_tabPWADs->end();
			} // Fl_Group* tabPWADs
			{
				Fl_Group* tabGameOptions =
					new Fl_Group(0, 25, 425, 175, "Game Options");
				{
					Fl_Box* o = new Fl_Box(
					    10, 35, 405, 20,
					    "Set gameplay options to modify your experience.");
					o->align(Fl_Align(132 | FL_ALIGN_INSIDE));
				} // Fl_Box* o
				{
					m_gameOptionsBrowser = new Fl_Check_Browser(10, 65, 405, 125);
					for (const auto& option : OPTIONS_LIST)
					{
						m_gameOptionsBrowser->add(option.first.c_str());
					}
				}
				tabGameOptions->end();

			} // Fl_Group* tabGameOptions
			{
				Fl_Group* tabWADDirs =
				    new Fl_Group(0, 25, 425, 175, "Resource Locations");
				tabWADDirs->hide();
				{
					Fl_Box* o = new Fl_Box(
					    10, 35, 405, 20,
					    "Add folders containing WAD files here so Odamex can find them.");
					o->align(Fl_Align(132 | FL_ALIGN_INSIDE));
				} // Fl_Box* o
				{
					m_WADDirList = new Fl_Hold_Browser(10, 65, 375, 125);
				} // Fl_Browser* wadDirList
				{
					Fl_Button* doDirAdd = new Fl_Button(395, 65, 20, 20, "@+");
					doDirAdd->callback(BootWindow::doDirAddCB, static_cast<void*>(this));
				} // Fl_Button* doDirAdd
				{
					Fl_Button* doDirUp = new Fl_Button(395, 90, 20, 20, "@2<<");
					doDirUp->callback(BootWindow::doDirUpCB, static_cast<void*>(this));
				} // Fl_Button* doDirUp
				{
					Fl_Button* doDirDown = new Fl_Button(395, 115, 20, 20, "@2>>");
					doDirDown->callback(BootWindow::doDirDownCB,
					                    static_cast<void*>(this));
				} // Fl_Button* doDirDown
				{
					Fl_Button* doDirRemove = new Fl_Button(395, 140, 20, 20, "@1+");
					doDirRemove->callback(BootWindow::doDirRemoveCB,
					                      static_cast<void*>(this));
				} // Fl_Button* doDirRemove
				tabWADDirs->end();
			} // Fl_Group* tabWADDirs
			tabs->end();
		} // Fl_Tabs* tabs
		{
			Fl_Check_Button* notAgain =
			    new Fl_Check_Button(10, 210, 20, 20, "Don\'t Show This Again");
			notAgain->down_box(FL_DOWN_BOX);
			notAgain->callback(BootWindow::doDontShowCB);
			notAgain->value(::i_skipbootwin);
		} // Fl_Check_Button* notAgain
		{
			Fl_Button* doQuit = new Fl_Button(275, 210, 65, 20, "Quit");
			doQuit->callback(BootWindow::doQuitCB);
		} // Fl_Button* doQuit
		{
			Fl_Return_Button* doPlay = new Fl_Return_Button(350, 210, 65, 20, "Play!");
			doPlay->callback(BootWindow::doPlayCB, static_cast<void*>(this));
		} // Fl_Return_Button* doPlay
		end();
		callback(BootWindow::doCallback);
		GUI_SetIcon(this);
	}

	// -- Game Select --

	static void tabsCB(Fl_Widget* w, void* data)
	{
		Fl_Tabs* tabs = static_cast<Fl_Tabs*>(w);
		BootWindow* boot = static_cast<BootWindow*>(data);
		Fl_Group* clicked = static_cast<Fl_Group*>(tabs->value());

		// Have waddirs changed?
		bool waddirsChanged = boot->m_genWADDirs != ::waddirs;

		// User clicked on the first tab, regenerate the
		// list of IWADs if waddirs changed.
		if ((clicked == boot->m_tabIWAD) && waddirsChanged)
		{
			boot->rescanIWADs();
			return;
		}

		bool pwadsIsEmpty = boot->m_PWADSelectBrowser->nitems() == 0;

		// User clicked on the second tab, regenerate the
		// list of PWADs if waddirs changed or browser is empty.
		if ((clicked == boot->m_tabPWADs) && (pwadsIsEmpty || waddirsChanged))
		{
			boot->rescanPWADs();
			return;
		}
	}

	static void doCallback(Fl_Widget*, void*) { CL_QuitCommand(); }

	static void doDontShowCB(Fl_Widget* w, void*)
	{
		Fl_Check_Button* check = static_cast<Fl_Check_Button*>(w);
		::i_skipbootwin.Set(check->value());
	}

	static void doQuitCB(Fl_Widget*, void*) { CL_QuitCommand(); }

	static void doPlayCB(Fl_Widget*, void* data)
	{
		BootWindow* boot = static_cast<BootWindow*>(data);
		if (boot->selectedIWAD() == NULL)
		{
			// A valid IWAD isn't selected.
			fl_message_title("Play Odamex");
			fl_alert("Please select a game to play.");
			return;
		}

		boot->selectedWADs();
		boot->setOptions();
		Fl::delete_widget(boot);
	}

	// -- PWAD Boot Order --

	static void doSearchCB(Fl_Widget*, void* data)
	{
		BootWindow* boot = static_cast<BootWindow*>(data);

		boot->filterPWADs();
	}

	static void doWADUpCB(Fl_Widget*, void* data)
	{
		BootWindow* boot = static_cast<BootWindow*>(data);

		const int val = boot->m_PWADOrderBrowser->value() - 1;
		if (val <= 0 || val >= static_cast<int>(boot->m_selectedPWADs.size()))
			return;

		std::iter_swap(boot->m_selectedPWADs.begin() + val,
		               boot->m_selectedPWADs.begin() + val - 1);
		boot->updatePWADOrderBrowser();
		boot->m_PWADOrderBrowser->value(val);
	}

	static void doWADDownCB(Fl_Widget*, void* data)
	{
		BootWindow* boot = static_cast<BootWindow*>(data);

		const int val = boot->m_PWADOrderBrowser->value() - 1;
		if (val < 0 || val >= static_cast<int>(boot->m_selectedPWADs.size() - 1))
			return;

		std::iter_swap(boot->m_selectedPWADs.begin() + val,
		               boot->m_selectedPWADs.begin() + val + 1);
		boot->updatePWADOrderBrowser();
		boot->m_PWADOrderBrowser->value(val + 2);
	}

	/**
	 * @brief Removes a WAD from the PWAD selection list.
	 */
	static void doWADRemoveCB(Fl_Widget*, void* data)
	{
		BootWindow* boot = static_cast<BootWindow*>(data);

		// Figure out which PWAD we're removing.
		const size_t removeIDX = size_t(boot->m_PWADOrderBrowser->value()) - 1;
		if (removeIDX > boot->m_selectedPWADs.size())
		{
			return;
		}
		scannedPWAD_t* selected = boot->m_selectedPWADs[removeIDX];

		// Uncheck the selected PWAD from the selection array.
		auto [selectIndex, pwadIndex] = FindScanned(boot->m_filterIndices, boot->m_PWADs, selected);
		if (pwadIndex == -1)
			return;
		boot->m_PWADs[pwadIndex].second = false;
		if (selectIndex != -1)
			boot->m_PWADSelectBrowser->checked(selectIndex + 1, 0);

		// Erase the selected PWAD from the order array.
		EraseSelected(boot->m_selectedPWADs, boot->m_selectedPWADs[removeIDX]);
		boot->updatePWADOrderBrowser();
	}

	// -- Resource Locations --

	/**
	 * @brief Adds a directory to the WAD dir list.
	 */
	static void doDirAddCB(Fl_Widget*, void* data)
	{
		BootWindow* boot = static_cast<BootWindow*>(data);

		Fl_Native_File_Chooser chooser;
		chooser.title("Add Directory");
		chooser.type(Fl_Native_File_Chooser::BROWSE_DIRECTORY);
		switch (chooser.show())
		{
		case 1:  // Cancelled.
		case -1: // Failed.
			return;
		default: // Picked a directory.
			boot->m_WADDirs.push_back(chooser.filename());
			boot->setWADDirs();
			boot->updateWADDirBrowser();
			break;
		}
	}

	static void doDirUpCB(Fl_Widget*, void* data)
	{
		BootWindow* boot = static_cast<BootWindow*>(data);

		const int val = boot->m_WADDirList->value() - 1;
		if (val <= 0 || val >= static_cast<int>(boot->m_WADDirs.size()))
			return;

		std::iter_swap(boot->m_WADDirs.begin() + val, boot->m_WADDirs.begin() + val - 1);
		boot->setWADDirs();
		boot->updateWADDirBrowser();
		boot->m_WADDirList->value(val);
	}

	static void doDirDownCB(Fl_Widget*, void* data)
	{
		BootWindow* boot = static_cast<BootWindow*>(data);

		const int val = boot->m_WADDirList->value() - 1;
		if (val < 0 || val >= static_cast<int>(boot->m_WADDirs.size() - 1))
			return;

		std::iter_swap(boot->m_WADDirs.begin() + val, boot->m_WADDirs.begin() + val + 1);
		boot->setWADDirs();
		boot->updateWADDirBrowser();
		boot->m_WADDirList->value(val + 2);
	}

	/**
	 * @brief Removes a directory from the WAD dir list.
	 */
	static void doDirRemoveCB(Fl_Widget*, void* data)
	{
		BootWindow* boot = static_cast<BootWindow*>(data);

		const int val = boot->m_WADDirList->value() - 1;
		if (val < 0 || val >= static_cast<int>(boot->m_WADDirs.size()))
			return;

		boot->m_WADDirs.erase(boot->m_WADDirs.begin() + val);
		boot->setWADDirs();
		boot->updateWADDirBrowser();
	}

	void rescanIWADs()
	{
		m_IWADBrowser->clear();
		m_IWADs = M_ScanIWADs();
		for (const auto& iwad : m_IWADs)
		{
			m_IWADBrowser->add(iwad.id->mNiceName.c_str(), (void*)iwad.id);
		}
		m_genWADDirs = ::waddirs.str();
	}

	void rescanPWADs()
	{
		m_PWADSelectBrowser->clear();
		int i = 0;
		for (const auto& pwad : M_ScanPWADs())
		{
			m_PWADs.emplace_back(pwad, false);
			m_PWADSelectBrowser->add(pwad.filename.c_str());
			m_filterIndices.emplace_back(pwad.filename, i);
			i++;
		}
		m_genWADDirs = ::waddirs.str();

		// clear order browser since selection browser is being reset
		m_PWADOrderBrowser->clear();
		m_selectedPWADs.clear();
	}

	void filterPWADs()
	{
		m_filterIndices.clear();
		m_PWADSelectBrowser->clear();
		int i = 0;
		for (const auto& [pwad, checked] : m_PWADs)
		{
			if (m_searchPWADs->value()[0] == 0 ||
				StdStringFind(pwad.filename, m_searchPWADs->value(), 0, m_searchPWADs->size(), true) != std::string::npos)
			{
				m_PWADSelectBrowser->add(pwad.filename.c_str(), checked);
				m_filterIndices.emplace_back(pwad.filename, i);
			}
			i++;
		}
		m_PWADSelectBrowser->redraw();
	}

	/**
	 * @brief Places selected PWADs into the order browser for load order selection.
	 */
	static void scanCheckedPWADsCB(Fl_Widget*, void* data)
	{
		BootWindow* boot = static_cast<BootWindow*>(data);

		// Scan all PWADs in the selection browser to see if they're checked.
		for (size_t i = 1; i <= boot->m_PWADSelectBrowser->nitems(); i++)
		{
			auto& [pwad, selected] = boot->m_PWADs[boot->m_filterIndices[i - 1].second];
			if (boot->m_PWADSelectBrowser->checked(static_cast<int>(i)))
			{
				selected = true;
				AddSelected(boot->m_selectedPWADs, &pwad);
			}
			else
			{
				selected = false;
				EraseSelected(boot->m_selectedPWADs, &pwad);
			}
		}

		// Update the PWAD order browser.
		boot->updatePWADOrderBrowser();
	}

	/**
	 * @brief Update the visible PWAD order browser widget from the vector.
	 */
	void updatePWADOrderBrowser()
	{
		const int val = m_PWADOrderBrowser->value();
		m_PWADOrderBrowser->clear();
		for (const auto& pwad : m_selectedPWADs)
		{
			m_PWADOrderBrowser->add(pwad->filename.c_str());
		}
		m_PWADOrderBrowser->value(val);
	}

	/**
	 * @brief Get WAD information for selected IWAD.
	 */
	const scannedIWAD_t* selectedIWAD()
	{
		const size_t value = static_cast<size_t>(m_IWADBrowser->value());
		if (value == 0)
			return NULL;
		return &m_IWADs[value - 1];
	}

	/**
	 * @brief Get WAD information for selected WADs.
	 */
	void selectedWADs()
	{
		// IWADs
		const size_t value = static_cast<size_t>(m_IWADBrowser->value());
		scannedIWAD_t iwad = m_IWADs[value - 1];
		g_SelectedWADs.iwad = iwad.path;

		if (iwad.id != NULL && iwad.id->mIdName == "CHEX QUEST")
		{
			g_SelectedWADs.options.push_back("-deh");
			g_SelectedWADs.options.push_back("chex.deh");
		}

		// PWADs
		for (const auto& pwad : m_selectedPWADs)
		{
			g_SelectedWADs.pwads.push_back(pwad->path);
		}
	}

	void setOptions()
	{
		for (int i = 1; i <= m_gameOptionsBrowser->nitems(); i++) {
			if (m_gameOptionsBrowser->checked(i))
				g_SelectedWADs.options.push_back(OPTIONS_LIST[i - 1].second);
		}
	}

	/**
	 * @brief Update the WAD dir browser widget from the vector.
	 */
	void updateWADDirBrowser()
	{
		const int val = m_WADDirList->value();
		m_WADDirList->clear();
		for (const auto& waddir : m_WADDirs)
		{
			m_WADDirList->add(waddir.c_str());
		}
		m_WADDirList->value(val);
	}

	/**
	 * @brief Initialize WAD directories from CVar.
	 */
	void initWADDirs() { m_WADDirs = TokenizeString(::waddirs.str(), PATHLISTSEP); }

	/**
	 * @brief Update the CVar from WAD directories list.
	 */
	void setWADDirs() { ::waddirs.Set(JoinStrings(m_WADDirs, PATHLISTSEP).c_str()); }
};

/**
 * @brief Shim function to call new BootWindow() with proper params.
 */
static BootWindow* MakeBootWindow()
{
	return new BootWindow(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, "Odamex " DOTVERSIONSTR);
}

#include "m_argv.h"

/**
 * @brief Create the boot window for Odamex.
 *
 * @return The IWAD file to use when starting the game.
 */
scannedWADs_t GUI_BootWindow()
{
	// Scale according to 1600x900.
	Fl::screen_scale(0, MAX(Fl::h() / 900.0f, Fl::screen_scale(0)));

	// This feature is too clever by half, and in my experience just
	// deforms the window.
	Fl::keyboard_screen_scaling(0);

	// find arguments for fltk
	DArgs fltkargs = DArgs();
	fltkargs.AppendArg(::Args.GetArg(0));
	const char* rawargs = ::Args.CheckValue("-fltk");
	if (rawargs != nullptr)
		for (auto& arg : TokenizeString(rawargs, " "))
			fltkargs.AppendArg(arg.c_str());

	BootWindow* win = MakeBootWindow();
	win->initWADDirs();
	win->updateWADDirBrowser();
	win->rescanIWADs();
	win->position((Fl::w() - win->w()) / 2, (Fl::h() - win->h()) / 2);
	win->show(fltkargs.NumArgs(), (char**)fltkargs.GetArgv().data());

	// Blocks until the boot window has been closed.
	Fl::run();

	// Return the full IWAD path.
	return g_SelectedWADs;
}
