/*
 * Xournal++
 *
 * Helper methods to parse .xoj / .xopp documents
 *
 * @author Xournal++ Team
 * https://github.com/xournalpp/xournalpp
 *
 * @license GNU GPLv2 or later
 */

#pragma once

#include <charconv>
#include <istream>
#include <optional>
#include <ostream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <system_error>
#include <type_traits>

#include <glib.h>

#include "util/Assert.h"
#include "util/Color.h"
#include "util/StringUtils.h"
#include "util/Util.h"
#include "util/utf8_view.h"

#include "config-features.h"
#include "filesystem.h"

class LineStyle;
class XmlParser;


namespace XmlParserHelper {

class AttributeMap {
public:
    /**
     * Build an attribute map for the current node. Names and values parameters
     * should be directly forwarded from the GMarkup callback.
     */
    AttributeMap(const char** attributeNames, const char** attributeValues);

    /**
     * Look up `name` in the attribute map
     * @return The matching value, or std::nullopt if it was not found.
     */
    std::optional<const char*> operator[](std::u8string_view name) const;

private:
    const char** names;
    const char** values;
};

using c_string_utf8_view = xoj::util::utf8_view<const char*, xoj::util::CharSentinelClass<char>>;
using string_utf8_view = xoj::util::utf8_view<const char*, const char*>;

// generic templates

/**
 * Look up an attribute and parse it as T if it was found.
 */
template <typename T>
std::optional<T> getAttrib(std::u8string_view name, const AttributeMap& attributeMap);
/**
 * Look up an attribute and parse it as T. If the attribute was not found, use a default and optionally print a warning.
 */
template <typename T>
T getAttribMandatory(std::u8string_view name, const AttributeMap& attributeMap, const T& defaultValue = {},
                     bool warn = true);
// specializations
template <>
std::optional<const char*> getAttrib<const char*>(std::u8string_view name, const AttributeMap& attributeMap);
template <>
std::optional<c_string_utf8_view> getAttrib<c_string_utf8_view>(std::u8string_view name,
                                                                const AttributeMap& attributeMap);
template <>
std::optional<string_utf8_view> getAttrib<string_utf8_view>(std::u8string_view name, const AttributeMap& attributeMap);
template <>
std::optional<fs::path> getAttrib<fs::path>(std::u8string_view name, const AttributeMap& attributeMap);
template <>
std::optional<LineStyle> getAttrib<LineStyle>(std::u8string_view name, const AttributeMap& attributeMap);

// "color" attribute
Color getAttribColorMandatory(const AttributeMap& attributeMap, const Color& defaultValue, bool bg = false);
// Attempt to match string with background-specific color aliases
std::optional<Color> parseBgColor(string_utf8_view sv);
// Parse a string as a RGBA hex color code
std::optional<Color> parseColorCode(std::string_view sv);
// Attempt to match string with predefined color names
std::optional<Color> parsePredefinedColor(string_utf8_view sv);

// Decode C-string of Base64-encoded data into a string of binary data
std::string decodeBase64(std::string_view base64data);

/**
 * Attempt parsing a number (floating point or integral) from a string
 *
 * Parses the string in the range [it, end) for a numeric value. On success,
 * `it` is updated to point to the first unparsed character.
 *
 * When available, we use std::from_chars, because it is about 10x faster than
 * streams and about 5x faster than g_ascii_strtod.
 * @param it  Pointer to the beginning of the string, modified to point to the
 *            first unparsed character
 * @param end Pointer to one character past the end of the string
 * @throws std::domain_error if no value could be parsed
 * @return The parsed value
 *
 * @note The fallback implementation requires that the string be null-terminated
 *       at end. The string must contain at least one parsable value.
 */
template <typename T>
T parseNumeric(const char*& it, const char* end) {
    static_assert(std::is_arithmetic_v<T>, "T must be numeric");

    T value{};
    if constexpr (std::is_integral_v<T> || ENABLE_FLOAT_FROM_CHARS) {
        auto [ptr, ec] = std::from_chars(it, end, value);
        if (ec != std::errc{}) {
            throw std::domain_error{std::make_error_condition(ec).message()};
        }
        it = ptr;
    } else {
        // Fallback for missing floating point from_chars implementation
        // strtod requires the input to be null-terminated.
        xoj_assert(*end == '\0');
        char* ptr = nullptr;
        value = static_cast<T>(g_ascii_strtod(it, &ptr));
        if (ptr == it) {
            throw std::domain_error{"g_ascii_strtod failed"};
        }
        it = ptr;
    }
    return value;
}

namespace detail {

// SFINAE logic for checking named enums and utf8 views
template <typename, typename = void>
inline constexpr bool has_names_v = false;

template <typename T>
inline constexpr bool has_names_v<T, std::void_t<decltype(T::NAMES)>> = true;


template <typename, typename = void>
inline constexpr bool has_value_enum_v = false;

template <typename T>
inline constexpr bool has_value_enum_v<T, std::void_t<typename T::Value>> = std::is_enum_v<typename T::Value>;


template <typename>
inline constexpr bool is_utf8_view_v = false;

template <typename It, typename Sen>
inline constexpr bool is_utf8_view_v<xoj::util::utf8_view<It, Sen>> = true;


template <typename T>
constexpr bool always_false = false;

// Exception type for partially parsed attributes
template <typename T>
class IncompleteParseError: public std::runtime_error {
public:
    /**
     * Creates an exception for partially parsed data
     * @param value The value that could be parsed from the input
     */
    IncompleteParseError(T value): std::runtime_error("Parsing did not consume full input"), value_(std::move(value)) {}

    // Retrieve the partially parsed value
    const T& value() const noexcept { return value_; }

private:
    T value_;
};

// Parse named enums
template <typename T>
T parseEnum(c_string_utf8_view sv) {
    static_assert(has_names_v<T>, "T must define a static T::NAMES array to perform lookup");
    static_assert(has_value_enum_v<T>, "T must define an underlying enum type T::Value");

    // Look up value in names array
    for (auto it = T::NAMES.begin(); it != T::NAMES.end(); ++it) {
        if (*it == sv) {
            return static_cast<typename T::Value>(std::distance(T::NAMES.begin(), it));
        }
    }

    // Value could not be found
    throw std::domain_error{"unknown value"};
}

// Parse numeric types
template <typename T>
T parseNumeric(std::string_view sv) {
    auto it = sv.data();
    const auto end = sv.data() + sv.size();
    auto value = ::XmlParserHelper::parseNumeric<T>(it, end);
    if (it != end) {
        throw IncompleteParseError{value};
    }
    return value;
}

// Attempt to stringify any value
template <typename T>
std::optional<std::string> toDebugString(const T& value) {
    if constexpr (detail::has_names_v<T> && detail::has_value_enum_v<T>) {
        xoj_assert(value < T::NAMES.size());
        return char_cast(T::NAMES[value]);
    } else if constexpr (std::is_arithmetic_v<T>) {
        return std::to_string(value);
    } else if constexpr (detail::is_utf8_view_v<T>) {
        // We need to do a double copy to get a std::string out of a utf8_view
        const auto u8str = value.str();
        return std::string{u8str.begin(), u8str.end()};
    } else if constexpr (std::is_same_v<T, fs::path>) {
        const auto u8str = value.u8string();
        return std::string{u8str.begin(), u8str.end()};
    } else if constexpr (std::is_constructible_v<std::string, T>) {
        return std::string{value};
    }

    return std::nullopt;
}
};  // namespace detail

};  // namespace XmlParserHelper


// implementations of template functions

template <typename T>
auto XmlParserHelper::getAttrib(std::u8string_view name, const AttributeMap& attributeMap) -> std::optional<T> {
    auto optionalCStr = attributeMap[name];  // mildly expensive operation: string search in array.
                                             // Use the operator[] only once and store the result.
    if (optionalCStr) {
        try {
            // Choose appropriate parsing strategy
            if constexpr (std::is_constructible_v<T, const char*>) {
                return T{*optionalCStr};  // Type is directly constructible from a C-string, e.g. std::string_view
            } else if constexpr (detail::has_names_v<T> && detail::has_value_enum_v<T>) {
                return detail::parseEnum<T>(*optionalCStr | xoj::util::utf8);
            } else if constexpr (std::is_arithmetic_v<T>) {
                return detail::parseNumeric<T>(*optionalCStr);
            } else {
                // Type-dependent false so the static_assert only triggers if this branch is instantiated
                static_assert(detail::always_false<T>, "No parser defined for this type");
            }
        } catch (const std::domain_error& e) {
            g_warning("XML parser: Attribute \"" SV_FMT "\" could not be parsed as %s: %s. The value is \"%s\"",
                      U8SV_ARG(name), Util::demangledTypeName<T>().c_str(), e.what(), *optionalCStr);
            return std::nullopt;
        } catch (const detail::IncompleteParseError<T>& e) {
            g_warning("XML parser: Attribute \"" SV_FMT "\" was not entirely parsed as %s. The value is \"%s\"",
                      U8SV_ARG(name), Util::demangledTypeName<T>().c_str(), *optionalCStr);
            return e.value();
        }
    } else {
        return std::nullopt;
    }
}

template <typename T>
auto XmlParserHelper::getAttribMandatory(std::u8string_view name, const AttributeMap& attributeMap,
                                         const T& defaultValue, bool warn) -> T {
    auto optionalAttr = getAttrib<T>(name, attributeMap);
    if (optionalAttr) {
        return *optionalAttr;
    } else {
        if (warn) {
            const auto defaultValueStr = detail::toDebugString(defaultValue);
            if (defaultValueStr) {
                g_warning("XML parser: Mandatory attribute \"" SV_FMT "\" not found. Using default value \"%s\"",
                          U8SV_ARG(name), defaultValueStr->c_str());
            } else {
                g_warning("XML parser: Mandatory attribute \"" SV_FMT "\" not found", U8SV_ARG(name));
            }
        }
        return defaultValue;
    }
}
