#include <algorithm>
#include <functional>
#include <iomanip>
#include <ostream>
#include <set>

#include "coverage.h"
#include "machine.h"
#include "katom.h"
#include "util.h"

namespace {

using target_set = std::set<std::string>;

// The written definitions of one klammer, split by what they tell us.  A
// definition filed under the general name is the only one whose coverage has
// to be worked out; a definition written for a target IS its own statement.
struct Written
{
    const Klammer::components* m_general { nullptr };
    target_set m_targets {};       // targets with a definition of their own
    bool m_declared { false };     // has a ".k" declaration
};

Written written_definitions(const Klammer& klammer)
{
    Written w {};
    for (const auto& def : klammer.m_defs) {
        if (def.target == Target_registry::general_name) {
            w.m_general = &def;
        } else if (def.target == Target_registry::declare_name) {
            w.m_declared = true;
        } else if (def.target != Target_registry::optionset_name) {
            w.m_targets.insert(def.target);
        }
    }
    return w;
}

// What a general body is made of.  The three findings are ordered by how
// firmly they settle the question: anything the engine cannot interpret ends
// the analysis, and only a body of plain klammer calls is derivable.
struct Body_scan
{
    bool m_undecidable { false };
    std::string m_reason {};
    std::vector<std::string> m_calls {};
};

Body_scan scan_body(const katom_list& body)
{
    Body_scan scan {};
    target_set seen {};
    for (const auto& k : body) {
        switch (k.m_type) {
        case katom_t::eval_begin:
            // Which targets a Python function answers for is undecidable, so
            // the analysis stops here and the targets must be declared.
            scan.m_undecidable = true;
            scan.m_reason = "@eval body";
            return scan;
        case katom_t::read_begin:
            scan.m_undecidable = true;
            scan.m_reason = "@read body";
            return scan;
        case katom_t::literal_begin:
            // A ^'...'^ span exists to carry raw target markup past the
            // escaping pass.  A general body holding one is target-specific
            // with nothing for the intersection rule to see -- the blind spot
            // a body of plain text would otherwise hide.
            scan.m_undecidable = true;
            scan.m_reason = "^'...'^ literal span";
            return scan;
        case katom_t::apply_begin: {
            // The body read here is the STORED body, which is the body as
            // written with one exception: a general klammer that takes no
            // parameters is a constant, and a constant's body is spliced into
            // later definitions at definition time.  So a call to a constant
            // does not appear here -- what appears is whatever the constant
            // expanded to.  That is the right thing for coverage (the calls
            // that remain are the ones that will be applied), but it makes
            // the "from" list a statement about the stored body, not about
            // the source text.
            std::string name = trim_char(k.m_text, '@');
            if (seen.insert(name).second) {
                scan.m_calls.push_back("@" + name);
            }
            break;
        }
        default:
            break;
        }
    }
    return scan;
}

target_set intersect(const target_set& a, const target_set& b)
{
    target_set result {};
    std::set_intersection(a.begin(), a.end(), b.begin(), b.end(),
                          std::inserter(result, result.begin()));
    return result;
}

std::vector<std::string> as_vector(const target_set& s)
{
    return { s.begin(), s.end() };
}

// Shorten a definition's pathname for the report.  What identifies a
// definition to a reader is its tail -- "sks/block/block.k" -- not the
// absolute path the file happened to be read from, which is the same long
// prefix on every row.  A klammer set outside $KLAMMERTEXT_HOME keeps its
// path in full rather than being shortened to something ambiguous.
std::string short_path(const std::string& path)
{
    auto pos = path.rfind("/sks/");
    if (pos != std::string::npos) {
        return path.substr(pos + 1);
    }
    const char* home = std::getenv("KLAMMERTEXT_HOME");
    if (home != nullptr) {
        std::string prefix = std::string(home) + "/";
        if (path.size() > prefix.size() && path.compare(0, prefix.size(), prefix) == 0) {
            return path.substr(prefix.size());
        }
    }
    return path;
}

} // namespace

std::vector<Klammer_coverage> klammer_coverage(const Machine& machine,
                                               const strings_t& defined_outside)
{
    target_set all_targets {};
    for (const auto& t : machine.m_targets.user_defined()) {
        all_targets.insert(t);
    }

    // Pass 1: what each klammer's definitions say, without resolving anything.
    std::map<std::string, Written> written {};
    std::map<std::string, Body_scan> scans {};
    for (const auto& [name, klammer] : machine.m_klammers.m_klammers) {
        written[name] = written_definitions(klammer);
        if (written[name].m_general != nullptr) {
            scans[name] = scan_body(written[name].m_general->body);
        }
    }

    // Pass 2: the greatest fixpoint.  Every general klammer starts optimistic
    // -- all targets -- and the intersection rule is applied until nothing
    // shrinks.  Starting optimistic is what makes a cycle terminate: two
    // klammers calling each other simply keep each other's sets, and a set
    // that only loses members cannot iterate forever.
    std::map<std::string, target_set> general {};
    for (const auto& [name, scan] : scans) {
        general[name] = all_targets;
    }
    // A klammer's coverage, for use as an operand of the intersection: what
    // its own definitions cover, plus whatever its general body covers.
    auto coverage_of = [&](const std::string& name) -> target_set {
        auto w = written.find(name);
        if (w == written.end()) return {};     // not defined; contributes nothing
        target_set result = w->second.m_targets;
        auto g = general.find(name);
        if (g != general.end()) {
            result.insert(g->second.begin(), g->second.end());
        }
        return result;
    };
    bool changed = true;
    while (changed) {
        changed = false;
        for (auto& [name, targets] : general) {
            const Body_scan& scan = scans[name];
            if (scan.m_undecidable || scan.m_calls.empty()) continue;
            target_set next = all_targets;
            for (const auto& call : scan.m_calls) {
                std::string called = trim_char(call, '@');
                if (called == name) continue;  // self-reference constrains nothing
                next = intersect(next, coverage_of(called));
            }
            if (next != targets) {
                targets = next;
                changed = true;
            }
        }
    }

    // Pass 3: classify and collect.  The filter applies HERE and not earlier:
    // passes 1 and 2 must see every klammer, because a derived coverage is the
    // intersection of what the called klammers cover and those are mostly the
    // klammerset's.
    std::vector<Klammer_coverage> result {};
    for (const auto& [name, klammer] : machine.m_klammers.m_klammers) {
        if (!defined_outside.empty() && !klammer.defined_outside(defined_outside)) {
            continue;
        }
        const Written& w = written.at(name);
        Klammer_coverage kc {};
        kc.m_name = name;
        kc.m_declared = w.m_declared;
        kc.m_written = as_vector(w.m_targets);
        // Written definitions only (m_defs), so this is where the klammer is
        // WRITTEN.  m_defloc would also carry the targets a general body was
        // copied to, which name the same file again.
        for (const auto& def : klammer.m_defs) {
            std::string file = short_path(def.loc.m_filename);
            if (!is_in(file, kc.m_files)) {
                kc.m_files.push_back(file);
            }
        }
        for (const auto& [target, loc] : klammer.m_defloc) {
            if (target != Target_registry::declare_name &&
                target != Target_registry::general_name &&
                target != Target_registry::optionset_name) {
                kc.m_effective.push_back(target);
            }
        }
        if (klammer.m_general_declared) {
            // The author has said "every target".  That is a statement, not
            // something to be re-derived from the body: the whole point of
            // writing ".*" is to assert what an @eval body cannot be read to
            // mean.
            kc.m_kind = coverage_t::all_declared;
            target_set covered = w.m_targets;
            covered.insert(all_targets.begin(), all_targets.end());
            kc.m_targets = as_vector(covered);
        } else if (w.m_general == nullptr) {
            kc.m_kind = w.m_targets.empty() ? coverage_t::none : coverage_t::declared;
            kc.m_targets = kc.m_written;
        } else {
            const Body_scan& scan = scans.at(name);
            target_set covered = w.m_targets;
            if (scan.m_undecidable) {
                kc.m_kind = coverage_t::undecidable;
                kc.m_reason = scan.m_reason;
                // What it covers is not knowable here; report what is written.
                kc.m_targets = kc.m_written;
            } else if (scan.m_calls.empty()) {
                kc.m_kind = coverage_t::all;
                covered.insert(all_targets.begin(), all_targets.end());
                kc.m_targets = as_vector(covered);
            } else {
                kc.m_kind = coverage_t::derived;
                kc.m_from = scan.m_calls;
                const target_set& g = general.at(name);
                covered.insert(g.begin(), g.end());
                kc.m_targets = as_vector(covered);
            }
        }
        result.push_back(kc);
    }
    return result;
}

namespace {

std::string list_of(const std::vector<std::string>& v)
{
    return v.empty() ? "--" : join(v, " ");
}

void section(std::ostream& os, const std::string& title, size_t count,
             const std::string& explanation)
{
    os << "\n" << title << " (" << count << ")\n";
    if (!explanation.empty()) {
        os << explanation;
    }
}

} // namespace

void report_coverage(const Machine& machine,
                     const std::vector<Klammer_coverage>& coverage,
                     bool full, std::ostream& os)
{
    strings_t targets = machine.m_targets.user_defined();
    os << "Klammer coverage\n"
       << "================\n\n"
       << coverage.size() << " " << plural("klammer", static_cast<int>(coverage.size()))
       << ", " << targets.size() << " " << plural("target", static_cast<int>(targets.size()))
       << ": " << join(targets, " ") << "\n";

    auto of_kind = [&coverage](coverage_t kind) {
        std::vector<const Klammer_coverage*> result {};
        for (const auto& kc : coverage) {
            if (kc.m_kind == kind) result.push_back(&kc);
        }
        return result;
    };
    size_t width = 0;
    for (const auto& kc : coverage) {
        width = std::max(width, kc.m_name.size());
    }
    // The indent of a continuation line, under the name column.
    std::string continuation(2 + 1 + width, ' ');

    auto name_of = [&](const Klammer_coverage& kc) {
        std::stringstream ss {};
        ss << "  @" << std::left << std::setw(width) << kc.m_name;
        return ss.str();
    };

    // A section's rows, held until the whole section is built so the
    // source-file column can be aligned.  The file goes LAST because it is
    // reference information: what the row says comes first, and the reader
    // looks right only when they want to go and edit it.  Only a row naming
    // ONE klammer carries a file -- the "All targets" section lists many
    // names on a line and has nothing to attach one to.
    using Row = std::pair<std::string, std::string>;   // text, file
    auto emit = [&os, full](const std::vector<Row>& rows) {
        size_t text_width = 0;
        if (full) {
            for (const auto& [text, file] : rows) {
                if (!file.empty()) text_width = std::max(text_width, text.size());
            }
        }
        for (const auto& [text, file] : rows) {
            if (full && !file.empty()) {
                os << std::left << std::setw(text_width) << text << "  " << file << "\n";
            } else {
                os << trim_right(text) << "\n";
            }
        }
    };

    // ---- what works, first --------------------------------------------
    //
    // The reporting categories come before the problems because a terminal is
    // read from the BOTTOM: an eighty-klammer listing scrolls a three-line
    // warning off the screen entirely, so the actionable part has to be last,
    // where "| tail" finds it.
    //
    // The two halves of the report hide an empty category for different
    // reasons, so they are two functions rather than one with a flag.
    using Row_of = std::function<std::string(const Klammer_coverage&)>;
    // The explanation under a heading is for a reader learning the categories,
    // so it appears only under "all" -- the same argument that shows the empty
    // ones.  A default report is headings and rows.
    auto write = [&](const std::string& title,
                     const std::vector<const Klammer_coverage*>& group,
                     const std::string& explanation, const Row_of& row) {
        section(os, title, group.size(), full ? explanation : "");
        std::vector<Row> rows {};
        for (const auto* kc : group) {
            rows.push_back({row(*kc), join(kc->m_files, ", ")});
        }
        emit(rows);
    };

    // A REPORTING category describes the shape of the klammer set, so an
    // empty one still says something ("nothing here uses .*") and "all" shows
    // it as a designer's checklist.
    auto reporting = [&](const std::string& title,
                         const std::vector<const Klammer_coverage*>& group,
                         const std::string& explanation, const Row_of& row) {
        if (group.empty() && !full) return;
        write(title, group, explanation, row);
    };

    // A PROBLEM category is different: it sits under a banner that says
    // "Needs attention", and an empty one does not.  Printing "Covers no
    // target (0)" there states the opposite of the heading above it, so it is
    // hidden whether or not "all" was given -- "all" is for information that
    // is missing, and a category with nothing in it is not missing anything.
    auto problem = [&](const std::string& title,
                       const std::vector<const Klammer_coverage*>& group,
                       const std::string& explanation, const Row_of& row) {
        if (group.empty()) return;
        write(title, group, explanation, row);
    };
    auto with_targets = [&](const Klammer_coverage& kc) {
        return name_of(kc) + "  " + list_of(kc.m_targets);
    };

    reporting("Defined per target", of_kind(coverage_t::declared), "", with_targets);

    reporting("All targets, declared", of_kind(coverage_t::all_declared),
             "  Written \".*\": the author states that these work for every target,\n"
             "  including targets that do not exist yet.\n", with_targets);

    // Many names on one line, so no file column: there is nothing for a file
    // to attach to.
    auto all = of_kind(coverage_t::all);
    if (!all.empty() || full) {
        section(os, "All targets, derived", all.size(),
                full ? "  No target suffix and a body of plain text, so nothing in them is\n"
                       "  target-specific.  This is the writer's macro form -- a repeated\n"
                       "  phrase, not a klammer set -- and needs no declaration.\n" : "");
        strings_t names {};
        for (const auto* kc : all) {
            names.push_back("@" + kc->m_name);
        }
        if (!names.empty()) {
            os << "  " << join(names, " ") << "\n";
        }
    }

    // A derived klammer covering NOTHING is unusable, so it is reported with
    // the problems rather than here.
    std::vector<const Klammer_coverage*> derived {};
    std::vector<const Klammer_coverage*> uncoverable {};
    for (const auto* kc : of_kind(coverage_t::derived)) {
        (kc->m_targets.empty() ? uncoverable : derived).push_back(kc);
    }
    reporting("Derived from the klammers the body calls", derived,
             "  No target suffix and a body of klammer calls, so the coverage is the\n"
             "  intersection of what those klammers cover.\n",
             [&](const Klammer_coverage& kc) {
                 std::stringstream ss {};
                 ss << name_of(kc) << "  " << std::left << std::setw(22) << list_of(kc.m_targets)
                    << "  from " << join(kc.m_from, " ");
                 return ss.str();
             });

    // ---- then what needs doing ----------------------------------------
    //
    // Four categories, ordered by severity: the first two mean the klammer
    // cannot be used at all, the third that its coverage is a guess, the
    // fourth that it is undocumented.  Counted by DISTINCT klammer -- "no .k"
    // is orthogonal to the others, so a klammer can be in two categories and
    // summing the counts would overstate the work.
    auto undecidable = of_kind(coverage_t::undecidable);
    auto none = of_kind(coverage_t::none);
    std::vector<const Klammer_coverage*> undescribed {};
    for (const auto& kc : coverage) {
        if (!kc.m_declared) undescribed.push_back(&kc);
    }
    target_set needing {};
    for (const auto* group : { &none, &uncoverable, &undecidable, &undescribed }) {
        for (const auto* kc : *group) needing.insert(kc->m_name);
    }
    // Nothing to attend to, nothing said -- the same rule as the categories
    // below it.  "Needs attention: 0" under "all" was the banner contradicting
    // itself, exactly as an empty category under it would.
    if (!needing.empty()) {
        os << "\nNeeds attention: " << needing.size() << " "
           << plural("klammer", static_cast<int>(needing.size())) << "\n";
    }

    problem("Declared but never defined", none,
             "  A \".k\" declaration with no definition for any target, so the klammer\n"
             "  can never be applied.\n",
             [&](const Klammer_coverage& kc) { return name_of(kc); });
    problem("Covers no target", uncoverable,
             "  The klammers this one calls have no target in common, so the\n"
             "  intersection is empty and it can never be applied.  The klammers\n"
             "  named are the ones to look at.\n",
             [&](const Klammer_coverage& kc) {
                 return name_of(kc) + "  from " + join(kc.m_from, " ");
             });
    problem("Must be declared", undecidable,
             "  A general definition whose body the engine cannot interpret, so it is\n"
             "  offered to EVERY target whether or not its code answers for that\n"
             "  target.  Name the targets it does answer for -- @@name.html,tex :: --\n"
             "  or, if it works for any target at all, @@name.* ::\n",
             [&](const Klammer_coverage& kc) { return name_of(kc) + "  " + kc.m_reason; });
    problem("No \".k\" declaration", undescribed,
             "  These render, but nothing describes them: a klammer without a \".k\"\n"
             "  has no description, so kdesc can say nothing about what it does and\n"
             "  \"kdesc -k <text>\" can only find it by name.\n", with_targets);

    // The progress number.  "Undecided" is the accurate label, and the one
    // that asserts no more than was measured: a klammer that does not cover a
    // target has not been declared unavailable there -- no notation for that
    // exists yet -- it simply has no definition.  "Unsupported" or "excluded"
    // would each claim a decision nobody made.
    os << "\nBy target\n";
    for (const auto& target : targets) {
        size_t covered = 0;
        size_t offered = 0;
        for (const auto& kc : coverage) {
            if (is_in(target, kc.m_targets)) covered++;
            if (is_in(target, kc.m_effective)) offered++;
        }
        os << "  " << std::left << std::setw(10) << target
           << std::right << std::setw(4) << covered << " covered"
           << std::setw(6) << (coverage.size() - covered) << " undecided";
        if (offered > covered) {
            os << "   (" << offered - covered
               << " more currently offered by an underivable general body)";
        }
        os << "\n";
    }
}
