/*
 * This file is part of DesktopBSD, see the README file.
 *
 * Author: Pieringer Johannes <jpieringer@desktopbsd.net>
 *         (C) 2006
 *
 * Copyright: BSD, see the COPYING file included in this distribution
 *
 */

#include "grubconfiguration.h"
#include "executor.h"
#include "sysctl.h"

#include <sys/utsname.h>
#include <stdio.h>
#include <qfile.h>
#include <qstringlist.h>
#include <qtextstream.h>
#include <qfile.h>
#include <qurloperator.h>
#include <qstringlist.h>
#include <unistd.h>
#include <qcstring.h>
#include <qdir.h>

const char *GrubConfiguration::GRUBBackgroundColorNames[8] =
   {"black", "blue", "green", "cyan", "red", "magenta", "brown", "light-gray"
   };
const char *GrubConfiguration::GRUBForegroundColorNames[16] =
   {"black", "blue", "green", "cyan", "red", "magenta", "brown", "light-gray",
    "dark-gray", "light-blue", "light-green", "light-cyan", "light-red",
    "light-magenta", "yellow", "white"
   };
const char* GrubConfiguration::KnownGRUBCommandsNames[12] =
   { "default", "timeout", "color", "title", "root", "kernel", "makeactive",
     "chainloader", "savedefault", "initrd", "splashimage"
   };


/** Creates a new GrubConfiguration Object
 */
GrubConfiguration::GrubConfiguration()
{
	init();
}

void GrubConfiguration::init()
{
	this->unknownCommands = QPtrList<GrubCommand<QString> > ();
	this->defaultTitle.setValue(-1);
	this->timeout.setValue(10);

	color = NULL;
	splashimage = NULL;
}


/** Reads the menu.lst from GRUB
 */
bool GrubConfiguration::readConfiguration(const QString& path)
{
	QStringList lines;
	QFile file(path);
	if (file.open(IO_ReadOnly))
	{
		this->titles.clear();
		init();

		QTextStream stream(&file);
		QString allLines = QString();	//all lines of a bootentry in the config-file;
		QString oneLine = QString();	//one line of a bootentry in the config-file;
		QString line = QString();
		QString comment = QString();	// All lines without a command before the next command
		QString commentAfterCommand = QString();	// A comment after a commando like "titel freebsd #comment"

		while (!stream.atEnd() || !line.isEmpty())
		{
			if (line.isEmpty())
			{ /* If the line is not empty, the line wasn't processed */
				line = stream.readLine(); /* line of text excluding '\n' */
				commentAfterCommand = "";
			}

			/* Splits comment and command. */
			line = line.stripWhiteSpace();

			if (line.isEmpty())
			{	/* Jumps to the next line if the current line is empty */
				//comment += '\n';
				commentAfterCommand = "";
				line = "";
				continue;
			}

			processComment(line, comment, commentAfterCommand);
			if (line.isEmpty())
				continue; //The line was only a comment;

			bool okInt = false;
			GrubBootentry *bootentry;
			/* Processes the line without the comment */
			/* The comments are saved in "comment" and "commentAfterCommand" */
			switch (this->getCommand(line))
			{
				case Default:
					this->defaultTitle = GrubCommand<int>(line.toInt(&okInt), comment, commentAfterCommand);
					if (okInt == false)
						this->defaultTitle.setValue(-1); //default of -1 is "saved"
					line = "";
					break;
				case Timeout:
					this->timeout = GrubCommand<int>(line.toInt(&okInt), comment, commentAfterCommand);
					if (okInt == false)
						this->timeout.setValue(0);
					line = "";
					break;
				case Color:
					this->color = new GrubCommand<QString>(line, comment, commentAfterCommand);
					line = "";
					break;
				case Splashimage:
					this->splashimage = new GrubCommand<QString>(line, comment, commentAfterCommand);
					line = "";
					break;
				case Title:
					bool in_title;
					in_title = true;
					line = line.stripWhiteSpace();
					bootentry = new GrubBootentry(line, comment, commentAfterCommand);
					comment = "";
					commentAfterCommand = "";
					allLines = "";

					while (!stream.atEnd() && in_title)
					{
						line = stream.readLine(); /* line of text excluding '\n' */
						oneLine = line + "\n";
						/* Splits comment and command. */
						line = line.stripWhiteSpace();

						if (line.isEmpty())
						{	/* Jumps to the next line if the current line is empty */
							//comment += '\n';
							commentAfterCommand = "";
							line = "";
							continue;
						}
						processComment(line, comment, commentAfterCommand);
						if (line.isEmpty())
							continue; //The line was only a comment;

						/* Processes the line without the comment */
						/* The comments are saved in "comment" and "commentAfterCommand" */
						int command = this->getCommand(line);
						GrubCommand<QString> *com = new GrubCommand<QString>(line, comment, commentAfterCommand);
						switch (command)
						{
							case Root:
							{
								bootentry->root = com;
								break;
							}
							case Kernel:
							{
								bootentry->kernel = com;
								break;
							}
							case Savedefault:
							{
								bootentry->savedefault = com;
								oneLine = "";
								break;
							}
							case Chainloader:
							{
								bootentry->chainloader = com;
								break;
							}
							case Makeactive:
							{
								bootentry->makeactive = com;
								break;
							}
							case Initrd:
							{
								bootentry->initrd = com;
								break;
							}
							case Title:
							{
								in_title = false;
								delete com;
								break;
							}
							default:
								bootentry->appendUnknownCommand(new GrubCommand<QString>(line, comment, commentAfterCommand));
								delete com;
								break;
						}
						if (in_title)
						{
							comment = "";
							commentAfterCommand = "";
							line = "";
							allLines += oneLine;
						}
					}
					if (!line.isEmpty())
					{
						line = "title " + line + commentAfterCommand;
					}
					allLines.remove(allLines.length() - 1 , 2);
					bootentry->setLines(allLines);
					this->titles.append(bootentry);
					break;
				default:
					this->unknownCommands.append(new GrubCommand<QString>(line, comment, commentAfterCommand));
					line = "";
					break;
			}
			if (!stream.atEnd() && line.isEmpty())
				comment = "";
		}
		this->lastComment = comment;	//Saves the comment from the end of the file.
		file.close();
	}
	return true;
}

/** Returns the command of the line. The line mustn't have a command! If there is no command or an UNKNOWN
* command the function returns UNKNOWN 
*/
GrubConfiguration::KnownGRUBCommands GrubConfiguration::getCommand(QString& line)
{

	for (int i = 0; i < (sizeof(KnownGRUBCommandsNames) / sizeof(char*)); ++i)
		if (line.startsWith(KnownGRUBCommandsNames[i], false))
		{
			line.remove(0, QString(KnownGRUBCommandsNames[i]).length());
			line = line.stripWhiteSpace();
			return (KnownGRUBCommands)i;
		}

	return Unknown;
}

/** Splits the line in the command with the value and the comment and the comment after the command and the 		* value
	*/
void GrubConfiguration::processComment(QString & line, QString & comment, QString & commentAfterCommand)
{
	int commentIndex;
	commentIndex = line.find('#');
	//printf("CommentIndex: %i\n", commentIndex);
	switch (commentIndex)
	{
		case - 1:		/* In this line is no comment */
			commentAfterCommand = "";
			break;
		case 0:		/* In this line is only a comment */
			comment += line;
			comment += '\n';
			line = "";
			commentAfterCommand = "";
			break;
		default:		/* In this line has a commando with a comment afterwards */
			commentAfterCommand = line;
			commentAfterCommand.remove(0, commentIndex);
			line.remove(commentIndex, line.length() - 1);
			line = line.stripWhiteSpace();
			break;
	}
}

/** Returns the changed menu.lst file in a string.
	*/
QString GrubConfiguration::toString()
{
	QString toString = "";

	//default
	if (this->defaultTitle.getValue() != -1)
	{
		this->defaultTitle.setCommand("default");
		toString += this->defaultTitle.toString();
	}

	//timeout
	this->timeout.setCommand("timeout");
	toString += this->timeout.toString();

	//splashimage
	if (this->splashimage != NULL)
	{
		this->splashimage->setCommand("splashimage");
		toString += this->splashimage->toString();
	}

	if (this->color != NULL)
	{
		this->color->setCommand("color");
		toString += this->color->toString();
	}

	for (GrubCommand<QString>* command = this->unknownCommands.first();
	      command; command = this->unknownCommands.next())
		toString += command->toString();

	for (GrubBootentry* entry = this->titles.first(); entry; entry = this->titles.next())
	{
		if (this->defaultTitle.getValue() != -1)
			entry->savedefault = NULL;
		else
			entry->savedefault = new GrubCommand<QString>();
		toString += entry->toString();
	}

	toString += lastComment;
	return toString;
}

/** Overwrites the configuration file
*/
bool GrubConfiguration::writeConfiguration()
{
	QStringList lines;
	QFile file(GRUB_MENU_FILE);
	if (file.open(IO_WriteOnly))
	{
		QTextStream stream(&file);
		stream << this->toString();
		file.flush();
		file.close();
		return true;
	}
	return false;
}

/** Returns all boot-menu-entries in a QPtrList
*/
QPtrList<GrubBootentry> GrubConfiguration::getBootmenuEntries()
{
	return this->titles;
}

/** Adds a boot-menu entry
*/
void GrubConfiguration::addBootmenuEntry(GrubBootentry* entry)
{
	if (entry != NULL)
		this->titles.append(entry);
}

/** Removes a bootmenu entry
*/
void GrubConfiguration::removeBootmenuEntry(int id)
{
	titles.remove(id);
}

/** Generates the bootmenu automatic via a shell-script
*/
bool GrubConfiguration::generateBootmenu()
{
	char val[] = GRUB_MENU_GEN_FILE_TEST;
	char* name = mktemp(val);

	if (name == NULL)
		return false;

	Executor::exec(QString(GRUB_AUTOMATIC_GENERATION_SCRIPT) + " > " + name);
	this->readConfiguration(name);
	unlink(name);
	return true;
}

/** Sets the timeout for GRUB to wait till he loads the default bootmenu entry
*/
void GrubConfiguration::setTimeout(int seconds)
{
	this->timeout.setValue(seconds);
}

/** Returns the timeout
*/
int GrubConfiguration::getTimeout()
{
	return this->timeout.getValue();
}

/** Sets the index of the new default title, if the value is -1, the default title is saved
*/
void GrubConfiguration::setDefaultTitle(int title)
{
	this->defaultTitle.setValue(title);
}

/** Returns the index of the default title or -1 if default is saved
*/
int GrubConfiguration::getDefaultTitle()
{
	return this->defaultTitle.getValue();
}

/** Sets the bootmenuentries
*/
void GrubConfiguration::setBootmenuEntries(QPtrList< GrubBootentry > entries)
{
	this->titles = entries;
}

/** Copys the splashimage from the path
*/
bool GrubConfiguration::setSplashimagePath(QString *splashimagePath)
{
	if (splashimagePath == NULL)
	{
		delete this->splashimage;
		this->splashimage = NULL;
		return true;
	}

	if (!QFile::exists(*splashimagePath))
		return false;

	(*splashimagePath) = splashimagePath->stripWhiteSpace();
	if (!splashimagePath->startsWith("/boot/grub/"))
	{
		QString command = "cp ";
		command += *splashimagePath;
		command += " /boot/grub/";
		Executor::exec(command);
	}

	(*splashimagePath) = "/boot/grub/" + QStringList::split(QChar('/'), *splashimagePath).last();

	if (this->splashimage == NULL)
		this->splashimage = new GrubCommand<QString>();

	this->splashimage->setValue(*splashimagePath);
	return true;
}

/** Returns the path of the splash image or null if no splashimage is set.
	*/
QString * GrubConfiguration::getSplashimagePath()
{
	if (this->splashimage == NULL)
		return NULL;
	else
		return new QString(this->splashimage->getValue());
}

/** Sets the colors of GRUB
	*/
void GrubConfiguration::setColor(GRUBForegroundColor* foreNormal, GRUBBackgroundColor* backNormal, bool *blinkNormal, GRUBForegroundColor* foreHigh, GRUBBackgroundColor* backHigh, bool *blinkHigh)
{
	if (this->color == NULL)
		this->color = new GrubCommand<QString>();

	if (foreNormal == NULL || foreHigh == NULL || backNormal == NULL || backHigh == NULL)
	{
		delete this->color;
		this->color = NULL;
		return;
	}

	QString color = "";

	if ((*blinkNormal))
		color += "blink-";

	color += GRUBForegroundColorNames[*foreNormal];
	color += "/";
	color += GRUBBackgroundColorNames[*backNormal];
	color += " ";

	if ((*blinkHigh))
		color += "blink-";

	color += GRUBForegroundColorNames[*foreHigh];
	color += "/";
	color += GRUBBackgroundColorNames[*backHigh];

	this->color->setValue(color);
}

/** Sets the foreground color
	*/
GrubConfiguration::GRUBForegroundColor *GrubConfiguration::getColorNormalForeground()
{
	if (this->color == NULL)
		return NULL;

	QString value = processColor(0);
	value = value.remove(' ');
	value = value.remove("blink-", false);

	for (uint i = 0; i < (sizeof(GRUBForegroundColorNames) / sizeof(char*)); ++i)
		if (value.lower() == QString(GRUBForegroundColorNames[i]))
			return (GRUBForegroundColor*)new uint(i);
	return NULL;
}

/** Sets the background color
	*/
GrubConfiguration::GRUBBackgroundColor *GrubConfiguration::getColorNormalBackground()
{
	if (this->color == NULL)
		return NULL;

	QString value = processColor(1);
	for (uint i = 0; i < (sizeof(GRUBBackgroundColorNames) / sizeof(char*)); ++i)
		if (value.lower() == QString(GRUBBackgroundColorNames[i]))
			return (GRUBBackgroundColor*)new uint(i);
	return NULL;
}

/** Sets the highlighted foreground color
	*/
GrubConfiguration::GRUBForegroundColor *GrubConfiguration::getColorHighlightForeground()
{
	if (this->color == NULL)
		return NULL;

	QString value = processColor(2);
	value = value.remove(' ');
	value = value.remove("blink-", false);

	for (uint i = 0; i < (sizeof(GRUBForegroundColorNames) / sizeof(char*)); ++i)
		if (value.lower() == QString(GRUBForegroundColorNames[i]))
			return (GRUBForegroundColor*)new uint(i);
	return NULL;
}

/** Sets the highlighted background color
	*/
GrubConfiguration::GRUBBackgroundColor *GrubConfiguration::getColorHighlightBackground()
{
	if (this->color == NULL)
		return NULL;

	QString value = processColor(3);
	for (uint i = 0; i < (sizeof(GRUBBackgroundColorNames) / sizeof(char*)); ++i)
		if (value.lower() == QString(GRUBBackgroundColorNames[i]))
			return (GRUBBackgroundColor*)new uint(i);
	return NULL;
}

/** Returns the color of the colorstring at the position of the parameter
	*/
QString GrubConfiguration::processColor(uint position)
{
	QString value = this->color->getValue();
	value = value.simplifyWhiteSpace();
	value = value.replace(" /", "/");
	value = value.replace("/ ", "/");
	value = value.replace(" -", "-");
	value = value.replace("- ", "-");

	QStringList list = QStringList::split(" ", value);

	if (position >= 4 || position < 0)
		return "";

	list = QStringList::split("/", list[position/2]);
	position = position % 2;

	return list[position];
}

/** Returns true if the normal color is blinking
 */
bool * GrubConfiguration::getNormalBlink()
{
	if (this->color == NULL)
		return NULL;

	QString value = processColor(0);
	return new bool(value.contains("blink", false) != 0);
}

/** Returns true if the highlighted color is blinking
 */
bool * GrubConfiguration::getHighlightBlink()
{
	if (this->color == NULL)
		return NULL;

	QString value = processColor(2);
	return new bool(value.contains("blink", false) != 0);
}

/** Installs GRUB on the system per the grub-install shell script
	*/
bool GrubConfiguration::installGrub()
{
	/* We most certainly need to write to a mounted disk here. */
	int geomflags = SysCtl::getIntCtl("kern.geom.debugflags");
	if(~geomflags & 0x10)
		SysCtl::setIntCtl("kern.geom.debugflags", geomflags | 0x10);
	
	QString ret = Executor::strFromCmd(QString(GRUB_INSTALL_EXECUTABLE) + " hd0");

	if(~geomflags & 0x10)
		SysCtl::setIntCtl("kern.geom.debugflags", geomflags);

	if (ret.contains("Installation finished. No error reported.", false) == 0)
		return false;

	return true;
}

/** Checks if the GRUB-folder exists.
	*/
bool GrubConfiguration::isGrubInstalled()
{
	return QFile::exists(QString(GRUB_FOLDER) + "/stage1");
}

/** Checks if GRUB is supported on this system (i386 only) */
bool GrubConfiguration::isGrubSupported()
{
	struct utsname u;
	uname(&u);
	return !strcmp("i386", u.machine);
}

/** Checks if the GRUB package is installed on the system,
  * but not if GRUB is installed as the boot loader.
  */
bool GrubConfiguration::isGrubAvailable()
{
	return QFile(GRUB_INSTALL_EXECUTABLE).exists();
}
