/****************************************************************************
**
** Copyright (C) 2015 The Qt Company Ltd.
** Contact: http://www.qt.io/licensing/
**
** This file is part of the test suite of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL21$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see http://www.qt.io/terms-conditions. For further
** information use the contact form at http://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 or version 3 as published by the Free
** Software Foundation and appearing in the file LICENSE.LGPLv21 and
** LICENSE.LGPLv3 included in the packaging of this file. Please review the
** following information to ensure the GNU Lesser General Public License
** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** As a special exception, The Qt Company gives you certain additional
** rights. These rights are described in The Qt Company LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include <QCloseEvent>
#include <QFileDialog>
#include <QFileInfo>
#include <QHeaderView>
#include <QMenu>
#include <QMessageBox>
#include <QProcess>
#include <QSettings>
#include <QTextStream>

#include "ASTItem.h"
#include "FunctionSignaturesView.h"
#include "Global.h"
#include "TestCaseView.h"
#include "TestResultView.h"
#include "TestSuite.h"
#include "TreeModel.h"
#include "TreeSortFilter.h"
#include "UserTestCase.h"

#include "MainWindow.h"

using namespace QPatternistSDK;

MainWindow::MainWindow() : m_userTC(new UserTestCase()),
                           m_currentTC(0)
{
    setupUi(this);

    /* I want to do this in Designer.. */
    testSuiteView->header()->setSortIndicator(0, Qt::AscendingOrder);
    testSuiteView->header()->setSortIndicatorShown(true);
    testSuiteView->header()->setClickable(true);

    setupActions();

    QStringList suiteHeaders;
    suiteHeaders << QLatin1String("Name")
                 << QLatin1String("Pass")
                 << QLatin1String("Fail")
                 << QLatin1String("Total");

    TreeSortFilter *const proxy = new TreeSortFilter(this);
    connect(searchInput,    SIGNAL(textChanged(const QString &)),
            proxy,          SLOT(setFilterFixedString(const QString &)));

    proxy->setSourceModel(new TreeModel(suiteHeaders, this));
    testSuiteView->setModel(proxy);

    /* --------- Test Result View ---------- */
    testResultView = new TestResultView(this);
    testResultView->setAllowedAreas(Qt::AllDockWidgetAreas);
    addDockWidget(Qt::RightDockWidgetArea, testResultView);
    /* ----------------------------------- */

    /* --------- Test Case View ---------- */
    testCaseView = new TestCaseView(this);
    testCaseView->setAllowedAreas(Qt::AllDockWidgetAreas);
    addDockWidget(Qt::LeftDockWidgetArea, testCaseView);

    connect(this,           SIGNAL(testCaseSelected(TestCase *const)),
            testCaseView,   SLOT(displayTestCase(TestCase *const)));
    connect(this,           SIGNAL(testCaseSelected(TestCase *const)),
            testResultView, SLOT(displayTestResult(TestCase *const)));
    connect(focusURI,       SIGNAL(textChanged(const QString &)),
            m_userTC,       SLOT(focusDocumentChanged(const QString &)));
    /* ----------------------------------- */

    /* ----- Function Signature View ----- */
    functionView = new FunctionSignaturesView(this);
    functionView->setAllowedAreas(Qt::AllDockWidgetAreas);
    addDockWidget(Qt::RightDockWidgetArea, functionView);
    /* ----------------------------------- */

    /* Appears here, because the menu uses actions in the QDockWidgets. */
    setupMenu();

    readSettings();

    /* Connect this after readSettings(), otherwise readSettings() triggers writeSettings(). */
    connect(sourceTab,      SIGNAL(currentChanged(int)),
                            SLOT(writeSettings()));
    connect(testSuiteView,  SIGNAL(clicked(const QModelIndex &)),
                            SLOT(writeSettings()));
    connect(sourceInput,    SIGNAL(textChanged()),
                            SLOT(writeSettings()));
}

MainWindow::~MainWindow()
{
    delete m_userTC;
}

QModelIndex MainWindow::sourceIndex(const QModelIndex &proxyIndex) const
{
    return static_cast<TreeSortFilter *>(testSuiteView->model())->mapToSource(proxyIndex);
}

TreeModel *MainWindow::sourceModel() const
{
    const TreeSortFilter *const proxy = static_cast<TreeSortFilter *>(testSuiteView->model());
    return static_cast<TreeModel *>(proxy->sourceModel());
}

void MainWindow::on_testSuiteView_clicked(const QModelIndex &index)
{
    if(index.isValid())
    {
        TestItem *const node = static_cast<TestItem *>(sourceIndex(index).internalPointer());
        Q_ASSERT(node);

        if(node->isFinalNode())
        {
            m_currentTC = static_cast<TestCase *>(node);
            testCaseSelected(m_currentTC);
            return;
        }
    }

    /* In all other cases: */
    m_currentTC = 0;
    testCaseSelected(0);
}

void MainWindow::on_sourceInput_textChanged()
{
    m_userTC->setSourceCode(sourceInput->toPlainText());
}

void MainWindow::on_actionOpen_triggered()
{
    const QString fileName(QFileDialog::getOpenFileName(this,
                                                        QLatin1String("Open Test Suite Catalog"),
                                                        m_previousOpenedCatalog.toLocalFile(),
                                                        QLatin1String("Test Suite Catalog file (*.xml)")));

    /* "If the user presses Cancel, it returns a null string." */
    if(fileName.isNull())
        return;

    m_currentSuiteType = TestSuite::XQuerySuite;
    openCatalog(QUrl::fromLocalFile(fileName), true, TestSuite::XQuerySuite);
}

void MainWindow::on_actionOpenXSLTSCatalog_triggered()
{
    const QString fileName(QFileDialog::getOpenFileName(this,
                                                        QLatin1String("Open Test Suite Catalog"),
                                                        m_previousOpenedCatalog.toLocalFile(),
                                                        QLatin1String("Test Suite Catalog file (*.xml)")));

    /* "If the user presses Cancel, it returns a null string." */
    if(fileName.isNull())
        return;

    m_currentSuiteType = TestSuite::XsltSuite;
    openCatalog(QUrl::fromLocalFile(fileName), true, TestSuite::XsltSuite);
}

void MainWindow::on_actionOpenXSDTSCatalog_triggered()
{
    const QString fileName(QFileDialog::getOpenFileName(this,
                                                        QLatin1String("Open Test Suite Catalog"),
                                                        m_previousOpenedCatalog.toLocalFile(),
                                                        QLatin1String("Test Suite Catalog file (*.xml)")));

    /* "If the user presses Cancel, it returns a null string." */
    if(fileName.isNull())
        return;

    m_currentSuiteType = TestSuite::XsdSuite;
    openCatalog(QUrl::fromLocalFile(fileName), true, TestSuite::XsdSuite);
}

void MainWindow::openCatalog(const QUrl &fileName,
                             const bool reportError,
                             const TestSuite::SuiteType suiteType)
{
    setCurrentFile(fileName);
    m_previousOpenedCatalog = fileName;

    QString errorMsg;
    TestSuite *const loadedSuite = TestSuite::openCatalog(fileName, errorMsg, false, suiteType);

    if(!loadedSuite)
    {
        if(reportError)
        {
            QMessageBox::information(this, QLatin1String("Failed to load catalog file"),
                                     errorMsg, QMessageBox::Ok);
        }

        return;
    }

    TreeModel *const prevModel = sourceModel();
    prevModel->setRoot(loadedSuite);
    m_currentTC = 0;

    testCaseCount->setText(QString::number(loadedSuite->resultSummary().second));
    /* Switch to the tab containing the loaded test suite. */
    sourceTab->setCurrentIndex(0);

    setWindowTitle(QCoreApplication::applicationName() +
                   QLatin1String(" -- ") +
                   QFileInfo(fileName.toLocalFile()).fileName());

    /* @p reportError is set when not auto-loading on startup, and
     * we only want to save when the user opens from the GUI. */
    if(reportError)
        writeSettings();
}

void MainWindow::on_sourceTab_currentChanged(int index)
{
    if(index == 1)
    {
        m_currentTC = m_userTC;
        testCaseSelected(m_userTC);
    }
    else
        on_testSuiteView_clicked(testSuiteView->currentIndex());
}

void MainWindow::on_actionExecute_triggered()
{
    Q_ASSERT(testCaseView);
    TestSuite *const ts = static_cast<TestSuite *>(sourceModel()->root());

    const TestItem::ExecutionStage stage = compileOnly->isChecked() ? TestItem::CompileOnly
                                                                    : TestItem::CompileAndRun;

    m_userTC->setLanguage(isXSLT20->isChecked() ? QXmlQuery::XSLT20 : QXmlQuery::XQuery10);

    if(m_currentTC)
    {
        const TestResult::List rlist(m_currentTC->execute(stage, ts));
        Q_ASSERT(rlist.count() == 1);
        const TestResult *const result = rlist.first();
        Q_ASSERT(result);
        testResultView->displayTestResult(result);
    }
    else
    {
        const QModelIndexList indexes = testSuiteView->selectionModel()->selectedIndexes();
        for (int i = 0; i < indexes.count(); ++i) {
            const QModelIndex source(sourceIndex(indexes.at(i)));

            TestItem *const ti = static_cast<TestItem *>(source.internalPointer());
            if(!ti)
                return;

            /* ti is a TestGroup. It now executes its children, changed(TreeItem *) signals is
             * emitted which the view receives, and thus updates. */
            ti->execute(stage, ts);
        }
    }
}

void MainWindow::readSettings()
{
    QSettings settings;

    settings.beginGroup(QLatin1String("MainWindow"));
    restoreState(settings.value(QLatin1String("state")).toByteArray(), Global::versionNumber);
    resize(settings.value(QLatin1String("size"), QSize(400, 400)).toSize());
    move(settings.value(QLatin1String("pos"), QPoint(200, 200)).toPoint());
    m_previousOpenedCatalog = settings.value(QLatin1String("PreviousOpenedCatalogFile")).toUrl();
    focusURI->setText(settings.value(QLatin1String("focusURI")).toString());
    isXSLT20->setChecked(settings.value(QLatin1String("isXSLT20")).toBool());
    compileOnly->setChecked(settings.value(QLatin1String("compileOnly")).toBool());
    m_currentSuiteType = (TestSuite::SuiteType)settings.value(QLatin1String("PreviousSuiteType"), isXSLT20->isChecked() ? TestSuite::XsltSuite : TestSuite::XQuerySuite).toInt();

    /* Open the previously opened catalog. */
    if(!m_previousOpenedCatalog.isEmpty())
    {
        openCatalog(m_previousOpenedCatalog, false, m_currentSuiteType);
    }

    sourceInput->setPlainText(settings.value(QLatin1String("sourceInput")).toString());
    testResultView->resultViewSelection->setCurrentIndex(
            settings.value(QLatin1String("ResultViewMethod"), 0).toInt());
    testResultView->outputStack->setCurrentIndex(settings.value(
            QLatin1String("ResultViewMethod"), 0).toInt());

    /* Restore the selected test case/group. */
    const QStringList rows(settings.value(QLatin1String("SelectedTestSuiteRow"),
                                          QString())
                           .toString().split(QLatin1Char(',')));

    if(!rows.isEmpty()) /* Ok, we have a selection. */
    {
        QAbstractItemModel *const model = testSuiteView->model();
        Q_ASSERT(model);
        QModelIndex p;

        for(int i = rows.count() - 1; i >= 0;  --i)
        {
            const QModelIndex childIndex(model->index(rows.at(i).toInt(), 0 , p));

            if(childIndex.isValid())
            {
                testSuiteView->scrollTo(p); /* Work around for Qt issue #87575. */
                p = childIndex;
            }
        }

        testSuiteView->scrollTo(p); /* Scrolls to it. */
        testSuiteView->setCurrentIndex(p); /* Selects it. */
        on_testSuiteView_clicked(p); /* Loads the test case in the Test Case View. */
    }

    /* Do it here. In this way the user-entered test case gets selected, if that tab
     * was previously used. */
    sourceTab->setCurrentIndex(settings.value(QLatin1String("SelectedTab"), 0).toInt());
    on_sourceTab_currentChanged(sourceTab->currentIndex());

    settings.endGroup();
}

void MainWindow::writeSettings()
{
    QSettings settings;

    settings.beginGroup(QLatin1String("MainWindow"));
    settings.setValue(QLatin1String("state"), saveState(Global::versionNumber));
    settings.setValue(QLatin1String("pos"), pos());
    settings.setValue(QLatin1String("size"), size());
    settings.setValue(QLatin1String("sourceInput"), sourceInput->toPlainText());
    settings.setValue(QLatin1String("PreviousOpenedCatalogFile"), m_previousOpenedCatalog);
    settings.setValue(QLatin1String("PreviousSuiteType"), m_currentSuiteType);
    settings.setValue(QLatin1String("SelectedTab"), sourceTab->currentIndex());
    settings.setValue(QLatin1String("ResultViewMethod"),
                      testResultView->resultViewSelection->currentIndex());
    settings.setValue(QLatin1String("focusURI"),
                      focusURI->text());
    settings.setValue(QLatin1String("isXSLT20"),
                      isXSLT20->isChecked());
    settings.setValue(QLatin1String("compileOnly"),
                      compileOnly->isChecked());

    /* Store the selected test case/group. */
    QModelIndex selected(sourceIndex(testSuiteView->currentIndex()));
    if(selected.isValid())
    {
        QString result;

        do
        {
            result.append(QString::number(selected.row()));
            selected = selected.parent();

            if(selected.isValid())
                result.append(QLatin1Char(','));
            else
                break;
        }
        while(true);

        settings.setValue(QLatin1String("SelectedTestSuiteRow"), result);
    }

    settings.endGroup();
}

void MainWindow::setCurrentFile(const QUrl &f)
{
    const QString fileName(f.toLocalFile());
    QSettings settings;
    settings.beginGroup(QLatin1String("MainWindow"));
    QStringList files(settings.value(QLatin1String("RecentFileList")).toStringList());

    files.removeAll(fileName);
    files.prepend(fileName);
    while(files.size() > MaximumRecentFiles)
        files.removeLast();

    settings.setValue(QLatin1String("RecentFileList"), files);
    settings.endGroup();

    updateRecentFileActions();
}

void MainWindow::updateRecentFileActions()
{
    QSettings settings;
    settings.beginGroup(QLatin1String("MainWindow"));
    const QStringList files(settings.value(QLatin1String("RecentFileList")).toStringList());
    settings.endGroup();

    const int numRecentFiles = qMin(files.size(), static_cast<int>(MaximumRecentFiles));

    for(int i = 0; i < numRecentFiles; ++i)
    {
        const QString text(QString::fromLatin1("&%1 %2").arg(i + 1).arg(QFileInfo(files[i]).filePath()));
        m_recentFileActs[i]->setText(text);
        m_recentFileActs[i]->setData(QUrl::fromLocalFile(files[i]));
        m_recentFileActs[i]->setVisible(true);
    }

    for(int j = numRecentFiles; j < MaximumRecentFiles; ++j)
        m_recentFileActs[j]->setVisible(false);
}

void MainWindow::openRecentFile()
{
    const QAction *const action = qobject_cast<QAction *>(sender());
    if(action)
        openCatalog(action->data().toUrl(), true, TestSuite::XQuerySuite);
}

void MainWindow::closeEvent(QCloseEvent *ev)
{
    writeSettings();
    ev->accept();
}

void MainWindow::setupActions()
{
    connect(actionQuit, SIGNAL(triggered()), qApp, SLOT(closeAllWindows()));

    for(int i = 0; i < MaximumRecentFiles; ++i)
    {
        m_recentFileActs[i] = new QAction(this);
        m_recentFileActs[i]->setVisible(false);
        connect(m_recentFileActs[i], SIGNAL(triggered()),
                this, SLOT(openRecentFile()));
    }
}

void MainWindow::setupMenu()
{
    QMenu *const menFile = findChild<QMenu *>(QLatin1String("menuFile"));
    Q_ASSERT(menFile);
    QAction *const actOpen = findChild<QAction *>(QLatin1String("actionExecute"));
    Q_ASSERT(actOpen);
    QMenu *const recent = new QMenu(QLatin1String("O&pen Recent"), this);

    menFile->insertMenu(actOpen, recent);
    menFile->insertSeparator(actOpen);

    for(int i = 0; i < MaximumRecentFiles; ++i)
        recent->addAction(m_recentFileActs[i]);

    updateRecentFileActions();

    QMenu *const menWindows = findChild<QMenu *>(QLatin1String("menuWindows"));
    Q_ASSERT(menWindows);

    menWindows->addAction(testCaseView->toggleViewAction());
    menWindows->addAction(testResultView->toggleViewAction());
    menWindows->addAction(functionView->toggleViewAction());
}

void MainWindow::on_actionRestart_triggered()
{
    if(QProcess::startDetached(QCoreApplication::applicationFilePath()))
        QApplication::closeAllWindows();
    else
    {
        QTextStream err(stderr);
        err << "Failed to start " << qPrintable(QCoreApplication::applicationFilePath()) << endl;
    }
}


// vim: et:ts=4:sw=4:sts=4
