/** @file
 * @brief Extract text and metadata using libarchive.
 */
/* Copyright (C) 2020 Parth Kapadia
 * Copyright (C) 2022,2023 Olly Betts
 *
 * 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 of the
 * License, 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, see
 * <https://www.gnu.org/licenses/>.
 */
#include <config.h>
#include "handler.h"

#include "msxmlparser.h"
#include "opendocmetaparser.h"
#include "opendocparser.h"
#include "stringutils.h"
#include "xlsxparser.h"
#include "xpsparser.h"

#include <archive.h>
#include <archive_entry.h>

#define DEFAULT_BLOCK_SIZE 10240

using namespace std;

static void
parse_metadata(const string& metadata)
{
    OpenDocMetaParser metaparser;
    metaparser.parse(metadata);
    send_field(FIELD_TITLE, metaparser.title);
    send_field(FIELD_KEYWORDS, metaparser.keywords);
    send_field(FIELD_AUTHOR, metaparser.author);
    send_field_created_date(metaparser.created);
    send_field_page_count(metaparser.pages);
}

static bool
extract_opendoc(struct archive* archive_obj)
{
    string styles;
    OpenDocParser parser;

    struct archive_entry* entry;
    while (archive_read_next_header(archive_obj, &entry) == ARCHIVE_OK) {
	size_t total;
	ssize_t size;
	string pathname = archive_entry_pathname(entry);
	if (pathname == "content.xml") {
	    total = archive_entry_size(entry);
	    string content(total, '\0');
	    size = archive_read_data(archive_obj, &content[0], total);

	    if (size <= 0) {
		send_field(FIELD_ERROR, "Failed to read content.xml");
		return false;
	    }
	    content.resize(size);
	    parser.parse(content);
	} else if (pathname == "styles.xml") {
	    total = archive_entry_size(entry);
	    styles.resize(total);
	    size = archive_read_data(archive_obj, &styles[0], total);

	    if (size <= 0) {
		send_field(FIELD_ERROR, "Failed to read styles.xml");
		return false;
	    }
	    styles.resize(size);
	} else if (pathname == "meta.xml") {
	    total = archive_entry_size(entry);
	    string metadata(total, '\0');
	    size = archive_read_data(archive_obj, &metadata[0], total);

	    if (size > 0) {
		// indexing file even if this fails
		metadata.resize(size);
		parse_metadata(metadata);
	    }
	}
    }

    // We want to parse styles.xml after content.xml, but they could be stored
    // in either order in the ZIP container.
    parser.parse(styles);

    send_field(FIELD_BODY, parser.dump);
    return true;
}

static bool
extract_xlsx(struct archive* archive_obj)
{
    int pages = 0;
    string sheets;
    XlsxParser parser;

    struct archive_entry* entry;
    while (archive_read_next_header(archive_obj, &entry) == ARCHIVE_OK) {
	string pathname = archive_entry_pathname(entry);
	if (pathname == "xl/styles.xml" ||
	    pathname == "xl/workbook.xml" ||
	    pathname == "xl/sharedStrings.xml") {
	    size_t total = archive_entry_size(entry);
	    string shared_strings(total, '\0');
	    ssize_t size = archive_read_data(archive_obj, &shared_strings[0],
					     total);

	    if (size > 0) {
		shared_strings.resize(size);
		parser.parse(shared_strings);
	    }
	} else if (startswith(pathname, "xl/worksheets/sheet")) {
	    auto i = sheets.size();
	    size_t total = archive_entry_size(entry);
	    sheets.resize(i + total);
	    ssize_t size = archive_read_data(archive_obj, &sheets[i], total);

	    if (size <= 0) {
		send_field(FIELD_ERROR, "Failed to read " + pathname);
		return false;
	    }
	    sheets.resize(i + size);
	    ++pages;
	} else if (pathname == "docProps/core.xml") {
	    size_t total = archive_entry_size(entry);
	    string metadata(total, '\0');
	    ssize_t size = archive_read_data(archive_obj, &metadata[0], total);
	    if (size > 0) {
		metadata.resize(size);
		parse_metadata(metadata);
	    }
	}
    }
    parser.parse(sheets);
    send_field(FIELD_BODY, parser.dump);
    send_field_page_count(pages);
    return true;
}

static bool
extract_msxml(struct archive* archive_obj,
	      const string& tail)
{
    size_t total;
    ssize_t size;
    struct archive_entry* entry;
    string content;

    if (startswith(tail, "wordprocessingml.")) {
	while (archive_read_next_header(archive_obj, &entry) == ARCHIVE_OK) {
	    string pathname = archive_entry_pathname(entry);
	    if (pathname == "word/document.xml") {
		auto i = content.size();
		total = archive_entry_size(entry);
		content.resize(i + total);
		size = archive_read_data(archive_obj, &content[i], total);

		if (size <= 0) {
		    send_field(FIELD_ERROR, "Failed to read word/document.xml");
		    return false;
		}
		content.resize(i + size);
	    } else if (startswith(pathname, "word/header") ||
		       startswith(pathname, "word/footer")) {
		auto i = content.size();
		total = archive_entry_size(entry);
		content.resize(i + total);
		size = archive_read_data(archive_obj, &content[i], total);

		if (size > 0) {
		    content.resize(i + size);
		} else {
		    // Ignore this as header/footer may not be present
		    content.resize(i);
		}
	    } else if (pathname == "docProps/core.xml") {
		// docProps/core.xml stores meta data
		total = archive_entry_size(entry);
		string metadata(total, '\0');
		size = archive_read_data(archive_obj, &metadata[0], total);
		if (size > 0) {
		    metadata.resize(size);
		    parse_metadata(metadata);
		}
	    }
	}
    } else if (startswith(tail, "presentationml.")) {
	int pages = 0;
	while (archive_read_next_header(archive_obj, &entry) == ARCHIVE_OK) {
	    string pathname = archive_entry_pathname(entry);
	    if (startswith(pathname, "ppt/slides/slide")) {
		++pages;
		goto handle_pptx_content;
	    } else if (startswith(pathname, "ppt/notesSlides/notesSlide") ||
		       startswith(pathname, "ppt/comments/comment")) {
handle_pptx_content:
		auto i = content.size();
		total = archive_entry_size(entry);
		content.resize(i + total);
		size = archive_read_data(archive_obj, &content[i], total);

		if (size <= 0) {
		    send_field(FIELD_ERROR, "Failed to read " + pathname);
		    return false;
		}
		content.resize(i + size);
	    } else if (pathname == "docProps/core.xml") {
		total = archive_entry_size(entry);
		string metadata(total, '\0');
		size = archive_read_data(archive_obj, &metadata[0], total);
		if (size > 0) {
		    metadata.resize(size);
		    parse_metadata(metadata);
		}
	    }
	}
	send_field_page_count(pages);
    }

    MSXmlParser parser;
    parser.parse(content);
    send_field(FIELD_BODY, parser.dump);
    return true;
}

static bool
extract_xps(struct archive* archive_obj)
{
    int pages = 0;
    string content;
    XpsParser parser;

    struct archive_entry* entry;
    while (archive_read_next_header(archive_obj, &entry) == ARCHIVE_OK) {
	string pathname = archive_entry_pathname(entry);
	if (startswith(pathname, "Documents/") &&
	    endswith(pathname, ".fpage") &&
	    pathname.find("/Pages/") != string::npos) {
	    size_t total = archive_entry_size(entry);
	    content.resize(total);
	    ssize_t size = archive_read_data(archive_obj, &content[0], total);

	    if (size <= 0) {
		send_field(FIELD_ERROR, "Failed to read " + pathname);
		return false;
	    }
	    content.resize(size);
	    parser.parse(content);
	    ++pages;
	} else if (pathname == "docProps/core.xml") {
	    // If present, docProps/core.xml stores meta data.
	    size_t total = archive_entry_size(entry);
	    content.resize(total);
	    ssize_t size = archive_read_data(archive_obj, &content[0], total);
	    if (size > 0) {
		content.resize(size);
		parse_metadata(content);
	    }
	}
    }

    send_field(FIELD_BODY, parser.dump);
    send_field_page_count(pages);
    return true;
}

bool
initialise(string&)
{
    return true;
}

void
extract(const string& filename,
	const string& mimetype)
{
    const char* file = filename.c_str();
    struct archive* archive_obj = archive_read_new();
    archive_read_support_format_zip(archive_obj);
    // Block size will be determined by libarchive automatically for
    // regular files. Specified block size will only be used for tape drives
    // 10240 is chosen as default size (20 records - 512 bytes each)
    int status_code = archive_read_open_filename(archive_obj, file,
						 DEFAULT_BLOCK_SIZE);

    if (status_code != ARCHIVE_OK) {
	send_field(FIELD_ERROR, "Failed to open file");
	return;
    }

    if (startswith(mimetype, "application/vnd.sun.xml.") ||
	startswith(mimetype, "application/vnd.oasis.opendocument.")) {
	if (!extract_opendoc(archive_obj))
	    return;
    } else if (startswith(mimetype,
			  "application/vnd.openxmlformats-officedocument."))
    {
	string tail(mimetype, 46);
	if (startswith(tail, "spreadsheetml.")) {
	    if (!extract_xlsx(archive_obj))
		return;
	} else {
	    if (!extract_msxml(archive_obj, tail))
		return;
	}
    } else if (mimetype == "application/oxps" ||
	       mimetype == "application/vnd.ms-xpsdocument") {
	if (!extract_xps(archive_obj))
	    return;
    }

    status_code = archive_read_free(archive_obj);
    if (status_code != ARCHIVE_OK) {
	send_field(FIELD_ERROR, archive_error_string(archive_obj));
	return;
    }
}
