/*
 * Copyright (C) 2010 Ixonos Plc.
 * Copyright (C) 2011-2024 Philipp Spitzer, gregor herrmann, Stefan Stahl
 *
 * This file is part of ConfClerk.
 *
 * ConfClerk is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation, either version 2 of the License, or (at your option)
 * any later version.
 *
 * ConfClerk 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 General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * ConfClerk.  If not, see <http://www.gnu.org/licenses/>.
 */
#include "mainwindow.h"

#include <QTreeView>
#include <QFile>
#include <QNetworkProxy>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QSslConfiguration>
#include <QDialog>
#include <QMessageBox>

#include "sqlengine.h"
#include "track.h"
#include "eventmodel.h"
#include "delegate.h"
#include "room.h"
#include "conference.h"
#include "ui_about.h"
#include "eventdialog.h"
#include "daynavigatorwidget.h"
#include "settingsdialog.h"
#include "conferenceeditor.h"
#include "schedulexmlparser.h"
#include "errormessage.h"
#include "tabcontainer.h"
#include "appsettings.h"
#include "application.h"

MainWindow::MainWindow(QWidget* parent): QMainWindow(parent) {
    setupUi(this);

    // Open database
    sqlEngine = new SqlEngine(this);
    searchTabContainer->setSqlEngine(sqlEngine);
    connect(sqlEngine, SIGNAL(dbError(QString)), this, SLOT(showError(QString)));
    sqlEngine->open();
    sqlEngine->createOrUpdateDbSchema();

    conferenceModel = new ConferenceModel(this);
    mXmlParser = new ScheduleXmlParser(sqlEngine, this);
    mNetworkAccessManager = new QNetworkAccessManager(this);
    systemTrayIcon = new QSystemTrayIcon(qApp->windowIcon(), this);
    alarmTimer = new QTimer(this);

    alarmTimer->setInterval(60000);
    alarmTimer->start();
    saved_title = windowTitle();

#ifdef N810
    tabWidget->setTabText(1,"Favs");
    //tabWidget->setTabText(2,"Day");
#endif

    // first time run aplication: -> let's have it direct connection in this case
    if(!AppSettings::contains("proxyIsDirectConnection"))
        AppSettings::setDirectConnection(true);

    QNetworkProxy proxy(
            AppSettings::isDirectConnection() ? QNetworkProxy::NoProxy : (QNetworkProxy::ProxyType)AppSettings::proxyType(),
            AppSettings::proxyAddress(),
            AppSettings::proxyPort(),
            AppSettings::proxyUsername(),
            AppSettings::proxyPassword());
    QNetworkProxy::setApplicationProxy(proxy);

    // event details have changed
    connect(dayTabContainer, SIGNAL(eventChanged(int,bool)), SLOT(onEventChanged(int,bool)));
    connect(favsTabContainer, SIGNAL(eventChanged(int,bool)), SLOT(onEventChanged(int,bool)));
    connect(tracksTabContainer, SIGNAL(eventChanged(int,bool)), SLOT(onEventChanged(int,bool)));
    connect(roomsTabContainer, SIGNAL(eventChanged(int,bool)), SLOT(onEventChanged(int,bool)));
    connect(searchTabContainer, SIGNAL(eventChanged(int,bool)), SLOT(onEventChanged(int,bool)));

    // date has changed
    connect(dayNavigator, SIGNAL(dateChanged(QDate)), dayTabContainer, SLOT(redisplayDate(QDate)));
    connect(dayNavigator, SIGNAL(dateChanged(QDate)), favsTabContainer, SLOT(redisplayDate(QDate)));
    connect(dayNavigator, SIGNAL(dateChanged(QDate)), tracksTabContainer, SLOT(redisplayDate(QDate)));
    connect(dayNavigator, SIGNAL(dateChanged(QDate)), roomsTabContainer, SLOT(redisplayDate(QDate)));
    connect(dayNavigator, SIGNAL(dateChanged(QDate)), searchTabContainer, SLOT(redisplayDate(QDate)));

    // search result has changed
    connect(searchTabContainer, SIGNAL(searchResultChanged()), SLOT(onSearchResultChanged()));

    // systm tray icon
    connect(systemTrayIcon, SIGNAL(messageClicked()), SLOT(onSystemTrayMessageClicked()));
    connect(systemTrayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), SLOT(onSystemTrayMessageClicked()));

    // timer
    connect(alarmTimer, SIGNAL(timeout()), SLOT(onAlarmTimerTimeout()));

    // add the actions from the main menu to the window, otherwise the shortcuts don't work on MAEMO
    addAction(conferencesAction);
    addAction(settingsAction);
    addAction(quitAction);

    // make it impossible to hide the toolbar by disallowing its context menu
    toolBar->setContextMenuPolicy(Qt::PreventContextMenu);

    // open conference
    useConference(Conference::activeConference());
    // optimization, see useConference() code
    try {
        initTabs();
    } catch (const OrmException& e) {
        qDebug() << "OrmException:" << e.text();
        clearTabs();
    }

    connect(mNetworkAccessManager, SIGNAL(sslErrors(QNetworkReply*, QList<QSslError>)), SLOT(sslErrors(QNetworkReply*, QList<QSslError>)));
    connect(mNetworkAccessManager, SIGNAL(finished(QNetworkReply*)), SLOT(networkQueryFinished(QNetworkReply*)));
    connect(mXmlParser, SIGNAL(parsingScheduleBegin()), conferenceModel, SLOT(newConferenceBegin()));
    connect(mXmlParser, SIGNAL(parsingScheduleEnd(int)), conferenceModel, SLOT(newConferenceEnd(int)));
}


MainWindow::~MainWindow() {
    sqlEngine->close();
}


void MainWindow::on_aboutAction_triggered()
{
    QDialog dialog(this);
    Ui::AboutDialog ui;
    ui.setupUi(&dialog);
    ui.labDescription->setText(ui.labDescription->text().arg(qApp->applicationVersion()));
#ifdef N810
    dialog.setFixedWidth(width());
#endif
    dialog.exec();
}


void MainWindow::on_reloadAction_triggered() {
    int confId = Conference::activeConference();
    if (confId== -1) return;
    Conference active = Conference::getById(confId);
    if (active.url().isEmpty()) return;
    importFromNetwork(active.url(), confId);
    setEnabled(false);
}


void MainWindow::on_nowAction_triggered() {
    int confId = Conference::activeConference();
    if (confId== -1) return;
    dayNavigator->setCurDate(QDate::currentDate());
    dayTabContainer->expandTimeGroup(QTime::currentTime(), confId);
}


void MainWindow::on_searchAction_triggered() {
    if (tabWidget->currentWidget() == searchTab)
        searchTabContainer->showSearchDialog(!searchTabContainer->searchDialogIsVisible());
    else {
        tabWidget->setCurrentWidget(searchTab);
        searchTabContainer->showSearchDialog();
    }
}


void MainWindow::on_expandAllAction_triggered() {
    if (tabWidget->currentWidget() == favouritesTab) favsTabContainer->treeView->expandAll();
    if (tabWidget->currentWidget() == dayViewTab) dayTabContainer->treeView->expandAll();
    if (tabWidget->currentWidget() == tracksTab) tracksTabContainer->treeView->expandAll();
    if (tabWidget->currentWidget() == roomsTab) roomsTabContainer->treeView->expandAll();
    if (tabWidget->currentWidget() == searchTab) searchTabContainer->treeView->expandAll();
}


void MainWindow::on_collapseAllAction_triggered() {
    if (tabWidget->currentWidget() == favouritesTab) favsTabContainer->treeView->collapseAll();
    if (tabWidget->currentWidget() == dayViewTab) dayTabContainer->treeView->collapseAll();
    if (tabWidget->currentWidget() == tracksTab) tracksTabContainer->treeView->collapseAll();
    if (tabWidget->currentWidget() == roomsTab) roomsTabContainer->treeView->collapseAll();
    if (tabWidget->currentWidget() == searchTab) searchTabContainer->treeView->collapseAll();
}


void MainWindow::onEventChanged(int aEventId, bool favouriteChanged) {
    dayTabContainer->redisplayEvent(aEventId);
    if (favouriteChanged) favsTabContainer->redisplayDate(dayNavigator->curDate());
    else favsTabContainer->redisplayEvent(aEventId);
    tracksTabContainer->redisplayEvent(aEventId);
    roomsTabContainer->redisplayEvent(aEventId);
    searchTabContainer->redisplayEvent(aEventId);
}


void MainWindow::onSearchResultChanged() {
    // Are results found on the current date?
    QDate date = dayNavigator->curDate();
    int count = searchTabContainer->searchResultCount(date);
    if (count > 0) {searchTabContainer->redisplayDate(date); return;}

    // Are results found in the future?
    for (date = date.addDays(1); date <= dayNavigator->endDate(); date = date.addDays(1)) {
        int count = searchTabContainer->searchResultCount(date);
        if (count > 0) {dayNavigator->setCurDate(date); return;}
    }

    // Are results found in the past?
    for (date = dayNavigator->startDate(); date < dayNavigator->curDate(); date = date.addDays(1)) {
        int count = searchTabContainer->searchResultCount(date);
        if (count > 0) {dayNavigator->setCurDate(date); return;}
    }
    // No results were found
    searchTabContainer->redisplayDate(dayNavigator->curDate());
}


void MainWindow::onSystemTrayMessageClicked() {
    systemTrayIcon->hide();
}


void MainWindow::onAlarmTimerTimeout() {
    // determine if an alarm is set on an event that's starting soon
    int conferenceId = Conference::activeConference();
    QList<Event> events = Event::getImminentAlarmEvents(AppSettings::preEventAlarmSec(), conferenceId);
    if (events.empty()) return;

    // build a message string
    const Conference conference = Conference::getById(conferenceId);
    Event event;
    QString title;
    QString message;
    if (events.size() == 1) {
        event = events.first();
        title = tr("Next event at %1").arg(conference.shiftTime(event.start().time()).toString("HH:mm"));
        message = tr("\"%1\"\n(%2)").arg(event.title()).arg(event.room()->name());
    } else {
        title = tr("%1 upcoming events").arg(events.size());
        QStringList messages;
        foreach (event, events) {
            messages += tr("%1: \"%2\" (%3)").arg(conference.shiftTime(event.start().time()).toString("HH:mm")).arg(event.title()).arg(event.room()->name());
        }
        message = messages.join("\n");
    }

    // and delete the corresponding alarm
    foreach (event, events) {
        event.setHasAlarm(false);
        event.update("alarm");
        onEventChanged(event.id(), false);
    }

    // show message
    systemTrayIcon->show();
    // The next two lines are to prevent a very strange position of the message box the first time at X11/aweseome (not Win32/XP)
    systemTrayIcon->showMessage("ConfClerk", tr("Your upcoming events"), QSystemTrayIcon::Information);
    qApp->processEvents();
    systemTrayIcon->showMessage(title, message, QSystemTrayIcon::Information, 60*60*24*1000);
    QApplication::alert(this);
    QApplication::beep();
}


void MainWindow::useConference(int conferenceId)
{
    if (conferenceId == -1)  // in case no conference is active
    {
        unsetConference();
        return;
    }
    try {
        int oldActiveConferenceId = Conference::activeConference();
        bool switchActiveConference = conferenceId != oldActiveConferenceId;
        if (switchActiveConference) Conference::getById(oldActiveConferenceId).update("active", 0);
        Conference activeConference = Conference::getById(conferenceId);
        if (switchActiveConference) activeConference.update("active",1);

        // looks like it does not work at n900
        setWindowTitle(activeConference.title());

        // optimization.
        // dont run initTabs() here
        // it takes much CPU, making travelling between conferences in ConferenceEditor longer
        // and is not seen in maemo WM anyway
        // instead run it explicitly
        // 1. at startup
        // 2. when ConferenceEditor finished
        // dont forget to protect the calls by try-catch!

        // just in case, clear conference selection instead
        clearTabs();

        // end of optimization
        // initTabs();
    } catch (const OrmException& e) {
        qDebug() << "OrmException:" << e.text();
        // cannon set an active conference
        unsetConference();   // TODO: as no active conference is now correctly managed this should be handled as a fatal error
        return;
    }

}

void MainWindow::initTabs()
{
    int confId = Conference::activeConference();
    if (confId != -1)   // only init tabs if a conference is active
    {
        Conference active = Conference::getById(confId);
        ((Application*) qApp)->setActiveConference(active);
        QDate startDate = active.start();
        QDate endDate = active.end();

        // 'dayNavigator' emits signal 'dateChanged' after setting valid START:END dates
        dayNavigator->setDates(startDate, endDate);
        nowAction->trigger();
    }
}

void MainWindow::clearTabs()
{
    dayTabContainer->clearModel();
    tracksTabContainer->clearModel();
    roomsTabContainer->clearModel();
    favsTabContainer->clearModel();
    searchTabContainer->clearModel();
}

void MainWindow::unsetConference()
{
    clearTabs();
    dayNavigator->unsetDates();
    setWindowTitle(saved_title);
    ((Application*) qApp)->unsetActiveConference();
}


void MainWindow::showError(const QString& message) {
    error_message(message);
}


void MainWindow::on_settingsAction_triggered()
{
    SettingsDialog dialog;
    dialog.loadDialogData();
    if (dialog.exec() == QDialog::Accepted) {
        dialog.saveDialogData();
        QNetworkProxy proxy(
                AppSettings::isDirectConnection() ? QNetworkProxy::NoProxy : (QNetworkProxy::ProxyType)AppSettings::proxyType(),
                AppSettings::proxyAddress(),
                AppSettings::proxyPort(),
                AppSettings::proxyUsername(),
                AppSettings::proxyPassword());
        QNetworkProxy::setApplicationProxy(proxy);
    }
}

/** Create and run ConferenceEditor dialog, making required connections for it.

This method manages, which classes actually perform changes in conference list.

There are several classes that modify the conferences:
this:
 deletion and URL update.
this, mXmlParser and mNetworkAccessManager:
 addition and refresh.
*/
void MainWindow::on_conferencesAction_triggered()
{
    ConferenceEditor dialog(conferenceModel, this);

    connect(&dialog, SIGNAL(haveConferenceUrl(const QString&, int)), SLOT(importFromNetwork(const QString&, int)));
    connect(&dialog, SIGNAL(haveConferenceFile(const QString&, int)), SLOT(importFromFile(const QString&, int)));
    connect(&dialog, SIGNAL(removeConferenceRequested(int)), SLOT(removeConference(int)));
    connect(&dialog, SIGNAL(changeUrlRequested(int, const QString&)),
                    SLOT(changeConferenceUrl(int, const QString&)));

    connect(&dialog, SIGNAL(haveConferenceSelected(int)), SLOT(useConference(int)));
    connect(&dialog, SIGNAL(noneConferenceSelected()), SLOT(unsetConference()));

    connect(mXmlParser, SIGNAL(parsingScheduleBegin()), &dialog, SLOT(importStarted()));
    connect(mXmlParser, SIGNAL(progressStatus(int)), &dialog, SLOT(showParsingProgress(int)));
    connect(mXmlParser, SIGNAL(parsingScheduleEnd(int)), &dialog, SLOT(importFinished(int)));

    connect(this, SIGNAL(conferenceRemoved()), &dialog, SLOT(conferenceRemoved()));

    dialog.exec();

    // optimization, see useConference() code
    try {
        initTabs();
    } catch (const OrmException& e) {
        qDebug() << "OrmException:" << e.text();
        clearTabs();
    }
}

void MainWindow::sslErrors(QNetworkReply *aReply, const QList<QSslError> &errors) {
    QString errorString;
    foreach (const QSslError &error, errors) {
        if (!errorString.isEmpty()) {
            errorString += ", ";
        }
        errorString += error.errorString();
    }

    if (QMessageBox::warning(
                this,
                tr("SSL errors"),
                tr("One or more SSL errors have occurred: %1", 0, errors.size()).arg(errorString),
                QMessageBox::Ignore | QMessageBox::Cancel) == QMessageBox::Ignore) {
        aReply->ignoreSslErrors();
    } else {
        aReply->abort();
    }
}

void MainWindow::networkQueryFinished(QNetworkReply *aReply) {
    if (aReply->error() != QNetworkReply::NoError) {
        error_message(tr("Error occurred during download: %1").arg(aReply->errorString()));
    } else {
        QUrl redirectUrl = aReply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
        if (!redirectUrl.isEmpty()) {
            if (redirectUrl != aReply->request().url()) {
                importFromNetwork(redirectUrl.toString(), aReply->request().attribute(QNetworkRequest::User).toInt());
                return; // don't enable controls
            } else {
                error_message(tr("Error: Cyclic redirection from %1 to itself.").arg(redirectUrl.toString()));
            }
        } else {
            importData(aReply->readAll(), aReply->url().toEncoded(), aReply->request().attribute(QNetworkRequest::User).toInt());
        }
    }
    setEnabled(true);
}

void MainWindow::importData(const QByteArray &aData, const QString& url, int conferenceId)
{
    mXmlParser->parseData(aData, url, conferenceId);
}

void MainWindow::importFromNetwork(const QString& url, int conferenceId)
{
    QNetworkRequest request;
    QSslConfiguration qSslConfiguration = request.sslConfiguration();
    qSslConfiguration.setProtocol(QSsl::AnyProtocol);
    qSslConfiguration.setPeerVerifyMode(QSslSocket::QueryPeer);
    request.setUrl(QUrl(url));
    request.setSslConfiguration(qSslConfiguration);
    request.setAttribute(QNetworkRequest::User, conferenceId);

    mNetworkAccessManager->setProxy(QNetworkProxy::applicationProxy());
    mNetworkAccessManager->get(request);
}

void MainWindow::importFromFile(const QString& filename, int conferenceId)
{
    QFile file(filename);
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {    
        static const QString format("Cannot read \"%1\": error %2");
        error_message(format.arg(filename, QString::number(file.error())));
    }

    importData(file.readAll(), "", conferenceId);
}


void MainWindow::removeConference(int id) {
    sqlEngine->deleteConference(id);
    conferenceModel->conferenceRemoved();
    emit conferenceRemoved();
}


void MainWindow::changeConferenceUrl(int id, const QString& url) {
    Conference::getById(id).setUrl(url);
}

