/** @file
 * @brief Extract text and metadata using LibreOfficeKit
 */
/* Copyright (C) 2014-2026 Olly Betts
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

#include <config.h>

#include "handler.h"

#include "htmlparser.h"
#include "loadfile.h"
#include "stringutils.h"
#include "tmpdir.h"
#include "urlencode.h"

using namespace std;

#include <climits>
#include <cstdlib>
#include <exception>
#include <iostream>
#include <memory>

#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <unistd.h>

#define LOK_USE_UNSTABLE_API // So we can use lok::Document::getParts().
#include <LibreOfficeKit/LibreOfficeKit.hxx>

using namespace std;
using namespace lok;

// We require at least LibreOffice 5.2 (for freeError()).
#define MIN_LIBREOFFICE_VERSIOM_MAJOR 5
#define MIN_LIBREOFFICE_VERSIOM_MINOR 2

// Install location for Debian packages (also Fedora on 32-bit architectures):
#define LO_PATH_DEBIAN "/usr/lib/libreoffice/program"

// Install location for Fedora packages on 64-bit architectures:
#define LO_PATH_FEDORA64 "/usr/lib64/libreoffice/program"

// Install location on macOS.  LibreOffice 26.2.2 required.
#define LO_PATH_MACOS "/Applications/LibreOffice.app/Contents/Frameworks"

// Find a LibreOffice installation to use.
static const char*
get_lo_path()
{
    const char* lo_path = getenv("LO_PATH");
    if (lo_path) return lo_path;

    struct stat sb;
#define CHECK_DIR(P) \
    if (stat(P"/versionrc", &sb) == 0 && S_ISREG(sb.st_mode)) return P
#ifdef __APPLE__
    CHECK_DIR(LO_PATH_MACOS);
#else
    CHECK_DIR(LO_PATH_DEBIAN);
    if constexpr (sizeof(void*) > 4) {
        CHECK_DIR(LO_PATH_FEDORA64);
    }
#endif

    // Check install locations for .deb files from libreoffice.org,
    // e.g. /opt/libreoffice6.3/program
    DIR* opt = opendir("/opt");
    if (opt) {
        unsigned long best_major = MIN_LIBREOFFICE_VERSIOM_MAJOR;
        unsigned long best_minor = MIN_LIBREOFFICE_VERSIOM_MINOR;
        // Set the best version seen to one before the minimum we support.
#if MIN_LIBREOFFICE_VERSIOM_MINOR == 0
        --best_major;
        best_minor = ULONG_MAX;
#else
        --best_minor;
#endif
        static string best_rc;
        struct dirent* d;
        while ((d = readdir(opt))) {
#ifdef DT_DIR
            // Opportunistically skip non-directories if we can spot them
            // just by looking at d_type.
            if (d->d_type != DT_DIR && d->d_type != DT_UNKNOWN) {
                continue;
            }
#endif
            if (memcmp(d->d_name, "libreoffice", strlen("libreoffice")) != 0) {
                continue;
            }

            char* p = d->d_name + strlen("libreoffice");
            unsigned long major = strtoul(p, &p, 10);
            if (major == ULONG_MAX) continue;
            unsigned long minor = 0;
            if (*p == '.') {
                minor = strtoul(p + 1, &p, 10);
                if (minor == ULONG_MAX) continue;

                string rc = "/opt/";
                rc += d->d_name;
                rc += "/program";
                if (stat((rc + "/versionrc").c_str(), &sb) != 0 ||
                    !S_ISREG(sb.st_mode)) {
                    continue;
                }

                if (major > best_major ||
                    (major == best_major && minor > best_minor)) {
                    best_major = major;
                    best_minor = minor;
                    best_rc = std::move(rc);
                }
            }
        }
        closedir(opt);
        if (!best_rc.empty()) {
            return best_rc.c_str();
        }
    }
    return nullptr;
}

static string output_file;
static string output_url;

static Office* llo;

bool
initialise(string& error)
{
    try {
        output_file = get_tmpfile("tmp.html");
        if (output_file.empty()) {
            error = "Couldn't create temporary directory";
            return false;
        }
        url_encode_path(output_url, output_file);

        const char* lo_path = get_lo_path();
        if (!lo_path) {
            error = "LibreOffice >= "
                STRINGIZE(MIN_LIBREOFFICE_VERSIOM_MAJOR) "."
                STRINGIZE(MIN_LIBREOFFICE_VERSIOM_MINOR) " install not found. "
                "Set LO_PATH to the 'program' directory, "
                "e.g.: export LO_PATH=/opt/libreoffice/program";
            return false;
        }
        llo = lok_cpp_init(lo_path);
        if (!llo) {
            error = "Failed to initialise LibreOfficeKit";
            return false;
        }
        return true;
    } catch (const exception& e) {
        error = "LibreOfficeKit threw exception: ";
        error += e.what();
        return false;
    }
}

void
extract(const string& filename, const string&)
try {
    const char* format = "html"; // FIXME or xhtml
    const char* options = "SkipImages";
    string input_url;
    url_encode_path(input_url, filename);
    unique_ptr<Document> lodoc(llo->documentLoad(input_url.c_str(), options));
    if (!lodoc.get()) {
        char* errmsg = llo->getError();
        send_field(FIELD_ERROR, errmsg ? errmsg : "Failed to load document");
        if (errmsg) llo->freeError(errmsg);
        return;
    }

    if (!lodoc->saveAs(output_url.c_str(), format, options)) {
        char* errmsg = llo->getError();
        send_field(FIELD_ERROR, errmsg ? errmsg : "Failed to export as HTML");
        if (errmsg) llo->freeError(errmsg);
        return;
    }

    string html;
    if (!load_file(output_file, html)) {
        unlink(output_file.c_str());
        send_field(FIELD_ERROR, "Failed to load LibreOffice HTML output");
        return;
    }
    HtmlParser p;
    p.ignore_metarobots();
    p.parse(html, "utf-8", true);
    unlink(output_file.c_str());
    send_field(FIELD_BODY, p.dump);
    send_field(FIELD_TITLE, p.title);
    send_field(FIELD_KEYWORDS, p.keywords);
    send_field(FIELD_KEYWORDS, p.topic);
    send_field(FIELD_AUTHOR, p.author);
    send_field_created_date(p.created);
    // The documentation comment in LibreOfficeKit.hxx says this method
    // returns a count of "individual sheets in a Calc, or slides in Impress,
    // and has no relevance for Writer" but it actually seems to return a
    // page count for writer documents.
    send_field_page_count(lodoc->getParts());
} catch (const exception& e) {
    send_field(FIELD_ERROR, e.what());
}
