// SPDX-FileCopyrightText: 2022 James R. Barlow
// SPDX-License-Identifier: MPL-2.0

#include "qpdf_pagelist.h"
#include "pikepdf.h"
#include "qpdf_lock.h"

#include <qpdf/QPDFPageDocumentHelper.hh>
#include <qpdf/QPDFPageLabelDocumentHelper.hh>
#include <qpdf/QPDFPageObjectHelper.hh>

static bool page_has_widget(QPDFObjectHandle page)
{
    if (!page.isDictionary())
        return false;
    auto annots = page.getKey("/Annots");
    if (!annots.isArray())
        return false;
    for (auto &a : annots.aitems()) {
        if (a.isDictionaryOfType("", "/Widget"))
            return true;
    }
    return false;
}

static bool action_is_named_goto(QPDFObjectHandle a)
{
    if (!a.isDictionary())
        return false;
    if (!a.getKey("/S").isNameAndEquals("/GoTo"))
        return false;
    auto d = a.getKey("/D");
    return d.isString() || d.isName();
}

static bool page_has_named_dest(QPDFObjectHandle page)
{
    if (!page.isDictionary())
        return false;
    auto annots = page.getKey("/Annots");
    if (!annots.isArray())
        return false;
    for (auto &a : annots.aitems()) {
        if (!a.isDictionary())
            continue;
        auto dest = a.getKey("/Dest");
        if (dest.isString() || dest.isName())
            return true;
        if (action_is_named_goto(a.getKey("/A")))
            return true;
    }
    return false;
}

static void warn_page_copy(bool forms, bool dests)
{
    if (!forms && !dests)
        return;
    std::string what;
    if (forms && dests)
        what = "interactive form fields or named destinations";
    else if (forms)
        what = "interactive form fields";
    else
        what = "named destinations";
    std::string msg = "Copying pages from another Pdf with pages.extend() does not "
                      "preserve " +
                      what +
                      "; form fields may not display in Adobe "
                      "Acrobat and internal links may not resolve. Use "
                      "Pdf.add_pages_from() to preserve them.";
    auto category = py::module_::import_("pikepdf._exceptions").attr("PageCopyWarning");
    python_warning(msg.c_str(), category.ptr(), /*stacklevel=*/1);
}

static QPDFPageObjectHelper as_page_helper(py::handle obj)
{
    try {
        return py::cast<QPDFPageObjectHelper>(obj);
    } catch (const py::cast_error &) {
        throw py::type_error((
            std::string(
                "only pikepdf pages can be assigned to a page list; tried to assign ") +
            py::cast<std::string>(py::str(py::inst_name(obj))))
                .c_str());
    }
}

size_t uindex_from_index(PageList &pl, py::ssize_t index)
{
    if (index < 0)
        index += pl.count();
    if (index < 0) // Still
        throw py::index_error("Accessing nonexistent PDF page number");
    size_t uindex = index;
    return uindex;
}

QPDFPageObjectHelper PageList::get_page(size_t index)
{
    auto pages = this->doc.getAllPages();
    if (index < pages.size())
        return pages.at(index);
    throw py::index_error("Accessing nonexistent PDF page number");
}

std::vector<QPDFPageObjectHelper> PageList::get_page_objs_impl(py::slice slice)
{
    auto [start, stop, step, slicelength] = slice.compute(this->count());
    std::vector<QPDFPageObjectHelper> result;
    result.reserve(slicelength);
    for (size_t i = 0; i < slicelength; ++i) {
        auto oh = this->get_page(start);
        result.push_back(oh);
        start += step;
    }
    return result;
}

py::list PageList::get_pages(py::slice slice)
{
    auto page_objs = this->get_page_objs_impl(slice);
    py::list result;
    for (auto &page_obj : page_objs) {
        result.append(py::cast(page_obj));
    }
    return result;
}

void PageList::set_page(size_t index, py::object obj)
{
    set_page(index, as_page_helper(obj));
}

void PageList::set_page(size_t index, QPDFPageObjectHelper page)
{
    this->insert_page(index, page);
    if (index != this->count()) {
        this->delete_page(index + 1);
    }
}

void PageList::set_pages_from_iterable(py::slice slice, py::iterable other)
{
    auto [start, stop, step, slicelength] = slice.compute(this->count());
    std::vector<QPDFPageObjectHelper> results;
    py::iterator it = py::iter(other);

    // Unpack list into iterable, check that each object is a page but
    // don't save the handles yet
    for (; it != py::iterator::sentinel(); ++it) {
        results.push_back(as_page_helper(*it));
    }

    if (step != 1) {
        // For an extended slice we must be replace an equal number of pages
        if (results.size() != slicelength) {
            throw py::value_error(
                (std::string("attempt to assign sequence of length ") +
                    std::to_string(results.size()) +
                    std::string(" to extended slice of size ") +
                    std::to_string(slicelength))
                    .c_str());
        }
        for (size_t i = 0; i < slicelength; ++i) {
            this->set_page(start + (i * step), results.at(i));
        }
    } else {
        // For simple slices, we can replace differing sizes
        // meaning results.size() could be slicelength, or not
        // so insert all pages first (to ensure nothing is freed yet)
        // and then delete all pages we no longer need

        // Insert first to ensure we don't delete any pages we will need
        for (size_t i = 0; i < results.size(); ++i) {
            this->insert_page(start + i, results.at(i));
        }

        size_t del_start = start + results.size();
        for (size_t i = 0; i < slicelength; ++i) {
            this->delete_page(del_start);
        }
    }
}

void PageList::delete_page(size_t index)
{
    auto page = this->get_page(index);
    this->doc.removePage(page);
}

void PageList::delete_pages_from_iterable(py::slice slice)
{
    // See above: need a way to dec_ref pages with another owner
    // Get handles for all pages, then remove them, since page numbers shift
    // after delete
    auto kill_list = this->get_page_objs_impl(slice);
    for (auto page : kill_list) {
        this->doc.removePage(page);
    }
}

size_t PageList::count()
{
    return this->doc.getAllPages().size();
}

void PageList::insert_page(size_t index, QPDFPageObjectHelper page)
{
    if (index != this->count()) {
        auto refpage = this->get_page(index);
        this->doc.addPageAt(page, true, refpage);
    } else {
        this->doc.addPage(page, false);
    }
}

void PageList::append_page(QPDFPageObjectHelper page)
{
    this->doc.addPage(page, false);
}

QPDFPageObjectHelper from_objgen(QPDF &q, QPDFObjGen og)
{
    auto h = q.getObjectByObjGen(og);
    if (!h.isPageObject())
        throw py::value_error("Object is not a page");
    return QPDFPageObjectHelper(h);
}

QPDFPageObjectHelper PageListIterator::next()
{
    if (this->index >= this->pages.size()) {
        throw py::stop_iteration();
    }
    auto page = this->pages.at(this->index);
    this->index++;
    return page;
}

void init_pagelist(py::module_ &m)
{
    py::class_<PageListIterator>(
        m, "_PageListIterator", py::type_slots(pikepdf_gc_slots))
        .def("__iter__", [](PageListIterator &it) { return it; })
        .def("__next__", &PageListIterator::next);

    py::class_<PageList>(m, "PageList", py::type_slots(pikepdf_gc_slots))
        .def(
            "__getitem__",
            [](PageList &pl, py::ssize_t index) {
                QpdfLockGuard lock(pl.qpdf.get());
                auto uindex = uindex_from_index(pl, index);
                return pl.get_page(uindex);
            },
            py::rv_policy::reference_internal)
        .def("__getitem__",
            [](PageList &pl, py::slice slice) {
                QpdfLockGuard lock(pl.qpdf.get());
                return pl.get_pages(slice);
            })
        .def("__setitem__",
            [](PageList &pl, py::ssize_t index, py::object page) {
                QpdfLockGuard lock(pl.qpdf.get());
                auto uindex = uindex_from_index(pl, index);
                pl.set_page(uindex, page);
            })
        .def("__setitem__",
            [](PageList &pl, py::slice slice, py::iterable other) {
                QpdfLockGuard lock(pl.qpdf.get());
                pl.set_pages_from_iterable(slice, other);
            })
        .def("__delitem__",
            [](PageList &pl, py::ssize_t index) {
                QpdfLockGuard lock(pl.qpdf.get());
                auto uindex = uindex_from_index(pl, index);
                pl.delete_page(uindex);
            })
        .def("__delitem__",
            [](PageList &pl, py::slice slice) {
                QpdfLockGuard lock(pl.qpdf.get());
                pl.delete_pages_from_iterable(slice);
            })
        .def("__len__",
            [](PageList &pl) {
                QpdfLockGuard lock(pl.qpdf.get());
                return pl.count();
            })
        .def(
            "p",
            [](PageList &pl, py::ssize_t pnum) {
                QpdfLockGuard lock(pl.qpdf.get());
                if (pnum <= 0) // Indexing past end is checked in .get_page
                    throw py::index_error(
                        "page access out of range in 1-based indexing");
                return pl.get_page(pnum - 1);
            },
            py::arg("pnum"))
        .def(
            "__iter__",
            [](PageList &pl) {
                QpdfLockGuard lock(pl.qpdf.get());
                return PageListIterator{pl, 0};
            },
            py::keep_alive<0, 1>())
        .def(
            "insert",
            [](PageList &pl, py::ssize_t index, QPDFPageObjectHelper &page) {
                QpdfLockGuard lock(pl.qpdf.get());
                auto uindex = uindex_from_index(pl, index);
                pl.insert_page(uindex, page);
            },
            py::arg("index"), // LCOV_EXCL_LINE
            py::arg("obj"))
        .def(
            "insert",
            [](PageList &pl, py::ssize_t index, py::object obj) {
                throw py::type_error("only pikepdf.Page can be inserted to PageList");
            },
            py::arg("index"), // LCOV_EXCL_LINE
            py::arg("obj"))
        .def("reverse",
            [](PageList &pl) {
                QpdfLockGuard lock(pl.qpdf.get());
                size_t n = pl.count();
                py::slice ordinary_indices((Py_ssize_t)0, (Py_ssize_t)n, (Py_ssize_t)1);
                py::object none_obj = py::none();
                py::slice reversed(none_obj, none_obj, py::int_(-1));
                py::list reversed_pages = pl.get_pages(reversed);
                pl.set_pages_from_iterable(
                    ordinary_indices, py::borrow<py::iterable>(reversed_pages));
            })
        .def(
            "append",
            [](PageList &pl, QPDFPageObjectHelper &page) {
                QpdfLockGuard lock(pl.qpdf.get());
                pl.append_page(page);
            },
            py::arg("page"))
        .def(
            "append",
            [](PageList &pl, py::handle h) {
                throw py::type_error("only pikepdf.Page can be appended to PageList");
            },
            py::arg("page"))
        .def(
            "extend",
            [](PageList &pl, PageList &other) {
                DualQpdfLockGuard lock(pl.qpdf.get(), other.qpdf.get());
                auto other_pages = other.doc.getAllPages();
                if (other.qpdf.get() != pl.qpdf.get()) {
                    bool forms = false, dests = false;
                    for (auto &page : other_pages) {
                        auto h = page.getObjectHandle();
                        forms = forms || page_has_widget(h);
                        dests = dests || page_has_named_dest(h);
                        if (forms && dests)
                            break;
                    }
                    warn_page_copy(forms, dests);
                }
                for (auto &page : other_pages) {
                    pl.append_page(page);
                }
            },
            py::arg("other"))
        .def(
            "extend",
            [](PageList &pl, py::iterable iterable) {
                QpdfLockGuard lock(pl.qpdf.get());
                bool forms = false, dests = false;
                py::iterator it = py::iter(iterable);
                while (it != py::iterator::sentinel()) {
                    auto page = as_page_helper(*it);
                    auto h = page.getObjectHandle();
                    if (h.getOwningQPDF() != pl.qpdf.get()) {
                        forms = forms || page_has_widget(h);
                        dests = dests || page_has_named_dest(h);
                    }
                    pl.append_page(page);
                    ++it;
                }
                warn_page_copy(forms, dests);
            },
            py::arg("iterable"))
        .def("remove",
            [](PageList &pl, QPDFPageObjectHelper &page) {
                QpdfLockGuard lock(pl.qpdf.get());
                try {
                    pl.doc.removePage(page);
                } catch (const QPDFExc &) {
                    throw py::value_error("pikepdf.Page is not referenced in the PDF");
                }
            })
        .def(
            "remove",
            [](PageList &pl, py::ssize_t pnum) {
                QpdfLockGuard lock(pl.qpdf.get());
                if (pnum <= 0) // Indexing past end is checked in .get_page
                    throw py::index_error(
                        "page access out of range in 1-based indexing");
                pl.delete_page(pnum - 1);
            },
            py::kw_only(),
            py::arg("p"))
        .def("index",
            [](PageList &pl, const QPDFObjectHandle &h) {
                QpdfLockGuard lock(pl.qpdf.get());
                return page_index(*pl.qpdf, h);
            })
        .def("index",
            [](PageList &pl, const QPDFPageObjectHelper &poh) {
                QpdfLockGuard lock(pl.qpdf.get());
                return page_index(*pl.qpdf, poh.getObjectHandle());
            })
        .def("__repr__",
            [](PageList &pl) {
                QpdfLockGuard lock(pl.qpdf.get());
                return std::string("<pikepdf._core.PageList len=") +
                       std::to_string(pl.count()) + std::string(">");
            })
        .def("from_objgen",
            [](PageList &pl, int obj, int gen) {
                QpdfLockGuard lock(pl.qpdf.get());
                return from_objgen(*pl.qpdf, QPDFObjGen(obj, gen));
            })
        .def("from_objgen", [](PageList &pl, std::pair<int, int> objgen) {
            QpdfLockGuard lock(pl.qpdf.get());
            return from_objgen(*pl.qpdf, QPDFObjGen(objgen.first, objgen.second));
        });
}
