#include "buffer.h"

#include <QTimer>

#include <notebook/node.h>
#include <utils/fileutils.h>
#include <widgets/viewwindow.h>
#include <utils/pathutils.h>

#include <core/configmgr.h>
#include <core/editorconfig.h>

#include "bufferprovider.h"
#include "exception.h"

using namespace vnotex;

static vnotex::ID generateBufferID()
{
    static vnotex::ID id = 0;
    return ++id;
}

Buffer::Buffer(const BufferParameters &p_parameters,
               QObject *p_parent)
    : QObject(p_parent),
      m_provider(p_parameters.m_provider),
      m_id(generateBufferID()),
      m_readOnly(m_provider->isReadOnly())
{
    m_autoSaveTimer = new QTimer(this);
    m_autoSaveTimer->setSingleShot(true);
    m_autoSaveTimer->setInterval(1000);
    connect(m_autoSaveTimer, &QTimer::timeout,
            this, &Buffer::autoSave);

    readContent();

    checkBackupFileOfPreviousSession();
}

Buffer::~Buffer()
{
    Q_ASSERT(m_attachedViewWindowCount == 0);
    Q_ASSERT(!m_viewWindowToSync);
    Q_ASSERT(!isModified());
    Q_ASSERT(m_backupFilePath.isEmpty());
}

int Buffer::getAttachViewWindowCount() const
{
    return m_attachedViewWindowCount;
}

void Buffer::attachViewWindow(ViewWindow *p_win)
{
    Q_UNUSED(p_win);
    Q_ASSERT(!(m_state & StateFlag::Discarded));
    ++m_attachedViewWindowCount;
}

void Buffer::detachViewWindow(ViewWindow *p_win)
{
    Q_UNUSED(p_win);
    Q_ASSERT(p_win != m_viewWindowToSync);

    --m_attachedViewWindowCount;
    Q_ASSERT(m_attachedViewWindowCount >= 0);

    if (m_attachedViewWindowCount == 0) {
        emit attachedViewWindowEmpty();
    }
}

ViewWindow *Buffer::createViewWindow(const QSharedPointer<FileOpenParameters> &p_paras, QWidget *p_parent)
{
    auto window = createViewWindowInternal(p_paras, p_parent);
    Q_ASSERT(window);
    window->attachToBuffer(this, p_paras);
    return window;
}

bool Buffer::match(const Node *p_node) const
{
    Q_ASSERT(p_node);
    return m_provider->match(p_node);
}

bool Buffer::match(const QString &p_filePath) const
{
    return m_provider->match(p_filePath);
}

QString Buffer::getName() const
{
    return m_provider->getName();
}

QString Buffer::getPath() const
{
    return m_provider->getPath();
}

QString Buffer::getContentPath() const
{
    return m_provider->getContentPath();
}

QString Buffer::getResourcePath() const
{
    return m_provider->getResourcePath();
}

ID Buffer::getId() const
{
    return m_id;
}

const QString &Buffer::getContent() const
{
    const_cast<Buffer *>(this)->syncContent();
    return m_content;
}

void Buffer::setContent(const QString &p_content, int &p_revision)
{
    m_viewWindowToSync = nullptr;
    m_content = p_content;
    p_revision = ++m_revision;
    setModified(true);
    m_autoSaveTimer->start();
    emit contentsChanged();
}

void Buffer::invalidateContent(const ViewWindow *p_win,
                               const std::function<void(int)> &p_setRevision)
{
    Q_ASSERT(!m_viewWindowToSync || m_viewWindowToSync == p_win);
    ++m_revision;
    p_setRevision(m_revision);
    m_viewWindowToSync = p_win;
    m_autoSaveTimer->start();
    emit contentsChanged();
}

int Buffer::getRevision() const
{
    return m_revision;
}

void Buffer::syncContent(const ViewWindow *p_win)
{
    if (m_viewWindowToSync == p_win) {
        syncContent();
    }
}

void Buffer::syncContent()
{
    if (m_viewWindowToSync) {
        // Need to sync content.
        m_content = m_viewWindowToSync->getLatestContent();
        m_viewWindowToSync = nullptr;
    }
}

bool Buffer::isModified() const
{
    return m_modified;
}

void Buffer::setModified(bool p_modified)
{
    if (m_modified == p_modified) {
        return;
    }

    m_modified = p_modified;
    emit modified(m_modified);
}

bool Buffer::isReadOnly() const
{
    return m_readOnly;
}

Buffer::OperationCode Buffer::save(bool p_force)
{
    Q_ASSERT(!m_readOnly);
    if (m_readOnly) {
        return OperationCode::Failed;
    }

    if (m_modified
        || p_force
        || m_state & (StateFlag::FileMissingOnDisk | StateFlag::FileChangedOutside)) {
        syncContent();

        // We do not involve user here to handle file missing and changed outside cases.
        // The active ViewWindow will check this periodically.
        // Check if file still exists.
        if (!p_force && !checkFileExistsOnDisk()) {
            qWarning() << "failed to save buffer due to file missing on disk" << getPath();
            return OperationCode::FileMissingOnDisk;
        }

        // Check if file is modified outside.
        if (!p_force && checkFileChangedOutside()) {
            qWarning() << "failed to save buffer due to file changed from outside" << getPath();
            return OperationCode::FileChangedOutside;
        }

        try {
            m_provider->write(m_content);
        } catch (Exception &p_e) {
            qWarning() << "failed to write the buffer content" << getPath() << p_e.what();
            return OperationCode::Failed;
        }

        setModified(false);
        m_state &= ~(StateFlag::FileMissingOnDisk | StateFlag::FileChangedOutside);
    }
    return OperationCode::Success;
}

Buffer::OperationCode Buffer::reload()
{
    // Check if file is missing.
    if (!checkFileExistsOnDisk()) {
        qWarning() << "failed to save buffer due to file missing on disk" << getPath();
        return OperationCode::FileMissingOnDisk;
    }

    if (m_modified
        || m_state & (StateFlag::FileMissingOnDisk | StateFlag::FileChangedOutside)) {
        readContent();

        emit modified(m_modified);
        emit contentsChanged();
    }
    return OperationCode::Success;
}

void Buffer::readContent()
{
    m_content = m_provider->read();
    ++m_revision;

    // Reset state.
    m_viewWindowToSync = nullptr;
    m_modified = false;
}

void Buffer::discard()
{
    Q_ASSERT(!(m_state & StateFlag::Discarded));
    Q_ASSERT(m_attachedViewWindowCount == 1);
    m_autoSaveTimer->stop();
    m_content.clear();
    m_state |= StateFlag::Discarded;
    ++m_revision;

    m_viewWindowToSync = nullptr;
    m_modified = false;
}

void Buffer::close()
{
    // Delete the backup file if exists.
    m_autoSaveTimer->stop();
    if (!m_backupFilePath.isEmpty()) {
        FileUtils::removeFile(m_backupFilePath);
        m_backupFilePath.clear();
    }
}

QString Buffer::getImageFolderPath() const
{
    return const_cast<Buffer *>(this)->m_provider->fetchImageFolderPath();
}

QString Buffer::insertImage(const QString &p_srcImagePath, const QString &p_imageFileName)
{
    Q_UNUSED(p_srcImagePath);
    Q_UNUSED(p_imageFileName);
    Q_ASSERT_X(false, "insertImage", "image insert is not supported");
    return QString();
}

QString Buffer::insertImage(const QImage &p_image, const QString &p_imageFileName)
{
    Q_UNUSED(p_image);
    Q_UNUSED(p_imageFileName);
    Q_ASSERT_X(false, "insertImage", "image insert is not supported");
    return QString();
}

void Buffer::removeImage(const QString &p_imagePath)
{
    Q_UNUSED(p_imagePath);
    Q_ASSERT_X(false, "removeImage", "image remove is not supported");
}

void Buffer::autoSave()
{
    if (m_readOnly) {
        m_autoSaveTimer->stop();
        return;
    }

    if (m_state & (StateFlag::FileMissingOnDisk | StateFlag::FileChangedOutside)) {
        qDebug() << "disable AutoSave due to file missing on disk or changed outside";
        return;
    }
    Q_ASSERT(!(m_state & StateFlag::Discarded));
    auto policy = ConfigMgr::getInst().getEditorConfig().getAutoSavePolicy();
    switch (policy) {
    case EditorConfig::AutoSavePolicy::None:
        return;

    case EditorConfig::AutoSavePolicy::AutoSave:
        if (save(false) != OperationCode::Success) {
            qWarning() << "AutoSave failed to save buffer, retry later";
        } else {
            emit autoSaved();
        }
        break;

    case EditorConfig::AutoSavePolicy::BackupFile:
        try {
            writeBackupFile();
        } catch (Exception &p_e) {
            qWarning() << "AutoSave failed to write backup file, retry later" << p_e.what();
        }
        break;
    }
}

void Buffer::writeBackupFile()
{
    if (m_backupFilePath.isEmpty()) {
        const auto &config = ConfigMgr::getInst().getEditorConfig();
        QString backupDirPath(QDir(getResourcePath()).filePath(config.getBackupFileDirectory()));
        backupDirPath = QDir::cleanPath(backupDirPath);
        auto backupFileName = FileUtils::generateFileNameWithSequence(backupDirPath,
                                                                      getName(),
                                                                      config.getBackupFileExtension());
        QDir backupDir(backupDirPath);
        backupDir.mkpath(backupDirPath);
        m_backupFilePath = backupDir.filePath(backupFileName);
    }

    Q_ASSERT(m_backupFilePathOfPreviousSession.isEmpty());

    // Just use FileUtils instead of notebook backend.
    FileUtils::writeFile(m_backupFilePath, generateBackupFileHead() + getContent());
}

QString Buffer::generateBackupFileHead() const
{
    return QString("vnotex_backup_file %1|").arg(getContentPath());
}

void Buffer::checkBackupFileOfPreviousSession()
{
    const auto &config = ConfigMgr::getInst().getEditorConfig();
    if (config.getAutoSavePolicy() != EditorConfig::AutoSavePolicy::BackupFile) {
        return;
    }

    QString backupDirPath(QDir(getResourcePath()).filePath(config.getBackupFileDirectory()));
    backupDirPath = QDir::cleanPath(backupDirPath);
    QDir backupDir(backupDirPath);
    QStringList backupFiles;
    {
        const QString nameFilter = QString("%1*%2").arg(getName(), config.getBackupFileExtension());
        backupFiles = backupDir.entryList(QStringList(nameFilter),
                                          QDir::Files | QDir::Hidden | QDir::NoSymLinks | QDir::NoDotAndDotDot);
    }

    if (backupFiles.isEmpty()) {
        return;
    }

    for (const auto &file : backupFiles) {
        const auto filePath = backupDir.filePath(file);
        if (isBackupFileOfBuffer(filePath)) {
            const auto backupContent = readBackupFile(filePath);
            if (backupContent == getContent()) {
                // Found backup file with identical content.
                // Just discard the backup file.
                FileUtils::removeFile(filePath);
                qInfo() << "delete identical backup file of previous session" << filePath;
            } else {
                m_backupFilePathOfPreviousSession = filePath;
                qInfo() << "found backup file of previous session" << filePath;
            }
            break;
        }
    }
}

bool Buffer::isBackupFileOfBuffer(const QString &p_file) const
{
    QFile file(p_file);
    if (!file.open(QFile::ReadOnly | QIODevice::Text)) {
        return false;
    }

    QTextStream st(&file);
    const auto head = st.readLine();
    return head.startsWith(generateBackupFileHead());
}

const QString &Buffer::getBackupFileOfPreviousSession() const
{
    return m_backupFilePathOfPreviousSession;
}

QString Buffer::readBackupFile(const QString &p_filePath)
{
    auto content = FileUtils::readTextFile(p_filePath);
    return content.mid(content.indexOf(QLatin1Char('|')) + 1);
}

void Buffer::discardBackupFileOfPreviousSession()
{
    Q_ASSERT(!m_backupFilePathOfPreviousSession.isEmpty());

    FileUtils::removeFile(m_backupFilePathOfPreviousSession);
    qInfo() << "discard backup file of previous session" << m_backupFilePathOfPreviousSession;
    m_backupFilePathOfPreviousSession.clear();
}

void Buffer::recoverFromBackupFileOfPreviousSession()
{
    Q_ASSERT(!m_backupFilePathOfPreviousSession.isEmpty());

    m_content = readBackupFile(m_backupFilePathOfPreviousSession);
    m_provider->write(m_content);
    ++m_revision;

    FileUtils::removeFile(m_backupFilePathOfPreviousSession);
    qInfo() << "recover from backup file of previous session" << m_backupFilePathOfPreviousSession;
    m_backupFilePathOfPreviousSession.clear();

    // Reset state.
    m_viewWindowToSync = nullptr;
    m_modified = false;

    emit modified(m_modified);
    emit contentsChanged();
}

bool Buffer::isChildOf(const Node *p_node) const
{
    return m_provider->isChildOf(p_node);
}

bool Buffer::isAttachmentSupported() const
{
    return !m_readOnly && m_provider->isAttachmentSupported();
}

bool Buffer::hasAttachment() const
{
    if (!isAttachmentSupported()) {
        return false;
    }

    if (m_provider->getAttachmentFolder().isEmpty()) {
        return false;
    }

    QDir dir(getAttachmentFolderPath());
    return !dir.isEmpty();
}

QString Buffer::getAttachmentFolderPath() const
{
    Q_ASSERT(isAttachmentSupported());
    return const_cast<Buffer *>(this)->m_provider->fetchAttachmentFolderPath();
}

QStringList Buffer::addAttachment(const QString &p_destFolderPath, const QStringList &p_files)
{
    if (p_files.isEmpty()) {
        return QStringList();
    }
    auto destFolderPath = p_destFolderPath.isEmpty() ? getAttachmentFolderPath() : p_destFolderPath;
    Q_ASSERT(PathUtils::pathContains(getAttachmentFolderPath(), destFolderPath));
    auto files = m_provider->addAttachment(destFolderPath, p_files);
    if (!files.isEmpty()) {
        emit attachmentChanged();
    }
    return files;
}

QString Buffer::newAttachmentFile(const QString &p_destFolderPath, const QString &p_name)
{
    Q_ASSERT(PathUtils::pathContains(getAttachmentFolderPath(), p_destFolderPath));
    auto filePath = m_provider->newAttachmentFile(p_destFolderPath, p_name);
    emit attachmentChanged();
    return filePath;
}

QString Buffer::newAttachmentFolder(const QString &p_destFolderPath, const QString &p_name)
{
    Q_ASSERT(PathUtils::pathContains(getAttachmentFolderPath(), p_destFolderPath));
    auto folderPath = m_provider->newAttachmentFolder(p_destFolderPath, p_name);
    emit attachmentChanged();
    return folderPath;
}

QString Buffer::renameAttachment(const QString &p_path, const QString &p_name)
{
    Q_ASSERT(PathUtils::pathContains(getAttachmentFolderPath(), p_path));
    return m_provider->renameAttachment(p_path, p_name);
}

void Buffer::removeAttachment(const QStringList &p_paths)
{
    m_provider->removeAttachment(p_paths);
    emit attachmentChanged();
}

bool Buffer::isAttachment(const QString &p_path) const
{
    return PathUtils::pathContains(getAttachmentFolderPath(), p_path);
}

bool Buffer::isTagSupported() const
{
    return m_provider->isTagSupported();
}

Buffer::ProviderType Buffer::getProviderType() const
{
    return m_provider->getType();
}

Node *Buffer::getNode() const
{
    return m_provider->getNode();
}

bool Buffer::checkFileExistsOnDisk()
{
    if (m_provider->checkFileExistsOnDisk()) {
        m_state &= ~StateFlag::FileMissingOnDisk;
        return true;
    } else {
        m_state |= StateFlag::FileMissingOnDisk;
        return false;
    }
}

bool Buffer::checkFileChangedOutside()
{
    if (m_provider->checkFileChangedOutside()) {
        m_state |= StateFlag::FileChangedOutside;
        return true;
    } else {
        m_state &= ~StateFlag::FileChangedOutside;
        return false;
    }
}

Buffer::StateFlags Buffer::state() const
{
    return m_state;
}

QSharedPointer<File> Buffer::getFile() const
{
    return m_provider->getFile();
}
