/* poppler-action.cc: glib wrapper for poppler	      -*- c-basic-offset: 8 -*-
 * Copyright (C) 2005, Red Hat, Inc.
 * Copyright (C) 2026 g10 Code GmbH, Author: Sune Stolborg Vuorela <sune@vuorela.dk>
 * Copyright (C) 2026 Albert Astals Cid <aacid@kde.org>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "poppler.h"
#include "poppler-private.h"

/**
 * SECTION:poppler-action
 * @short_description: Action links
 * @title: PopplerAction
 */

G_DEFINE_BOXED_TYPE(PopplerDest, poppler_dest, poppler_dest_copy, poppler_dest_free)

/**
 * poppler_dest_copy:
 * @dest: a #PopplerDest
 *
 * Copies @dest, creating an identical #PopplerDest.
 *
 * Return value: a new destination identical to @dest
 **/
PopplerDest *poppler_dest_copy(PopplerDest *dest)
{
    PopplerDest *new_dest;

    new_dest = g_slice_dup(PopplerDest, dest);

    if (dest->named_dest) {
        new_dest->named_dest = g_strdup(dest->named_dest);
    }

    return new_dest;
}

/**
 * poppler_dest_free:
 * @dest: a #PopplerDest
 *
 * Frees @dest
 **/
void poppler_dest_free(PopplerDest *dest)
{
    if (!dest) {
        return;
    }

    if (dest->named_dest) {
        g_free(dest->named_dest);
    }

    g_slice_free(PopplerDest, dest);
}

static void poppler_action_layer_free(PopplerActionLayer *action_layer)
{
    if (!action_layer) {
        return;
    }

    if (action_layer->layers) {
        g_list_free_full(action_layer->layers, g_object_unref);
        action_layer->layers = nullptr;
    }

    g_slice_free(PopplerActionLayer, action_layer);
}

static PopplerActionLayer *poppler_action_layer_copy(PopplerActionLayer *action_layer)
{
    PopplerActionLayer *retval = g_slice_dup(PopplerActionLayer, action_layer);

    retval->layers = g_list_copy(action_layer->layers);
    for (GList *l = retval->layers; l != nullptr; l = l->next) {
        g_object_ref(l->data);
    }

    return retval;
}

G_DEFINE_BOXED_TYPE(PopplerAction, poppler_action, poppler_action_copy, poppler_action_free)

/**
 * poppler_action_free:
 * @action: a #PopplerAction
 *
 * Frees @action
 **/
void poppler_action_free(PopplerAction *action)
{
    if (action == nullptr) {
        return;
    }

    /* Action specific stuff */
    switch (action->type) {
    case POPPLER_ACTION_GOTO_DEST:
        poppler_dest_free(action->goto_dest.dest);
        break;
    case POPPLER_ACTION_GOTO_REMOTE:
        poppler_dest_free(action->goto_remote.dest);
        g_free(action->goto_remote.file_name);
        break;
    case POPPLER_ACTION_URI:
        g_free(action->uri.uri);
        break;
    case POPPLER_ACTION_LAUNCH:
        g_free(action->launch.file_name);
        g_free(action->launch.params);
        break;
    case POPPLER_ACTION_NAMED:
        g_free(action->named.named_dest);
        break;
    case POPPLER_ACTION_MOVIE:
        if (action->movie.movie) {
            g_object_unref(action->movie.movie);
        }
        break;
    case POPPLER_ACTION_RENDITION:
        if (action->rendition.media) {
            g_object_unref(action->rendition.media);
        }
        break;
    case POPPLER_ACTION_OCG_STATE:
        if (action->ocg_state.state_list) {
            g_list_free_full(action->ocg_state.state_list, reinterpret_cast<GDestroyNotify>(poppler_action_layer_free));
        }
        break;
    case POPPLER_ACTION_JAVASCRIPT:
        if (action->javascript.script) {
            g_free(action->javascript.script);
        }
        break;
    case POPPLER_ACTION_RESET_FORM:
        if (action->reset_form.fields) {
            g_list_free_full(action->reset_form.fields, g_free);
        }
        break;
    default:
        break;
    }

    g_free(action->any.title);
    g_slice_free(PopplerAction, action);
}

/**
 * poppler_action_copy:
 * @action: a #PopplerAction
 *
 * Copies @action, creating an identical #PopplerAction.
 *
 * Return value: a new action identical to @action
 **/
PopplerAction *poppler_action_copy(PopplerAction *action)
{
    PopplerAction *new_action;

    g_return_val_if_fail(action != nullptr, NULL);

    /* Do a straight copy of the memory */
    new_action = g_slice_dup(PopplerAction, action);

    if (action->any.title != nullptr) {
        new_action->any.title = g_strdup(action->any.title);
    }

    switch (action->type) {
    case POPPLER_ACTION_GOTO_DEST:
        new_action->goto_dest.dest = poppler_dest_copy(action->goto_dest.dest);
        break;
    case POPPLER_ACTION_GOTO_REMOTE:
        new_action->goto_remote.dest = poppler_dest_copy(action->goto_remote.dest);
        if (action->goto_remote.file_name) {
            new_action->goto_remote.file_name = g_strdup(action->goto_remote.file_name);
        }
        break;
    case POPPLER_ACTION_URI:
        if (action->uri.uri) {
            new_action->uri.uri = g_strdup(action->uri.uri);
        }
        break;
    case POPPLER_ACTION_LAUNCH:
        if (action->launch.file_name) {
            new_action->launch.file_name = g_strdup(action->launch.file_name);
        }
        if (action->launch.params) {
            new_action->launch.params = g_strdup(action->launch.params);
        }
        break;
    case POPPLER_ACTION_NAMED:
        if (action->named.named_dest) {
            new_action->named.named_dest = g_strdup(action->named.named_dest);
        }
        break;
    case POPPLER_ACTION_MOVIE:
        if (action->movie.movie) {
            new_action->movie.movie = static_cast<PopplerMovie *> g_object_ref(action->movie.movie);
        }
        break;
    case POPPLER_ACTION_RENDITION:
        if (action->rendition.media) {
            new_action->rendition.media = static_cast<PopplerMedia *> g_object_ref(action->rendition.media);
        }
        break;
    case POPPLER_ACTION_OCG_STATE:
        if (action->ocg_state.state_list) {
            GList *l;
            GList *new_list = nullptr;

            for (l = action->ocg_state.state_list; l; l = g_list_next(l)) {
                auto *alayer = static_cast<PopplerActionLayer *>(l->data);
                new_list = g_list_prepend(new_list, poppler_action_layer_copy(alayer));
            }

            new_action->ocg_state.state_list = g_list_reverse(new_list);
        }

        break;
    case POPPLER_ACTION_JAVASCRIPT:
        if (action->javascript.script) {
            new_action->javascript.script = g_strdup(action->javascript.script);
        }
        break;
    case POPPLER_ACTION_RESET_FORM:
        if (action->reset_form.fields) {
            GList *iter;

            new_action->reset_form.fields = nullptr;
            for (iter = action->reset_form.fields; iter != nullptr; iter = iter->next) {
                new_action->reset_form.fields = g_list_append(new_action->reset_form.fields, g_strdup(static_cast<char *>(iter->data)));
            }
        }
        break;
    default:
        break;
    }

    return new_action;
}

static PopplerDest *dest_new_goto(PopplerDocument *document, const LinkDest *link_dest)
{
    PopplerDest *dest;

    dest = g_slice_new0(PopplerDest);

    if (link_dest == nullptr) {
        dest->type = POPPLER_DEST_UNKNOWN;
        return dest;
    }

    switch (link_dest->getKind()) {
    case destXYZ:
        dest->type = POPPLER_DEST_XYZ;
        break;
    case destFit:
        dest->type = POPPLER_DEST_FIT;
        break;
    case destFitH:
        dest->type = POPPLER_DEST_FITH;
        break;
    case destFitV:
        dest->type = POPPLER_DEST_FITV;
        break;
    case destFitR:
        dest->type = POPPLER_DEST_FITR;
        break;
    case destFitB:
        dest->type = POPPLER_DEST_FITB;
        break;
    case destFitBH:
        dest->type = POPPLER_DEST_FITBH;
        break;
    case destFitBV:
        dest->type = POPPLER_DEST_FITBV;
        break;
    default:
        dest->type = POPPLER_DEST_UNKNOWN;
    }

    if (link_dest->isPageRef()) {
        if (document) {
            const Ref page_ref = link_dest->getPageRef();
            dest->page_num = document->doc->findPage(page_ref);
        } else {
            /* FIXME: We don't keep areound the page_ref for the
             * remote doc, so we can't look this up.  Guess that
             * it's 0*/
            dest->page_num = 0;
        }
    } else {
        dest->page_num = link_dest->getPageNum();
    }

    dest->left = link_dest->getLeft();
    dest->bottom = link_dest->getBottom();
    dest->right = link_dest->getRight();
    dest->top = link_dest->getTop();
    dest->zoom = link_dest->getZoom();
    dest->change_left = link_dest->getChangeLeft();
    dest->change_top = link_dest->getChangeTop();
    dest->change_zoom = link_dest->getChangeZoom();

    if (document && dest->page_num > 0) {
        PopplerPage *page;

        page = poppler_document_get_page(document, dest->page_num - 1);

        if (page) {
            dest->left -= page->page->getCropBox().x1;
            dest->bottom -= page->page->getCropBox().x1;
            dest->right -= page->page->getCropBox().y1;
            dest->top -= page->page->getCropBox().y1;

            g_object_unref(page);
        } else {
            g_warning("Invalid page %d in Link Destination\n", dest->page_num);
            dest->page_num = 0;
        }
    }

    return dest;
}

static PopplerDest *dest_new_named(const GooString *named_dest)
{
    PopplerDest *dest;

    dest = g_slice_new0(PopplerDest);

    if (named_dest == nullptr) {
        dest->type = POPPLER_DEST_UNKNOWN;
        return dest;
    }

    const std::string &str = named_dest->toStr();

    dest->type = POPPLER_DEST_NAMED;
    dest->named_dest = poppler_named_dest_from_bytestring(reinterpret_cast<const guint8 *>(str.data()), str.size());

    return dest;
}

static void build_goto_dest(PopplerDocument *document, PopplerAction *action, const LinkGoTo *link)
{
    const LinkDest *link_dest;
    const GooString *named_dest;

    /* Return if it isn't OK */
    if (!link->isOk()) {
        action->goto_dest.dest = dest_new_goto(nullptr, nullptr);
        return;
    }

    link_dest = link->getDest();
    named_dest = link->getNamedDest();

    if (link_dest != nullptr) {
        action->goto_dest.dest = dest_new_goto(document, link_dest);
    } else if (named_dest != nullptr) {
        action->goto_dest.dest = dest_new_named(named_dest);
    } else {
        action->goto_dest.dest = dest_new_goto(document, nullptr);
    }
}

static void build_goto_remote(PopplerAction *action, const LinkGoToR *link)
{
    const LinkDest *link_dest;
    const GooString *named_dest;

    /* Return if it isn't OK */
    if (!link->isOk()) {
        action->goto_remote.dest = dest_new_goto(nullptr, nullptr);
        return;
    }

    action->goto_remote.file_name = _poppler_goo_string_to_utf8(link->getFileName()->toStr());

    link_dest = link->getDest();
    named_dest = link->getNamedDest();

    if (link_dest != nullptr) {
        action->goto_remote.dest = dest_new_goto(nullptr, link_dest);
    } else if (named_dest != nullptr) {
        action->goto_remote.dest = dest_new_named(named_dest);
    } else {
        action->goto_remote.dest = dest_new_goto(nullptr, nullptr);
    }
}

static void build_launch(PopplerAction *action, const LinkLaunch *link)
{
    if (link->getFileName()) {
        action->launch.file_name = _poppler_goo_string_to_utf8(link->getFileName()->toStr());
    }
    if (link->getParams()) {
        action->launch.params = g_strdup(link->getParams()->c_str());
    }
}

static void build_uri(PopplerAction *action, const LinkURI *link)
{
    const gchar *uri = link->getURI().c_str();
    if (uri != nullptr) {
        action->uri.uri = g_strdup(uri);
    }
}

static void build_named(PopplerAction *action, const LinkNamed *link)
{
    const gchar *name = link->getName().c_str();
    if (name != nullptr) {
        action->named.named_dest = g_strdup(name);
    }
}

static AnnotMovie *find_annot_movie_for_action(PopplerDocument *document, const LinkMovie *link)
{
    AnnotMovie *annot = nullptr;
    XRef *xref = document->doc->getXRef();
    Object annotObj;

    if (link->hasAnnotRef()) {
        const Ref *ref = link->getAnnotRef();

        annotObj = xref->fetch(*ref);
    } else if (link->hasAnnotTitle()) {
        const std::string &title = link->getAnnotTitle();
        int i;

        for (i = 1; i <= document->doc->getNumPages(); ++i) {
            Page *p = document->doc->getPage(i);
            if (!p) {
                continue;
            }

            Object annots = p->getAnnotsObject();
            if (annots.isArray()) {
                int j;
                bool found = false;

                for (j = 0; j < annots.arrayGetLength() && !found; ++j) {
                    annotObj = annots.arrayGet(j);
                    if (annotObj.isDict()) {
                        Object obj1 = annotObj.dictLookup("Subtype");
                        if (!obj1.isName("Movie")) {
                            continue;
                        }

                        obj1 = annotObj.dictLookup("T");
                        if (obj1.isString() && obj1.getString() == title) {
                            found = true;
                        }
                    }
                    if (!found) {
                        annotObj.setToNull();
                    }
                }
                if (found) {
                    break;
                }
                annotObj.setToNull();
            }
        }
    }

    if (annotObj.isDict()) {
        Object tmp;

        annot = new AnnotMovie(document->doc.get(), std::move(annotObj), tmp);
        if (!annot->isOk()) {
            delete annot;
            annot = nullptr;
        }
    }

    return annot;
}

static void build_movie(PopplerDocument *document, PopplerAction *action, const LinkMovie *link)
{
    AnnotMovie *annot;

    switch (link->getOperation()) {
    case LinkMovie::operationTypePause:
        action->movie.operation = POPPLER_ACTION_MOVIE_PAUSE;
        break;
    case LinkMovie::operationTypeResume:
        action->movie.operation = POPPLER_ACTION_MOVIE_RESUME;
        break;
    case LinkMovie::operationTypeStop:
        action->movie.operation = POPPLER_ACTION_MOVIE_STOP;
        break;
    default:
    case LinkMovie::operationTypePlay:
        action->movie.operation = POPPLER_ACTION_MOVIE_PLAY;
        break;
    }

    annot = find_annot_movie_for_action(document, link);
    if (annot) {
        action->movie.movie = _poppler_movie_new(annot->getMovie());
        delete annot;
    }
}

static void build_javascript(PopplerAction *action, const LinkJavaScript *link)
{
    if (link->isOk()) {
        const std::string &script = link->getScript();
        action->javascript.script = _poppler_goo_string_to_utf8(script);
    }
}

static void build_reset_form(PopplerAction *action, const LinkResetForm *link)
{
    const std::vector<std::string> &fields = link->getFields();

    if (action->reset_form.fields != nullptr) {
        g_list_free_full(action->reset_form.fields, g_free);
    }

    action->reset_form.fields = nullptr;
    for (const auto &field : fields) {
        action->reset_form.fields = g_list_append(action->reset_form.fields, g_strdup(field.c_str()));
    }

    action->reset_form.exclude = link->getExclude();
}

static void build_rendition(PopplerAction *action, const LinkRendition *link)
{
    action->rendition.op = link->getOperation();
    if (link->getMedia()) {
        action->rendition.media = _poppler_media_new(link->getMedia());
    }
    // TODO: annotation reference
}

static PopplerLayer *get_layer_for_ref(PopplerDocument *document, GList *layers, const Ref ref, gboolean preserve_rb)
{
    GList *l;

    for (l = layers; l; l = g_list_next(l)) {
        auto *layer = static_cast<Layer *>(l->data);

        if (layer->oc) {
            const Ref ocgRef = layer->oc->getRef();

            if (ref == ocgRef) {
                GList *rb_group = nullptr;

                if (preserve_rb) {
                    rb_group = _poppler_document_get_layer_rbgroup(document, layer);
                }
                return _poppler_layer_new(document, layer, rb_group);
            }
        }

        if (layer->kids) {
            PopplerLayer *retval = get_layer_for_ref(document, layer->kids, ref, preserve_rb);
            if (retval) {
                return retval;
            }
        }
    }

    return nullptr;
}

static void build_ocg_state(PopplerDocument *document, PopplerAction *action, const LinkOCGState *ocg_state)
{
    const std::vector<LinkOCGState::StateList> &st_list = ocg_state->getStateList();
    bool preserve_rb = ocg_state->getPreserveRB();
    GList *layer_state = nullptr;

    if (!document->layers) {
        if (!_poppler_document_get_layers(document)) {
            return;
        }
    }

    for (const LinkOCGState::StateList &list : st_list) {
        auto *action_layer = g_slice_new0(PopplerActionLayer);

        switch (list.st) {
        case LinkOCGState::On:
            action_layer->action = POPPLER_ACTION_LAYER_ON;
            break;
        case LinkOCGState::Off:
            action_layer->action = POPPLER_ACTION_LAYER_OFF;
            break;
        case LinkOCGState::Toggle:
            action_layer->action = POPPLER_ACTION_LAYER_TOGGLE;
            break;
        }

        for (const Ref &ref : list.list) {
            PopplerLayer *layer = get_layer_for_ref(document, document->layers, ref, preserve_rb);

            action_layer->layers = g_list_prepend(action_layer->layers, layer);
        }

        layer_state = g_list_prepend(layer_state, action_layer);
    }

    action->ocg_state.state_list = g_list_reverse(layer_state);
}

PopplerAction *_poppler_action_new(PopplerDocument *document, const LinkAction *link, const gchar *title)
{
    PopplerAction *action;

    action = g_slice_new0(PopplerAction);

    if (title) {
        action->any.title = g_strdup(title);
    }

    if (link == nullptr) {
        action->type = POPPLER_ACTION_NONE;
        return action;
    }

    switch (link->getKind()) {
    case actionGoTo:
        action->type = POPPLER_ACTION_GOTO_DEST;
        build_goto_dest(document, action, static_cast<const LinkGoTo *>(link));
        break;
    case actionGoToR:
        action->type = POPPLER_ACTION_GOTO_REMOTE;
        build_goto_remote(action, static_cast<const LinkGoToR *>(link));
        break;
    case actionLaunch:
        action->type = POPPLER_ACTION_LAUNCH;
        build_launch(action, static_cast<const LinkLaunch *>(link));
        break;
    case actionURI:
        action->type = POPPLER_ACTION_URI;
        build_uri(action, static_cast<const LinkURI *>(link));
        break;
    case actionNamed:
        action->type = POPPLER_ACTION_NAMED;
        build_named(action, static_cast<const LinkNamed *>(link));
        break;
    case actionMovie:
        action->type = POPPLER_ACTION_MOVIE;
        build_movie(document, action, static_cast<const LinkMovie *>(link));
        break;
    case actionRendition:
        action->type = POPPLER_ACTION_RENDITION;
        build_rendition(action, static_cast<const LinkRendition *>(link));
        break;
    case actionOCGState:
        action->type = POPPLER_ACTION_OCG_STATE;
        build_ocg_state(document, action, static_cast<const LinkOCGState *>(link));
        break;
    case actionJavaScript:
        action->type = POPPLER_ACTION_JAVASCRIPT;
        build_javascript(action, static_cast<const LinkJavaScript *>(link));
        break;
    case actionResetForm:
        action->type = POPPLER_ACTION_RESET_FORM;
        build_reset_form(action, dynamic_cast<const LinkResetForm *>(link));
        break;
    case actionUnknown:
    default:
        action->type = POPPLER_ACTION_UNKNOWN;
        break;
    }

    return action;
}

PopplerDest *_poppler_dest_new_goto(PopplerDocument *document, LinkDest *link_dest)
{
    return dest_new_goto(document, link_dest);
}
