/* This file is part of the KDE project
   Copyright (C) 2006 Jaroslaw Staniek <js@iidea.pl>

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Library General Public
   License as published by the Free Software Foundation; either
   version 2 of the License, or (at your option) any later version.

   This library 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
   Library General Public License for more details.

   You should have received a copy of the GNU Library General Public License
   along with this library; see the file COPYING.LIB.  If not, write to
   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
*/

#include "altertable.h"

#include <unistd.h>

#include <tqapplication.h>
#include <tqfile.h>
#include <tqdir.h>
#include <tqregexp.h>
#include <tqclipboard.h>

#include <kdebug.h>

#include <main/keximainwindowimpl.h>
#include <core/kexiaboutdata.h>
#include <core/kexidialogbase.h>
#include <core/kexiviewbase.h>
#include <core/kexipartitem.h>
#include <core/kexitabledesignerinterface.h>
#include <core/kexiinternalpart.h>
#include <kexiutils/utils.h>
#include <koproperty/set.h>
#include <kexidb/connection.h>
#include <kexidb/utils.h>

TQString testFilename;
TQFile testFile;
TQTextStream testFileStream;
TQStringList testFileLine;
uint testLineNumber = 0;
TQString origDbFilename, dbFilename;
int variableI = 1; // simple variable 'i' support
int newArgc;
char** newArgv;
KexiMainWindowImpl* win = 0;
KexiProject* prj = 0;

void showError(const TQString& msg)
{
	TQString msg_(msg);
	msg_.prepend(TQString("Error at line %1: ").arg(testLineNumber));
	kdDebug() << msg_ << endl;
}

/* Reads a single line from testFileStream, fills testFileLine, updates testLineNumber
 text in quotes is extracted, e.g. \"ab c\" is treat as one item "ab c"
 Returns flas on failure (e.g. end of file).
 Empty lines and lines or parts of lines with # (comments) are omitted. */
tristate readLineFromTestFile(const TQString& expectedCommandName = TQString())
{
	TQString s;
	bool blockComment = false;
	while (true) {
		if (testFileStream.atEnd())
			return cancelled;
		testLineNumber++;
		s = testFileStream.readLine().stripWhiteSpace();
		if (blockComment) {
			if (s.endsWith("*/"))
				blockComment = false;
			continue;
		}
		if (!blockComment && s.startsWith("/*")) {
			blockComment = true;
			continue;
		}
		if (s.startsWith("#"))
			continue; //skip commented line
		if (!s.isEmpty())
			break;
	}
	s.append(" "); //sentinel
	TQString item;
	testFileLine.clear();
	const int len = s.length();
	bool skipWhiteSpace = true, quoted = false;
	for (int i=0; i<len; i++) {
		const TQChar ch( s.ref(i) );
		if (skipWhiteSpace) {
			if (ch=='#')
				break; //eoln
			if (ch==' ' || ch=='\t')
				continue;
			skipWhiteSpace = false;
			if (ch=='\"') {
				quoted = true;
				continue;
			}
			item.append(ch);
		}
		else {
			if ((quoted && ch=='\"') || (!quoted && (ch==' ' || ch=='\t'))) { //end of item
				skipWhiteSpace = true;
				quoted = false;
				testFileLine.append( item );
				item = TQString();
				continue;
			}
			item.append(ch);
		}
	}
	if (!expectedCommandName.isEmpty() && testFileLine[0]!=expectedCommandName) {
		showError( TQString("Invalid command '%1', expected '%2'")
			.arg(testFileLine[0]).arg(expectedCommandName));
		return false;
	}
	if (quoted) {
		showError( "Invalid contents" );
		return false;
	}
	return true;
}

bool checkItemsNumber(int expectedNumberOfItems, int optionalNumberOfItems = -1)
{
	bool ok = expectedNumberOfItems==(int)testFileLine.count();
	if (optionalNumberOfItems>0)
		ok = ok || optionalNumberOfItems==(int)testFileLine.count();
	if (!ok) {
		TQString msg = TQString("Invalid number of args (%1) for command '%2', expected: %3")
			.arg(testFileLine.count()).arg(testFileLine[0]).arg(expectedNumberOfItems);
		if (optionalNumberOfItems>0)
			msg.append( TQString(" or %1").arg(optionalNumberOfItems) );
		showError( msg );
		return false;
	}
	return true;
}

TQVariant::Type typeNameToTQVariantType(const TQCString& name_)
{
	TQCString name( name_.lower() );
	if (name=="string")
		return TQVariant::String;
	if (name=="int")
		return TQVariant::Int;
	if (name=="bool" || name=="boolean")
		return TQVariant::Bool;
	if (name=="double" || name=="float")
		return TQVariant::Double;
	if (name=="date")
		return TQVariant::Date;
	if (name=="datetime")
		return TQVariant::DateTime;
	if (name=="time")
		return TQVariant::Time;
	if (name=="bytearray")
		return TQVariant::ByteArray;
	if (name=="longlong")
		return TQVariant::LongLong;
//todo more types
	showError(TQString("Invalid type '%1'").arg(name_));
	return TQVariant::Invalid;
}

// casts string to TQVariant
bool castStringToTQVariant( const TQString& string, const TQCString& type, TQVariant& result )
{
	if (string.lower()=="<null>") {
		result = TQVariant();
		return true;
	}
	if (string=="\"\"") {
		result = TQString("");
		return true;
	}
	const TQVariant::Type vtype = typeNameToTQVariantType( type );
	bool ok;
	result = KexiDB::stringToVariant( string, vtype, ok );
	return ok;
}

// returns a number parsed from argument; if argument is i or i++, variableI is used
// 'ok' is set to false on failure
static int getNumber(const TQString& argument, bool& ok)
{
	int result;
	ok = true;
	if (argument=="i" || argument=="i++") {
		result = variableI;
		if (argument=="i++")
			variableI++;
	}
	else {
		result = argument.toInt(&ok);
		if (!ok) {
			showError(TQString("Invalid value '%1'").arg(argument));
			return -1;
		}
	}
	return result;
}

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

AlterTableTester::AlterTableTester()
 : TQObject()
 , m_finishedCopying(false)
{
	//copy the db file to a temp file
	tqInitNetworkProtocols();
	TQPtrList<TQNetworkOperation> list = m_copyOperator.copy( 
		"file://" + TQDir::current().path() + "/" + origDbFilename, 
		"file://" + TQDir::current().path() + "/" + dbFilename, false, false );
	connect(&m_copyOperator, TQ_SIGNAL(finished(TQNetworkOperation*)), 
		this, TQ_SLOT(slotFinishedCopying(TQNetworkOperation*)));
}

AlterTableTester::~AlterTableTester()
{
	TQFile(dbFilename).remove();
}

void AlterTableTester::slotFinishedCopying(TQNetworkOperation* oper)
{
	if (oper->operation()==TQNetworkProtocol::OpPut)
		m_finishedCopying = true;
}

bool AlterTableTester::changeFieldProperty(KexiTableDesignerInterface* designerIface)
{
	if (!checkItemsNumber(5))
		return false;
	TQVariant newValue;
	TQCString propertyName( testFileLine[2].latin1() );
	TQCString propertyType( testFileLine[3].latin1() );
	TQString propertyValueString(testFileLine[4]);
	if (propertyName=="type")
		newValue = (int)KexiDB::Field::typeForString(testFileLine[4]);
	else {
		if (!castStringToTQVariant(propertyValueString, propertyType, newValue)) {
			showError( TQString("Could not set property '%1' value '%2' of type '%3'")
				.arg(propertyName).arg(propertyValueString).arg(propertyType) );
			return false;
		}
	}
	bool ok;
	int row = getNumber(testFileLine[1], ok)-1;
	if (!ok)
		return false;
	designerIface->changeFieldPropertyForRow( row, propertyName, newValue, 0, true );
	if (propertyName=="type") {
		//clean subtype name, e.g. from "longText" to "LongText", because dropdown list is case-sensitive
		TQString realSubTypeName;
		if (KexiDB::Field::BLOB == KexiDB::Field::typeForString(testFileLine[4]))
//! @todo hardcoded!
			realSubTypeName = "image";
		else
			realSubTypeName = KexiDB::Field::typeString( KexiDB::Field::typeForString(testFileLine[4]) );
		designerIface->changeFieldPropertyForRow( row, "subType", realSubTypeName, 0, true );
	}
	return true;
}

//helper
bool AlterTableTester::getSchemaDump(KexiDialogBase* dlg, TQString& schemaDebugString)
{
	KexiTableDesignerInterface* designerIface 
		= dynamic_cast<KexiTableDesignerInterface*>( dlg->selectedView() );
	if (!designerIface)
		return false;

	// Get the result
	tristate result;
	schemaDebugString = designerIface->debugStringForCurrentTableSchema(result);
	if (true!=result) {
		showError( TQString("Loading modified schema failed. Result: %1")
			.arg(~result ? "cancelled" : "false") );
		return false;
	}
	schemaDebugString.remove(TQRegExp(",$")); //no need to have "," at the end of lines
	return true;
}

bool AlterTableTester::showSchema(KexiDialogBase* dlg, bool copyToClipboard)
{
	TQString schemaDebugString;
	if (!getSchemaDump(dlg, schemaDebugString))
		return false;
	if (copyToClipboard)
		TQApplication::clipboard()->setText( schemaDebugString );
	else
		kdDebug() << TQString("Schema for '%1' table:\n").arg(dlg->partItem()->name())
			+ schemaDebugString + "\nendSchema" << endl;
	return true;
}

bool AlterTableTester::checkInternal(KexiDialogBase* dlg,
	TQString& debugString, const TQString& endCommand, bool skipColonsAndStripWhiteSpace)
{
	Q_UNUSED(dlg);
	TQTextStream resultStream(&debugString, IO_ReadOnly);
	// Load expected result, compare
	TQString expectedLine, resultLine;
	while (true) {
		const bool testFileStreamAtEnd = testFileStream.atEnd();
		if (!testFileStreamAtEnd) {
			testLineNumber++;
			expectedLine = testFileStream.readLine();
			if (skipColonsAndStripWhiteSpace) {
				expectedLine = expectedLine.stripWhiteSpace();
				expectedLine.remove(TQRegExp(",$")); //no need to have "," at the end of lines
			}
		}
		if (testFileStreamAtEnd || endCommand==expectedLine.stripWhiteSpace()) {
			if (!resultStream.atEnd()) {
				showError( "Test file ends unexpectedly." );
				return false;
			}
			break;
		}
		//test line loaded, load result
		if (resultStream.atEnd()) {
			showError( TQString("Result ends unexpectedly. There is at least one additinal test line: '")
				+ expectedLine +"'" );
			return false;
		}
		resultLine = resultStream.readLine();
		if (skipColonsAndStripWhiteSpace) {
			resultLine = resultLine.stripWhiteSpace();
			resultLine.remove(TQRegExp(",$")); //no need to have "," at the end of lines
		}
		if (resultLine!=expectedLine) {
			showError(
				TQString("Result differs from the expected:\nExpected: ")
				+expectedLine+"\n????????: "+resultLine+"\n");
			return false;
		}
	}
	return true;
}

bool AlterTableTester::checkSchema(KexiDialogBase* dlg)
{
	TQString schemaDebugString;
	if (!getSchemaDump(dlg, schemaDebugString))
		return false;
	bool result = checkInternal(dlg, schemaDebugString, "endSchema", true /*skipColonsAndStripWhiteSpace*/);
	kdDebug() << TQString("Schema check for table '%1': %2").arg(dlg->partItem()->name())
		.arg(result ? "OK" : "Failed") << endl;
	return result;
}

bool AlterTableTester::getActionsDump(KexiDialogBase* dlg, TQString& actionsDebugString)
{
	KexiTableDesignerInterface* designerIface 
		= dynamic_cast<KexiTableDesignerInterface*>( dlg->selectedView() );
	if (!designerIface)
		return false;
	tristate result = designerIface->simulateAlterTableExecution(&actionsDebugString);
	if (true!=result) {
		showError( TQString("Computing simplified actions for table '%1'  failed.").arg(dlg->partItem()->name()) );
		return false;
	}
	return true;
}

bool AlterTableTester::showActions(KexiDialogBase* dlg, bool copyToClipboard)
{
	TQString actionsDebugString;
	if (!getActionsDump(dlg, actionsDebugString))
		return false;
	if (copyToClipboard)
		TQApplication::clipboard()->setText( actionsDebugString );
	else
		kdDebug() << TQString("Simplified actions for altering table '%1':\n").arg(dlg->partItem()->name())
			+ actionsDebugString+"\n" << endl;
	return true;
}

bool AlterTableTester::checkActions(KexiDialogBase* dlg)
{
	TQString actionsDebugString;
	if (!getActionsDump(dlg, actionsDebugString))
		return false;
	bool result = checkInternal(dlg, actionsDebugString, "endActions", true /*skipColonsAndStripWhiteSpace*/);
		kdDebug() << TQString("Actions check for table '%1': %2").arg(dlg->partItem()->name())
		.arg(result ? "OK" : "Failed") << endl;
	return result;
}

bool AlterTableTester::saveTableDesign(KexiDialogBase* dlg)
{
	KexiTableDesignerInterface* designerIface 
		= dynamic_cast<KexiTableDesignerInterface*>( dlg->selectedView() );
	if (!designerIface)
		return false;
	tristate result = designerIface->executeRealAlterTable();
	if (true!=result) {
		showError( TQString("Saving design of table '%1' failed.").arg(dlg->partItem()->name()) );
		return false;
	}
	return true;
}

bool AlterTableTester::getTableDataDump(KexiDialogBase* dlg, TQString& dataString)
{
	KexiTableDesignerInterface* designerIface 
		= dynamic_cast<KexiTableDesignerInterface*>( dlg->selectedView() );
	if (!designerIface)
		return false;

	TQMap<TQString,TQString> args;
	TQTextStream ts( &dataString, IO_WriteOnly );
	args["textStream"] = KexiUtils::ptrToString<TQTextStream>( &ts );
	args["destinationType"]="file";
	args["delimiter"]="\t";
	args["textQuote"]="\"";
	args["itemId"] = TQString::number( 
		prj->dbConnection()->tableSchema( dlg->partItem()->name() )->id() );
	if (!KexiInternalPart::executeCommand("csv_importexport", win, "KexiCSVExport", &args)) {
		showError( "Error exporting table contents." );
		return false;
	}
	return true;
}

bool AlterTableTester::showTableData(KexiDialogBase* dlg, bool copyToClipboard)
{
	TQString dataString;
	if (!getTableDataDump(dlg, dataString))
		return false;
	if (copyToClipboard)
		TQApplication::clipboard()->setText( dataString );
	else
		kdDebug() << TQString("Contents of table '%1':\n").arg(dlg->partItem()->name())+dataString+"\n" << endl;
	return true;
}

bool AlterTableTester::checkTableData(KexiDialogBase* dlg)
{
	TQString dataString;
	if (!getTableDataDump(dlg, dataString))
		return false;
	bool result = checkInternal(dlg, dataString, "endTableData", false /*!skipColonsAndStripWhiteSpace*/);
		kdDebug() << TQString("Table '%1' contents: %2").arg(dlg->partItem()->name())
			.arg(result ? "OK" : "Failed") << endl;
	return result;
}

bool AlterTableTester::closeWindow(KexiDialogBase* dlg)
{
	if (!dlg)
		return true;
	TQString name = dlg->partItem()->name();
	tristate result = true == win->closeDialog(dlg, true/*layoutTaskBar*/, true/*doNotSaveChanges*/);
	kdDebug() << TQString("Closing window for table '%1': %2").arg(name)
		.arg(result==true ? "OK" : (result==false ? "Failed" : "Cancelled")) << endl;
	return result == true;
}

//! Processes test file
tristate AlterTableTester::run(bool &closeAppRequested)
{
	closeAppRequested = false;
	while (!m_finishedCopying)
		tqApp->processEvents(300);

	kdDebug() << "Database copied to temporary: " << dbFilename << endl;

	if (!checkItemsNumber(2))
		return false;

	tristate res = win->openProject( dbFilename, 0 );
	if (true != res)
		return res;
	prj = win->project();

	//open table in design mode
	res = readLineFromTestFile("designTable");
	if (true != res)
		return ~res;
	
	TQString tableName(testFileLine[1]);
	KexiPart::Item *item = prj->itemForMimeType("kexi/table", tableName);
	if (!item) {
		showError(TQString("No such table '%1'").arg(tableName));
		return false;
	}
	bool openingCancelled;
	KexiDialogBase* dlg = win->openObject(item, Kexi::DesignViewMode, openingCancelled);
	if (!dlg) {
		showError(TQString("Could not open table '%1'").arg(item->name()));
		return false;
	}
	KexiTableDesignerInterface* designerIface 
		= dynamic_cast<KexiTableDesignerInterface*>( dlg->selectedView() );
	if (!designerIface)
		return false;

	//dramatic speedup: temporary hide the window and propeditor
	TQWidget * propeditor 
		= KexiUtils::findFirstChild<TQWidget>(tqApp->mainWidget(), "KexiPropertyEditorView");
	if (propeditor)
		propeditor->hide();
	dlg->hide();

	bool designTable = true;
	while (!testFileStream.atEnd()) {
		res = readLineFromTestFile();
		if (true != res)
			return ~res;
		TQString command( testFileLine[0] );
		if (designTable) {
			//subcommands available within "designTable" commands
			if (command=="endDesign") {
				if (!checkItemsNumber(1))
					return false;
				//end of the design session: unhide the window and propeditor
				dlg->show();
				if (propeditor)
					propeditor->show();
				designTable = false;
				continue;
			}
			else if (command=="removeField") {
				if (!checkItemsNumber(2))
					return false;
				bool ok;
				int row = getNumber(testFileLine[1], ok)-1;
				if (!ok)
					return false;
				designerIface->deleteRow( row, true );
				continue;
			}
			else if (command=="insertField") {
				if (!checkItemsNumber(3))
					return false;
				bool ok;
				int row = getNumber(testFileLine[1], ok)-1;
				if (!ok)
					return false;
				designerIface->insertField( row, testFileLine[2], true );
				continue;
			}
			else if (command=="insertEmptyRow") {
				if (!checkItemsNumber(2))
					return false;
				bool ok;
				int row = getNumber(testFileLine[1], ok)-1;
				if (!ok)
					return false;
				designerIface->insertEmptyRow( row, true );
				continue;
			}
			else if (command=="changeFieldProperty") {
				if (!checkItemsNumber(5) || !changeFieldProperty(designerIface))
					return false;
				continue;
			}
			else if (command.startsWith("i=")) {
				bool ok;
				variableI = command.mid(2).toInt(&ok);
				if (!ok) {
					showError(TQString("Invalid variable initialization '%1'").arg(command));
					return false;
				}
				continue;
			}
			else if (command.startsWith("i++")) {
				variableI++;
				continue;
			}
		}
		else {
			//top-level commands available outside of "designTable"
			if (command=="showSchema") {
				if (!checkItemsNumber(1, 2) || !showSchema(dlg, testFileLine[1]=="clipboard"))
					return false;
				continue;
			}
			else if (command=="checkSchema") {
				if (!checkItemsNumber(1) || !checkSchema(dlg))
					return false;
				continue;
			}
			else if (command=="showActions") {
				if (!checkItemsNumber(1, 2) || !showActions(dlg, testFileLine[1]=="clipboard"))
					return false;
				continue;
			}
			else if (command=="checkActions") {
				if (!checkItemsNumber(1) || !checkActions(dlg))
					return false;
				continue;
			}
			else if (command=="saveTableDesign") {
				if (!checkItemsNumber(1) || !saveTableDesign(dlg))
					return false;
				continue;
			}
			else if (command=="showTableData") {
				if (!checkItemsNumber(1, 2) || !showTableData(dlg, testFileLine[1]=="clipboard"))
					return false;
				continue;
			}
			else if (command=="checkTableData") {
				if (!checkItemsNumber(1) || !checkTableData(dlg))
					return false;
				continue;
			}
		}
		//common commands
		if (command=="stop") {
			if (!checkItemsNumber(1))
				return false;
			kdDebug() << TQString("Test STOPPED at line %1.").arg(testLineNumber) << endl;
			break;
		}
		else if (command=="closeWindow") {
			if (!checkItemsNumber(1) || !closeWindow(dlg))
				return false;
			else
				dlg = 0;
			continue;
		}
		else if (command=="quit") {
			if (!checkItemsNumber(1) || !closeWindow(dlg))
				return false;
			closeAppRequested = true;
			kdDebug() << TQString("Quitting the application...") << endl;
			break;
		}
		else {
			showError( TQString("No such command '%1'").arg(command) );
			return false;
		}
	}
	return true;
}

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

int quit(int result)
{
	testFile.close();
	delete tqApp;
	if (newArgv)
		delete [] newArgv;
	return result;
}

int main(int argc, char *argv[])
{
	// args: <.altertable test filename>
	if (argc < 2) {
		kdWarning() << "Please specify test filename.\nOptions: \n"
		"\t-close - closes the main window when test finishes" << endl;
		return quit(1);
	}
	
	// options:
	const bool closeOnFinish = argc > 2 && 0==qstrcmp(argv[1], "-close");
		
	// open test file
	testFilename = argv[argc-1];
	testFile.setName(testFilename);
	if (!testFile.open(IO_ReadOnly)) {
		kdWarning() << TQString("Opening test file %1 failed.").arg(testFilename) << endl;
		return quit(1);
	}
	//load db name
	testFileStream.setDevice( &testFile );
	tristate res = readLineFromTestFile("openDatabase");
	if (true != res)
		return quit( ~res ? 0 : 1 );
	origDbFilename = testFileLine[1];
	dbFilename = origDbFilename + ".tmp";

	newArgc = 2;
	newArgv = new char*[newArgc];
	newArgv[0] = tqstrdup(argv[0]);
	newArgv[1] = tqstrdup( "--skip-startup-dialog" );

	TDEAboutData* aboutdata = Kexi::createAboutData();
	aboutdata->setProgramName( "Kexi Alter Table Test" );
	int result = KexiMainWindowImpl::create(newArgc, newArgv, aboutdata);
	if (!tqApp)
		return quit(result);

	win = KexiMainWindowImpl::self();
	AlterTableTester tester;
	//TQObject::connect(win, TQ_SIGNAL(projectOpened()), &tester, TQ_SLOT(run()));

	bool closeAppRequested;
	res = tester.run(closeAppRequested);
	if (true != res) {
		if (false == res)
			kdWarning() << TQString("Running test for file '%1' failed.").arg(testFilename) << endl;
		return quit(res==false ? 1 : 0);
	}
	kdDebug() << TQString("Tests from file '%1': OK").arg(testFilename) << endl;
	result = (closeOnFinish || closeAppRequested) ? 0 : tqApp->exec();
	quit(result);
	return result;
}

#include "altertable.moc"
