/* poppler-optcontent.cc: qt interface to poppler
 *
 * Copyright (C) 2007, Brad Hards <bradh@kde.org>
 * Copyright (C) 2008, 2014, Pino Toscano <pino@kde.org>
 * Copyright (C) 2008, Carlos Garcia Campos <carlosgc@gnome.org>
 * Copyright (C) 2015-2019, 2022, 2025, Albert Astals Cid <aacid@kde.org>
 * Copyright (C) 2017, Hubert Figuière <hub@figuiere.net>
 * 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) 2025, 2026 g10 Code GmbH, Author: Sune Stolborg Vuorela <sune@vuorela.dk>
 * Copyright (C) 2026 Adam Sampson <ats@offog.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-optcontent.h"

#include "poppler-optcontent-private.h"

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

#include <QtCore/QDebug>
#include <QtCore/QtAlgorithms>

#include "poppler/OptionalContent.h"
#include "poppler/Link.h"

namespace Poppler {

RadioButtonGroup::RadioButtonGroup(OptContentModelPrivate *ocModel, Array *rbarray)
{
    itemsInGroup.reserve(rbarray->getLength());
    for (int i = 0; i < rbarray->getLength(); ++i) {
        const Object &ref = rbarray->getNF(i);
        if (!ref.isRef()) {
            qDebug() << "expected ref, but got:" << ref.getType();
        }
        OptContentItem *item = ocModel->itemFromRef(QString::number(ref.getRefNum()));
        itemsInGroup.append(item);
    }
    for (OptContentItem *item : std::as_const(itemsInGroup)) {
        item->appendRBGroup(this);
    }
}

RadioButtonGroup::~RadioButtonGroup() = default;

QSet<OptContentItem *> RadioButtonGroup::setItemOn(OptContentItem *itemToSetOn)
{
    QSet<OptContentItem *> changedItems;
    for (OptContentItem *thisItem : std::as_const(itemsInGroup)) {
        if (thisItem != itemToSetOn) {
            QSet<OptContentItem *> newChangedItems;
            thisItem->setState(OptContentItem::Off, false /*obeyRadioGroups*/, newChangedItems);
            changedItems += newChangedItems;
        }
    }
    return changedItems;
}

OptContentItem::OptContentItem(OptionalContentGroup *group)
{
    m_group = group;
    m_parent = nullptr;
    m_name = UnicodeParsedString(group->getName());
    if (group->getState() == OptionalContentGroup::On) {
        m_state = OptContentItem::On;
    } else {
        m_state = OptContentItem::Off;
    }
    m_stateBackup = m_state;
    m_enabled = true;
}

OptContentItem::OptContentItem(const QString &label)
{
    m_parent = nullptr;
    m_name = label;
    m_group = nullptr;
    m_state = OptContentItem::HeadingOnly;
    m_stateBackup = m_state;
    m_enabled = true;
}

OptContentItem::OptContentItem() : m_parent(nullptr), m_enabled(true) { }

OptContentItem::~OptContentItem() = default;

void OptContentItem::appendRBGroup(RadioButtonGroup *rbgroup)
{
    m_rbGroups.append(rbgroup);
}

void OptContentItem::setState(ItemState state, bool obeyRadioGroups, QSet<OptContentItem *> &changedItems)
{
    if (state == m_state) {
        return;
    }

    m_state = state;
    m_stateBackup = m_state;
    changedItems.insert(this);
    QSet<OptContentItem *> empty;
    Q_FOREACH (OptContentItem *child, m_children) {
        ItemState oldState = child->m_stateBackup;
        child->setState(state == OptContentItem::On ? child->m_stateBackup : OptContentItem::Off, true /*obeyRadioGroups*/, empty);
        child->m_enabled = state == OptContentItem::On;
        child->m_stateBackup = oldState;
    }
    if (!m_group) {
        return;
    }
    if (state == OptContentItem::On) {
        m_group->setState(OptionalContentGroup::On);
        if (obeyRadioGroups) {
            for (RadioButtonGroup *rbgroup : std::as_const(m_rbGroups)) {
                changedItems += rbgroup->setItemOn(this);
            }
        }
    } else if (state == OptContentItem::Off) {
        m_group->setState(OptionalContentGroup::Off);
    }
}

void OptContentItem::addChild(OptContentItem *child)
{
    m_children += child;
    child->setParent(this);
}

QSet<OptContentItem *> OptContentItem::recurseListChildren(bool includeMe) const
{
    QSet<OptContentItem *> ret;
    if (includeMe) {
        ret.insert(const_cast<OptContentItem *>(this));
    }
    Q_FOREACH (OptContentItem *child, m_children) {
        ret += child->recurseListChildren(true);
    }
    return ret;
}

OptContentModelPrivate::OptContentModelPrivate(OptContentModel *qq, const OCGs *optContent) : q(qq)
{
    m_rootNode = new OptContentItem();
    const auto &ocgs = optContent->getOCGs();

    for (const auto &ocg : ocgs) {
        auto *node = new OptContentItem(ocg.second.get());
        m_optContentItems.insert(QString::number(ocg.first.num), node);
    }

    if (optContent->getOrderArray() == nullptr) {
        // no Order array, so drop them all at the top level
        QMapIterator<QString, OptContentItem *> i(m_optContentItems);
        while (i.hasNext()) {
            i.next();
            addChild(m_rootNode, i.value());
        }
    } else {
        parseOrderArray(m_rootNode, optContent->getOrderArray());
    }

    parseRBGroupsArray(optContent->getRBGroupsArray());
}

OptContentModelPrivate::~OptContentModelPrivate()
{
    qDeleteAll(m_optContentItems);
    qDeleteAll(m_rbgroups);
    qDeleteAll(m_headerOptContentItems);
    delete m_rootNode;
}

void OptContentModelPrivate::parseOrderArray(OptContentItem *parentNode, const Array *orderArray)
{
    OptContentItem *lastItem = parentNode;
    for (int i = 0; i < orderArray->getLength(); ++i) {
        Object orderItem = orderArray->get(i);
        if (orderItem.isDict()) {
            const Object &item = orderArray->getNF(i);
            if (item.isRef()) {
                OptContentItem *ocItem = m_optContentItems.value(QString::number(item.getRefNum()));
                if (ocItem) {
                    addChild(parentNode, ocItem);
                    lastItem = ocItem;
                } else {
                    qDebug() << "could not find group for object" << item.getRefNum();
                }
            }
        } else if (orderItem.isArrayOfLengthAtLeast(1)) {
            parseOrderArray(lastItem, orderItem.getArray());
        } else if (orderItem.isString()) {
            const std::string &label = orderItem.getString();
            auto *header = new OptContentItem(UnicodeParsedString(label));
            m_headerOptContentItems.append(header);
            addChild(parentNode, header);
            parentNode = header;
            lastItem = header;
        } else {
            qDebug() << "something unexpected";
        }
    }
}

void OptContentModelPrivate::parseRBGroupsArray(const Array *rBGroupArray)
{
    if (!rBGroupArray) {
        return;
    }
    // This is an array of array(s)
    for (int i = 0; i < rBGroupArray->getLength(); ++i) {
        Object rbObj = rBGroupArray->get(i);
        if (!rbObj.isArray()) {
            qDebug() << "expected inner array, got:" << rbObj.getType();
            return;
        }
        Array *rbarray = rbObj.getArray();
        auto *rbg = new RadioButtonGroup(this, rbarray);
        m_rbgroups.append(rbg);
    }
}

OptContentModel::OptContentModel(const OCGs *optContent, QObject *parent) : QAbstractItemModel(parent)
{
    d = new OptContentModelPrivate(this, optContent);
}

OptContentModel::~OptContentModel()
{
    delete d;
}

void OptContentModelPrivate::setRootNode(OptContentItem *node)
{
    q->beginResetModel();
    delete m_rootNode;
    m_rootNode = node;
    q->endResetModel();
}

QModelIndex OptContentModel::index(int row, int column, const QModelIndex &parent) const
{
    if (row < 0 || column != 0) {
        return QModelIndex();
    }

    OptContentItem *parentNode = d->nodeFromIndex(parent);
    if (row < parentNode->childList().count()) {
        return createIndex(row, column, parentNode->childList().at(row));
    }
    return QModelIndex();
}

QModelIndex OptContentModel::parent(const QModelIndex &child) const
{
    OptContentItem *childNode = d->nodeFromIndex(child);
    if (!childNode) {
        return QModelIndex();
    }
    return d->indexFromItem(childNode->parent(), child.column());
}

QModelIndex OptContentModelPrivate::indexFromItem(OptContentItem *node, int column) const
{
    if (!node) {
        return QModelIndex();
    }
    OptContentItem *parentNode = node->parent();
    if (!parentNode) {
        return QModelIndex();
    }
    const int row = parentNode->childList().indexOf(node);
    return q->createIndex(row, column, node);
}

int OptContentModel::rowCount(const QModelIndex &parent) const
{
    OptContentItem *parentNode = d->nodeFromIndex(parent);
    if (!parentNode) {
        return 0;
    }
    return parentNode->childList().count();
}

int OptContentModel::columnCount(const QModelIndex & /*parent*/) const
{
    return 1;
}

QVariant OptContentModel::data(const QModelIndex &index, int role) const
{
    OptContentItem *node = d->nodeFromIndex(index, true);
    if (!node) {
        return QVariant();
    }

    switch (role) {
    case Qt::DisplayRole:
        return node->name();
        break;
    case Qt::EditRole:
        if (node->state() == OptContentItem::On) {
            return true;
        } else if (node->state() == OptContentItem::Off) {
            return false;
        }
        break;
    case Qt::CheckStateRole:
        if (node->state() == OptContentItem::On) {
            return Qt::Checked;
        } else if (node->state() == OptContentItem::Off) {
            return Qt::Unchecked;
        }
        break;
    }

    return QVariant();
}

bool OptContentModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    OptContentItem *node = d->nodeFromIndex(index, true);
    if (!node) {
        return false;
    }

    switch (role) {
    case Qt::CheckStateRole: {
        const bool newvalue = value.toBool();
        QSet<OptContentItem *> changedItems;
        node->setState(newvalue ? OptContentItem::On : OptContentItem::Off, true /*obeyRadioGroups*/, changedItems);

        if (!changedItems.isEmpty()) {
            changedItems += node->recurseListChildren(false);
            QModelIndexList indexes;
            Q_FOREACH (OptContentItem *item, changedItems) {
                indexes.append(d->indexFromItem(item, 0));
            }
            std::stable_sort(indexes.begin(), indexes.end()); // NOLINT(modernize-use-ranges) QModelIndexList doesn't support ranges requirements
            Q_FOREACH (const QModelIndex &changedIndex, indexes) {
                Q_EMIT dataChanged(changedIndex, changedIndex);
            }
            return true;
        }
        break;
    }
    }

    return false;
}

Qt::ItemFlags OptContentModel::flags(const QModelIndex &index) const
{
    OptContentItem *node = d->nodeFromIndex(index);
    Qt::ItemFlags itemFlags = Qt::ItemIsSelectable | Qt::ItemIsUserCheckable;
    if (node->isEnabled()) {
        itemFlags |= Qt::ItemIsEnabled;
    }
    return itemFlags;
}

QVariant OptContentModel::headerData(int section, Qt::Orientation orientation, int role) const
{
    return QAbstractItemModel::headerData(section, orientation, role);
}

void OptContentModel::applyLink(LinkOCGState *link)
{
    LinkOCGStatePrivate *linkPrivate = link->d_func();

    QSet<OptContentItem *> changedItems;

    const std::vector<::LinkOCGState::StateList> &statesList = linkPrivate->stateList;
    for (const ::LinkOCGState::StateList &stateList : statesList) {
        const std::vector<Ref> &refsList = stateList.list;
        for (const Ref &ref : refsList) {
            OptContentItem *item = d->itemFromRef(QString::number(ref.num));

            if (stateList.st == ::LinkOCGState::On) {
                item->setState(OptContentItem::On, linkPrivate->preserveRB, changedItems);
            } else if (stateList.st == ::LinkOCGState::Off) {
                item->setState(OptContentItem::Off, linkPrivate->preserveRB, changedItems);
            } else {
                OptContentItem::ItemState newState = item->state() == OptContentItem::On ? OptContentItem::Off : OptContentItem::On;
                item->setState(newState, linkPrivate->preserveRB, changedItems);
            }
        }
    }

    if (!changedItems.isEmpty()) {
        QSet<OptContentItem *> aux;
        Q_FOREACH (OptContentItem *item, aux) {
            changedItems += item->recurseListChildren(false);
        }

        QModelIndexList indexes;
        Q_FOREACH (OptContentItem *item, changedItems) {
            indexes.append(d->indexFromItem(item, 0));
        }
        std::stable_sort(indexes.begin(), indexes.end()); // NOLINT(modernize-use-ranges) Qt5 QModelIndexList doesn't support ranges requirements
        Q_FOREACH (const QModelIndex &changedIndex, indexes) {
            Q_EMIT dataChanged(changedIndex, changedIndex);
        }
    }
}

void OptContentModelPrivate::addChild(OptContentItem *parent, OptContentItem *child)
{
    parent->addChild(child);
}

OptContentItem *OptContentModelPrivate::itemFromRef(const QString &ref) const
{
    return m_optContentItems.value(ref);
}

OptContentItem *OptContentModelPrivate::nodeFromIndex(const QModelIndex &index, bool canBeNull) const
{
    if (index.isValid()) {
        return static_cast<OptContentItem *>(index.internalPointer());
    }
    return canBeNull ? nullptr : m_rootNode;
}
}
