#include <QString>
#include <QStringList>
#include <QFileDialog>
#include <QTextEdit>
#include <QFile>
#include <QTextStream>
#include <QTemporaryFile>
#include <QMessageBox>
#include <QProcess>
#include <QSettings>
#include <QDesktopServices>
#include <QUrl>
#include <QDialog>

#include <iostream>
#include <list>
#include <stdexcept>
#include <sstream>
#include <iterator>

#include "main_window.hpp"
#include "preferences_dialog.hpp"
#include "parser.h"
#include "ct_transform.h"
#include "cbcs.h"
#include "types.h"
#include "distance.h"
#include "bionj_clean.h"
#include "progress_update.h"
#include "parse_distance_matrix.hpp"
#include "ui_about_dialog.h"
#include "memory_resource.hpp"
#include "newickize.h"

#define TMP_FILE_TEMPLATE "cbcanalyzer_tmp_tree_XXXXXX.tre"

MainWindow::MainWindow()
	: newCbcDetectOutput(false),
	cbc_detect_matrix(""),
	cbc_plus_half_detect_matrix(""),
	cbc_tree(""),
    cbc_plus_half_tree(""),
    tmpFile1(TMP_FILE_TEMPLATE),
    tmpFile2(TMP_FILE_TEMPLATE),
    tmpFile3(TMP_FILE_TEMPLATE)
{
	setupUi(this);
	setWindowIcon(QIcon(":/images/starlight.xpm"));
	setInputsEditable(false);
	
	connect(actionOpen, SIGNAL(triggered()), this, SLOT(open()));
	connect(actionSaveAs, SIGNAL(triggered()), this, SLOT(saveAs()));
	connect(actionInputEditable, SIGNAL(toggled(bool)), this, SLOT(setInputsEditable(bool)));
	connect(actionPreferences, SIGNAL(triggered()), this, SLOT(preferences()));
	connect(actionRun, SIGNAL(triggered()), this, SLOT(run()));
	connect(actionShowTree, SIGNAL(triggered()), this, SLOT(showTree()));
	connect(tabWidget, SIGNAL(currentChanged(int)), this, SLOT(tabChanged(int)));
	connect(link4Sale, SIGNAL(linkActivated(const QString&)), this, SLOT(openLink(const QString&)));
	connect(linkMarna, SIGNAL(linkActivated(const QString&)), this, SLOT(openLink(const QString&)));
	connect(linkRnaForester, SIGNAL(linkActivated(const QString&)), this, SLOT(openLink(const QString&)));
	connect(actionAbout, SIGNAL(triggered()), this, SLOT(about()));
	connect(actionHelp, SIGNAL(triggered()), this, SLOT(help()));
	
	setGeometry(100, 100, 600, 700);
}

void MainWindow::startTreeViewer(const QString& fileName)
{
	QSettings settings;
	int selected = settings.value("selected_tree_viewer").toInt();
	if(selected < 0 || selected > 2)
		selected = 0;
	if(selected == 0)
	{
		QMessageBox::information(this, tr("No TreeViewer specified"),
			tr("There was no tree viewer specified in the settings.\n"
				"Please open the preferences dialog and select a tree\n"
				"viewer of your choice."));
		return;
	}
	
	QString file;
	
	if(selected == 1)
		file = settings.value("tree_viewer_1").toString();
	else
		file = settings.value("tree_viewer_2").toString();
	
	QStringList arguments;
	arguments << fileName;
	QProcess* process = new QProcess(this);
	process->start(file, arguments);
	process->waitForStarted();
}

void MainWindow::open()
{
	QTextEdit* input = 0;
	QString fileFilter;
	
	switch(tabWidget->currentIndex())
	{
		case CT_TRANSFORM: input = ctTransformInput; fileFilter = "CT Files (*.ct);;All files (*)"; break;
		case CBC_DETECT: input = cbcDetectInput; fileFilter = "Marna/RnaForester files (*.txt);;Fasta with structure information (*.xfasta);;Bracket dot bracket (*.bdb);;All files (*)";  break;
		case CBC_TREE: input = cbcTreeInput; fileFilter = "Text Files (*.txt);; All files (*)";  break;
		default: return;
	}
	
	QString fileName = QFileDialog::getOpenFileName(this, tr("Open file"), "", fileFilter);
	if(fileName.isNull())
		return;
	
	QFile file(fileName);
	
	if(!file.open(QIODevice::ReadOnly | QIODevice::Text))
		return;
	
	QTextStream stream(&file);
	input->setText(stream.readAll());
	file.close();
}

void MainWindow::saveAs()
{
	QString fileName = QFileDialog::getSaveFileName(this, tr("Save as"));
	
	if(fileName.isNull())
		return;
	
	QTextEdit* output = 0;
	
	switch(tabWidget->currentIndex())
	{
		case CT_TRANSFORM: output = ctTransformOutput; break;
		case CBC_DETECT: output = cbcDetectOutput; break;
		case CBC_TREE: output = cbcTreeOutput; break;
		default: return;
	}
	
	QFile file(fileName);
	if(!file.open(QIODevice::WriteOnly | QIODevice::Text))
		return;
	
	QTextStream stream(&file);
	stream << output->toPlainText().toAscii();
	file.close();
}

void MainWindow::preferences()
{
	PreferencesDialog dialog;
	dialog.exec();
}

QString MainWindow::prepareTmpFile(QTemporaryFile& file, const QString& str)
{
    QString fileName;
    if(!file.open())
    {
        QMessageBox::warning(this, tr("Error"),
            tr("Could not write contents to temporary file"));
        return QString();
    }
    file.resize(0);
    {
        QTextStream stream(&file);
        stream << str.toAscii().data();
        fileName = file.fileName();
    }
    file.close();

    return fileName;
}

void MainWindow::showTree()
{
#if 1
    QStringList trees = cbcTreeOutput->toPlainText().split("\n", QString::SkipEmptyParts);


    QSettings settings;
    int checkBoxState = settings.value("multiple_instances").toInt();
    QString fn = "";

    if(!checkBoxState)
    {
        QString fn = prepareTmpFile(tmpFile1, cbcTreeOutput->toPlainText());
        if(fn == "") return;
        startTreeViewer(fn);
    }
    else
    {
        fn = prepareTmpFile(tmpFile1, trees[0]);
        if(fn == "") return;
        startTreeViewer(fn);

        fn = prepareTmpFile(tmpFile2, trees[1]);
        if(fn == "") return;
        startTreeViewer(fn);

        fn = prepareTmpFile(tmpFile3, trees[2]);
        if(fn == "") return;
        startTreeViewer(fn);
    }

#else
	QString fileName;
	if(!tmpFile1.open())
	{
		QMessageBox::warning(this, tr("Error"),
			tr("Could not write contents to temporary file"));
		return;
	}
	tmpFile1.resize(0);
	{
		QTextStream stream(&tmpFile1);
		stream << cbc_tree.c_str();
		fileName = tmpFile1.fileName();
	}
	tmpFile1.close();
	
	startTreeViewer(fileName);
	
	if(!tmpFile2.open())
	{
		QMessageBox::warning(this, tr("Error"),
			tr("Could not write contents to temporary file"));
		return;
	}
	tmpFile2.resize(0);
	{
		QTextStream stream(&tmpFile2);
		stream << cbc_plus_half_tree.c_str();
		fileName = tmpFile2.fileName();
	}
	tmpFile2.close();
	
	startTreeViewer(fileName);

    if(!tmpFile3.open())
    {
        QMessageBox::warning(this, tr("Error"),
            tr("Could not write contents to temporary file"));
        return;
    }
    tmpFile3.resize(0);
    {
        QTextStream stream(&tmpFile3);
        stream << cbc_plus_half_tree.c_str();
        fileName = tmpFile3.fileName();
    }
    tmpFile3.close();
	
	startTreeViewer(fileName);
#endif
}

void MainWindow::run()
{
	switch(tabWidget->currentIndex())
	{
		case CT_TRANSFORM: runTransform(); break;
		case CBC_DETECT: runDetect(); break;
		case CBC_TREE: runTree(); break;
	}
}

void MainWindow::about()
{
	QDialog dia(this);
	Ui::AboutDialog ui;
	ui.setupUi(&dia);
	ui.labelText->setText( tr(
		"<h1>CBC Analyzer 2.0.0</h1>"
		"<p>The CBCAnalyzer  (CBC = compensatory base change) is a custom"
		"written software toolbox consisting of three parts, CTTransform, "
		"CBCDetect, and CBCTree. CTTransform reads several CT-file formats "
		"(ct, RNAviz ct or Mac ct), and generates a so called ``bracket-dot-bracket'' "
		"format that typically is used as input for other tools such as "
		"RNAforester, RNAmovie or MARNA. The latter one creates a multiple "
		"alignment based on primary sequences and secondary structures that now "
		"can be used as input for CBCDetect. Herewith we count for CBCs in all "
		"against all of the aligned sequences, which is important in detecting "
		"species, discriminated on their sexual incompatibility. The count "
		"(distance) matrix obtained by CBCDetect is used as input for CBCTree "
		"that reconstructs a phylogram by using the algorithm of BIONJ.<p>"
		"<p>Please contact <a href=\"mailto: Matthias.Wolf@biozentrum.uni-wuerzburg.de\">"
		"Matthias Wolf</a> if you encounter any bugs or if you want to send us "
		"some ideas to improve CBC Analyzer.</p>"
		"<p>You can also visit the project homepage at <a href=\"http://cbcanalyzer.bioapps.biozentrum.uni-wuerzburg.de\">"
		"http://cbcanalyzer.bioapps.biozentrum.uni-wuerzburg.de</a></p>") );
	connect(ui.labelText, SIGNAL(linkActivated(const QString&)), this, SLOT(openLink(const QString&)));
	dia.exec();
}

void MainWindow::help()
{
	openLink("http://cbcanalyzer.bioapps.biozentrum.uni-wuerzburg.de");
}

void MainWindow::setInputsEditable(bool editable)
{
	ctTransformInput->setReadOnly(!editable);
	cbcDetectInput->setReadOnly(!editable);
	cbcTreeInput->setReadOnly(!editable);
}

void MainWindow::tabChanged(int index)
{
	if(index == CBC_TREE && newCbcDetectOutput)
	{
		if(QMessageBox::Yes == QMessageBox::question(this, tr("cbcTree"),
			tr("There is new cbcDetect output.\n"
				"Do you want to copy it to the\n"
				"cbcTree input?"),
			QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes))
		{
			cbcTreeInput->setPlainText(cbcDetectOutput->toPlainText());
			newCbcDetectOutput = false;
		}
	}
}

void MainWindow::openLink(const QString& link)
{
	QDesktopServices::openUrl(QUrl(link));
}

#if 0
bool MainWindow::runTransform()
{
	if(ctTransformInput->toPlainText().size() == 0)
		return false;
	bool is_ct = false;

	QTemporaryFile file;
	if(!file.open())
	{
		QMessageBox::warning(this, tr("Error"), tr("Could not save data to temporary file"));
		return false;
	}
	QTextStream stream(&file);
	stream << ctTransformInput->toPlainText().toAscii();
	QString fileName = file.fileName();
	
	std::list<file_sequence> sequences;
	try {
		parse_ct( fileName.toStdString().c_str(), sequences );
		is_ct = true;
	}
	catch( std::runtime_error const& e ) 
	{
		if( sequences.size() ) {
			QString msg;
			msg += tr("Continue to Transform?\n")
				+ tr("CT-Parser: ") + e.what()
				+ tr(" but ") + QString().setNum(sequences.size())
				+ tr(" Sequences found.\n"); 
			
			if(QMessageBox::question(this, tr("Continue transform?"),
				msg,
				QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::No)
				return false;
		}
		else 
		{
			QString msg = tr("Failure while parsing!\n")
				+ tr("CT-Parser: ") + e.what() + "\n";
		
			try{ 
				sequences.clear();
				parse_bdb( fileName.toStdString().c_str(), sequences );
				is_ct = false;
			}
			catch( std::runtime_error const& e ) {
				msg += e.what() + QString("\n");
				
				QMessageBox::critical(this, tr("Failure"), msg);
				return false;
			}
		}
	}
	{
		std::ostringstream out;
		
		if( is_ct )
			to_mixed_fasta( sequences, out );
		else 
		{
			typedef sequence_data< brace_fold_data< num_nuc<name_data< base_adaptor<file_sequence::iterator> > > > > fold_type;
			to_ct( fold_type( sequences ), out );
		}
		out.flush();
		ctTransformOutput->setPlainText(out.str().c_str());
	}
	return true;
}
#endif

bool MainWindow::runTransform()
{
    if(ctTransformInput->toPlainText().size() == 0)
        return false;
    bool is_ct = false;

    MemoryResource mr(ctTransformInput->toPlainText().toAscii().data());

    std::list<file_sequence> sequences;
    try {
        parse_ct( mr.begin(), mr.end(), sequences );
        is_ct = true;
    }
    catch( std::runtime_error const& e )
    {
        if( sequences.size() ) {
            QString msg;
            msg += tr("Continue to Transform?\n")
                + tr("CT-Parser: ") + e.what()
                + tr(" but ") + QString().setNum(sequences.size())
                + tr(" Sequences found.\n");

            if(QMessageBox::question(this, tr("Continue transform?"),
                msg,
                QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::No)
                return false;
        }
        else
        {
            QString msg = tr("Failure while parsing!\n")
                + tr("CT-Parser: ") + e.what() + "\n";

            try{
                sequences.clear();
                parse_bdb( mr.begin(), mr.end(), sequences );
                is_ct = false;
            }
            catch( std::runtime_error const& e ) {
                msg += e.what() + QString("\n");

                QMessageBox::critical(this, tr("Failure"), msg);
                return false;
            }
        }
    }
    {
        std::ostringstream out;

        if( is_ct )
            to_mixed_fasta( sequences, out );
        else
        {
            typedef sequence_data< brace_fold_data< num_nuc<name_data< base_adaptor<file_sequence::iterator> > > > > fold_type;
            to_ct( fold_type( sequences ), out );
        }
        out.flush();
        ctTransformOutput->setPlainText(out.str().c_str());
    }
    return true;
}


bool MainWindow::runDetect()
{
	if(cbcDetectInput->toPlainText().size() == 0)
		return false;
	
	std::list<file_sequence> sequences;
	
	MemoryResource mr( cbcDetectInput->toPlainText().toAscii().data() );
	
	const int MARNA = 1;
	const int BDB = 2;
	
        int parse_as = 0;
	QString msg = tr("Error while parsing!\n");
	try {
                parse_forester( mr.begin(), mr.end(), sequences );
	} catch( std::runtime_error const& e ) {
		parse_as = MARNA;
		msg += tr("Forester parser: ") + e.what() + "\n"; 
	}
	
	if( parse_as == MARNA )
	{
		try {
			sequences.clear();
                        parse_marna( mr.begin(), mr.end(), sequences );
		} catch( std::runtime_error const& e ) {
			parse_as = BDB;
			msg += QString("Marna parser: ") + e.what() + "\n";
		}
	}
	
	if(parse_as == BDB)
	{
		try {
			sequences.clear();
                        parse_bdb(mr.begin(), mr.end(), sequences);
		}
		catch(const std::runtime_error& e)
		{
			msg += QString("BDB parser: ") + e.what() + "\n";
			QMessageBox::critical(this, tr("Failure"), msg);
			return false;
		}
	}

#if 0
	CbcDetector detector;

	profdist::distance_matrix distances;
	
	typedef sequence_data< brace_fold_data< num_sequences<base_adaptor<file_sequence::iterator> > > > fold_type;
	
	profdist::compute_distance( fold_type( sequences ), distances, detector );
	
	// Compute half CBCs
	HalfCbcDetector half_detector;
	
	profdist::distance_matrix half_distances;
	
	profdist::compute_distance( fold_type( sequences ), half_distances, half_detector );
	
	name_data<base_adaptor<file_sequence::iterator> > name_data( sequences ); 

	{
		std::ostringstream out1;
		profdist::print_distance_matrix( out1, distances, name_data.name_begin(), name_data.name_end(), true );
		out1.flush();
		
		cbc_detect_matrix = out1.str();
		
		std::ostringstream out2;
		profdist::print_distance_matrix( out2, distances + half_distances, name_data.name_begin(), name_data.name_end(), true );
		out2.flush();
		
		cbc_plus_half_detect_matrix = out2.str();
		
		cbcDetectOutput->setPlainText((out1.str() + out2.str()).c_str());
	}
	
    newCbcDetectOutput = true;
#else

    typedef sequence_data< brace_fold_data< num_sequences<base_adaptor<file_sequence::iterator> > > > fold_type;

    enum { CBCS = 0, HALF_CBCS, CBCS_PLUS_HALF };
    /*
     * matrices stores the distance matrices in the following order:
     * matrices[0] = CBCs,
     * matrices[1] = half CBCs,
     * matrices[2] = CBCs + half CBCs
     */
    std::vector<profdist::distance_matrix> matrices(3, profdist::distance_matrix());
    std::ostringstream out;
    name_data<base_adaptor<file_sequence::iterator> > names( sequences );
    std::list<std::string> seqNames;
    //std::copy(name_data.name_begin(), name_data.name_end(), std::back_inserter(seqNames));
    for(name_data<base_adaptor<file_sequence::iterator> >::const_name_iterator i = names.name_begin(),
        e = names.name_end(); i != e; ++i)
    {
        seqNames.push_back(std::string(i->first, i->second));
    }

    for(std::list<std::string>::iterator i = seqNames.begin(), e = seqNames.end(); i != e; ++i)
        *i = profdist::newickize(*i);


    {
        CbcDetector detector;
        profdist::compute_distance( fold_type(sequences), matrices[CBCS], detector);
        profdist::print_distance_matrix(out, matrices[CBCS], seqNames.begin(), seqNames.end(), true);
    }

    {
        HalfCbcDetector detector;
        profdist::compute_distance( fold_type(sequences), matrices[HALF_CBCS], detector);
        matrices[CBCS_PLUS_HALF] = matrices[CBCS] + matrices[HALF_CBCS];
        profdist::print_distance_matrix(out, matrices[HALF_CBCS], seqNames.begin(), seqNames.end(), true);
        profdist::print_distance_matrix(out, matrices[CBCS_PLUS_HALF], seqNames.begin(), seqNames.end(), true);
    }

    cbcDetectOutput->setPlainText( out.str().c_str() );
    newCbcDetectOutput = true;
#endif

	return true;
}

void parseMatrices(std::istream& in,
				   std::list<profdist::distance_matrix>& matrices,
				   std::list<std::string>& sequenceNames)
{
	/*
	in >> std::ws;
	size_t dimension; // dimension of the matrix/matrices.
	in >> dimension;
	sequenceNames.clear();
	
	for(size_t i = 0; i < dimension; ++i)
	{
		std::string name;
		in >> std::ws >> name >> std::ws;
		sequenceNames.push_back(name);
		profdist::distance_matrix mat;
		
		for(size_t j = 0; j < dimension; ++j)
		{
			in >> mat(i, j);
		}
	}
	*/
	
    profdist::distance_matrix mat;
    profdist::read_distance_matrix(in, mat, std::back_inserter(sequenceNames));
    if(in)
        matrices.push_back(mat);
	
	while(in && !in.eof())
	{
        profdist::read_distance_matrix(in, mat);
        if(in)
            matrices.push_back(mat);
	}
}

bool MainWindow::runTree()
{
    if(cbcTreeInput->toPlainText().size() == 0)
		return false;
	
	std::istringstream in(cbcTreeInput->toPlainText().toAscii().data());
	
    std::list<profdist::distance_matrix> matrices;
    std::list<std::string> sequenceNames;
	
	parseMatrices(in, matrices, sequenceNames);

	if(matrices.empty())
	{
        QMessageBox::critical(this, tr("Error"), tr("No matrix could be parsed"));
        return false;
    }

    if(sequenceNames.size() < 3)
    {
        QMessageBox::critical(this,
                              tr("Error"),
                              tr("BIONJ needs a distance matrix with at least 3 sequences"));
        return false;
    }

    cbcTreeOutput->clear();

    std::ostringstream out;
    ProgressSink sink;
    std::vector<std::string> names(sequenceNames.size());
    std::copy(sequenceNames.begin(), sequenceNames.end(), names.begin());

    for(std::list<profdist::distance_matrix>::const_iterator i = matrices.begin(),
            e = matrices.end();
            i != e;
            ++i)
    {
        profdist::bionj(*i, names, out, sink);
    }

    cbcTreeOutput->setPlainText(out.str().c_str());
}

/*
bool MainWindow::runTree()
{
	//if(cbcTreeInput->toPlainText().size() == 0)
	//	return false;
	
	if(cbc_detect_matrix.empty() && cbc_plus_half_detect_matrix.empty())
		return false;
	
	using namespace std;
    std::vector<string> seq_names;
	profdist::distance_matrix mat1;
	profdist::distance_matrix mat2;
	
	try
        {
            std::istringstream in1(cbc_detect_matrix);
			std::istringstream in2(cbc_plus_half_detect_matrix);
            std::list<std::string> names1;
			std::list<std::string> names2;
            profdist::read_distance_matrix(in1, mat1, std::back_inserter(names1));
			profdist::read_distance_matrix(in2, mat2, std::back_inserter(names2));
            
			if(names1 != names2)
			{
				QMessageBox::critical(this, tr("Error"), tr(""));
				return false;
			}
			
			seq_names.resize(names1.size());
            std::copy(names1.begin(), names1.end(), seq_names.begin());
	}
	catch(const std::runtime_error& e)
	{
		QMessageBox::critical(this, tr("Error"), tr("Failure while parsing matrix:\n") + e.what());
		return false;
	}
	
	if( mat1.nRows() < 3 || mat2.nRows() < 3)
	{
		///Message Error if not the size of the matrix is
		QMessageBox::critical(this, tr("Error"), tr("BIONJ needs at least 3 nodes!"));
		return false;
	}
	
	//int num_steps = mat.nRows() - 3;
	
	cbcTreeOutput->clear();
	{
		std::ostringstream out1;
		std::ostringstream out2;
		ProgressSink sink;
		profdist::bionj( mat1, seq_names, out1, sink );
		profdist::bionj( mat2, seq_names, out2, sink );
		cbc_tree = out1.str();
		cbc_plus_half_tree = out2.str();
		cbcTreeOutput->setPlainText((cbc_tree + cbc_plus_half_tree).c_str());
	}

	return true;
}
*/
