/* This file is part of the KDE project
 * SPDX-FileCopyrightText: 2005 David Faure <faure@kde.org>
 * SPDX-FileCopyrightText: 2007-2009 Thomas Zander <zander@kde.org>
 * SPDX-FileCopyrightText: 2007-2008 Sebastian Sauer <mail@dipe.org>
 * SPDX-FileCopyrightText: 2007-2008 Pierre Ducroquet <pinaraf@gmail.com>
 * SPDX-FileCopyrightText: 2007-2008 Thorsten Zachmann <zachmann@kde.org>
 *
 * SPDX-License-Identifier: LGPL-2.0-or-later
 */

#include "KWOdfWriter.h"
#include "KWDocument.h"
#include "KWPage.h"

#include "frames/KWTextFrameSet.h"
#include <KoGenStyles.h>
#include <KoOdfWriteStore.h>
#include <KoShapeSavingContext.h>
#include <KoXmlWriter.h>

#include <KoAnnotationLayoutManager.h>
#include <KoGridData.h>
#include <KoGuidesData.h>
#include <KoParagraphStyle.h>
#include <KoShapeGroup.h>
#include <KoShapeLayer.h>
#include <KoStyleManager.h>
#include <KoTextDocument.h>
#include <KoTextShapeData.h>

#include <KoGenChanges.h>
#include <KoInlineTextObjectManager.h>
#include <KoTextSharedSavingData.h>
#include <KoVariableManager.h>
#include <changetracker/KoChangeTracker.h>

#include <KoDocumentRdfBase.h>
#include <KoStoreDevice.h>

#include <QBuffer>
#include <QTemporaryFile>
#include <QTextCursor>
#include <WordsDebug.h>

static const struct {
    const char *tag;
} headerFooterTag[] = {{"style:header"}, {"style:header-left"}, {"style:footer"}, {"style:footer-left"}};

QByteArray KWOdfWriter::serializeHeaderFooter(KoShapeSavingContext &context, KWTextFrameSet *fs)
{
    const char *tag = nullptr;
    switch (fs->textFrameSetType()) {
    case Words::OddPagesHeaderTextFrameSet:
        tag = headerFooterTag[0].tag;
        break;
    case Words::EvenPagesHeaderTextFrameSet:
        tag = headerFooterTag[1].tag;
        break;
    case Words::OddPagesFooterTextFrameSet:
        tag = headerFooterTag[2].tag;
        break;
    case Words::EvenPagesFooterTextFrameSet:
        tag = headerFooterTag[3].tag;
        break;
    default:
        return QByteArray();
    }

    QByteArray content;
    QBuffer buffer(&content);
    buffer.open(QIODevice::WriteOnly);
    KoXmlWriter writer(&buffer);

    KoXmlWriter &savedWriter = context.xmlWriter();

    KoShapeSavingContext::ShapeSavingOptions options = context.options();
    context.setOptions(KoShapeSavingContext::AutoStyleInStyleXml | KoShapeSavingContext::ZIndex);
    context.setXmlWriter(writer);

    Q_ASSERT(!fs->shapes().isEmpty());
    KoTextShapeData *shapedata = qobject_cast<KoTextShapeData *>(fs->shapes().first()->userData());
    Q_ASSERT(shapedata);

    writer.startElement(tag);
    shapedata->saveOdf(context, m_document->documentRdf());
    writer.endElement();

    context.setOptions(options);
    context.setXmlWriter(savedWriter);

    return content;
}

// rename to save pages ?
void KWOdfWriter::saveHeaderFooter(KoShapeSavingContext &context)
{
    // debugWords<< "START saveHeaderFooter ############################################";
    //  first get all the framesets in a nice quick-to-access data structure
    //  this avoids iterating till we drop
    QHash<KWPageStyle, QHash<int, KWTextFrameSet *>> data;
    foreach (KWFrameSet *fs, m_document->frameSets()) {
        KWTextFrameSet *tfs = dynamic_cast<KWTextFrameSet *>(fs);
        if (!tfs)
            continue;
        if (!Words::isAutoGenerated(tfs))
            continue;
        if (tfs->textFrameSetType() == Words::MainTextFrameSet)
            continue;
        QHash<int, KWTextFrameSet *> set = data.value(tfs->pageStyle());
        set.insert(tfs->textFrameSetType(), tfs);
        Q_ASSERT(tfs->pageStyle().isValid());
        data.insert(tfs->pageStyle(), set);
    }

    // save page styles that don't have a header or footer which will be handled later
    foreach (const KWPageStyle &pageStyle, m_document->pageManager()->pageStyles()) {
        if (data.contains(pageStyle))
            continue;

        KoGenStyle masterStyle(KoGenStyle::MasterPageStyle);
        KoGenStyle layoutStyle = pageStyle.saveOdf();
        if (!pageStyle.displayName().isEmpty() && pageStyle.displayName() != pageStyle.name())
            masterStyle.addProperty("style:display-name", pageStyle.displayName());
        if (!pageStyle.nextStyleName().isEmpty())
            masterStyle.addProperty("style:next-style-name", pageStyle.nextStyleName());
        masterStyle.addProperty("style:page-layout-name", context.mainStyles().insert(layoutStyle, "pm"));
        QString name = context.mainStyles().insert(masterStyle, pageStyle.name(), KoGenStyles::DontAddNumberToName);
        m_masterPages.insert(pageStyle, name);
    }

    // We need to flush them out ordered as defined in the specs.
    QList<Words::TextFrameSetType> order;
    order << Words::OddPagesHeaderTextFrameSet << Words::EvenPagesHeaderTextFrameSet << Words::OddPagesFooterTextFrameSet << Words::EvenPagesFooterTextFrameSet;

    QHash<KWPageStyle, QHash<int, KWTextFrameSet *>>::ConstIterator it = data.constBegin();
    QHash<KWPageStyle, QHash<int, KWTextFrameSet *>>::ConstIterator end = data.constEnd();
    for (; it != end; ++it) {
        const KWPageStyle &pageStyle = it.key();
        const QHash<int, KWTextFrameSet *> &headersAndFooters = it.value();

        KoGenStyle masterStyle(KoGenStyle::MasterPageStyle);
        // masterStyle.setAutoStyleInStylesDotXml(true);
        KoGenStyle layoutStyle = pageStyle.saveOdf();
        if (!pageStyle.displayName().isEmpty() && pageStyle.displayName() != pageStyle.name())
            masterStyle.addProperty("style:display-name", pageStyle.displayName());
        if (!pageStyle.nextStyleName().isEmpty())
            masterStyle.addProperty("style:next-style-name", pageStyle.nextStyleName());
        masterStyle.addProperty("style:page-layout-name", context.mainStyles().insert(layoutStyle, "pm"));

        int index = 0;
        foreach (int type, order) {
            if (!headersAndFooters.contains(type))
                continue;
            KWTextFrameSet *fs = headersAndFooters.value(type);
            Q_ASSERT(fs);
            if (fs->shapeCount() == 0) // don't save empty framesets
                continue;

            QByteArray content = serializeHeaderFooter(context, fs);

            if (content.isNull())
                continue;

            masterStyle.addChildElement(QString::number(++index), QString::fromUtf8(content));
        }
        // append the headerfooter-style to the main-style
        QString name = context.mainStyles().insert(masterStyle, pageStyle.name(), KoGenStyles::DontAddNumberToName);
        m_masterPages.insert(pageStyle, name);
    }

    // foreach (KoGenStyles::NamedStyle s, mainStyles.styles(KoGenStyle::ParagraphAutoStyle))
    //     mainStyles.markStyleForStylesXml(s.name);

    // debugWords << "END saveHeaderFooter ############################################";
}

KWOdfWriter::KWOdfWriter(KWDocument *document)
    : QObject()
    , m_document(document)
    , m_shapeTree(4, 2)
{
}

KWOdfWriter::~KWOdfWriter() = default;

// 1.6: KWDocument::saveOasisHelper()
bool KWOdfWriter::save(KoOdfWriteStore &odfStore, KoEmbeddedDocumentSaver &embeddedSaver)
{
    // debugWords << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!";

    KoStore *store = odfStore.store();

    if (!store->open("settings.xml")) {
        return false;
    }

    saveOdfSettings(store);

    if (!store->close())
        return false;

    KoXmlWriter *manifestWriter = odfStore.manifestWriter();

    manifestWriter->addManifestEntry("settings.xml", "text/xml");

    KoXmlWriter *contentWriter = odfStore.contentWriter();
    if (!contentWriter)
        return false;

    QTemporaryFile tmpChangeFile;
    tmpChangeFile.open();
    KoXmlWriter *changeWriter = new KoXmlWriter(&tmpChangeFile, 1);
    if (!changeWriter)
        return false;

    QTemporaryFile tmpTextBodyFile;
    tmpTextBodyFile.open();
    KoXmlWriter *tmpBodyWriter = new KoXmlWriter(&tmpTextBodyFile, 1);
    if (!tmpBodyWriter)
        return false;

    KoGenStyles mainStyles;

    KoGenChanges changes;

    KoChangeTracker *changeTracker = m_document->resourceManager()->resource(KoText::ChangeTracker).value<KoChangeTracker *>();

    KoShapeSavingContext context(*tmpBodyWriter, mainStyles, embeddedSaver);
    context.addOption(KoShapeSavingContext::ZIndex);

    KoTextSharedSavingData *sharedData = new KoTextSharedSavingData;
    sharedData->setGenChanges(changes);
    context.addSharedData(KOTEXT_SHARED_SAVING_ID, sharedData);

    // Save the named styles
    if (KoStyleManager *styleManager = m_document->resourceManager()->resource(KoText::StyleManager).value<KoStyleManager *>()) {
        styleManager->saveOdf(context);
    }

    // TODO get the pagestyle for the first page and store that as 'style:default-page-layout'

    // Header and footers save their content into master-styles/master-page, and their
    // styles into the page-layout automatic-style.
    saveHeaderFooter(context);

    KoXmlWriter *bodyWriter = odfStore.bodyWriter();
    bodyWriter->startElement("office:body");
    bodyWriter->startElement("office:text");
    if (m_document->isMasterDocument()) {
        bodyWriter->addAttribute("text:global", "true");
    }
    // FIXME: text:use-soft-page-breaks

    calculateZindexOffsets();

    KWTextFrameSet *mainTextFrame = nullptr;

    foreach (KWFrameSet *fs, m_document->frameSets()) {
        // For the purpose of saving to ODF we have 3 types of frames.
        //  1) auto-generated frames.  This includes header/footers and the main text FS.
        //  2) frames that are anchored to text.  They have a parent and their parent will save them.
        //  3) frames that are not anchored but freely positioned somewhere on the page.
        //     in ODF terms those frames are page-anchored.

        if (fs->shapeCount() == 1) {
            // may be a frame that is anchored to text, don't save those here.
            KoShapeAnchor *anchor = fs->shapes().first()->anchor();
            if (anchor && anchor->anchorType() != KoShapeAnchor::AnchorPage)
                continue;
        }

        KWTextFrameSet *tfs = dynamic_cast<KWTextFrameSet *>(fs);
        if (tfs) {
            if (tfs->textFrameSetType() == Words::MainTextFrameSet) {
                mainTextFrame = tfs;
                continue;
            } else if (Words::isAutoGenerated(tfs)) {
                continue;
            }
        }

        int counter = 1;
        QSet<QString> uniqueNames;
        foreach (KoShape *shape, fs->shapes()) { // make sure all shapes have names.
            if (counter++ == 1)
                shape->setName(fs->name());
            else if (shape->name().isEmpty() || uniqueNames.contains(shape->name()))
                shape->setName(QString("%1-%2").arg(fs->name(), QString::number(counter)));
            uniqueNames << shape->name();
        }
        foreach (KoShape *shape, fs->shapes()) {
            KWPage page = m_document->pageManager()->page(shape);
            if (m_document->annotationLayoutManager()->isAnnotationShape(shape)) {
                // Skip to save annotation shapes.
                continue;
            }

            if (shape->minimumHeight() > 1) {
                shape->setAdditionalAttribute("fo:min-height", QString::number(shape->minimumHeight()) + "pt");
            }

            // shape properties
            const qreal pagePos = page.offsetInDocument();

            shape->setAdditionalAttribute("text:anchor-page-number", QString::number(page.pageNumber()));
            context.addShapeOffset(shape, QTransform(1, 0, 0, 1, 0, -pagePos));
            m_document->anchorOfShape(shape)->saveOdf(context);
            context.removeShapeOffset(shape);
            shape->removeAdditionalAttribute("fo:min-height");
            shape->removeAdditionalAttribute("text:anchor-page-number");
            shape->removeAdditionalAttribute("text:anchor-type");
        }
    }

    if (mainTextFrame) {
        if (!mainTextFrame->shapes().isEmpty() && mainTextFrame->shapes().first()) {
            KoTextShapeData *shapeData = qobject_cast<KoTextShapeData *>(mainTextFrame->shapes().first()->userData());
            if (shapeData) {
                shapeData->saveOdf(context, m_document->documentRdf());
            }
        }
    }

    // we save the changes before starting the page sequence element because odf validator insist on having <tracked-changes> right after the <office:text> tag
    mainStyles.saveOdfStyles(KoGenStyles::DocumentAutomaticStyles, contentWriter);

    if (!changeTracker || !changeTracker->recordChanges()) {
        changes.saveOdfChanges(changeWriter, false);
    } else {
        changes.saveOdfChanges(changeWriter, true);
    }

    delete changeWriter;
    changeWriter = nullptr;

    tmpChangeFile.close();

    bodyWriter->addCompleteElement(&tmpChangeFile);

    // Save user defined variable declarations
    if (KoVariableManager *variableManager = m_document->inlineTextObjectManager()->variableManager()) {
        variableManager->saveOdf(bodyWriter);
    }

    // Do not write out text:page-sequence, if there is a maintTextFrame
    // The ODF specification does not allow text:page-sequence in office:text
    // if there is e.g. text:p or text:h there
    if (!mainTextFrame) {
        bodyWriter->startElement("text:page-sequence");
        foreach (const KWPage &page, m_document->pageManager()->pages()) {
            Q_ASSERT(m_masterPages.contains(page.pageStyle()));
            bodyWriter->startElement("text:page");
            bodyWriter->addAttribute("text:master-page-name", m_masterPages.value(page.pageStyle()));
            bodyWriter->endElement(); // text:page
        }
        bodyWriter->endElement(); // text:page-sequence
    }

    delete tmpBodyWriter;
    tmpBodyWriter = nullptr;

    tmpTextBodyFile.close();
    bodyWriter->addCompleteElement(&tmpTextBodyFile);

    bodyWriter->endElement(); // office:text
    bodyWriter->endElement(); // office:body

    odfStore.closeContentWriter();

    // add manifest line for content.xml
    manifestWriter->addManifestEntry("content.xml", "text/xml");

    // update references to xml:id to be to new xml:id
    // in the external Rdf
    if (KoDocumentRdfBase *rdf = m_document->documentRdf()) {
        QMap<QString, QString> m = sharedData->getRdfIdMapping();
        rdf->updateXmlIdReferences(m);
    }
    // save the styles.xml
    if (!mainStyles.saveOdfStylesDotXml(store, manifestWriter))
        return false;

    if (!context.saveDataCenter(store, manifestWriter)) {
        return false;
    }

    return true;
}

bool KWOdfWriter::saveOdfSettings(KoStore *store)
{
    KoStoreDevice settingsDev(store);
    KoXmlWriter *settingsWriter = KoOdfWriteStore::createOasisXmlWriter(&settingsDev, "office:document-settings");

    settingsWriter->startElement("office:settings");
    settingsWriter->startElement("config:config-item-set");
    settingsWriter->addAttribute("config:name", "view-settings");

    m_document->saveUnitOdf(settingsWriter);

    settingsWriter->endElement(); // config:config-item-set

    settingsWriter->startElement("config:config-item-set");
    settingsWriter->addAttribute("config:name", "ooo:view-settings");
    settingsWriter->startElement("config:config-item-map-indexed");
    settingsWriter->addAttribute("config:name", "Views");
    settingsWriter->startElement("config:config-item-map-entry");

    m_document->guidesData().saveOdfSettings(*settingsWriter);
    m_document->gridData().saveOdfSettings(*settingsWriter);

    settingsWriter->endElement(); // config:config-item-map-entry
    settingsWriter->endElement(); // config:config-item-map-indexed
    settingsWriter->endElement(); // config:config-item-set

    settingsWriter->startElement("config:config-item-set");
    settingsWriter->addAttribute("config:name", "ooo:configuration-settings");
    KoTextDocument doc(m_document->mainFrameSet()->document());

    settingsWriter->startElement("config:config-item");
    settingsWriter->addAttribute("config:name", "TabsRelativeToIndent");
    settingsWriter->addAttribute("config:type", "boolean");
    settingsWriter->addTextSpan(doc.relativeTabs() ? "true" : "false");
    settingsWriter->endElement();

    settingsWriter->startElement("config:config-item");
    settingsWriter->addAttribute("config:name", "AddParaTableSpacingAtStart");
    settingsWriter->addAttribute("config:type", "boolean");
    settingsWriter->addTextSpan(doc.paraTableSpacingAtStart() ? "true" : "false");
    settingsWriter->endElement();

    // OOo requires this config item to display files saved by wors correctly.
    // If true, then the fo:text-indent attribute will be ignored.
    settingsWriter->startElement("config:config-item");
    settingsWriter->addAttribute("config:name", "IgnoreFirstLineIndentInNumbering");
    settingsWriter->addAttribute("config:type", "boolean");
    settingsWriter->addTextSpan("false");
    settingsWriter->endElement();

    settingsWriter->endElement(); // config:config-item-set

    settingsWriter->endElement(); // office:settings
    settingsWriter->endElement(); // office:document-settings

    settingsWriter->endDocument();

    delete settingsWriter;

    return true;
}

void KWOdfWriter::calculateZindexOffsets()
{
    Q_ASSERT(m_zIndexOffsets.isEmpty()); // call this method only once, please.
    foreach (KWFrameSet *fs, m_document->frameSets()) {
        if (Words::isAutoGenerated(fs))
            continue;
        foreach (KoShape *shape, fs->shapes()) {
            addShapeToTree(shape);
        }
    }

    foreach (const KWPage &page, m_document->pageManager()->pages()) {
        // TODO handle pageSpread here.
        int minZIndex = 0;
        foreach (KoShape *shape, m_shapeTree.intersects(page.rect()))
            minZIndex = qMin(shape->zIndex(), minZIndex);
        m_zIndexOffsets.insert(page, -minZIndex);
    }
}

void KWOdfWriter::addShapeToTree(KoShape *shape)
{
    if (!dynamic_cast<KoShapeGroup *>(shape) && !dynamic_cast<KoShapeLayer *>(shape))
        m_shapeTree.insert(shape->boundingRect(), shape);

    // add the children of a KoShapeContainer
    KoShapeContainer *container = dynamic_cast<KoShapeContainer *>(shape);
    if (container) {
        foreach (KoShape *containerShape, container->shapes()) {
            addShapeToTree(containerShape);
        }
    }
}
