// See the file "COPYING" in the main distribution directory for copyright.

#include "zeek/zeekygen/ScriptInfo.h"

#include "zeek/DebugLogger.h"
#include "zeek/Desc.h"
#include "zeek/Reporter.h"
#include "zeek/Scope.h"
#include "zeek/Type.h"
#include "zeek/zeekygen/IdentifierInfo.h"
#include "zeek/zeekygen/Manager.h"
#include "zeek/zeekygen/ReStructuredTextTable.h"
#include "zeek/zeekygen/utils.h"

using namespace std;

namespace zeek::zeekygen::detail {

bool IdInfoComp::operator()(const IdentifierInfo* lhs, const IdentifierInfo* rhs) const {
    return lhs->Name() < rhs->Name();
}

static vector<string> summary_comment(const vector<string>& cmnts) {
    vector<string> rval;

    for ( const auto& cmnt : cmnts ) {
        size_t end = end_of_first_sentence(cmnt);

        if ( end == string::npos ) {
            if ( is_all_whitespace(cmnt) )
                break;

            rval.push_back(cmnt);
        }
        else {
            rval.push_back(cmnt.substr(0, end + 1));
            break;
        }
    }

    return rval;
}

static void add_summary_rows(const ODesc& id_desc, const vector<string>& cmnts, ReStructuredTextTable* table) {
    vector<string> row;
    row.emplace_back(id_desc.Description());

    if ( cmnts.empty() ) {
        row.emplace_back();
        table->AddRow(row);
        return;
    }

    row.push_back(cmnts[0]);
    table->AddRow(row);

    for ( size_t i = 1; i < cmnts.size(); ++i ) {
        row.clear();
        row.emplace_back();
        row.push_back(cmnts[i]);
        table->AddRow(row);
    }
}

static string make_summary(const string& heading, char underline, char border, const id_info_list& id_list) {
    if ( id_list.empty() )
        return "";

    ReStructuredTextTable table(2);

    for ( IdentifierInfo* info : id_list ) {
        auto* id = info->GetID();
        ODesc d;
        d.SetQuotes(true);
        id->DescribeReSTShort(&d);
        add_summary_rows(d, summary_comment(info->GetComments()), &table);
    }

    return make_heading(heading, underline) + table.AsString(border) + "\n";
}

static string make_redef_summary(const string& heading, char underline, char border, const string& from_script,
                                 const id_info_set& id_set) {
    if ( id_set.empty() )
        return "";

    ReStructuredTextTable table(2);

    for ( IdentifierInfo* info : id_set ) {
        auto* id = info->GetID();
        ODesc d;
        d.SetQuotes(true);
        id->DescribeReSTShort(&d);

        using redef_list = std::list<IdentifierInfo::Redefinition>;
        redef_list redefs = info->GetRedefs(from_script);

        for ( redef_list::const_iterator iit = redefs.begin(); iit != redefs.end(); ++iit ) {
            add_summary_rows(d, summary_comment(iit->comments), &table);

            if ( ! id->IsType() )
                continue;

            if ( id->GetType()->Tag() == TYPE_ENUM ) {
                for ( const auto& [enum_name, v] : id->GetType()->AsEnumType()->Names() ) {
                    auto info = zeek::detail::zeekygen_mgr->GetIdentifierInfo(enum_name);

                    if ( ! info )
                        continue;

                    if ( ! info->IsFromRedef() )
                        continue;

                    if ( ! info->GetDeclaringScript() )
                        continue;

                    if ( info->GetDeclaringScript()->Name() != from_script )
                        continue;

                    vector<string> row;
                    row.emplace_back("");
                    row.emplace_back("");
                    table.AddRow(row);

                    auto comments = info->GetComments();
                    auto summary_comments = summary_comment(comments);
                    auto enum_id = info->GetID();

                    auto colon = summary_comments.empty() ? "" : ":";
                    row[1] = util::fmt("* :zeek:enum:`%s`%s", enum_id->Name(), colon);
                    table.AddRow(row);

                    for ( auto& sc : summary_comments ) {
                        row[1] = util::fmt("  %s", sc.data());
                        table.AddRow(row);
                    }
                }
            }
            else if ( id->GetType()->Tag() == TYPE_RECORD ) {
                auto info = zeek::detail::zeekygen_mgr->GetIdentifierInfo(id->Name());

                if ( ! info || ! info->GetDeclaringScript() )
                    continue;

                auto rt = id->GetType()->AsRecordType();
                bool added_new_field_docs = false;

                for ( auto i = 0; i < rt->NumFields(); ++i ) {
                    auto field_name = rt->FieldName(i);

                    if ( ! info->FieldIsFromRedef(field_name) )
                        continue;

                    auto declaring_script = info->GetDeclaringScriptForField(field_name);

                    if ( declaring_script != from_script )
                        continue;

                    vector<string> row;
                    row.emplace_back("");
                    row.emplace_back("");
                    table.AddRow(row);

                    if ( ! added_new_field_docs ) {
                        added_new_field_docs = true;
                        row[1] = util::fmt(":New Fields: :zeek:type:`%s`", id->Name());
                        table.AddRow(row);
                        row[1] = "";
                        table.AddRow(row);
                    }

                    auto td = rt->FieldDecl(i);

                    ODesc fd;
                    fd.SetQuotes(true);
                    td->DescribeReST(&fd, true);

                    row[1] = util::fmt("  %s", fd.Description());
                    table.AddRow(row);

                    auto comments = info->GetFieldComments(field_name);
                    auto summary_comments = summary_comment(comments);

                    for ( auto& sc : summary_comments ) {
                        row[1] = util::fmt("    %s", sc.data());
                        table.AddRow(row);
                    }
                }
            }
        }
    }

    return make_heading(heading, underline) + table.AsString(border) + "\n";
}

static string make_details(const string& heading, char underline, const id_info_list& id_list) {
    if ( id_list.empty() )
        return "";

    string rval = make_heading(heading, underline);

    for ( IdentifierInfo* info : id_list ) {
        rval += info->ReStructuredText();
        rval += "\n\n";
    }

    return rval;
}

static string make_redef_details(const string& heading, char underline, const id_info_set& id_set) {
    if ( id_set.empty() )
        return "";

    string rval = make_heading(heading, underline);

    for ( IdentifierInfo* info : id_set ) {
        rval += info->ReStructuredText(true);
        rval += "\n\n";
    }

    return rval;
}

ScriptInfo::ScriptInfo(string arg_name, string arg_path)
    : Info(),
      name(std::move(arg_name)),
      path(std::move(arg_path)),
      is_pkg_loader(util::detail::is_package_loader(name)),
      dependencies(),
      module_usages(),
      comments(),
      id_info(),
      redef_options(),
      constants(),
      state_vars(),
      types(),
      events(),
      hooks(),
      functions(),
      redefs() {}

void ScriptInfo::AddIdentifierInfo(IdentifierInfo* info) { id_info[info->Name()] = info; }

void ScriptInfo::DoInitPostScript() {
    for ( id_info_map::const_iterator it = id_info.begin(); it != id_info.end(); ++it ) {
        IdentifierInfo* info = it->second;
        auto* id = info->GetID();

        if ( ! is_public_api(id) )
            continue;

        if ( id->IsType() ) {
            types.push_back(info);
            DBG_LOG(DBG_ZEEKYGEN, "Filter id '%s' in '%s' as a type", id->Name(), name.c_str());
            continue;
        }

        if ( IsFunc(id->GetType()->Tag()) ) {
            switch ( id->GetType()->AsFuncType()->Flavor() ) {
                case FUNC_FLAVOR_HOOK:
                    DBG_LOG(DBG_ZEEKYGEN, "Filter id '%s' in '%s' as a hook", id->Name(), name.c_str());
                    hooks.push_back(info);
                    break;
                case FUNC_FLAVOR_EVENT:
                    DBG_LOG(DBG_ZEEKYGEN, "Filter id '%s' in '%s' as a event", id->Name(), name.c_str());
                    events.push_back(info);
                    break;
                case FUNC_FLAVOR_FUNCTION:
                    DBG_LOG(DBG_ZEEKYGEN, "Filter id '%s' in '%s' as a function", id->Name(), name.c_str());
                    functions.push_back(info);
                    break;
                default: reporter->InternalError("Invalid function flavor"); break;
            }

            continue;
        }

        if ( id->IsConst() ) {
            if ( id->GetAttr(zeek::detail::ATTR_REDEF) ) {
                DBG_LOG(DBG_ZEEKYGEN, "Filter id '%s' in '%s' as a redef_option", id->Name(), name.c_str());
                redef_options.push_back(info);
            }
            else {
                DBG_LOG(DBG_ZEEKYGEN, "Filter id '%s' in '%s' as a constant", id->Name(), name.c_str());
                constants.push_back(info);
            }

            continue;
        }
        else if ( id->IsOption() ) {
            DBG_LOG(DBG_ZEEKYGEN, "Filter id '%s' in '%s' as an runtime option", id->Name(), name.c_str());
            options.push_back(info);

            continue;
        }

        if ( id->GetType()->Tag() == TYPE_ENUM )
            // Enums are always referenced/documented from the type's
            // documentation.
            continue;

        DBG_LOG(DBG_ZEEKYGEN, "Filter id '%s' in '%s' as a state variable", id->Name(), name.c_str());
        state_vars.push_back(info);
    }

    // The following enum types are automatically created internally in Zeek,
    // so just manually associating them with scripts for now.
    if ( name == "base/frameworks/input/main.zeek" ) {
        const auto& id = zeek::detail::global_scope()->Find("Input::Reader");
        types.push_back(new IdentifierInfo(id, this));
    }
    else if ( name == "base/init-bare.zeek" ) {
        const auto& id = zeek::detail::global_scope()->Find("ConnKey::Tag");
        types.push_back(new IdentifierInfo(id, this));
    }
    else if ( name == "base/frameworks/logging/main.zeek" ) {
        const auto& id = zeek::detail::global_scope()->Find("Log::Writer");
        types.push_back(new IdentifierInfo(id, this));
    }
    else if ( name == "base/frameworks/cluster/main.zeek" ) {
        const auto& backend_id = zeek::detail::global_scope()->Find("Cluster::BackendTag");
        types.push_back(new IdentifierInfo(backend_id, this));
        const auto& event_serializer_id = zeek::detail::global_scope()->Find("Cluster::EventSerializerTag");
        types.push_back(new IdentifierInfo(event_serializer_id, this));
        const auto& log_serializer_id = zeek::detail::global_scope()->Find("Cluster::LogSerializerTag");
        types.push_back(new IdentifierInfo(log_serializer_id, this));
    }
    else if ( name == "base/frameworks/storage/main.zeek" ) {
        const auto& backend_id = zeek::detail::global_scope()->Find("Storage::Backend");
        types.push_back(new IdentifierInfo(backend_id, this));
        const auto& serializer_id = zeek::detail::global_scope()->Find("Storage::Serializer");
        types.push_back(new IdentifierInfo(serializer_id, this));
    }
}

vector<string> ScriptInfo::GetComments() const { return comments; }

string ScriptInfo::DoReStructuredText(bool roles_only) const {
    string rval;

    rval += ":tocdepth: 3\n\n";
    rval += make_heading(name, '=');

    for ( const auto& usage : module_usages )
        rval += ".. zeek:namespace:: " + usage + "\n";

    rval += "\n";

    for ( const auto& cmnt : comments )
        rval += cmnt + "\n";

    rval += "\n";

    if ( ! module_usages.empty() ) {
        rval += module_usages.size() > 1 ? ":Namespaces: " : ":Namespace: ";

        for ( string_set::const_iterator it = module_usages.begin(); it != module_usages.end(); ++it ) {
            if ( it != module_usages.begin() )
                rval += ", ";

            rval += *it;
        }

        rval += "\n";
    }

    if ( ! dependencies.empty() ) {
        rval += ":Imports: ";

        for ( string_set::const_iterator it = dependencies.begin(); it != dependencies.end(); ++it ) {
            if ( it != dependencies.begin() )
                rval += ", ";

            string path = util::find_script_file(*it, util::zeek_path());
            string doc = *it;

            if ( ! path.empty() && util::is_dir(path.c_str()) )
                // Reference the package.
                doc += "/index";

            rval += util::fmt(":doc:`%s </scripts/%s>`", it->c_str(), doc.c_str());
        }

        rval += "\n";
    }

    // rval += util::fmt(":Source File: :download:`/scripts/%s`\n", name.c_str());
    rval += "\n";
    rval += make_heading("Summary", '~');
    rval += make_summary("Runtime Options", '#', '=', options);
    rval += make_summary("Redefinable Options", '#', '=', redef_options);
    rval += make_summary("Constants", '#', '=', constants);
    rval += make_summary("State Variables", '#', '=', state_vars);
    rval += make_summary("Types", '#', '=', types);
    rval += make_redef_summary("Redefinitions", '#', '=', name, redefs);
    rval += make_summary("Events", '#', '=', events);
    rval += make_summary("Hooks", '#', '=', hooks);
    rval += make_summary("Functions", '#', '=', functions);
    rval += "\n";
    rval += make_heading("Detailed Interface", '~');
    rval += make_details("Runtime Options", '#', options);
    rval += make_details("Redefinable Options", '#', redef_options);
    rval += make_details("Constants", '#', constants);
    rval += make_details("State Variables", '#', state_vars);
    rval += make_details("Types", '#', types);
    // rval += make_redef_details("Redefinitions", '#', redefs);
    rval += make_details("Events", '#', events);
    rval += make_details("Hooks", '#', hooks);
    rval += make_details("Functions", '#', functions);

    return rval;
}

time_t ScriptInfo::DoGetModificationTime() const {
    time_t most_recent = get_mtime(path);

    for ( const auto& dep : dependencies ) {
        Info* info = zeek::detail::zeekygen_mgr->GetScriptInfo(dep);

        if ( ! info ) {
            string pkg_name = dep + "/__load__.zeek";
            info = zeek::detail::zeekygen_mgr->GetScriptInfo(pkg_name);

            if ( ! info )
                reporter->InternalWarning("Zeekygen failed to get mtime of %s", dep.c_str());
            continue;
        }

        time_t dep_mtime = info->GetModificationTime();

        if ( dep_mtime > most_recent )
            most_recent = dep_mtime;
    }

    return most_recent;
}

} // namespace zeek::zeekygen::detail
