/*****************************************************************************
 * $CAMITK_LICENCE_BEGIN$
 *
 * CamiTK - Computer Assisted Medical Intervention ToolKit
 * (c) 2001-2025 Univ. Grenoble Alpes, CNRS, Grenoble INP - UGA, TIMC, 38000 Grenoble, France
 *
 * Visit http://camitk.imag.fr for more information
 *
 * This file is part of CamiTK.
 *
 * CamiTK is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License version 3
 * only, as published by the Free Software Foundation.
 *
 * CamiTK 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 Lesser General Public License version 3 for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * version 3 along with CamiTK.  If not, see <http://www.gnu.org/licenses/>.
 *
 * $CAMITK_LICENCE_END$
 ****************************************************************************/

// -- Core stuff
#include "CamiTKLogger.h"
#include "Action.h"
#include "Component.h"
#include "Viewer.h"
#include "Application.h"
#include "MainWindow.h"
#include "ActionExtension.h"
#include "ComponentExtension.h"
#include "ViewerExtension.h"
#include "Log.h"

#include <iostream>

#include <QDateTime>
#include <QMessageBox>
#include <QTextStream>
#include <QFileInfo>
#include <QMutex>

namespace camitk {

// --------------------- Constructor ---------------------
CamiTKLogger::CamiTKLogger() :
    logFileDirectory{QDir(QDir::tempPath() + "/CamiTK")},
    logStartTime{QDateTime::currentDateTime()} {
    level = InterfaceLogger::WARNING;
    messageBoxLevel = InterfaceLogger::NONE;
    logToStdOut = true;
    displayDebugInformation = false;
    displayTimeStampInformation = true;
    logToFile = false;
    logFile = nullptr;
    logStream = nullptr;
}

// --------------------- Destructor ---------------------
CamiTKLogger::~CamiTKLogger() {
    closeLogFile();
    log("Log finished", TRACE, __FILE__, Q_FUNC_INFO, __LINE__);
}

// --------------------- getLogLevel ---------------------
CamiTKLogger::LogLevel CamiTKLogger::getLogLevel() {
    return level;
}

// --------------------- setLogLevel ---------------------
void CamiTKLogger::setLogLevel(camitk::InterfaceLogger::LogLevel level) {
    if (level != this->level) {
        this->level = level;
        log("Log level changed to: " + Log::getLevelAsString(level), TRACE, __FILE__, Q_FUNC_INFO, __LINE__);
    }
}

// --------------------- setLogToStandardOutput ---------------------
void CamiTKLogger::setLogToStandardOutput(bool logToStdOut) {
    if (logToStdOut != this->logToStdOut) {
        this->logToStdOut = logToStdOut;
        log("Logging to standard output: " + QString((logToStdOut ? "true" : "false")), TRACE, __FILE__, Q_FUNC_INFO, __LINE__);
    }
}

// --------------------- getLogToStandardOutput ---------------------
bool CamiTKLogger::getLogToStandardOutput() {
    return logToStdOut;
}

// --------------------- setLogToFile ---------------------
bool CamiTKLogger::setLogToFile(bool logToFile) {
    if (logToFile != this->logToFile) {
        log("Logging to file: " + QString((logToFile ? "true" : "false")), TRACE, __FILE__, Q_FUNC_INFO, __LINE__);

        //-- if logger was already writing into a log file, close it properly before opening a new one...
        if (logToFile) {
            openLogFile();
        }
        else {
            closeLogFile();
        }
    }
    return this->logToFile;
}

// --------------------- setLogFileDirectory ---------------------
bool CamiTKLogger::setLogFileDirectory(QDir directoryName, bool moveExistingLogFile) {
    if (logToFile) {
        // if the directory is the same as the given one, nothing to do
        if (directoryName != logFileDirectory.path()) {
            QFileInfo oldLogFile(logFile->fileName());
            closeLogFile();
            logFileDirectory = directoryName;
            if (!moveExistingLogFile) {
                // a new file is going to be created with a new name
                logStartTime = QDateTime::currentDateTime();
            }
            openLogFile(moveExistingLogFile, oldLogFile);
        }
    }
    else {
        logFileDirectory = directoryName;
    }
    return logToFile;
}

// --------------------- getLogToFile ---------------------
bool CamiTKLogger::getLogToFile() {
    return logToFile;
}

// --------------------- openLogFile ---------------------
bool CamiTKLogger::openLogFile(bool moveFile, QFileInfo fileToMove) {
    if (logToFile) {
        return logToFile;
    }

    //-- Check the write status of logFileDirectory
    if (!logFileDirectory.exists()) {
        // check if it can be created
        if (!logFileDirectory.mkpath(logFileDirectory.path())) {
            log("Cannot create directory \"" + logFileDirectory.path() + "\"", ERROR, __FILE__, Q_FUNC_INFO, __LINE__);
            return logToFile;
        }
    }

    //-- Move or create the name of the log file with time stamp
    // Replace ":" by "-" as windows has sometimes difficilties with ":" in filename
    QString logFileName = "log-" + logStartTime.toString(Qt::ISODate).replace(":", "-") + ".camitklog";

    //-- move file if required
    if (moveFile) {
        QFile::rename(fileToMove.absoluteFilePath(), logFileDirectory.path() + "/" + logFileName);
    }

    //-- open the file
    logFile = new QFile(logFileDirectory.path() + "/" + logFileName);

    //-- check that we can create a file in logFileDirectory
    if (!logFile->open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
        log("Cannot open \"" + logFile->fileName() + "\" for logging", ERROR, __FILE__, Q_FUNC_INFO, __LINE__);
        logToFile = false;
        delete logFile;
        logFile = nullptr;
    }
    else {
        logToFile = true;
        logStream = new QTextStream(logFile);
        log("Log in file \"" + logFile->fileName() + "\" started", TRACE, __FILE__, Q_FUNC_INFO, __LINE__);
    }
    return logToFile;

}

// --------------------- closeLogFile ---------------------
void CamiTKLogger::closeLogFile() {
    if (logFile != nullptr) {
        log("Log in file \"" + logFile->fileName() + "\" finished", TRACE, __FILE__, Q_FUNC_INFO, __LINE__);
        logStream->flush();
        if (logStream->status() != QTextStream::Ok) {
            log("Error closing log output stream. Log output stream status is: " + QString::number(logStream->status()), ((logStream->status() == QTextStream::WriteFailed) ? ERROR : WARNING), __FILE__, Q_FUNC_INFO, __LINE__);
        }
        delete logStream;
        logStream = nullptr;
        logFile->close();
        delete logFile;
        logFile = nullptr;
    }
    logToFile = false;
}

// --------------------- getLogFileInfo ---------------------
QFileInfo CamiTKLogger::getLogFileInfo() {
    if (logToFile) {
        return QFileInfo(logFile->fileName());
    }
    else {
        return QFileInfo();
    }
}

// --------------------- setMessageBoxLevel ---------------------
void CamiTKLogger::setMessageBoxLevel(InterfaceLogger::LogLevel level) {
    if (messageBoxLevel != level) {
        messageBoxLevel = level;
        log("Message box level changed to: " + Log::getLevelAsString(level), TRACE, __FILE__, Q_FUNC_INFO, __LINE__);
    }
}

// --------------------- getMessageBoxLevel ---------------------
InterfaceLogger::LogLevel CamiTKLogger::getMessageBoxLevel() {
    return messageBoxLevel;
}

// --------------------- setDebugInformation ---------------------
void CamiTKLogger::setDebugInformation(bool showDebugInformation) {
    if (showDebugInformation != displayDebugInformation) {
        displayDebugInformation = showDebugInformation;
        log("Logging debug information: " + QString((displayDebugInformation ? "true" : "false")), TRACE, __FILE__, Q_FUNC_INFO, __LINE__);
    }
}

// --------------------- getDebugInformation ---------------------
bool CamiTKLogger::getDebugInformation() {
    return displayDebugInformation;
}

// --------------------- setTimeStampInformation ---------------------
void CamiTKLogger::setTimeStampInformation(bool showTimeStamp) {
    if (showTimeStamp != displayTimeStampInformation) {
        displayTimeStampInformation = showTimeStamp;
        log("Logging time stamp: " + QString((displayTimeStampInformation ? "true" : "false")), TRACE, __FILE__, Q_FUNC_INFO, __LINE__);
    }
}

// --------------------- getTimeStampInformation ---------------------
bool CamiTKLogger::getTimeStampInformation() {
    return displayTimeStampInformation;
}

// --------------------- log ---------------------
QString CamiTKLogger::log(const QString msg, const LogLevel level, char const* fileName, char const* methodName, int lineNumber, const QObject* sender) {
    // To ensure thread safety of the logger
    // see http://www.kdab.com/wp-content/uploads/stories/slides/Day2/KaiKoehne_Qt%20Logging%20Framework%2016_9_0.pdf page 22
    static QMutex mutex;

    QString message;
    if (level <= this->level && mutex.tryLock(500)) {
        QString userMsg = msg;
        message = buildLogMessage(userMsg, level, fileName, methodName, lineNumber, sender);
        if (logToStdOut) {
            std::cout << message.toStdString() << std::endl;
            std::cout.flush();
        }
        if (logToFile) {
            (*logStream) << message << Qt::endl;
            logStream->flush();
            if (logStream->status() != QTextStream::Ok) {
                std::cerr << buildLogMessage("Error closing log output stream. Log output stream status is: " + QString::number(logStream->status()), ((logStream->status() == QTextStream::WriteFailed) ? ERROR : WARNING), fileName, methodName, lineNumber, sender).toStdString() << std::endl;
                std::cerr.flush();
            }
        }
        if (level <= messageBoxLevel) {
            QMessageBox::Icon msgBoxIcon;
            switch (level) {
                case WARNING:
                    msgBoxIcon = QMessageBox::Warning;
                    break;
                case ERROR:
                    msgBoxIcon = QMessageBox::Critical;
                    break;
                case TRACE:
                case INFO:
                default:
                    msgBoxIcon = QMessageBox::Information;
                    break;
            }

            // if log was sent in the middle of an operation, restore cursor before displaying the dialog
            QApplication::restoreOverrideCursor();

            QMessageBox msgBox(msgBoxIcon, Log::getLevelAsString(level), getCamiTKAPIInformation(sender) + msg, QMessageBox::Close);
            msgBox.setEscapeButton(QMessageBox::Close);
            msgBox.setDetailedText(message);
            msgBox.exec();
        }
        mutex.unlock();
    }
    // ⚠ WARNING : if the mutex is not unlocked after 500ms, the log message sent by another thread will be lost...

    return message;
}


// --------------------- buildLogMessage ---------------------
QString CamiTKLogger::buildLogMessage(QString message, LogLevel level, char const* fileName, char const* methodName, int lineNumber, const QObject* sender) {
    QString logMessage;

    if (displayTimeStampInformation) {
        logMessage = QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss.zzz") + " ";
    }

    logMessage += "[" + Log::getLevelAsString(level).leftJustified(7, ' ') + "] ";

    if (displayDebugInformation) {
        logMessage += "[" + QString(methodName) + "@" + QFileInfo(QString(fileName)).baseName() + "." + QFileInfo(QString(fileName)).completeSuffix() + ":" + QString::number(lineNumber) + "] ";
    }

    // specific CamiTK API processing
    logMessage += getCamiTKAPIInformation(sender);

    logMessage += message;

    return logMessage;
}

// --------------------- getCamiTKAPIInformation ---------------------
QString CamiTKLogger::getCamiTKAPIInformation(const QObject* sender) {
    // sender is an action
    const auto* camitkAction = dynamic_cast<const camitk::Action*>(sender);
    if (camitkAction != nullptr) {
        return "Action \"" + camitkAction->getName() + "\" - ";
    }

    // sender is a component
    const auto* camitkComponent = dynamic_cast<const camitk::Component*>(sender);
    if (camitkComponent != nullptr) {
        return QString(camitkComponent->metaObject()->className()) + " \"" + camitkComponent->getName() + "\" - ";
    }

    // sender is a ActionExtension
    auto* nonConstSender = const_cast<QObject*>(sender); // beware!
    auto* camitkActionExt = dynamic_cast<camitk::ActionExtension*>(nonConstSender);
    if (camitkActionExt != nullptr) {
        return QString("Action Extension \"") + camitkActionExt->getName() + "\" - ";
    }

    // sender is a ComponentExtension
    const auto* camitkComponentExt = dynamic_cast<const camitk::ComponentExtension*>(sender);
    if (camitkComponentExt != nullptr) {
        return QString("Component Extension \"") + camitkComponentExt->getName() + "\" - ";
    }

    // sender is a ComponentExtension
    auto* camitkViewerExt = dynamic_cast<camitk::ViewerExtension*>(nonConstSender);
    if (camitkViewerExt != nullptr) {
        return QString("Viewer Extension \"") + camitkViewerExt->getName() + "\" - ";
    }

    // sender is an application
    const auto* camitkApplication = dynamic_cast<const camitk::Application*>(sender);
    if (camitkApplication != nullptr) {
        return QString("Application \"") + camitkApplication->getName() + "\" - ";
    }

    // sender is a main window
    const auto* camitkMainWindow = dynamic_cast<const camitk::MainWindow*>(sender);
    if (camitkMainWindow != nullptr) {
        return QString("Main Window \"") + camitkMainWindow->getName() + "\" - ";
    }

    // sender is a viewer
    const auto* camitkViewer = dynamic_cast<const camitk::Viewer*>(sender);
    if (camitkViewer != nullptr) {
        return QString("Viewer \"") + camitkViewer->objectName() + "\" - ";
    }

    return "";
}

} // end namespace
