//========================================================================
//
// Outline.cc
//
// Copyright 2002-2003 Glyph & Cog, LLC
//
//========================================================================

//========================================================================
//
// Modified under the Poppler project - http://poppler.freedesktop.org
//
// All changes made under the Poppler project to this file are licensed
// under GPL version 2 or later
//
// Copyright (C) 2005 Marco Pesenti Gritti <mpg@redhat.com>
// Copyright (C) 2008, 2016-2019, 2021, 2023, 2025, 2026 Albert Astals Cid <aacid@kde.org>
// Copyright (C) 2009 Nick Jones <nick.jones@network-box.com>
// Copyright (C) 2016 Jason Crain <jason@aquaticape.us>
// Copyright (C) 2017 Adrian Johnson <ajohnson@redneon.com>
// Copyright (C) 2018 Klarälvdalens Datakonsult AB, a KDAB Group company, <info@kdab.com>. Work sponsored by the LiMux project of the city of Munich
// Copyright (C) 2018 Adam Reichold <adam.reichold@t-online.de>
// Copyright (C) 2019, 2020 Oliver Sander <oliver.sander@tu-dresden.de>
// Copyright (C) 2021 RM <rm+git@arcsin.org>
// Copyright (C) 2024-2026 g10 Code GmbH, Author: Sune Stolborg Vuorela <sune@vuorela.dk>
// Copyright (C) 2026 Stefan Brüns <stefan.bruens@rwth-aachen.de>
//
// To see a description of the changes please see the Changelog file that
// came with your tarball or type make ChangeLog if you are building from git
//
//========================================================================

#include <config.h>

#include "PDFDoc.h"
#include "XRef.h"
#include "Link.h"
#include "PDFDocEncoding.h"
#include "Outline.h"
#include "UTF.h"

//------------------------------------------------------------------------

Outline::Outline(Object *outlineObjA, XRef *xrefA, PDFDoc *docA)
{
    outlineObj = outlineObjA;
    xref = xrefA;
    doc = docA;
    items = nullptr;
    if (!outlineObj->isDict()) {
        return;
    }
    const Object &first = outlineObj->dictLookupNF("First");
    items = OutlineItem::readItemList(nullptr, &first, xref, doc);
}

Outline::~Outline()
{
    if (items) {
        for (auto *entry : *items) {
            delete entry;
        }
        delete items;
    }
}

static void insertChildHelper(const std::string &itemTitle, int destPageNum, unsigned int pos, Ref parentObjRef, PDFDoc *doc, XRef *xref, std::vector<OutlineItem *> &items)
{
    std::vector<OutlineItem *>::const_iterator it;
    if (pos >= items.size()) {
        it = items.end();
    } else {
        it = items.begin() + pos;
    }

    auto a = std::make_unique<Array>(xref);
    Ref *pageRef = doc->getCatalog()->getPageRef(destPageNum);
    if (pageRef != nullptr) {
        a->add(Object(*pageRef));
    } else {
        // if the page obj doesn't exist put the page number
        // PDF32000-2008 12.3.2.2 Para 2
        // as if it's a "Remote-Go-To Actions"
        // it's not strictly valid, but most viewers seem
        // to handle it without crashing
        // alternately, could put 0, or omit it
        a->add(Object(destPageNum - 1));
    }
    a->add(Object::name("Fit"));

    Object outlineItem = Object(std::make_unique<Dict>(xref));

    outlineItem.dictSet("Title", Object(std::string { itemTitle }));
    outlineItem.dictSet("Dest", Object(std::move(a)));
    outlineItem.dictSet("Count", Object(1));
    outlineItem.dictAdd("Parent", Object(parentObjRef));

    // add one to the main outline Object's count
    Object parentObj = xref->fetch(parentObjRef);
    int parentCount = parentObj.dictLookup("Count").getInt();
    parentObj.dictSet("Count", Object(parentCount + 1));
    xref->setModifiedObject(&parentObj, parentObjRef);

    Object prevItemObject;
    Object nextItemObject;

    Ref outlineItemRef = xref->addIndirectObject(outlineItem);

    // the next two statements fix up the parent object
    // for clarity we separate this out
    if (it == items.begin()) {
        // we will be the first item in the list
        // fix our parent
        parentObj.dictSet("First", Object(outlineItemRef));
    }
    if (it == items.end()) {
        // we will be the last item on the list
        // fix up our parent
        parentObj.dictSet("Last", Object(outlineItemRef));
    }

    if (it == items.end()) {
        if (!items.empty()) {
            // insert at the end, we handle this separately
            prevItemObject = xref->fetch((*(it - 1))->getRef());
            prevItemObject.dictSet("Next", Object(outlineItemRef));
            outlineItem.dictSet("Prev", Object((*(it - 1))->getRef()));
            xref->setModifiedObject(&prevItemObject, (*(it - 1))->getRef());
        }
    } else {
        nextItemObject = xref->fetch((*it)->getRef());
        nextItemObject.dictSet("Prev", Object(outlineItemRef));
        xref->setModifiedObject(&nextItemObject, (*it)->getRef());

        outlineItem.dictSet("Next", Object((*it)->getRef()));

        if (it != items.begin()) {
            prevItemObject = xref->fetch((*(it - 1))->getRef());
            prevItemObject.dictSet("Next", Object(outlineItemRef));
            outlineItem.dictSet("Prev", Object((*(it - 1))->getRef()));
            xref->setModifiedObject(&prevItemObject, (*(it - 1))->getRef());
        }
    }

    auto *item = new OutlineItem(outlineItem.getDict(), outlineItemRef, nullptr, xref, doc);

    items.insert(it, item);
}

void Outline::insertChild(const std::string &itemTitle, int destPageNum, unsigned int pos)
{
    Ref outlineObjRef = xref->getCatalog().dictLookupNF("Outlines").getRef();
    insertChildHelper(itemTitle, destPageNum, pos, outlineObjRef, doc, xref, *items);
}

// ref is a valid reference to a list
// walk the list and free any children
// returns the number items deleted (just in case)
static int recursiveRemoveList(Ref ref, XRef *xref)
{
    int count = 0;
    bool done = false;

    Ref nextRef;
    Object tempObj;

    while (!done) {
        tempObj = xref->fetch(ref);

        if (!tempObj.isDict()) {
            // something horrible has happened
            break;
        }

        const Object &firstRef = tempObj.dictLookupNF("First");
        if (firstRef.isRef()) {
            count += recursiveRemoveList(firstRef.getRef(), xref);
        }

        const Object &nextObjRef = tempObj.dictLookupNF("Next");
        if (nextObjRef.isRef()) {
            nextRef = nextObjRef.getRef();
        } else {
            done = true;
        }
        xref->removeIndirectObject(ref);
        count++;
        ref = nextRef;
    }
    return count;
}

static void removeChildHelper(unsigned int pos, XRef *xref, std::vector<OutlineItem *> &items)
{
    std::vector<OutlineItem *>::const_iterator it;
    if (pos >= items.size()) {
        // position is out of range, do nothing
        return;
    }
    it = items.begin() + pos;

    //  relink around this node
    Object itemObject = xref->fetch((*it)->getRef());
    Object parentObj = itemObject.dictLookup("Parent");
    Object prevItemObject = itemObject.dictLookup("Prev");
    Object nextItemObject = itemObject.dictLookup("Next");

    // delete 1 from the parent Count if it's positive
    Object countObj = parentObj.dictLookup("Count");
    int count = countObj.getInt();
    if (count > 0) {
        count--;
        parentObj.dictSet("Count", Object(count));
        xref->setModifiedObject(&parentObj, itemObject.dictLookupNF("Parent").getRef());
    }

    if (!prevItemObject.isNull() && !nextItemObject.isNull()) {
        // deletion is in the middle
        prevItemObject.dictSet("Next", Object((*(it + 1))->getRef()));
        xref->setModifiedObject(&prevItemObject, (*(it - 1))->getRef());

        nextItemObject.dictSet("Prev", Object((*(it - 1))->getRef()));
        xref->setModifiedObject(&nextItemObject, (*(it + 1))->getRef());
    } else if (prevItemObject.isNull() && nextItemObject.isNull()) {
        // deletion is only child
        parentObj.dictRemove("First");
        parentObj.dictRemove("Last");
        xref->setModifiedObject(&parentObj, itemObject.dictLookupNF("Parent").getRef());
    } else if (prevItemObject.isNull()) {
        // deletion at the front
        parentObj.dictSet("First", Object((*(it + 1))->getRef()));
        xref->setModifiedObject(&parentObj, itemObject.dictLookupNF("Parent").getRef());

        nextItemObject.dictRemove("Prev");
        xref->setModifiedObject(&nextItemObject, (*(it + 1))->getRef());
    } else {
        // deletion at the end
        parentObj.dictSet("Last", Object((*(it - 1))->getRef()));
        xref->setModifiedObject(&parentObj, itemObject.dictLookupNF("Parent").getRef());
        prevItemObject.dictRemove("Next");
        xref->setModifiedObject(&prevItemObject, (*(it - 1))->getRef());
    }

    // free any children
    const Object &firstRef = itemObject.dictLookupNF("First");
    if (firstRef.isRef()) {
        recursiveRemoveList(firstRef.getRef(), xref);
    }

    // free the pdf objects and the representation
    xref->removeIndirectObject((*it)->getRef());
    OutlineItem *oi = *it;
    items.erase(it);
    // deletion of the OutlineItem will delete all child
    // outline items in its destructor
    delete oi;
}

void Outline::removeChild(unsigned int pos)
{
    removeChildHelper(pos, xref, *items);
}

//------------------------------------------------------------------------

int Outline::addOutlineTreeNodeList(const std::vector<OutlineTreeNode> &nodeList, Ref &parentRef, Ref &firstRef, Ref &lastRef)
{
    firstRef = Ref::INVALID();
    lastRef = Ref::INVALID();
    if (nodeList.empty()) {
        return 0;
    }

    int itemCount = 0;
    Ref prevNodeRef = Ref::INVALID();

    for (const auto &node : nodeList) {

        auto a = std::make_unique<Array>(doc->getXRef());
        Ref *pageRef = doc->getCatalog()->getPageRef(node.destPageNum);
        if (pageRef != nullptr) {
            a->add(Object(*pageRef));
        } else {
            // if the page obj doesn't exist put the page number
            // PDF32000-2008 12.3.2.2 Para 2
            // as if it's a "Remote-Go-To Actions"
            // it's not strictly valid, but most viewers seem
            // to handle it without crashing
            // alternately, could put 0, or omit it
            a->add(Object(node.destPageNum - 1));
        }
        a->add(Object::name("Fit"));

        Object outlineItem = Object(std::make_unique<Dict>(doc->getXRef()));
        Ref outlineItemRef = doc->getXRef()->addIndirectObject(outlineItem);

        if (firstRef == Ref::INVALID()) {
            firstRef = outlineItemRef;
        }
        lastRef = outlineItemRef;

        outlineItem.dictSet("Title", Object(std::string { node.title }));
        outlineItem.dictSet("Dest", Object(std::move(a)));
        itemCount++;

        if (prevNodeRef != Ref::INVALID()) {
            outlineItem.dictSet("Prev", Object(prevNodeRef));

            // maybe easier way to fix up the previous object
            Object prevOutlineItem = xref->fetch(prevNodeRef);
            prevOutlineItem.dictSet("Next", Object(outlineItemRef));
            xref->setModifiedObject(&prevOutlineItem, prevNodeRef);
        }
        prevNodeRef = outlineItemRef;

        Ref firstChildRef;
        Ref lastChildRef;
        itemCount += addOutlineTreeNodeList(node.children, outlineItemRef, firstChildRef, lastChildRef);

        if (firstChildRef != Ref::INVALID()) {
            outlineItem.dictSet("First", Object(firstChildRef));
            outlineItem.dictSet("Last", Object(lastChildRef));
        }
        outlineItem.dictSet("Count", Object(itemCount));
        outlineItem.dictAdd("Parent", Object(parentRef));
    }
    return itemCount;
}

/* insert an outline into a PDF
   outline->setOutline({ {"page 1", 1,
                                         { { "1.1", 1, {} } }   },
                            {"page 2", 2, {} },
                            {"page 3", 3, {} },
                            {"page 4", 4,{ { "4.1", 4, {} },
                                           { "4.2", 4, {} },
                                         },
                            }
                       });
 */

void Outline::setOutline(const std::vector<OutlineTreeNode> &nodeList)
{
    // check if outlineObj is an object, if it's not make sure it exists
    if (!outlineObj->isDict()) {
        outlineObj = doc->getCatalog()->getCreateOutline();

        // make sure it was created
        if (!outlineObj->isDict()) {
            return;
        }
    }

    Ref outlineObjRef = xref->getCatalog().dictLookupNF("Outlines").getRef();
    Ref firstChildRef;
    Ref lastChildRef;

    // free any OutlineItem objects that will be replaced
    const Object &firstChildRefObj = outlineObj->dictLookupNF("First");
    if (firstChildRefObj.isRef()) {
        recursiveRemoveList(firstChildRefObj.getRef(), xref);
    }

    const int count = addOutlineTreeNodeList(nodeList, outlineObjRef, firstChildRef, lastChildRef);

    // modify the parent Outlines dict
    if (firstChildRef != Ref::INVALID()) {
        outlineObj->dictSet("First", Object(firstChildRef));
        outlineObj->dictSet("Last", Object(lastChildRef));
    } else {
        // nothing was inserted into the outline, so just remove the
        // child references in the top-level outline
        outlineObj->dictRemove("First");
        outlineObj->dictRemove("Last");
    }
    outlineObj->dictSet("Count", Object(count));
    xref->setModifiedObject(outlineObj, outlineObjRef);

    // reload the outline object from the xrefs

    if (items) {
        for (auto *entry : *items) {
            delete entry;
        }
        delete items;
    }
    const Object &first = outlineObj->dictLookupNF("First");
    // we probably want to allow readItemList to create an empty list
    // but for now just check and do it ourselves here
    if (first.isRef()) {
        items = OutlineItem::readItemList(nullptr, &first, xref, doc);
    } else {
        items = new std::vector<OutlineItem *>();
    }
}

//------------------------------------------------------------------------

OutlineItem::OutlineItem(const Dict *dict, Ref refA, OutlineItem *parentA, XRef *xrefA, PDFDoc *docA)
{
    Object obj1;

    ref = refA;
    parent = parentA;
    xref = xrefA;
    doc = docA;
    kids = nullptr;

    obj1 = dict->lookup("Title");
    if (obj1.isString()) {
        const std::string &s = obj1.getString();
        title = TextStringToUCS4(s);
        // All downstream users treats empty titles
        // as this item (and children) doesn't exist
        // but there exists documents in the wild
        // where outline is empty.
        // In order to don't break downstreams, do
        // replace it with a zero width space.
        if (title.empty()) {
            static const std::vector<Unicode> zeroWidthSpace { 0x200B };
            title = UTF16toUCS4(zeroWidthSpace);
        }
    }

    obj1 = dict->lookup("Dest");
    if (!obj1.isNull()) {
        action = LinkAction::parseDest(&obj1);
    } else {
        obj1 = dict->lookup("A");
        if (!obj1.isNull()) {
            action = LinkAction::parseAction(&obj1);
        }
    }

    startsOpen = false;
    obj1 = dict->lookup("Count");
    if (obj1.isInt()) {
        if (obj1.getInt() > 0) {
            startsOpen = true;
        }
    }
}

OutlineItem::~OutlineItem()
{
    if (kids) {
        for (auto *entry : *kids) {
            delete entry;
        }
        delete kids;
        kids = nullptr;
    }
}

std::vector<OutlineItem *> *OutlineItem::readItemList(OutlineItem *parent, const Object *firstItemRef, XRef *xrefA, PDFDoc *docA)
{
    auto *items = new std::vector<OutlineItem *>();

    RefRecursionChecker alreadyRead;

    OutlineItem *parentO = parent;
    while (parentO) {
        alreadyRead.insert(parentO->getRef());
        parentO = parentO->parent;
    }

    Object tempObj = firstItemRef->copy();
    while (tempObj.isRef() && (tempObj.getRefNum() >= 0) && (tempObj.getRefNum() < xrefA->getNumObjects()) && alreadyRead.insert(tempObj.getRef())) {
        Object obj = tempObj.fetch(xrefA);
        if (!obj.isDict()) {
            break;
        }
        auto *item = new OutlineItem(obj.getDict(), tempObj.getRef(), parent, xrefA, docA);
        items->push_back(item);
        tempObj = obj.dictLookupNF("Next").copy();
    }
    return items;
}

void OutlineItem::open()
{
    if (!kids) {
        Object itemDict = xref->fetch(ref);
        if (itemDict.isDict()) {
            const Object &firstRef = itemDict.dictLookupNF("First");
            kids = readItemList(this, &firstRef, xref, doc);
        } else {
            kids = new std::vector<OutlineItem *>();
        }
    }
}

void OutlineItem::setTitle(const std::string &titleA)
{
    Object dict = xref->fetch(ref);
    title = TextStringToUCS4(titleA);
    dict.dictSet("Title", Object(std::string { titleA }));
    xref->setModifiedObject(&dict, ref);
}

bool OutlineItem::setPageDest(int i)
{
    Object dict = xref->fetch(ref);
    Object obj1;

    if (i < 1) {
        return false;
    }

    obj1 = dict.dictLookup("Dest");
    if (obj1.isArray()) {
        Array *array = obj1.getArray();
        const int arrayLength = array->getLength();
        for (int index = 0; index < arrayLength; index++) {
            array->remove(0);
        }
        array->add(Object(i - 1));
        array->add(Object::name("Fit"));

        // unique_ptr will destroy previous on assignment
        action = LinkAction::parseDest(&obj1);
    } else {
        obj1 = dict.dictLookup("A");
        if (!obj1.isNull()) {
            // RM 20210505 Implement
        } else {
        }
        return false;
    }

    xref->setModifiedObject(&dict, ref);
    return true;
}

void OutlineItem::insertChild(const std::string &itemTitle, int destPageNum, unsigned int pos)
{
    open();
    insertChildHelper(itemTitle, destPageNum, pos, ref, doc, xref, *kids);
}

void OutlineItem::removeChild(unsigned int pos)
{
    open();
    removeChildHelper(pos, xref, *kids);
}

void OutlineItem::setStartsOpen(bool value)
{
    startsOpen = value;
    Object dict = xref->fetch(ref);
    Object obj1 = dict.dictLookup("Count");
    if (obj1.isInt()) {
        const int count = obj1.getInt();
        if ((count > 0 && !value) || (count < 0 && value)) {
            // states requires change of sign
            dict.dictSet("Count", Object(-count));
            xref->setModifiedObject(&dict, ref);
        }
    }
}

bool OutlineItem::hasKids()
{
    open();
    return !kids->empty();
}

const std::vector<OutlineItem *> *OutlineItem::getKids()
{
    open();

    if (!kids || kids->empty()) {
        return nullptr;
    }
    return kids;
}
