/***************************************************************************
 *  The FreeMedForms project is a set of free, open source medical         *
 *  applications.                                                          *
 *  (C) 2008-2012 by Eric MAEKER, MD (France) <eric.maeker@gmail.com>      *
 *  All rights reserved.                                                   *
 *                                                                         *
 *  This program 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 3 of the License, or      *
 *  (at your option) any later version.                                    *
 *                                                                         *
 *  This program 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 this program (COPYING.FREEMEDFORMS file).                   *
 *  If not, see <http://www.gnu.org/licenses/>.                            *
 ***************************************************************************/
/***************************************************************************
 *   Main developers : Eric MAEKER, <eric.maeker@gmail.com>                *
 *   Contributors :                                                        *
 *       NAME <MAIL@ADDRESS.COM>                                           *
 *       NAME <MAIL@ADDRESS.COM>                                           *
 ***************************************************************************/
#include "xmlformio.h"
#include "constants.h"
#include "xmlformcontentreader.h"
#include "xmliobase.h"

#include <extensionsystem/pluginmanager.h>
#include <utils/log.h>
#include <utils/global.h>
#include <utils/versionnumber.h>
#include <utils/genericupdateinformation.h>
#include <translationutils/constants.h>
#include <translationutils/trans_filepathxml.h>

#include <coreplugin/icore.h>
#include <coreplugin/ipatient.h>
#include <coreplugin/isettings.h>
#include <coreplugin/constants_tokensandsettings.h>

//#include <formmanagerplugin/formmanager.h>
#include <formmanagerplugin/iformitem.h>
#include <formmanagerplugin/iformwidgetfactory.h>

#include <categoryplugin/categoryitem.h>
#include <categoryplugin/categorycore.h>

#include <pmhplugin/pmhcore.h>
#include <pmhplugin/pmhcategorymodel.h>

#include <QApplication>
#include <QDir>
#include <QTextCodec>
#include <QFileInfo>

using namespace XmlForms;
using namespace Internal;
using namespace Trans::ConstantTranslations;

////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////  Inline static functions  //////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////

inline static ExtensionSystem::PluginManager *pluginManager() {return ExtensionSystem::PluginManager::instance();}
static inline Core::ISettings *settings()  { return Core::ICore::instance()->settings(); }
static inline Category::CategoryCore *categoryCore() {return  Category::CategoryCore::instance();}
static inline PMH::PmhCore *pmhCore() {return PMH::PmhCore::instance();}
static inline Internal::XmlFormContentReader *reader() {return Internal::XmlFormContentReader::instance();}
static inline Internal::XmlIOBase *base() {return Internal::XmlIOBase::instance();}

static inline XmlFormName &formName(const QString &uuid, QHash<QString, XmlFormName> &cache)
{
    if (!cache.contains(uuid)) {
        cache.insert(uuid, XmlFormName(uuid));
    }
    return cache[uuid];
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////  XmlFormIO  /////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////
XmlFormIO::XmlFormIO(QObject *parent) :
        IFormIO(parent), m_Mute(false)
{
    setObjectName("XmlFormIO");
}

XmlFormIO::~XmlFormIO()
{
}

QString XmlFormIO::lastestXmlVersion()
{
    return "0.3.0";
}

bool XmlFormIO::canReadForms(const QString &uuidOrAbsPath) const
{
    Form::FormIOQuery query;
    query.setFormUuid(uuidOrAbsPath);
    return canReadForms(query);
}

bool XmlFormIO::canReadForms(const Form::FormIOQuery &query) const
{
    XmlFormName &form = formName(query.formUuid(), m_FormNames);
//    qWarning() << "CanRead" << query.formUuid() << form.uid << form.absFileName;

    if (m_ReadableForms.keys().contains(form.absFileName)) {
        return m_ReadableForms.value(form.absFileName);
    }
    m_Error.clear();
    m_AbsFileName.clear();
    QFileInfo formFile(form.absFileName);

    QString content;
    if (!query.forceFileReading()) {
        // Get it from database (save it first if needed)
        if (!base()->isFormExists(form, XmlIOBase::FullContent, form.modeName)) {
            base()->saveForm(form);
        }
        content = base()->getFormContent(form.uid, XmlIOBase::FullContent, form.modeName);
    } else {
        // Get from local file
        if (!formFile.exists()) {
            LOG_ERROR(tkTr(Trans::Constants::FILE_1_DOESNOT_EXISTS).arg(formFile.absoluteFilePath()));
            m_Error.append(tkTr(Trans::Constants::FILE_1_DOESNOT_EXISTS).arg(formFile.absoluteFilePath()));
            return false;
        }
        if (formFile.suffix().toLower()=="xml")
            content = Utils::readTextFile(formFile.absoluteFilePath(), Utils::DontWarnUser);
    }

    // check form content
    if (!reader()->checkFileContent(formFile.absoluteFilePath(), content)) {
        LOG_ERROR(tr("Invalid form file detected: %1").arg(formFile.absoluteFilePath()));
        Utils::warningMessageBox(tr("Invalid file detected."),
                                 tr("An invalid file was found. Please contact your software administrator.\n"
                                    "Wrong file: %1\n"
                                    "Error: %2")
                                 .arg(form.absFileName)
                                 .arg(reader()->lastError()));
        m_ReadableForms.insert(form.absFileName, false);
        m_ReadableForms.insert(form.uid, false);
        return false;
    }

    // check included xml files too
    QDomDocument *doc = reader()->fromCache(formFile.absoluteFilePath());
    Q_ASSERT(doc);
    if (doc) {
        QDomNodeList list = doc->elementsByTagName("file");

        for(int i=0; i < list.count(); ++i) {
            const QDomNode &node = list.at(i);
            const QString &include = node.toElement().text();
            if (include.endsWith(".xml", Qt::CaseInsensitive)) {
                Form::FormIOQuery queryInclude;
                queryInclude.setFormUuid(include);
                queryInclude.setForceFileReading(query.forceFileReading());
                if (!canReadForms(queryInclude))
                    LOG_ERROR("Unable to read included form: " + include);
            }
        }
    }

    m_AbsFileName = form.absFileName;
    m_ReadableForms.insert(form.uid, true);
    m_ReadableForms.insert(form.absFileName, true);
    return true;
}

bool XmlFormIO::canReadScripts(const Form::FormIOQuery &query) const
{
//    XmlFormName form(query.formUuid());
    XmlFormName &form = formName(query.formUuid(), m_FormNames);
//    qWarning() << Q_FUNC_INFO << query.formUuid() << form.uid << form.absFileName << form.modeName;

    if (m_ReadableForms.keys().contains(form.absFileName)) {
        return m_ReadableForms.value(form.absFileName);
    }
    m_Error.clear();
    m_AbsFileName.clear();
    QFileInfo scriptFile(form.absFileName);

    QString content;
    if (!query.forceFileReading()) {
        // Get it from database
        if (!base()->isFormExists(form, XmlIOBase::ScriptFile, form.modeName)) {
            base()->saveForm(form);
        }
        content = base()->getFormContent(form.uid, XmlIOBase::ScriptFile, form.modeName);
    } else {
        if (!scriptFile.exists()) {
            LOG_ERROR(tkTr(Trans::Constants::FILE_1_DOESNOT_EXISTS).arg(scriptFile.absoluteFilePath()));
            m_Error.append(tkTr(Trans::Constants::FILE_1_DOESNOT_EXISTS).arg(scriptFile.absoluteFilePath()));
            return false;
        }
        if (scriptFile.suffix().toLower()=="js")
            content = Utils::readTextFile(scriptFile.absoluteFilePath(), Utils::DontWarnUser);
    }

    // check form content
    if (!reader()->checkFileContent(scriptFile.absoluteFilePath(), content)) {
        LOG_ERROR(tr("Invalid script file detected: %1").arg(scriptFile.absoluteFilePath()));
        Utils::warningMessageBox(tr("Invalid file detected."),
                                 tr("An invalid file was found. Please contact your software administrator.\n"
                                    "Wrong file: %1\n"
                                    "Error: %2")
                                 .arg(form.absFileName)
                                 .arg(reader()->lastError()));
        m_ReadableScripts.insert(form.absFileName, false);
        return false;
    }
    m_AbsFileName = form.absFileName;
    m_ReadableScripts.insert(form.absFileName, true);
    return true;
}

Form::FormIODescription *XmlFormIO::readFileInformation(const QString &uuidOrAbsPath) const
{
    return reader()->readFileInformation(uuidOrAbsPath);
}

QList<Form::FormIODescription *> XmlFormIO::getFormFileDescriptions(const Form::FormIOQuery &query) const
{
    QList<Form::FormIODescription *> toReturn;
    QStringList includedUids;
//    qWarning() << query.formUuid() << query.forceFileReading();

    if (!query.forceFileReading()) {
        // Get from database
        toReturn = base()->getFormDescription(query);
        if (!toReturn.isEmpty() && !query.getAllAvailableFormDescriptions())
            return toReturn;
        for(int i=0; i<toReturn.count(); ++i) {
            includedUids << toReturn.at(i)->data(Form::FormIODescription::UuidOrAbsPath).toString();
        }
    }

    // Get a specific form description
    if (!query.formUuid().isEmpty()) {
        XmlFormName &form = formName(query.formUuid(), m_FormNames);
//        XmlFormName form(query.formUuid());
        if (canReadForms(query)) {
            Form::FormIODescription *desc = reader()->readFileInformation(form.absFileName, query);
            if (desc) {
                desc->setData(Form::FormIODescription::IsCompleteForm, true);
                toReturn.append(desc);
                return toReturn;
            }
        }
        return toReturn;
    }

    // Get all form files
    if (query.typeOfForms() & Form::FormIOQuery::CompleteForms) {
        QStringList path;
        path << settings()->path(Core::ISettings::CompleteFormsPath);
        path << settings()->path(Core::ISettings::UserCompleteFormsPath);
        path << settings()->path(Core::ISettings::DataPackCompleteFormsInstallPath);
        foreach(const QString &startPath, path) {
            QDir start(startPath);
            // get all forms included in this path
            foreach(const QFileInfo &file, Utils::getFiles(start, "central.xml", Utils::Recursively)) {
                const QString &fileName = file.absoluteFilePath();
                XmlFormName &form = formName(fileName, m_FormNames);
                if (includedUids.contains(form.uid))
                    continue;

                if (canReadForms(fileName)) {
                    Form::FormIODescription *desc = reader()->readFileInformation(fileName);
                    if (desc) {
                        desc->setData(Form::FormIODescription::IsCompleteForm, true);
                        toReturn.append(desc);
                    }
                }
            }
        }
    }
    if (query.typeOfForms() & Form::FormIOQuery::SubForms) {
        QStringList path;
        path << settings()->path(Core::ISettings::SubFormsPath);
        path << settings()->path(Core::ISettings::UserSubFormsPath);
        path << settings()->path(Core::ISettings::DataPackSubFormsInstallPath);
        foreach(const QString &startPath, path) {
            QDir start(startPath);
            foreach(const QFileInfo &file, Utils::getFiles(start, "central.xml", Utils::Recursively)) {
                const QString &fileName = file.absoluteFilePath();
                XmlFormName &form = formName(fileName, m_FormNames);
                if (includedUids.contains(form.uid))
                    continue;

                if (canReadForms(fileName)) {
                    Form::FormIODescription *desc = reader()->readFileInformation(fileName);
                    if (desc) {
                        desc->setData(Form::FormIODescription::IsSubForm, true);
                        toReturn.append(desc);
                    }
                }
            }
        }
    }

    // TODO: check all forms for params of Query, check forms versions, remove duplicates
    for(int i = 0; i < toReturn.count(); ++i) {
        toReturn.at(i)->setIoFormReader((Form::IFormIO*)this);
    }
    return toReturn;
}

QList<Form::FormMain *> XmlFormIO::loadAllRootForms(const QString &uuidOrAbsPath) const
{
    XmlFormName &form = formName(uuidOrAbsPath, m_FormNames);
//    XmlFormName form(uuidOrAbsPath);
//    qWarning() << Q_FUNC_INFO << uuidOrAbsPath << form.uid << form.absFileName;

    QList<Form::FormMain *> toReturn;
    QString uuid = uuidOrAbsPath;
    if (uuidOrAbsPath.isEmpty()) {
        if (m_AbsFileName.isEmpty()) {
            LOG_ERROR(tr("No form file name"));
            return toReturn;
        } else {
            uuid = m_AbsFileName;
        }
    }
    if (!canReadForms(form.uid)) {
        LOG_ERROR("Can not read form " + form.uid);
        return toReturn;
    }

    QFileInfo formFile(form.absFileName);
    QDir dir;
    // remove modeName for the fileName
    if (formFile.isDir())
        dir.setPath(formFile.absoluteFilePath());
    else
        dir.setPath(formFile.absolutePath());

    // Populate DB with all the files of this form if needed
    if (!base()->isFormExists(form)) {
        base()->saveForm(form);
    }

    QHash<QString, QString> mode_contents = base()->getAllFormFullContent(form.uid);

    reader()->refreshPluginFactories();
    QHashIterator<QString, QString> it(mode_contents);
    while (it.hasNext()) {
        it.next();
        Form::FormMain *root = m_ActualForm = new Form::FormMain;
        root->setModeUniqueName(it.key());
        root->setUuid(form.uid);
        root->setIoFormReader((XmlFormIO*)this);
        QString fakeFileName;
        QFileInfo info(form.absFileName);
        if (formFile.isDir())
            fakeFileName = info.absoluteFilePath() + "/" + it.key() + ".xml";
        else
            fakeFileName = info.absolutePath() + "/" + it.key() + ".xml";

        if (!reader()->isInCache(fakeFileName)) {
            if (!reader()->checkFileContent(fakeFileName, it.value()))
                continue;
        }
        XmlFormName mode(form.uid);
        mode.absFileName = fakeFileName;
//        qWarning() << "MODE" << mode.absFileName << mode.uid;
        if (!reader()->loadForm(mode, root)) {
            LOG_ERROR("Form not readable: " + fakeFileName);
        } else {
            toReturn.append(root);
        }
        // Emit the formLoaded signal for all subForms loaded
        QList<Form::FormMain *> forms = root->flattenFormMainChildren();
        for(int i=0; i < forms.count(); ++i) {
            forms.at(i)->emitFormLoaded();
        }
    }
    return toReturn;
}

bool XmlFormIO::loadPmhCategories(const QString &uuidOrAbsPath) const
{
    XmlFormName &form = formName(uuidOrAbsPath, m_FormNames);
//    WARN_FUNC << uuidOrAbsPath << form;
    pmhCore()->pmhCategoryModel()->setRootFormUid(form.uid);
    pmhCore()->pmhCategoryModel()->refreshFromDatabase();
    LOG("Category retreived");
    return true;
}


QList<QPixmap> XmlFormIO::screenShots(const QString &uuidOrAbsPath) const
{
    Q_UNUSED(uuidOrAbsPath);
    QList<QPixmap> toReturn;
    return toReturn;
}

QPixmap XmlFormIO::screenShot(const QString &uuidOrAbsPath, const QString &name) const
{
    return base()->getScreenShot(uuidOrAbsPath, name);
}

void XmlFormIO::checkForUpdates() const
{
    reader()->clearCache();
    checkDatabaseFormFileForUpdates();
}

/** Check the database form version and try to update them with the local files. */
bool XmlFormIO::checkDatabaseFormFileForUpdates() const
{
    QList<Form::FormIODescription *> fromFiles;
    QList<Form::FormIODescription *> fromDb;
    LOG("Checking for form update");

    // get all available descriptions from database
    Form::FormIOQuery querydb;
    querydb.setGetAllAvailableFormDescriptions(true);
    fromDb = base()->getFormDescription(querydb);

    // Test all forms for an update and populate a list
    QList<XmlFormName> formsToUpdate;
    bool readError = false;
    QStringList msg;
    foreach(Form::FormIODescription *descDb, fromDb) {
        qDeleteAll(fromFiles);
        fromFiles.clear();
        Form::FormIOQuery query;
        query.setFormUuid(descDb->data(Form::FormIODescription::UuidOrAbsPath).toString());
        query.setForceFileReading(true);
        Utils::VersionNumber db(descDb->data(Form::FormIODescription::Version).toString());
        fromFiles = getFormFileDescriptions(query);
        foreach(Form::FormIODescription *descFile , fromFiles) {
            // check version number of forms
            Utils::VersionNumber file(descFile->data(Form::FormIODescription::Version).toString());
            if (file.versionString()=="test" || file>db) {
                // update database
                XmlFormName &form = formName(descFile->data(Form::FormIODescription::UuidOrAbsPath).toString(), m_FormNames);
                // Construct the detailled text of the user's question messagebox
                QString html;
                html = QString("<b>%1</b><br />&nbsp;&nbsp;•&nbsp;%2<br />&nbsp;&nbsp;•&nbsp;%3<br />")
                        .arg(tr("Form: ") + descFile->data(Form::FormIODescription::ShortDescription).toString())
                        .arg(tr("New version: %1").arg(file.versionString()))
                        .arg(tr("Database version: %1").arg(db.versionString()))
                        ;
                foreach(const Utils::GenericUpdateInformation &u, descFile->updateInformationForVersion(db)) {
                    html += "&nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;" + Utils::firstLetterUpperCase(tkTr(Trans::Constants::FROM_1_TO_2).arg(u.fromVersion()).arg(u.toVersion())) + "&nbsp;: " + u.text() + "<br />";
                }
                msg << html;
                if (!formsToUpdate.contains(form))
                    formsToUpdate << form;
            }
        }
    }

    if (!readError && !formsToUpdate.isEmpty()) {
        // Ask user for update
        bool yes = Utils::yesNoMessageBox(tr("Form update detected."),
                                          tr("A form update has been detected. Do you want to update the forms?"),
                                          msg.join("<br /><br />"));
        if (yes) {
            // Update all checked forms
            for(int i = 0; i < formsToUpdate.count(); ++i) {
                XmlFormName &form = formsToUpdate[i];
                if (!base()->saveForm(form)) { //, doc->toString(2), XmlIOBase::FullContent, file.baseName(), QDateTime::currentDateTime())) {
                    LOG_ERROR("Unable to update form database. Form: " + form.uid + " " + form.absFileName);
                } else {
                    LOG("Form updated: "  + form.uid + " " + form.absFileName);
                }
            }
        }
    }

    // Clear cache
    m_ReadableForms.clear();
    reader()->clearCache();

    // Clear pointers
    qDeleteAll(fromFiles);
    fromFiles.clear();
    qDeleteAll(fromDb);
    fromDb.clear();
    return true;
}
