#include <iostream>
#include <iterator>
#include <regex>

#include "util.h"
#include "kutil.h"
#include "error.h"
#include "file.h"
#include "html_util.h"
#include "log.h"
#include "show.h"

std::ostream& operator<<(std::ostream& os, const attr_t& a)
{
    if (std::holds_alternative<int>(a))
        os << std::get<int>(a);
    else if (std::holds_alternative<float>(a))
        os << std::get<float>(a);
    else if (std::holds_alternative<std::string>(a))
        os << std::get<std::string>(a);
    return os;
}

int _indent = 0;

std::ostream& operator<<(std::ostream& os, const html_element_t& h)
{
    if (std::holds_alternative<std::string>(h))
        os << std::get<std::string>(h);
    else if (std::holds_alternative<HTML>(h))
        os << std::get<HTML>(h);
    return os;
}


std::ostream& operator<<(std::ostream& os, const elements_t& elements)
{
//    for (const HTML& h : elements) {
    for (const html_element_t& h : elements) {
/*n
  if (std::holds_alternative<std::string>(h))
  os << h;
  else if (std::holds_alternative<HTML>(h))
  os << h;
  }
*/
        os << h;
    }
    return os;
}

bool attribute_with_value(attr_t attr)
{
    return std::holds_alternative<std::string>(attr)
        && !(std::get<std::string>(attr)).empty();
}

std::ostream& operator<<(std::ostream& os, const HTML& h)
{
    bool void_element = h.void_element(h.m_tag);
    //bool newline = !(h.void_element(h.m_tag) or  h.empty_element(h.m_tag));
    //bool empty_element = h.empty_element(h.m_tag) and trim(h.m_text).size() == 0;

    os << "<" << h.m_tag;
    for (auto [t,v] : h.m_attrs) {
        os << " " << t;
        if (attribute_with_value(v)) {
            os << "=\"" << v << "\"";
        }
    }
    os << ">";
    if (h.m_elements.size() > 0) {
        os << "\n" << h.m_elements;
        
    }

    //if (!empty_element) {
    if (trim(h.m_text).size() > 0) {
        if (h.m_text.find(' ') != std::string::npos) {
            os << "\n";
        }
        os << trim(h.m_text); // << "\n";
    }

    /*
    //os << h.m_text;
    std::string intext = "\n" + h.m_text;
    std::string sp(_indent, ' ');
    intext = std::regex_replace(intext, std::regex("\n"), "\n" + sp);
    os << intext;
    */
    //os << trim(h.m_text) << "\n";
    // }
    /*
      if (true or newline) {
      os << "\n";
      _indent -= 2;
      }
    */

    //if (!void_element and !empty_element) {    
    if (!void_element) {    
        if (h.m_text.find(' ') != std::string::npos) {
            os << "\n";
        }
        os << "</" << h.m_tag << ">";
    }
    os << "\n";

    /*
      if (true or !void_element and !empty_element) {
      //std::string sp(_indent, ' ');
      //os << sp << "</" << h.m_tag << ">";
      os << "\n</" << h.m_tag << ">";
      }
      os << "\n";
    */

    return os;
}

std::string to_string(elements_t e)
{
    std::stringstream ss{};
    ss << e;
    return ss.str();
}







bool HTML::void_element(const std::string& tag) const
{
    if (tag == "!DOCTYPE")
        return true;
    else
        return std::find
            (m_void_tags.begin(), m_void_tags.end(), tag) != m_void_tags.end();
}

bool HTML::empty_element(const std::string& tag) const
{
    if (tag == preamble_tag)
        return true;
    else
        return std::find
            (m_empty_tags.begin(), m_empty_tags.end(), tag) != m_empty_tags.end();
}

//HTML& HTML::attr(std::string name, std::variant<int, float, std::string> value)
HTML& HTML::attr(const std::string& name, attr_t value)
{
    m_attrs.push_back({name, value});
    return *this;
}

HTML& HTML::attrs(const std::string& s)
{
    static const std::regex attr_rgx(R"((\w+)\s*=\s*\"(.*?)\")");
    auto attrs_begin = std::sregex_iterator(s.begin(), s.end(), attr_rgx);
    auto attrs_end = std::sregex_iterator();
    //std::cout << "Found " << std::distance(attrs_begin, attrs_end) << " attrs\n";
    for (std::sregex_iterator iter = attrs_begin; iter != attrs_end; ++iter) {
        std::smatch match = *iter;
        //std::cout << "attr: " << s << ": " << (*iter)[1] << " " << (*iter)[2] << "\n";
        m_attrs.push_back({(*iter)[1], (*iter)[2]});
    }
    return *this;
}



// Functions:

namespace html {

    HTML elt(const std::string& tag)
    {
        HTML h(tag);
        return h;
    }

    HTML elt(const std::string& tag, const std::string& text)
    {
        HTML h(tag, trim(text));
        return h;
    }

    HTML elt(const std::string& tag, elements_t elements)
    {
        HTML h(tag, elements);
        return h;
    }

    HTML elt(const std::string& tag, HTML e, elements_t elts)
    {
        elements_t elements = {e};
        elements.insert(elements.end(), elts.begin(), elts.end());
        HTML h(tag, elements);
        return h;
    }

    HTML preamble()
    {
        return elt(preamble_tag);
    }


// <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
// <meta http-equiv="Pragma" content="no-cache" />
// <meta http-equiv="Expires" content="0" />

    elements_t meta_elements()
    {
        elements_t result {
            elt("meta")
            .attr("name", "viewport")
            .attr("content", "width=device-width, initial-scale=1"),
            elt("meta")
            .attr("charset", "UTF-8"),
            //  Does not conform to HTML 5:
            // elt("meta")
            // .attr("http-equiv", "Cache-Control")
            // .attr("content", "no-cache, no-store, must-revalidate"),
            // elt("meta")
            // .attr("http-equiv", "Pragma")
            // .attr("content", "no-cache"),
            // elt("meta")
            // .attr("http-equiv", "Expires")
            // .attr("content", "0")
        };
        /*
          result.push_back(
          elt("link")
          .attr("rel", "icon")
          .attr("href", "/favicon.ico")
          .attr("type", "image/x-icon"))
        */
        return result;
    }

    elements_t local_font_elements(const strings_t& fontnames)
    {
        elements_t result {};
        for (auto name : fontnames) {
            // std::cout << "Font: " << name << "\n";
            result.push_back(elt("link")
                             .attr("href", "fonts/" + name + ".css")
                             .attr("rel", "stylesheet"));
        }
        return result;
    }

    elements_t javascript(const std::string& output_dir, const strings_t& js_filenames)
    {
        //elements_t result { jquery_elements() };
        elements_t result {};

        std::string js_dir = output_dir + "/js/";
        fs::create_directory(js_dir);
        for (auto fname : js_filenames) {
            /*
            std::string base = file_basename(fname) + ".js";
            std::string js_page_filename = js_dir + base;
            if (file_exists(js_page_filename)) {
                fs::remove(js_page_filename);
            }
            fs::copy(fname, js_page_filename);
            */
            result.push_back(elt("script")
                             .attr("src", fname));  //"js/" + base));
        }
        return result;
    }

    HTML head(const std::string& title,
              const std::string& css,
              const strings_t& css_filenames,
              const strings_t& local_fonts)
    {
        elements_t elts = meta_elements();
        // msg() << "ELTS sks: " << elts << "\n";
        for (auto e : local_font_elements(local_fonts))
            elts.push_back(e);

        //std::string css_dir = output_dir + "/css/";
        // No; a single file at the top level...?

        // std::string css_dir = output_dir + "/";
        // fs::create_directory(css_dir);

        for (auto fname : css_filenames) {
            // msg() << "CSS: " << fname << "\n";
            /*
            std::string base = file_basename(fname) + ".css";
            std::string page_filename = css_dir + base;
            if (!file_exists(page_filename)) {
                fs::copy(fname, page_filename);
            }
            */
            elts.push_back(elt("link")
                           .attr("rel", "stylesheet")
                           .attr("href", fname)); // base));
        }
        if (!css.empty()) {
            elts.push_back(elt("style", css));
        }

        if (!trim(title).empty()) {
            elts.push_back(elt("title", trim(title)));
        }
        HTML result = elt("head", elts);
        return result;
    }    


    HTML button(const std::string& id, const std::string& name,
                const std::string& attrib="", const std::string& value="")
    {
        std::vector<std::string> hidden = {"Clear", "Back"};
        HTML result = elt("span", string_replace(name, " ", "&#160;")).id(id).cls("navb");
        if (attrib != "") {
            result.attr(attrib, value);
        }
        if (std::find(hidden.begin(), hidden.end(), name) != hidden.end()) {
            result.attr("hidden", "");
        }
        return result;
    }


    elements_t navtools(int maximum_depth=5)
    {
        elements_t result {};
        for (auto [name, id] : std::vector<std::pair<std::string, std::string>>
                 {{ "Text only", "textshow" },
                  { "Next", "next" },
                  { "Previous", "previous" }}) {
                  //{ "Fit", "fit" }}) {
            result.push_back(button(id, name));
        }
        for (int n = 1; n <= maximum_depth; n++) {
            result.push_back(
                elt("span", std::to_string(n)).cls("navb depthchoice").attr("data-depth", std::to_string(n)));
        }
        elements_t fit_elements = {
            elt("input", "")
            .id("fit_check")
            .attr("name", "fit_check")
            .attr("type", "checkbox")
            .attr("active", "false")
            .attr("autocomplete", "off"),
            //.attr("checked", ""),
            elt("label", "Fit")
            .id("fit_check_label")
            .attr("for", "fit_check")
        };
        result.push_back(elt("span", fit_elements)
                         .attr("id", "fit"));

        elements_t linkshow_elements = {
            elt("input", "")
            .id("linkshow_check")
            .attr("name", "linkshow_check")
            .attr("type", "checkbox")
            .attr("active", "false")
            .attr("autocomplete", "off"),
            //.attr("checked", ""),
            elt("label", "Links")
            .id("linkshow_check_label")
            .attr("for", "linkshow_check")
        };

        result.push_back(elt("span", linkshow_elements)
                         .attr("id", "linkshow"));

        return result;
    }

    elements_t search()
    {
        elements_t result {};
        for (auto [name, id] : std::vector<std::pair<std::string, std::string>>
                 {{ "Clear", "search_clear" },
                  { "Back", "search_back_label"},
                  { "Search", "search_label" }
                 })
            result.push_back(button(id, name));            
        elements_t search_input { elt("span", "&#9421;").id("search_clear_input").attr("hidden", "") };
        result.push_back(
            elt("span", {
                    elt("input", "").id("search_input")
                    .attr("type", "text").attr("name", "search_input")
                    .attr("autocomplete", "off")
                    .attr("required", "required"),
                    elt("span", search_input).id("search_clear_input_box")
                }).id("search_box"));
        //result.push_back(button("help", "Help"));
        /*
        elements_t search_input {};
        search_input.push_back(
            elt("span", result).id("foldersearch"));
        return search_input;
        */
        return result;
    }
      
    HTML navigation(int max_level) {
        return elt("div", {
                elt("span", navtools(max_level)).id("navtools"),
                    elt("span", { elt("span", search()).id("searchtools"),
                                 elt("span", button("help", "Help"))})})
            .id("nav");
    }

    elements_t middle(std::string text,
                      //std::string nav,
                      std::string toc)
    {
        elements_t result {};
        toc = trim(toc);
        text = trim(text);
        if (!toc.empty()) {
            result.push_back(elt("div", toc)
                             .attr("id", "toc"));

            result.push_back(elt("div")
                             .attr("id", "resizer"));

        }

        if (!text.empty()) {
           text += "<div class=\"endspace\"></div>\n";
            elements_t content {};
            content.push_back(elt("div", text).attr("id", "text"));
            //if (!nav.empty()) {
            //content.push_back(elt("div", "<h1>SEARCH</h1>")
            //                    .attr("id", "search_page"));
                //content.push_back(elt("div", "<h1>HELP</h1>").attr("id", "help_page"));
            //}
            result.push_back(elt("div", content).attr("id", "content"));
        }
        else {
            throw Argument_error("The text content of the HTML page is not defined");
        }
        return result; 
    }

    elements_t status(const std::string& date, const std::string& version, const std::string& copyright)
    {
        elements_t result {};
        if (!date.empty()) {
            result.push_back(elt("span", date).cls("footeritem"));
        }
        if (!version.empty()) {
            result.push_back(elt("span", version).cls("footeritem"));
        }
        if (!copyright.empty()) {
            result.push_back(elt("span", copyright).cls("footeritem"));
        }
        return result;
    }

    elements_t page(
        const std::string& title,
        std::string page_title,
        const std::string& output_dir,
        const std::string& nav,
        int max_level,
        const std::string& toc,
        const std::string& text,
        const std::string& date,
        const std::string& version,
        const std::string& copyright,
        const std::string& css,
        const strings_t& css_filenames,
        const strings_t& js_filenames,
        const strings_t& local_fonts,
        const std::string& logo)
    {
        if (page_title.empty())
            page_title = title;
        elements_t body {};
        if (!title.empty()) {
            elements_t title_bar {};
            if (!trim(title).empty()) {
                title_bar.push_back(elt("span", trim(title)).attr("id", "title_text"));
            }
            if (!logo.empty()) {
                title_bar.push_back(elt("span", trim(logo)).attr("id", "logo"));
            }
            if (!title_bar.empty()) {
                body.push_back(elt("div", title_bar).attr("id", "title"));
            }
        }
        if (!nav.empty()) {
            body.push_back(navigation(max_level));
        }

        if (!nav.empty() and !toc.empty()) {
            body.push_back(elt("div",
                               elt("div",
                                   middle(text, toc))
                               .attr("id", "middle")));
        } else {
            //std::cout << "ONLY TEXT\n" << text << "\n\n";
            body.push_back(elt("div", 
                               elt("div",
                                   text)
                               .attr("id", "text"))
                           .attr("id", "middle"));
        }

        elements_t status_elements = status(date, version, copyright);
        if (!empty(status_elements)) {
            body.push_back(elt("div", status_elements).attr("id", "status"));
        }
        //if (!status.empty())
        //    body.push_back(elt("div", trim(status)).attr("id", "status"));

        if (!nav.empty() and !toc.empty())
            body.push_back(elt("div", "").attr("id", "pagecache"));

        if (!js_filenames.empty()) {
            auto js_elements { javascript(output_dir, js_filenames) };
            body.insert(std::end(body), std::begin(js_elements), std::end(js_elements));
        }
        elements_t result { preamble() };
        if (page_title.empty())
            page_title = title;
        result.push_back(
            elt("html",
                { head(page_title, css, css_filenames, local_fonts),
                  elt("body", body)
                }).attr("lang", "en"));
        return result;
    }


    void add_title(elements_t& body, const std::string& title, const std::string& logo)
    {
        // No title bar at all when there is nothing to put in it (an
        // untitled document); a logo alone still gets the bar.
        elements_t title_bar {};
        if (!trim(title).empty()) {
            title_bar.push_back(elt("span", trim(title)).attr("id", "title_text"));
        }
        if (!logo.empty()) {
            title_bar.push_back(elt("span", trim(logo)).attr("id", "logo"));
        }
        if (!title_bar.empty()) {
            body.push_back(elt("div", title_bar).attr("id", "title"));
        }
    }

    void add_nav(elements_t& body, int max_level)
    {
        body.push_back(navigation(max_level));
    }

    void add_text(elements_t& body, const std::string& text, const std::string& toc, bool text_only)
    {
        if (text_only) {
            elements_t content {};
            content.push_back(elt("div", text).attr("id", "text"));
            body.push_back(
                elt("div", elt("div", content).attr("id", "content")).attr("id", "middle"));
        } else {
            body.push_back(elt("div", middle(text, toc)).attr("id", "middle"));
        }

    }

    void add_bottom_spacer(elements_t& body)
    {
        body.push_back(elt("div").cls("endspace"));
    }

    void add_status_bar(
        elements_t& body, const std::string& date, const std::string& version, const std::string& copyright)
    {
        elements_t status_elements = status(date, version, copyright);
        if (!empty(status_elements)) {
            body.push_back(elt("div", status_elements).attr("id", "status"));
        }
    }        

    void add_page_cache(elements_t& body)
    {
            body.push_back(elt("div", "").attr("id", "pagecache"));
    }

    void add_js_links(elements_t& body, const strings_t& js_filenames, const std::string& output_dir) 
    {
        if (!js_filenames.empty()) {
            auto js_elements { javascript(output_dir, js_filenames) };
            body.insert(std::end(body), std::begin(js_elements), std::end(js_elements));
        }
    }

    elements_t make_page(
        elements_t body,
        std::string page_title, const std::string& css,
        const strings_t& css_filenames, const strings_t& local_fonts)
    {
        elements_t page { preamble() };
        page.push_back(
            elt("html",
                { head(page_title, css, css_filenames, local_fonts),
                  elt("body", body)
                }).attr("lang", "en"));
        return page;
    }

    bool tag_is_block_element(const std::string& tag)
    {
        auto location = std::find(block_elements.begin(), block_elements.end(), tag);
        return location != block_elements.end();
    }

    bool block_element(std::string par)
    {
        (void)K::log(3);
        bool result = false;
        par = trim(par);
        if (par[0] == '<') {
            static const std::regex element_rgx(R"(^\s*</?([^\s>]+).*?>)");
            std::smatch match {};
            std::regex_search(par, match, element_rgx);
            result = tag_is_block_element(match[1]);
        }
        return result;
    }

    std::string make_paragraphs(const std::string& html_text)
    {
        (void)K::log(3);
        std::string result {};
        //html_text = std::regex_replace(html_text, std::regex(R"((<pre .*?>)\s+)"), "$1\n");
        //html_text = std::regex_replace(html_text, std::regex(R"(\s+</pre>)"), "\n</pre>");
        static const std::regex paragraph_split_rgx(R"(\n *(\n *)+)");
        for (auto par : regex_split(html_text, paragraph_split_rgx)) {
            par = trim(par);
            if (par.size() > 0) {
                if (!block_element(par)) {
                    par = "<p>" + par + "</p>";
                }
                result += par + "\n\n";
            }
        }
        return result;
    }

    std::string minimize_css(const std::string& src)
    {
        (void)K::log(3);
        std::string result = src;
        //std::smatch match {};
        std::regex line_end_space(R"(\\}[ ]+)", std::regex::awk);
        result = regex_replace(result, line_end_space, "}");
        std::regex whitespace(R"([\n ]+)", std::regex::awk);
        result = regex_replace(result, whitespace, " ");
        std::regex line_end(R"(}[ ]+)", std::regex::awk);
        result = regex_replace(result, line_end, "}\n");
        return result;
    }

    std::string minimize_js(const std::string& src)
    {
        (void)K::log(3);
        return src;
    }

}
