#include "latexstyleparser.h"

LatexStyleParser::LatexStyleParser(QObject *parent, QString baseDirName, QString kpsecmd) :
	SafeThread(parent)
{
	baseDir = baseDirName;
	stopped = false;
	kpseWhichCmd = kpsecmd;
	mFiles.clear();
	//check if pdflatex is present
	texdefDir = kpsecmd.left(kpsecmd.length() - 9);
	QProcess myProc(0);
	//myProc.start(texdefDir+"texdef");
	myProc.start(texdefDir + "pdflatex --version");
	myProc.waitForFinished();
	texdefMode = (myProc.exitCode() == 0);
	if (texdefMode) {
		QString output = myProc.readAllStandardOutput();
		output = output.split("\n").first().trimmed();
		if (output.contains("MiKTeX")) {
			kpseWhichCmd.chop(9);
			kpseWhichCmd.append("findtexmf");
		}
	}
}

void LatexStyleParser::stop()
{
	stopped = true;
	mFilesAvailable.release();
}

void LatexStyleParser::run()
{
	forever {
		//wait for enqueued lines
		mFilesAvailable.acquire();
		if (stopped && mFiles.count() == 0) break;
		mFilesLock.lock();
		QString fn = mFiles.dequeue();
		mFilesLock.unlock();
		QString topPackage = "";
		QString dirName = "";
		if (fn.contains("/")) {
			int i = fn.indexOf("/");
			if (i > -1) {
				dirName = fn.mid(i + 1);
				fn = fn.left(i);
			}
		}
		if (fn.contains('#')) {
			QStringList lst = fn.split('#');
			if (!lst.isEmpty())
				topPackage = lst.takeFirst();
			if (!lst.isEmpty())
				fn = lst.last();
		} else {
			topPackage = fn;
			topPackage.chop(4);
		}
		QString fullName = kpsewhich(fn); // find file
		if (fullName.isEmpty()) {
			if (!dirName.isEmpty()) {
				fullName = kpsewhich(fn, dirName); // find file
			}
		}
		if (fullName.isEmpty())
			continue;

		QStringList results, parsedPackages;
		results = readPackage(fullName, parsedPackages); // parse package(s)
		results.sort();

		if (texdefMode) {
			QStringList appendList;
			//QStringList texdefResults=readPackageTexDef(fn); // parse package(s) by texdef as well for indirectly defined commands
			QStringList texdefResults = readPackageTracing(fn);
			texdefResults.sort();
			// add only additional commands to results
			if ( !results.isEmpty() && !texdefResults.isEmpty()) {
				QStringList::const_iterator texdefIterator;
				QStringList::const_iterator resultsIterator = results.constBegin();
				QString result = *resultsIterator;
				for (texdefIterator = texdefResults.constBegin(); texdefIterator != texdefResults.constEnd(); ++texdefIterator) {
					QString td = *texdefIterator;
					if (td.startsWith('#'))
						continue;
					int i = td.indexOf("#");
					if (i >= 0)
						td = td.left(i);
					while (result < td && resultsIterator != results.constEnd()) {
						++resultsIterator;
						if (resultsIterator != results.constEnd()) {
							result = *resultsIterator;
						} else {
							result.clear();
						}
					}
					//compare result/td
					bool addCommand = true;

					if (result.startsWith(td)) {
						if (result.length() > td.length()) {
							QChar c = result.at(td.length());
							switch (c.toLatin1()) {
							case '#':
								;
							case '{':
								;
							case '[':
								addCommand = false;
							default:
								break;
							}
						} else {
							addCommand = false; //exactly equal
						}
					}

					if (addCommand) {
						appendList << *texdefIterator;
						//qDebug()<<td;
					}
				}
			} else {
				if (results.isEmpty())
					results << texdefResults;
			}
			results << appendList;
		}


		// if included styles call for additional generation, do it.
		QStringList included = results.filter(QRegExp("#include:.+"));
		foreach (QString elem, included) {
			elem = elem.mid(9);
			if (!QFileInfo("cwl:" + elem + ".cwl").exists()) {
				QString hlp = kpsewhich(elem + ".sty");
				if (!hlp.isEmpty()) {
					if (!topPackage.isEmpty())
						elem = topPackage + "#" + elem + ".sty";
					addFile(elem);
				}
			}
		}

		// write results
		if (!results.isEmpty()) {
			QFileInfo info(fn);
			QString baseName = info.completeBaseName();
			if (!baseName.isEmpty()) {
				QFile data(joinPath(baseDir, "completion/autogenerated", baseName + ".cwl"));
				if (data.open(QFile::WriteOnly | QFile::Truncate)) {
					QTextStream out(&data);
					out << "# autogenerated by txs\n";
					foreach (const QString &elem, results) {
						out << elem << "\n";
					}
				}
				if (!topPackage.isEmpty() && topPackage != baseName)
					baseName = topPackage + "#" + baseName;
				emit scanCompleted(baseName);
			}
		}
	}
}

void LatexStyleParser::addFile(QString filename)
{
	mFilesLock.lock();
	mFiles.enqueue(filename);
	mFilesLock.unlock();
	mFilesAvailable.release();
}

QStringList LatexStyleParser::readPackage(QString fn, QStringList &parsedPackages)
{
	if (parsedPackages.contains(fn))
		return QStringList();
	QFile data(fn);
	QStringList results;
	if (data.open(QIODevice::ReadOnly | QIODevice::Text)) {
		QTextStream stream(&data);
		parsedPackages << fn;
		QString line;
		QRegExp rxDef("\\\\def\\s*(\\\\[\\w@]+)\\s*(#\\d+)?");
		QRegExp rxLet("\\\\let\\s*(\\\\[\\w@]+)");
		QRegExp rxCom("\\\\(newcommand|providecommand|DeclareRobustCommand)\\*?\\s*\\{(\\\\\\w+)\\}\\s*\\[?(\\d+)?\\]?");
		QRegExp rxCom2("\\\\(newcommand|providecommand|DeclareRobustCommand)\\*?\\s*(\\\\\\w+)\\s*\\[?(\\d+)?\\]?");
		QRegExp rxEnv("\\\\newenvironment\\s*\\{(\\w+)\\}\\s*\\[?(\\d+)?\\]?");
		QRegExp rxInput("\\\\input\\s*\\{?([\\w._]+)");
		QRegExp rxRequire("\\\\RequirePackage\\s*\\{(\\S+)\\}");
		rxRequire.setMinimal(true);
		QRegExp rxRequireStart("\\\\RequirePackage\\s*\\{(.+)");
		QRegExp rxDecMathSym("\\\\DeclareMathSymbol\\s*\\{\\\\(\\w+)\\}");
		QRegExp rxNewLength("\\\\newlength\\s*\\{\\\\(\\w+)\\}");
		QRegExp rxNewCounter("\\\\newcounter\\s*\\{(\\w+)\\}");
		QRegExp rxLoadClass("\\\\LoadClass\\s*\\{(\\w+)\\}");
		bool inReq = false;
		while (!stream.atEnd()) {
			line = stream.readLine();
			line = LatexParser::cutComment(line);
			if (line.startsWith("\\endinput"))
				break;
			int options = 0;
			if (inReq) {
				int col = line.indexOf('}');
				if (col > -1) {
					QString zw = line.left(col);
					foreach (QString elem, zw.split(',')) {
						QString package = elem.remove(' ');
						if (!package.isEmpty())
							results << "#include:" + package;
					}
					inReq = false;
				} else {
					foreach (QString elem, line.split(',')) {
						QString package = elem.remove(' ');
						if (!package.isEmpty())
							results << "#include:" + package;
					}
				}
				continue;
			}
			if (rxDef.indexIn(line) > -1) {
				QString name = rxDef.cap(1);
				if (name.contains("@"))
					continue;
				QString optionStr = rxDef.cap(2);
				//qDebug()<< name << ":"<< optionStr;
				options = optionStr.mid(1).toInt(); //returns 0 if conversion fails
				for (int j = 0; j < options; j++) {
					name.append(QString("{arg%1}").arg(j + 1));
				}
				name.append("#S");
				if (!results.contains(name))
					results << name;
				continue;
			}
			if (rxLet.indexIn(line) > -1) {
				QString name = rxLet.cap(1);
				if (name.contains("@"))
					continue;
				name.append("#S");
				if (!results.contains(name))
					results << name;
				continue;
			}
			if (rxCom.indexIn(line) > -1) {
				QString name = rxCom.cap(2);
				if (name.contains("@"))
					continue;
				QString optionStr = rxCom.cap(3);
				//qDebug()<< name << ":"<< optionStr;
				options = optionStr.toInt(); //returns 0 if conversion fails
				for (int j = 0; j < options; j++) {
					name.append(QString("{arg%1}").arg(j + 1));
				}
				name.append("#S");
				if (!results.contains(name))
					results << name;
				continue;
			}
			if (rxCom2.indexIn(line) > -1) {
				QString name = rxCom2.cap(2);
				if (name.contains("@"))
					continue;
				QString optionStr = rxCom2.cap(3);
				//qDebug()<< name << ":"<< optionStr;
				options = optionStr.toInt(); //returns 0 if conversion fails
				for (int j = 0; j < options; j++) {
					name.append(QString("{arg%1}").arg(j + 1));
				}
				name.append("#S");
				if (!results.contains(name))
					results << name;
				continue;
			}
			if (rxEnv.indexIn(line) > -1) {
				QString name = rxEnv.cap(1);
				if (name.contains("@"))
					continue;
				QString optionStr = rxEnv.cap(2);
				//qDebug()<< name << ":"<< optionStr;
				QString zw = "\\begin{" + name + "}#S";
				if (!results.contains(zw))
					results << zw;
				zw = "\\end{" + name + "}#S";
				if (!results.contains(zw))
					results << zw;
				continue;
			}
			if (rxInput.indexIn(line) > -1) {
				QString name = rxInput.cap(1);
				name = kpsewhich(name);
				if (!name.isEmpty() && name != fn) // avoid indefinite loops
					results << readPackage(name, parsedPackages);
				continue;
			}
			if (rxNewLength.indexIn(line) > -1) {
				QString name = "\\" + rxNewLength.cap(1);
				if (name.contains("@"))
					continue;
				if (!results.contains(name))
					results << name;
				continue;
			}
			if (rxNewCounter.indexIn(line) > -1) {
				QString name = "\\the" + rxNewCounter.cap(1);
				if (name.contains("@"))
					continue;
				if (!results.contains(name))
					results << name;
				continue;
			}
			if (rxDecMathSym.indexIn(line) > -1) {
				QString name = "\\" + rxDecMathSym.cap(1);
				if (name.contains("@"))
					continue;
				name.append("#Sm");
				if (!results.contains(name))
					results << name;
				continue;
			}
			if (rxRequire.indexIn(line) > -1) {
				QString arg = rxRequire.cap(1);
				foreach (QString elem, arg.split(',')) {
					QString package = elem.remove(' ');
					if (!package.isEmpty())
						results << "#include:" + package;
				}
				continue;
			}
			if (rxRequireStart.indexIn(line) > -1) {
				QString arg = rxRequireStart.cap(1);
				foreach (QString elem, arg.split(',')) {
					QString package = elem.remove(' ');
					if (!package.isEmpty())
						results << "#include:" + package;
				}
				inReq = true;
			}
			if (rxLoadClass.indexIn(line) > -1) {
				QString arg = rxLoadClass.cap(1);
				if (!arg.isEmpty()) {
					if (mPackageAliases.contains(arg))
						foreach (QString elem, mPackageAliases.values(arg)) {
							results << "#include:" + elem;
						}
					else
						results << "#include:" + arg;
				}
				continue;
			}
		} // while line
	} // open data
	return results;
}

QString LatexStyleParser::kpsewhich(QString name, QString dirName)
{
	if (name.startsWith("."))
		return "";  // don't check .sty/.cls
	QString fn = name;
	if (!kpseWhichCmd.isEmpty()) {
		QProcess myProc(0);
		QStringList options;
		if (!dirName.isEmpty()) {
			options << "-path=" + dirName;
		}
		options << fn;
		myProc.start(kpseWhichCmd, options);
		myProc.waitForFinished();
		if (myProc.exitCode() == 0) {
			fn = myProc.readAllStandardOutput();
			fn = fn.split('\n').first().trimmed(); // in case more than one results are present
		} else
			fn.clear();
	}
	return fn;
}

QStringList LatexStyleParser::readPackageTexDef(QString fn)
{
	if (!fn.endsWith(".sty"))
		return QStringList();

	QString fname = fn.left(fn.length() - 4);
	QProcess myProc(0);

	//add exec search path
	if (!texdefDir.isEmpty()) {
		QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
		if (env.value("SHELL") == "/bin/bash") {
			env.insert("PATH", env.value("PATH") + ":" + texdefDir);
			myProc.setProcessEnvironment(env);
		}
	}
	QStringList args;
	QString result;
	args << "-t" << "latex" << "-l" << "-p" << fname;
	myProc.start(texdefDir + "texdef", args);
	args.clear();
	myProc.waitForFinished();
	if (myProc.exitCode() != 0)
		return QStringList();

	result = myProc.readAllStandardOutput();
	QStringList lines = result.split('\n');

	bool incl = false;
	for (int i = 0; i < lines.length(); i++) {
		if (lines.at(i).startsWith("Defined")) {
			QString name = lines.at(i);
			name = name.mid(17);
			name = name.left(name.length() - 2);
			incl = (name == fn);
			if (!incl)
				args << "#include:" + name;
		}
		if (incl && lines.at(i).startsWith("\\")) {
			if (lines.at(i).startsWith("\\\\")) {
				QString zw = lines.at(i) + "#S";
				zw.remove(0, 1);
				//args<<zw;
			} else {
				args << lines.at(i) + "#S";
			}
		}
	}
	// replace tex env def by latex commands
	QStringList zw = args.filter(QRegExp("\\\\end.+"));
	foreach (const QString &elem, zw) {
		QString begin = elem;
		begin.remove(1, 3);
		int i = args.indexOf(begin);
		if (i != -1) {
			QString env = begin.mid(1, begin.length() - 3);
			args.replace(i, "\\begin{" + env + "}");
			i = args.indexOf(elem);
			args.replace(i, "\\end{" + env + "}");
		}
	}

	return args;
}

QStringList LatexStyleParser::readPackageTracing(QString fn)
{
	if (!fn.endsWith(".sty"))
		return QStringList();

	fn.chop(4);

	QString tempPath = QDir::tempPath() + QDir::separator() + "." + QDir::separator();
	QTemporaryFile *tf = new QTemporaryFile(tempPath + "XXXXXX.tex");
	if (!tf) return QStringList();
	tf->open();

	QTextStream out(tf);
	out << "\\documentclass{article}\n";
	out << "\\usepackage{filehook}\n";
	out << "\\usepackage{currfile}\n";
	out << "\\AtBeginOfEveryFile{\\message{^^Jentering file \\currfilename ^^J}}\n";
	out << "\\AtEndOfEveryFile{\\message{^^Jleaving file \\currfilename ^^J}}\n";
	out << "\\tracingonline=1\n";
	out << "\\tracingassigns=1\n";
	out << "\\usepackage{" << fn << "}\n";
	out << "\\tracingassigns=0\n";
	out << "\\AtBeginOfEveryFile{}\n";
	out << "\\AtEndOfEveryFile{}\n";
	out << "\\begin{document}\n";
	out << "\\end{document}";
	tf->close();


	QProcess myProc(0);
	//add exec search path
	if (!texdefDir.isEmpty()) {
		QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
		if (env.value("SHELL") == "/bin/bash") {
			env.insert("PATH", env.value("PATH") + ":" + texdefDir);
			myProc.setProcessEnvironment(env);
		}
	}
	QStringList args;
	QString result;
	args << "-draftmode" << "-interaction=nonstopmode" << "--disable-installer" << tf->fileName();
	myProc.setWorkingDirectory(QFileInfo(tf->fileName()).absoluteDir().absolutePath());
	myProc.start(texdefDir + "pdflatex", args);
	args.clear();
	myProc.waitForFinished();
	if (myProc.exitCode() != 0)
		return QStringList();

	result = myProc.readAllStandardOutput();
	QStringList lines = result.split('\n');

	QStack<QString> stack;
	foreach (const QString elem, lines) {
		if (elem.startsWith("entering file")) {
			QString name = elem.mid(14);
			stack.push(name);
			if (stack.size() == 2) { // first level of include
				args << "#include:" + name;
			}
		}
		if (elem.startsWith("leaving file")) {
			stack.pop();
			if (stack.isEmpty())
				break;
		}
		if (stack.size() == 1 && elem.endsWith("=undefined}")) {
			if (elem.startsWith("{changing ")  && !elem.contains("@")) {
				QString zw = elem.mid(10);
				zw.chop(11);
				if (!args.contains(zw + "#S"))
					args << zw + "#S";
			}
		}
	}

	// replace tex env def by latex commands
	QStringList zw = args.filter(QRegExp("\\\\end.+"));
	foreach (const QString &elem, zw) {
		QString begin = elem;
		begin.remove(1, 3);
		int i = args.indexOf(begin);
		if (i != -1) {
			QString env = begin.mid(1, begin.length() - 3);
			args.replace(i, "\\begin{" + env + "}");
			i = args.indexOf(elem);
			args.replace(i, "\\end{" + env + "}");
		}
	}

	delete tf;
	return args;
}
