//========================================================================
//
// Link.cc
//
// Copyright 1996-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) 2006, 2008 Pino Toscano <pino@kde.org>
// Copyright (C) 2007, 2010, 2011 Carlos Garcia Campos <carlosgc@gnome.org>
// Copyright (C) 2008 Hugo Mercier <hmercier31@gmail.com>
// Copyright (C) 2008-2010, 2012-2014, 2016-2024 Albert Astals Cid <aacid@kde.org>
// Copyright (C) 2009 Kovid Goyal <kovid@kovidgoyal.net>
// Copyright (C) 2009 Ilya Gorenbein <igorenbein@finjan.com>
// Copyright (C) 2012 Tobias Koening <tobias.koenig@kdab.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 Intevation GmbH <intevation@intevation.de>
// Copyright (C) 2018, 2020 Adam Reichold <adam.reichold@t-online.de>
// Copyright (C) 2019, 2020 Oliver Sander <oliver.sander@tu-dresden.de>
// Copyright (C) 2020 Marek Kasik <mkasik@redhat.com>
// Copyright (C) 2024 Pratham Gandhi <ppg.1382@gmail.com>
// Copyright (C) 2025 g10 Code GmbH, Author: Sune Stolborg Vuorela <sune@vuorela.dk>
//
// 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 <cstddef>
#include <cstring>
#include <memory>
#include "goo/gmem.h"
#include "goo/GooString.h"
#include "Error.h"
#include "Object.h"
#include "Array.h"
#include "Dict.h"
#include "Link.h"
#include "Sound.h"
#include "FileSpec.h"
#include "Rendition.h"
#include "Annot.h"

//------------------------------------------------------------------------
// LinkAction
//------------------------------------------------------------------------
LinkAction::LinkAction() = default;

LinkAction::~LinkAction() = default;

std::unique_ptr<LinkAction> LinkAction::parseDest(const Object *obj)
{
    auto action = std::unique_ptr<LinkAction>(new LinkGoTo(obj));
    if (!action->isOk()) {
        action.reset();
    }
    return action;
}

std::unique_ptr<LinkAction> LinkAction::parseAction(const Object *obj, const std::optional<std::string> &baseURI)
{
    std::set<int> seenNextActions;
    return parseAction(obj, baseURI, &seenNextActions);
}

std::unique_ptr<LinkAction> LinkAction::parseAction(const Object *obj, const std::optional<std::string> &baseURI, std::set<int> *seenNextActions)
{

    if (!obj->isDict()) {
        error(errSyntaxWarning, -1, "parseAction: Bad annotation action for URI '{0:s}'", baseURI ? baseURI->c_str() : "NULL");
        return nullptr;
    }

    std::unique_ptr<LinkAction> action;
    Object obj2 = obj->dictLookup("S");

    // GoTo action
    if (obj2.isName("GoTo")) {
        Object obj3 = obj->dictLookup("D");
        action = std::make_unique<LinkGoTo>(&obj3);

        // GoToR action
    } else if (obj2.isName("GoToR")) {
        Object obj3 = obj->dictLookup("F");
        Object obj4 = obj->dictLookup("D");
        action = std::make_unique<LinkGoToR>(&obj3, &obj4);

        // Launch action
    } else if (obj2.isName("Launch")) {
        action = std::make_unique<LinkLaunch>(obj);

        // URI action
    } else if (obj2.isName("URI")) {
        Object obj3 = obj->dictLookup("URI");
        action = std::make_unique<LinkURI>(&obj3, baseURI);

        // Named action
    } else if (obj2.isName("Named")) {
        Object obj3 = obj->dictLookup("N");
        action = std::make_unique<LinkNamed>(&obj3);

        // Movie action
    } else if (obj2.isName("Movie")) {
        action = std::make_unique<LinkMovie>(obj);

        // Rendition action
    } else if (obj2.isName("Rendition")) {
        action = std::make_unique<LinkRendition>(obj);

        // Sound action
    } else if (obj2.isName("Sound")) {
        action = std::make_unique<LinkSound>(obj);

        // JavaScript action
    } else if (obj2.isName("JavaScript")) {
        Object obj3 = obj->dictLookup("JS");
        action = std::make_unique<LinkJavaScript>(&obj3);

        // Set-OCG-State action
    } else if (obj2.isName("SetOCGState")) {
        action = std::make_unique<LinkOCGState>(obj);

        // Hide action
    } else if (obj2.isName("Hide")) {
        action = std::make_unique<LinkHide>(obj);

        // ResetForm action
    } else if (obj2.isName("ResetForm")) {
        action = std::make_unique<LinkResetForm>(obj);

        // SubmitForm action
    } else if (obj2.isName("SubmitForm")) {
        action = std::make_unique<LinkSubmitForm>(obj);

        // unknown action
    } else if (obj2.isName()) {
        action = std::make_unique<LinkUnknown>(obj2.getName());

        // action is missing or wrong type
    } else {
        error(errSyntaxWarning, -1, "parseAction: Unknown annotation action object: URI = '{0:s}'", baseURI ? baseURI->c_str() : "NULL");
        action = nullptr;
    }

    if (action && !action->isOk()) {
        action.reset();
        return nullptr;
    }

    if (!action) {
        return nullptr;
    }

    // parse the next actions
    const Object nextObj = obj->dictLookup("Next");
    std::vector<std::unique_ptr<LinkAction>> actionList;
    if (nextObj.isDict()) {

        // Prevent circles in the tree by checking the ref against used refs in
        // our current tree branch.
        const Object &nextRefObj = obj->dictLookupNF("Next");
        if (nextRefObj.isRef()) {
            const Ref ref = nextRefObj.getRef();
            if (!seenNextActions->insert(ref.num).second) {
                error(errSyntaxWarning, -1, "parseAction: Circular next actions detected.");
                return action;
            }
        }

        actionList.reserve(1);
        actionList.push_back(parseAction(&nextObj, {}, seenNextActions));
    } else if (nextObj.isArray()) {
        const Array *a = nextObj.getArray();
        const int n = a->getLength();
        actionList.reserve(n);
        for (int i = 0; i < n; ++i) {
            const Object obj3 = a->get(i);
            if (!obj3.isDict()) {
                error(errSyntaxWarning, -1, "parseAction: Next array does not contain only dicts");
                continue;
            }

            // Similar circle check as above.
            const Object &obj3Ref = a->getNF(i);
            if (obj3Ref.isRef()) {
                const Ref ref = obj3Ref.getRef();
                if (!seenNextActions->insert(ref.num).second) {
                    error(errSyntaxWarning, -1, "parseAction: Circular next actions detected in array.");
                    return action;
                }
            }

            actionList.push_back(parseAction(&obj3, {}, seenNextActions));
        }
    }

    action->nextActionList = std::move(actionList);

    return action;
}

const std::vector<std::unique_ptr<LinkAction>> &LinkAction::nextActions() const
{
    return nextActionList;
}

//------------------------------------------------------------------------
// LinkDest
//------------------------------------------------------------------------

LinkDest::LinkDest(const Array *a)
{
    // initialize fields
    left = bottom = right = top = zoom = 0;
    changeLeft = changeTop = changeZoom = false;
    ok = false;

    // get page
    if (a->getLength() < 2) {
        error(errSyntaxWarning, -1, "Annotation destination array is too short");
        return;
    }
    const Object &obj0 = a->getNF(0);
    if (obj0.isInt()) {
        pageNum = obj0.getInt() + 1;
        pageIsRef = false;
    } else if (obj0.isRef()) {
        pageRef = obj0.getRef();
        pageIsRef = true;
    } else {
        error(errSyntaxWarning, -1, "Bad annotation destination");
        return;
    }

    // get destination type
    Object obj1 = a->get(1);

    // XYZ link
    if (obj1.isName("XYZ")) {
        kind = destXYZ;
        if (a->getLength() < 3) {
            changeLeft = false;
        } else {
            Object obj2 = a->get(2);
            if (obj2.isNull()) {
                changeLeft = false;
            } else if (obj2.isNum()) {
                changeLeft = true;
                left = obj2.getNum();
            } else {
                error(errSyntaxWarning, -1, "Bad annotation destination position");
                return;
            }
        }
        if (a->getLength() < 4) {
            changeTop = false;
        } else {
            Object obj2 = a->get(3);
            if (obj2.isNull()) {
                changeTop = false;
            } else if (obj2.isNum()) {
                changeTop = true;
                top = obj2.getNum();
            } else {
                error(errSyntaxWarning, -1, "Bad annotation destination position");
                return;
            }
        }
        if (a->getLength() < 5) {
            changeZoom = false;
        } else {
            Object obj2 = a->get(4);
            if (obj2.isNull()) {
                changeZoom = false;
            } else if (obj2.isNum()) {
                zoom = obj2.getNum();
                changeZoom = (zoom == 0) ? false : true;
            } else {
                error(errSyntaxWarning, -1, "Bad annotation destination position");
                return;
            }
        }

        // Fit link
    } else if (obj1.isName("Fit")) {
        kind = destFit;

        // FitH link
    } else if (obj1.isName("FitH")) {
        kind = destFitH;
        if (a->getLength() < 3) {
            changeTop = false;
        } else {
            Object obj2 = a->get(2);
            if (obj2.isNull()) {
                changeTop = false;
            } else if (obj2.isNum()) {
                changeTop = true;
                top = obj2.getNum();
            } else {
                error(errSyntaxWarning, -1, "Bad annotation destination position");
                kind = destFit;
            }
        }

        // FitV link
    } else if (obj1.isName("FitV")) {
        if (a->getLength() < 3) {
            error(errSyntaxWarning, -1, "Annotation destination array is too short");
            return;
        }
        kind = destFitV;
        Object obj2 = a->get(2);
        if (obj2.isNull()) {
            changeLeft = false;
        } else if (obj2.isNum()) {
            changeLeft = true;
            left = obj2.getNum();
        } else {
            error(errSyntaxWarning, -1, "Bad annotation destination position");
            kind = destFit;
        }

        // FitR link
    } else if (obj1.isName("FitR")) {
        if (a->getLength() < 6) {
            error(errSyntaxWarning, -1, "Annotation destination array is too short");
            return;
        }
        kind = destFitR;
        Object obj2 = a->get(2);
        if (obj2.isNum()) {
            left = obj2.getNum();
        } else {
            error(errSyntaxWarning, -1, "Bad annotation destination position");
            kind = destFit;
        }
        obj2 = a->get(3);
        if (obj2.isNum()) {
            bottom = obj2.getNum();
        } else {
            error(errSyntaxWarning, -1, "Bad annotation destination position");
            kind = destFit;
        }
        obj2 = a->get(4);
        if (obj2.isNum()) {
            right = obj2.getNum();
        } else {
            error(errSyntaxWarning, -1, "Bad annotation destination position");
            kind = destFit;
        }
        obj2 = a->get(5);
        if (obj2.isNum()) {
            top = obj2.getNum();
        } else {
            error(errSyntaxWarning, -1, "Bad annotation destination position");
            kind = destFit;
        }

        // FitB link
    } else if (obj1.isName("FitB")) {
        kind = destFitB;

        // FitBH link
    } else if (obj1.isName("FitBH")) {
        if (a->getLength() < 3) {
            error(errSyntaxWarning, -1, "Annotation destination array is too short");
            return;
        }
        kind = destFitBH;
        Object obj2 = a->get(2);
        if (obj2.isNull()) {
            changeTop = false;
        } else if (obj2.isNum()) {
            changeTop = true;
            top = obj2.getNum();
        } else {
            error(errSyntaxWarning, -1, "Bad annotation destination position");
            kind = destFit;
        }

        // FitBV link
    } else if (obj1.isName("FitBV")) {
        if (a->getLength() < 3) {
            error(errSyntaxWarning, -1, "Annotation destination array is too short");
            return;
        }
        kind = destFitBV;
        Object obj2 = a->get(2);
        if (obj2.isNull()) {
            changeLeft = false;
        } else if (obj2.isNum()) {
            changeLeft = true;
            left = obj2.getNum();
        } else {
            error(errSyntaxWarning, -1, "Bad annotation destination position");
            kind = destFit;
        }

        // unknown link kind
    } else {
        error(errSyntaxWarning, -1, "Unknown annotation destination type");
        return;
    }

    ok = true;
}

//------------------------------------------------------------------------
// LinkGoTo
//------------------------------------------------------------------------

LinkGoTo::LinkGoTo(const Object *destObj)
{
    // named destination
    if (destObj->isName()) {
        namedDest = std::make_unique<GooString>(destObj->getNameString());
    } else if (destObj->isString()) {
        namedDest = destObj->getString()->copy();

        // destination dictionary
    } else if (destObj->isArray()) {
        dest = std::make_unique<LinkDest>(destObj->getArray());
        if (!dest->isOk()) {
            dest.reset();
        }

        // error
    } else {
        error(errSyntaxWarning, -1, "Illegal annotation destination");
    }
}

LinkGoTo::~LinkGoTo() = default;

//------------------------------------------------------------------------
// LinkGoToR
//------------------------------------------------------------------------

LinkGoToR::LinkGoToR(Object *fileSpecObj, Object *destObj)
{
    // get file name
    Object obj1 = getFileSpecNameForPlatform(fileSpecObj);
    if (obj1.isString()) {
        fileName = obj1.getString()->copy();
    }

    // named destination
    if (destObj->isName()) {
        namedDest = std::make_unique<GooString>(destObj->getName());
    } else if (destObj->isString()) {
        namedDest = destObj->getString()->copy();

        // destination dictionary
    } else if (destObj->isArray()) {
        dest = std::make_unique<LinkDest>(destObj->getArray());
        if (!dest->isOk()) {
            dest.reset();
        }

        // error
    } else {
        error(errSyntaxWarning, -1, "Illegal annotation destination");
    }
}

LinkGoToR::~LinkGoToR() = default;

//------------------------------------------------------------------------
// LinkLaunch
//------------------------------------------------------------------------

LinkLaunch::LinkLaunch(const Object *actionObj)
{

    if (actionObj->isDict()) {
        Object obj1 = actionObj->dictLookup("F");
        if (!obj1.isNull()) {
            Object obj3 = getFileSpecNameForPlatform(&obj1);
            if (obj3.isString()) {
                fileName = obj3.getString()->copy();
            }
        } else {
#ifdef _WIN32
            obj1 = actionObj->dictLookup("Win");
#else
            //~ This hasn't been defined by Adobe yet, so assume it looks
            //~ just like the Win dictionary until they say otherwise.
            obj1 = actionObj->dictLookup("Unix");
#endif
            if (obj1.isDict()) {
                Object obj2 = obj1.dictLookup("F");
                Object obj3 = getFileSpecNameForPlatform(&obj2);
                if (obj3.isString()) {
                    fileName = obj3.getString()->copy();
                }
                obj2 = obj1.dictLookup("P");
                if (obj2.isString()) {
                    params = obj2.getString()->copy();
                }
            } else {
                error(errSyntaxWarning, -1, "Bad launch-type link action");
            }
        }
    }
}

LinkLaunch::~LinkLaunch() = default;

//------------------------------------------------------------------------
// LinkURI
//------------------------------------------------------------------------

LinkURI::LinkURI(const Object *uriObj, const std::optional<std::string> &baseURI)
{
    hasURIFlag = false;
    if (uriObj->isString()) {
        hasURIFlag = true;
        const std::string &uri2 = uriObj->getString()->toStr();
        size_t n = strcspn(uri2.c_str(), "/:");
        if (n < uri2.size() && uri2[n] == ':') {
            // "http:..." etc.
            uri = uri2;
        } else if (!uri2.compare(0, 4, "www.")) {
            // "www.[...]" without the leading "http://"
            uri = "http://" + uri2;
        } else {
            // relative URI
            if (baseURI) {
                uri = *baseURI;
                if (!uri.empty()) {
                    char c = uri.back();
                    if (c != '/' && c != '?') {
                        uri += '/';
                    }
                }
                if (uri2[0] == '/') {
                    uri.append(uri2.c_str() + 1, uri2.size() - 1);
                } else {
                    uri += uri2;
                }
            } else {
                uri = uri2;
            }
        }
    } else {
        error(errSyntaxWarning, -1, "Illegal URI-type link");
    }
}

LinkURI::~LinkURI() = default;

//------------------------------------------------------------------------
// LinkNamed
//------------------------------------------------------------------------

LinkNamed::LinkNamed(const Object *nameObj)
{
    hasNameFlag = false;
    if (nameObj->isName()) {
        name = nameObj->getNameString();
        hasNameFlag = true;
    }
}

LinkNamed::~LinkNamed() = default;

//------------------------------------------------------------------------
// LinkMovie
//------------------------------------------------------------------------

LinkMovie::LinkMovie(const Object *obj)
{
    annotRef = Ref::INVALID();
    hasAnnotTitleFlag = false;

    const Object &annotationObj = obj->dictLookupNF("Annotation");
    if (annotationObj.isRef()) {
        annotRef = annotationObj.getRef();
    }

    Object tmp = obj->dictLookup("T");
    if (tmp.isString()) {
        annotTitle = tmp.getString()->toStr();
        hasAnnotTitleFlag = true;
    }

    if ((!hasAnnotTitleFlag) && (annotRef == Ref::INVALID())) {
        error(errSyntaxError, -1, "Movie action is missing both the Annot and T keys");
    }

    tmp = obj->dictLookup("Operation");
    if (tmp.isName()) {
        const char *name = tmp.getName();

        if (!strcmp(name, "Play")) {
            operation = operationTypePlay;
        } else if (!strcmp(name, "Stop")) {
            operation = operationTypeStop;
        } else if (!strcmp(name, "Pause")) {
            operation = operationTypePause;
        } else if (!strcmp(name, "Resume")) {
            operation = operationTypeResume;
        }
    }
}

LinkMovie::~LinkMovie() = default;

//------------------------------------------------------------------------
// LinkSound
//------------------------------------------------------------------------

LinkSound::LinkSound(const Object *soundObj)
{
    volume = 1.0;
    sync = false;
    repeat = false;
    mix = false;
    sound = nullptr;
    if (soundObj->isDict()) {
        // volume
        Object tmp = soundObj->dictLookup("Volume");
        if (tmp.isNum()) {
            volume = tmp.getNum();
        }
        // sync
        tmp = soundObj->dictLookup("Synchronous");
        if (tmp.isBool()) {
            sync = tmp.getBool();
        }
        // repeat
        tmp = soundObj->dictLookup("Repeat");
        if (tmp.isBool()) {
            repeat = tmp.getBool();
        }
        // mix
        tmp = soundObj->dictLookup("Mix");
        if (tmp.isBool()) {
            mix = tmp.getBool();
        }
        // 'Sound' object
        tmp = soundObj->dictLookup("Sound");
        sound = Sound::parseSound(&tmp);
    }
}

LinkSound::~LinkSound() = default;

//------------------------------------------------------------------------
// LinkRendition
//------------------------------------------------------------------------

LinkRendition::LinkRendition(const Object *obj)
{
    operation = NoRendition;
    media = nullptr;
    int operationCode = -1;

    screenRef = Ref::INVALID();

    if (obj->isDict()) {
        Object tmp = obj->dictLookup("JS");
        if (!tmp.isNull()) {
            if (tmp.isString()) {
                js = tmp.getString()->toStr();
            } else if (tmp.isStream()) {
                Stream *stream = tmp.getStream();
                stream->fillString(js);
            } else {
                error(errSyntaxWarning, -1, "Invalid Rendition Action: JS not string or stream");
            }
        }

        tmp = obj->dictLookup("OP");
        if (tmp.isInt()) {
            operationCode = tmp.getInt();
            if (js.empty() && (operationCode < 0 || operationCode > 4)) {
                error(errSyntaxWarning, -1, "Invalid Rendition Action: unrecognized operation valued: {0:d}", operationCode);
            } else {
                // retrieve rendition object
                Object renditionObj = obj->dictLookup("R");
                if (renditionObj.isDict()) {
                    media = new MediaRendition(&renditionObj);
                } else if (operationCode == 0 || operationCode == 4) {
                    error(errSyntaxWarning, -1, "Invalid Rendition Action: no R field with op = {0:d}", operationCode);
                    renditionObj.setToNull();
                }

                const Object &anObj = obj->dictLookupNF("AN");
                if (anObj.isRef()) {
                    screenRef = anObj.getRef();
                } else if (operation >= 0 && operation <= 4) {
                    error(errSyntaxWarning, -1, "Invalid Rendition Action: no AN field with op = {0:d}", operationCode);
                }
            }

            switch (operationCode) {
            case 0:
                operation = PlayRendition;
                break;
            case 1:
                operation = StopRendition;
                break;
            case 2:
                operation = PauseRendition;
                break;
            case 3:
                operation = ResumeRendition;
                break;
            case 4:
                operation = PlayRendition;
                break;
            }
        } else if (js.empty()) {
            error(errSyntaxWarning, -1, "Invalid Rendition action: no OP or JS field defined");
        }
    }
}

LinkRendition::~LinkRendition()
{
    delete media;
}

//------------------------------------------------------------------------
// LinkJavaScript
//------------------------------------------------------------------------

LinkJavaScript::LinkJavaScript(Object *jsObj)
{
    isValid = false;

    if (jsObj->isString()) {
        js = jsObj->getString()->toStr();
        isValid = true;
    } else if (jsObj->isStream()) {
        Stream *stream = jsObj->getStream();
        stream->fillString(js);
        isValid = true;
    }
}

LinkJavaScript::~LinkJavaScript() = default;

Object LinkJavaScript::createObject(XRef *xref, const std::string &js)
{
    Dict *linkDict = new Dict(xref);
    linkDict->add("S", Object(objName, "JavaScript"));
    linkDict->add("JS", Object(std::make_unique<GooString>(js)));

    return Object(linkDict);
}

//------------------------------------------------------------------------
// LinkOCGState
//------------------------------------------------------------------------
LinkOCGState::LinkOCGState(const Object *obj) : isValid(true)
{
    Object obj1 = obj->dictLookup("State");
    if (obj1.isArray()) {
        StateList stList;

        for (int i = 0; i < obj1.arrayGetLength(); ++i) {
            const Object &obj2 = obj1.arrayGetNF(i);
            if (obj2.isName()) {
                if (!stList.list.empty()) {
                    stateList.push_back(stList);
                }

                const char *name = obj2.getName();
                stList.list.clear();
                if (!strcmp(name, "ON")) {
                    stList.st = On;
                } else if (!strcmp(name, "OFF")) {
                    stList.st = Off;
                } else if (!strcmp(name, "Toggle")) {
                    stList.st = Toggle;
                } else {
                    error(errSyntaxWarning, -1, "Invalid name '{0:s}' in OCG Action state array", name);
                    isValid = false;
                }
            } else if (obj2.isRef()) {
                stList.list.push_back(obj2.getRef());
            } else {
                error(errSyntaxWarning, -1, "Invalid item in OCG Action State array");
                isValid = false;
            }
        }
        // Add the last group
        if (!stList.list.empty()) {
            stateList.push_back(stList);
        }
    } else {
        error(errSyntaxWarning, -1, "Invalid OCGState action");
        isValid = false;
    }

    preserveRB = obj->dictLookup("PreserveRB").getBoolWithDefaultValue(true);
}

LinkOCGState::~LinkOCGState() = default;

//------------------------------------------------------------------------
// LinkHide
//------------------------------------------------------------------------

LinkHide::LinkHide(const Object *hideObj)
{
    hasTargetNameFlag = false;
    show = false; // Default

    if (hideObj->isDict()) {
        const Object targetObj = hideObj->dictLookup("T");
        if (targetObj.isString()) {
            targetName = targetObj.getString()->toStr();
            hasTargetNameFlag = true;
        }
        const Object shouldHide = hideObj->dictLookup("H");
        if (shouldHide.isBool()) {
            show = !shouldHide.getBool();
        }
    }
}

LinkHide::~LinkHide() = default;

//------------------------------------------------------------------------
// LinkResetForm
//------------------------------------------------------------------------

LinkResetForm::LinkResetForm(const Object *obj)
{
    Object obj1;

    exclude = false;

    obj1 = obj->dictLookup("Fields");
    if (obj1.isArray()) {
        fields.resize(obj1.arrayGetLength());
        for (int i = 0; i < obj1.arrayGetLength(); ++i) {
            const Object &obj2 = obj1.arrayGetNF(i);
            if (obj2.isName()) {
                fields[i] = obj2.getNameString();
            } else if (obj2.isString()) {
                fields[i] = obj2.getString()->toStr();
            } else if (obj2.isRef()) {
                fields[i] = std::to_string(obj2.getRef().num);
                fields[i].append(" ");
                fields[i].append(std::to_string(obj2.getRef().gen));
                fields[i].append(" R");
            } else {
                error(errSyntaxWarning, -1, "LinkResetForm: unexpected Field type");
            }
        }
    }

    obj1 = obj->dictLookup("Flags");
    if (obj1.isInt()) {
        int flags = obj1.getInt();

        if (flags & 0x1) {
            exclude = true;
        }
    }
}

LinkResetForm::~LinkResetForm() = default;

//------------------------------------------------------------------------
// LinkSubmitForm
//------------------------------------------------------------------------

LinkSubmitForm::LinkSubmitForm(const Object *obj)
{
    if (!obj->isDict()) {
        return;
    }

    const Object objFields = obj->dictLookup("Fields");
    if (objFields.isArray()) {
        fields.resize(objFields.arrayGetLength());
        for (int i = 0; i < objFields.arrayGetLength(); ++i) {
            const Object &objNF = objFields.arrayGetNF(i);
            if (objNF.isName()) {
                fields[i] = objNF.getNameString();
            } else if (objNF.isString()) {
                fields[i] = objNF.getString()->toStr();
            } else if (objNF.isRef()) {
                fields[i] = std::to_string(objNF.getRef().num);
                fields[i].append(" ");
                fields[i].append(std::to_string(objNF.getRef().gen));
                fields[i].append(" R");
            } else {
                error(errSyntaxWarning, -1, "LinkSubmitForm: unexpected Field type");
            }
        }
    }

    Object objFileSpecification = obj->dictLookup("F");
    if (objFileSpecification.isDict()) {
        objFileSpecification = objFileSpecification.dictLookup("F");
        if (objFileSpecification.isString()) {
            url = objFileSpecification.getString()->toStr();
        }
    } else if (objFileSpecification.isString()) {
        url = objFileSpecification.getString()->toStr();
    }

    const Object objFlags = obj->dictLookup("Flags");
    if (objFlags.isInt()) {
        flags = objFlags.getInt();
    }
    // It is the responsibility of the frontend to remove fields that have NoExport flag set.
}

LinkSubmitForm::~LinkSubmitForm() = default;

//------------------------------------------------------------------------
// LinkUnknown
//------------------------------------------------------------------------

LinkUnknown::LinkUnknown(const char *actionA)
{
    action = std::string(actionA ? actionA : "");
}

LinkUnknown::~LinkUnknown() = default;

//------------------------------------------------------------------------
// Links
//------------------------------------------------------------------------

Links::Links(Annots *annots)
{
    if (!annots) {
        return;
    }

    for (const std::shared_ptr<Annot> &annot : annots->getAnnots()) {

        if (annot->getType() != Annot::typeLink) {
            continue;
        }
        links.push_back(std::static_pointer_cast<AnnotLink>(annot));
    }
}

Links::~Links() = default;
