/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * 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 "debug_state_dumper.hpp"
#include "debug_state_context.hpp"
#include "check_dumper.hpp"
#include "document_impl.hpp"
#include "sheet_impl.hpp"
#include "ostream_utils.hpp"

#include <ixion/formula_name_resolver.hpp>
#include <ixion/named_expressions_iterator.hpp>

#include <fstream>
#include <algorithm>

namespace orcus { namespace spreadsheet { namespace detail {

namespace {

constexpr const char* indent_unit_s = "  ";

void print_named_expressions(const ixion::model_context& cxt, ixion::named_expressions_iterator iter, std::ostream& os)
{
    auto resolver = ixion::formula_name_resolver::get(ixion::formula_name_resolver_t::excel_a1, &cxt);

    if (!resolver)
        return;

    const ixion::abs_address_t origin{0, 0, 0};
    ixion::print_config config;
    config.display_sheet = ixion::display_sheet_t::always;

    for (; iter.has(); iter.next())
    {
        auto name = iter.get();

        std::string exp = ixion::print_formula_tokens(
            config, cxt, origin, *resolver, name.expression->tokens);

        os << "- name: " << *name.name << std::endl;
        os << "  origin: " << resolver->get_name(name.expression->origin, origin, true) << std::endl;
        os << "  expression: " << exp << std::endl;
    }
}

void print_auto_filter(const auto_filter_t& filter, std::ostream& os)
{
    using print_node_type = std::function<void(const filter_node_t& node, int level)>;

    print_node_type _print_node = [&_print_node, &os](const filter_node_t& node, int level)
    {
        std::string base_indent;
        for (int i = 0; i < level; ++i)
            base_indent.append(indent_unit_s);

        os << base_indent << indent_unit_s << "operator: " << node.op() << "\n";

        if (!node.empty())
        {
            os << base_indent << indent_unit_s << "children:\n";
            std::string bullet = indent_unit_s;
            bullet[0] = '-';
            std::string indent = base_indent + indent_unit_s + indent_unit_s;

            for (std::size_t pos = 0; pos < node.size(); ++pos)
            {
                const filterable* child = node.at(pos);

                if (auto* child_node = dynamic_cast<const filter_node_t*>(child); child_node)
                {
                    os << indent << bullet << "type: filter-node\n";
                    _print_node(*child_node, level + 2);
                }
                else if (auto* item = dynamic_cast<const filter_item_t*>(child); item)
                {
                    os << indent << bullet << "type: filter-item\n";
                    os << indent << indent_unit_s << "field: " << item->field() << "\n";
                    os << indent << indent_unit_s << "operator: " << item->op() << "\n";
                    os << indent << indent_unit_s << "regex: " << item->regex() << "\n";
                    os << indent << indent_unit_s << "value:\n";

                    filter_value_t v = item->value();
                    switch (v.type())
                    {
                        case filter_value_t::value_type::empty:
                            os << indent << indent_unit_s << indent_unit_s<< "type: empty\n";
                            break;
                        case filter_value_t::value_type::numeric:
                            os << indent << indent_unit_s << indent_unit_s<< "type: numeric\n";
                            os << indent << indent_unit_s << indent_unit_s<< "value: " << v.numeric() << "\n";
                            break;
                        case filter_value_t::value_type::string:
                            os << indent << indent_unit_s << indent_unit_s<< "type: string\n";
                            os << indent << indent_unit_s << indent_unit_s<< "value: " << v.string() << "\n";
                            break;
                    }
                }
                else if (auto* item_set = dynamic_cast<const filter_item_set_t*>(child); item_set)
                {
                    os << indent << bullet << "type: filter-item-set\n";
                    os << indent << indent_unit_s << "field: " << item_set->field() << "\n";
                    os << indent << indent_unit_s << "values:\n";

                    for (const auto& value : item_set->values())
                        os << indent << indent_unit_s << indent_unit_s << bullet << value << "\n";
                }
            }
        }
    };

    os << "filter-root:\n";
    _print_node(filter.root, 0);
}

} // anonymous namespace

doc_debug_state_dumper::doc_debug_state_dumper(const debug_state_context& cxt, const document_impl& doc) :
    m_cxt(cxt), m_doc(doc) {}

void doc_debug_state_dumper::dump(const fs::path& outdir) const
{
    dump_properties(outdir);
    dump_styles(outdir);
    dump_named_expressions(outdir);
}

void doc_debug_state_dumper::dump_properties(const fs::path& outdir) const
{
    const fs::path outpath = outdir / "properties.yaml";
    std::ofstream of{outpath.native()};
    if (!of)
        return;

    of << "formula-grammar: " << m_doc.grammar << std::endl;
    of << "origin-date: " << m_doc.origin_date << std::endl;
    of << "output-precision: " << short(m_doc.doc_config.output_precision) << std::endl;
}

void doc_debug_state_dumper::dump_styles(const fs::path& outdir) const
{
    const fs::path outpath = outdir / "styles.yaml";
    std::ofstream of{outpath.native()};
    if (!of)
        return;

    of << std::boolalpha;

    auto to_string = [](std::optional<bool> v) -> std::string
    {
        if (!v)
            return "(unset)";

        return *v ? "true" : "false";
    };

    auto dump_xf = [&of,to_string](std::size_t i, const cell_format_t& xf)
    {
        of << "  - id: " << i << std::endl
           << "    font: " << xf.font << std::endl
           << "    fill: " << xf.fill << std::endl
           << "    border: " << xf.border << std::endl
           << "    protection: " << xf.protection << std::endl
           << "    number-format: " << xf.number_format << std::endl
           << "    style-xf: " << xf.style_xf << std::endl
           << "    horizontal-alignment: " << xf.hor_align << std::endl
           << "    vertical-alignment: " << xf.ver_align << std::endl
           << "    apply-number-format: " << xf.apply_num_format << std::endl
           << "    apply-font: " << xf.apply_font << std::endl
           << "    apply-fill: " << xf.apply_fill << std::endl
           << "    apply-border: " << xf.apply_border << std::endl
           << "    apply-alignment: " << xf.apply_alignment << std::endl
           << "    apply-protection: " << xf.apply_protection << std::endl
           << "    wrap-text: " << to_string(xf.wrap_text) << std::endl
           << "    shrink-to-fit: " << to_string(xf.shrink_to_fit) << std::endl;
    };

    auto optional_value = [&of](std::string_view name, const auto& v, int level=2)
    {
        // v is of type std::optional<T>.

        constexpr char q = '"';

        std::string indent = indent_unit_s;
        for (int i = 0; i < level - 1; ++i)
            indent += indent_unit_s;

        of << indent << name << ": ";

        if (v)
        {
            std::ostringstream os;
            os << *v;
            std::string s = os.str();
            bool quote = s.find_first_of("#:-") != s.npos;
            if (quote)
                of << q << s << q;
            else
                of << s;
        }
        else
            of << "(unset)";

        of << std::endl;
    };

    auto dump_border = [&optional_value](const border_attrs_t& _attrs)
    {
        optional_value("style", _attrs.style, 3);
        optional_value("color", _attrs.border_color, 3);
        optional_value("width", _attrs.border_width, 3);
    };

    of << "cell-styles:" << std::endl;

    for (std::size_t i = 0; i < m_doc.styles_store.get_cell_styles_count(); ++i)
    {
        const cell_style_t* cs = m_doc.styles_store.get_cell_style(i);
        assert(cs);

        of << "  - id: " << i << std::endl
           << "    name: " << cs->name << std::endl
           << "    display-name: " << cs->display_name << std::endl
           << "    parent: " << cs->parent_name << std::endl
           << "    xf: " << cs->xf << std::endl
           << "    builtin: " << cs->builtin << std::endl;
    }

    of << "cell-style-formats:" << std::endl;

    for (std::size_t i = 0; i < m_doc.styles_store.get_cell_style_formats_count(); ++i)
    {
        const cell_format_t* xf = m_doc.styles_store.get_cell_style_format(i);
        assert(xf);
        dump_xf(i, *xf);
    }

    of << "cell-formats:" << std::endl;

    for (std::size_t i = 0; i < m_doc.styles_store.get_cell_formats_count(); ++i)
    {
        const cell_format_t* xf = m_doc.styles_store.get_cell_format(i);
        assert(xf);
        dump_xf(i, *xf);
    }

    of << "fonts:" << std::endl;

    for (std::size_t i = 0; i < m_doc.styles_store.get_font_count(); ++i)
    {
        const font_t* font = m_doc.styles_store.get_font(i);
        assert(font);

        of << "  - id: " << i << std::endl;
        optional_value("name", font->name, 2);
        optional_value("name-asian", font->name_asian, 2);
        optional_value("name-complex", font->name_complex, 2);
        optional_value("size", font->size, 2);
        optional_value("size-asian", font->size_asian, 2);
        optional_value("size-complex", font->size_complex, 2);
        optional_value("bold", font->bold, 2);
        optional_value("bold-asian", font->bold_asian, 2);
        optional_value("bold-complex", font->bold_complex, 2);
        optional_value("italic", font->italic, 2);
        optional_value("italic-asian", font->italic_asian, 2);
        optional_value("italic-complex", font->italic_complex, 2);
        optional_value("underline-style", font->underline.style, 2);
        optional_value("underline-width", font->underline.thickness, 2);
        optional_value("underline-mode", font->underline.spacing, 2);
        optional_value("underline-type", font->underline.count, 2);
        optional_value("underline-color", font->underline.color, 2);
        optional_value("color", font->color, 2);
        optional_value("strikethrough-style", font->strikethrough.style, 2);
        optional_value("strikethrough-width", font->strikethrough.width, 2);
        optional_value("strikethrough-type", font->strikethrough.type, 2);
        optional_value("strikethrough-text", font->strikethrough.text, 2);
    }

    of << "fills:" << std::endl;

    for (std::size_t i = 0; i < m_doc.styles_store.get_fill_count(); ++i)
    {
        const fill_t* fill = m_doc.styles_store.get_fill(i);
        assert(fill);

        of << "  - id: " << i << std::endl;
        optional_value("pattern", fill->pattern_type, 2);
        optional_value("fg-color", fill->fg_color, 2);
        optional_value("bg-color", fill->bg_color, 2);
    }

    of << "borders:" << std::endl;

    for (std::size_t i = 0; i < m_doc.styles_store.get_border_count(); ++i)
    {
        const border_t* border = m_doc.styles_store.get_border(i);
        assert(border);

        of << "  - id: " << i << std::endl;

        of << "    top:" << std::endl;
        dump_border(border->top);
        of << "    bottom:" << std::endl;
        dump_border(border->bottom);
        of << "    left:" << std::endl;
        dump_border(border->left);
        of << "    right:" << std::endl;
        dump_border(border->right);
        of << "    diagonal:" << std::endl;
        dump_border(border->diagonal);
        of << "    diagonal-bl-tr:" << std::endl;
        dump_border(border->diagonal_bl_tr);
        of << "    diagonal-tl-br:" << std::endl;
        dump_border(border->diagonal_tl_br);
    }

    of << "protections:" << std::endl;

    for (std::size_t i = 0; i < m_doc.styles_store.get_protection_count(); ++i)
    {
        const protection_t* prot = m_doc.styles_store.get_protection(i);
        assert(prot);

        of << "  - id: " << i << std::endl;
        optional_value("locked", prot->locked, 2);
        optional_value("hidden", prot->hidden, 2);
        optional_value("print-content", prot->print_content, 2);
        optional_value("formula-hidden", prot->formula_hidden, 2);
    }

    of << "number-formats:" << std::endl;

    for (std::size_t i = 0; i < m_doc.styles_store.get_number_format_count(); ++i)
    {
        const number_format_t* numfmt = m_doc.styles_store.get_number_format(i);
        assert(numfmt);

        of << "  - id: " << i << std::endl;
        optional_value("identifier", numfmt->identifier, 2);
        optional_value("format-string", numfmt->format_string, 2);
    }
}

void doc_debug_state_dumper::dump_named_expressions(const fs::path& outdir) const
{
    const fs::path outpath = outdir / "named-expressions.yaml";
    std::ofstream of{outpath.native()};
    if (!of)
        return;

    print_named_expressions(m_doc.context, m_doc.context.get_named_expressions_iterator(), of);
}

sheet_debug_state_dumper::sheet_debug_state_dumper(
    const debug_state_context& cxt, const sheet_impl& sheet, std::string_view sheet_name) :
    m_cxt(cxt), m_sheet(sheet), m_sheet_name(sheet_name) {}

void sheet_debug_state_dumper::dump(const fs::path& outdir) const
{
    dump_cell_values(outdir);
    dump_cell_formats(outdir);
    dump_column_formats(outdir);
    dump_row_formats(outdir);
    dump_column_widths(outdir);
    dump_row_heights(outdir);
    dump_auto_filter(outdir);
    dump_named_expressions(outdir);
}

void sheet_debug_state_dumper::dump_cell_values(const fs::path& outdir) const
{
    check_dumper dumper{m_sheet, m_sheet_name};
    fs::path outpath = outdir / "cell-values.txt";
    std::ofstream of{outpath.native()};
    if (of)
        dumper.dump(of);
}

void sheet_debug_state_dumper::dump_cell_formats(const fs::path& outdir) const
{
    fs::path outpath = outdir / "cell-formats.yaml";
    std::ofstream of{outpath.native()};
    if (!of)
        return;

    std::vector<col_t> columns;
    for (const auto& node : m_sheet.cell_formats)
        columns.push_back(node.first);

    std::sort(columns.begin(), columns.end());

    for (const col_t col : columns)
    {
        of << "column: " << col << std::endl;

        auto it = m_sheet.cell_formats.find(col);
        assert(it != m_sheet.cell_formats.end());
        const segment_row_index_type& rows = *it->second;

        for (const auto& seg : rows.segment_range())
        {
            // NB: end position is not inclusive.
            of << "  - rows: " << seg.start << '-' << (seg.end - 1) << std::endl;
            of << "    xf: " << seg.value << std::endl;
        }
    }
}

void sheet_debug_state_dumper::dump_column_formats(const fs::path& outdir) const
{
    fs::path outpath = outdir / "column-formats.yaml";
    std::ofstream of{outpath.native()};
    if (!of)
        return;

    for (const auto& seg : m_sheet.column_formats.segment_range())
    {
        of << "- columns: " << seg.start << '-' << (seg.end - 1) << std::endl;
        of << "  xf: " << seg.value << std::endl;
    }
}

void sheet_debug_state_dumper::dump_row_formats(const fs::path& outdir) const
{
    fs::path outpath = outdir / "row-formats.yaml";
    std::ofstream of{outpath.native()};
    if (!of)
        return;

    for (const auto& seg : m_sheet.row_formats.segment_range())
    {
        of << "- rows: " << seg.start << '-' << (seg.end - 1) << std::endl;
        of << "  xf: " << seg.value << std::endl;
    }
}

void sheet_debug_state_dumper::dump_column_widths(const fs::path& outdir) const
{
    fs::path outpath = outdir / "column-widths.yaml";
    std::ofstream of{outpath.native()};
    if (!of)
        return;

    for (const auto& seg : m_sheet.col_widths.segment_range())
    {
        of << "- columns: " << seg.start << '-' << (seg.end - 1) << std::endl;
        of << "  width: ";

        if (seg.value == get_default_column_width())
            of << "(default)";
        else
            of << seg.value;

        of << std::endl;
    }
}

void sheet_debug_state_dumper::dump_row_heights(const fs::path& outdir) const
{
    fs::path outpath = outdir / "row-heights.yaml";
    std::ofstream of{outpath.native()};
    if (!of)
        return;

    for (const auto& seg : m_sheet.row_heights.segment_range())
    {
        of << "- rows: " << seg.start << '-' << (seg.end - 1) << std::endl;
        of << "  height: ";

        if (seg.value == get_default_row_height())
            of << "(default)";
        else
            of << seg.value;

        of << std::endl;
    }
}

void sheet_debug_state_dumper::dump_auto_filter(const fs::path& outdir) const
{
    if (!m_sheet.auto_filter)
        return;

    fs::path outpath = outdir / "auto-filter.yaml";
    std::ofstream of{outpath.native()};
    if (!of)
        return;

    const auto& filter = *m_sheet.auto_filter;
    of << "range: " << m_cxt.print_range(filter.range) << "\n";
    print_auto_filter(filter, of);
}

void sheet_debug_state_dumper::dump_named_expressions(const fs::path& outdir) const
{
    const fs::path outpath = outdir / "named-expressions.yaml";
    std::ofstream of{outpath.native()};
    if (!of)
        return;

    const ixion::model_context& cxt = m_sheet.doc.get_model_context();
    print_named_expressions(cxt, cxt.get_named_expressions_iterator(m_sheet.sheet_id), of);
}

}}}

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
