#pragma once

#include <algorithm>
#include <string>
#include <sstream>
#include <pugixml.hpp>
#include <vector>
#include <chrono>
#include <fmt/chrono.h>

#include "extraction_param.hpp"

namespace SynDomUtils {

struct xml_string_writer: xml_writer {
    std::string result;
    virtual void write(const void* data, size_t size) {
        result.append(static_cast<const char*>(data), size);
    }
};

const std::string ATOM_LINK_TAGS[] = {
    "link", "atom:link", "atom10:link"
};

inline bool __trim_filter(unsigned char ch) {
    return !std::isspace(ch);
}

/** Trims a string from the start, in place */
inline void ltrim(std::string &s) {
    s.erase(s.begin(), std::find_if(s.begin(), s.end(), __trim_filter));
}

/** Trims a string from the end, in place */
inline void rtrim(std::string &s) {
    s.erase(
        std::find_if(s.rbegin(), s.rend(), __trim_filter).base(), s.end()
    );
}

/** Trims a string from both ends, in place */
inline void trim(std::string &s) { ltrim(s); rtrim(s); }

/** Changes the string case to all lowercase, in place */
inline void lower(std::string &s) {
    std::transform(s.begin(), s.end(), s.begin(), ::tolower);
}

/**
* Checks if the string `s` has the prefix `prefix`.
*
* @param s the input string.
* @param prefix the prefix to search for.
*/
inline bool str_has_prefix(std::string s, std::string prefix) {
    return (s.rfind(prefix, 0) == 0);
}

/**
* Splits a string into an std::vector of strings, using a single character
* delimiter.
*
* @param s the input string.
* @param delim the single character delimiter to be used to split the string.
*/
inline std::vector<std::string> split(std::string s, char delim) {
    std::vector<std::string> result;
    std::stringstream ss(s);
    std::string item;
    while (getline (ss, item, delim)) {
        result.push_back(item);
    }
    return result;
}

/**
* Returns the current date and time in ISO 8601 format, with UTC offset.
*/
inline std::string current_time() {
    auto now = std::chrono::system_clock::now();
    std::string res = fmt::format("{:%Y-%m-%dT%H:%M:%S}", now);
    return res;
}

/** Rudimentarily checks if the provided string is a URL. */
inline bool is_url(std::string s) {
    return !s.empty() && (
        str_has_prefix(s, "https://") ||
        str_has_prefix(s, "http://")
    );
}

/**
* Searches for a certain value, starting from a provided `pugi::xml_node`,
* using a vector of ExtractionParam objects to examine different possible
* alternatives.
*
* @param node the `pugi::xml_node` treated as the root node for the search.
* @param params see ExtractionParam
*/
inline std::string extract_from_node(
        pugi::xml_node node, std::vector<ExtractionParam> params
) {
    std::string res = "";
    for (auto param: params) {
        auto child = node;
        for (auto tag: param.tags) {
            if (!child) break;
            child = child.child(tag.c_str());
        }
        if (!child) continue;

        if (param.type == ExtractionParam::ParamType::CHILD) {
            res = child.text().as_string();
            // if the child content is not CDATA or text, then it's probably
            // xhtml, so we grab that with the writer
            if (
                    child && res.empty() &&
                    child.first_child().type() != node_cdata &&
                    child.first_child()
            ) {
                xml_node child_cpy = child;
                child_cpy.set_name("article");
                xml_string_writer writer;
                child_cpy.print(writer, "");
                if (writer.result != "<article />") {
                    res = writer.result;
                }
            }
        }
        else {
            res = child.attribute(param.attribute.c_str()).value();
        }

        if (!res.empty()) {
            trim(res);
            return res;
        }
    }
    return "";
}

/**
* Searches for a certain value pertaining to a `<link />` node.
*
* @param node the `pugi::xml` root node containing the `<link />` node(s).
* @param rels a list of possible acceptable values for the `rel` attribute.
* @param types a list of possible acceptable values for the `type` attribute.
* @param opt_rel can optionally be set to true to accept values where the `rel`
*        attribute is absent or empty.
* @param opt_type can optionally be set to true to accept values where the
*        `type` attribute is absent or empty.
*/
inline std::string extract_link(
        pugi::xml_node node,
        std::vector<std::string> rels,
        std::vector<std::string> types,
        bool opt_rel=false, bool opt_type=false
) {
    std::string res = "";
    std::string attr_rel, attr_type;
    for (auto link_tag: ATOM_LINK_TAGS) {
        pugi::xml_node link_node = node.child(link_tag.c_str());
        while(link_node) {
            attr_rel = link_node.attribute("rel").value();
            attr_type = link_node.attribute("type").value();
            if (
                    ((opt_rel && attr_rel.empty()) || std::find(
                        rels.begin(), rels.end(), attr_rel
                    ) != rels.end()) &&
                    ((opt_type && attr_type.empty()) || std::find(
                        types.begin(), types.end(), attr_type
                    ) != types.end())
            ) {
                res = link_node.attribute("href").value();
                if (!res.empty()) return res;
            }
            link_node = link_node.next_sibling(link_tag.c_str());
        }
    }

    return "";
}

};
