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

#pragma once

#include "zeek/zeek-config.h"

#include <sys/stat.h>
#include <cerrno>
#include <ctime>
#include <map>
#include <string>
#include <vector>

#include "zeek/ID.h"
#include "zeek/Reporter.h"
#include "zeek/zeekygen/Configuration.h"
#include "zeek/zeekygen/SpicyModuleInfo.h"

namespace zeek {

class TypeDecl;

namespace zeekygen::detail {

class PackageInfo;
class ScriptInfo;

/**
 * Map of info objects.  Just a wrapper around std::map to improve code
 * readability (less typedefs for specific map types and not having to use
 * iterators directly to find a particular info object).
 */
template<class T>
struct InfoMap {
    using map_type = std::map<std::string, T*>;

    /**
     * @param name Name of an info object to retrieve.
     * @return The info object associated with \a name.
     */
    T* GetInfo(const std::string& name) const {
        typename map_type::const_iterator it = map.find(name);
        return it == map.end() ? 0 : it->second;
    }

    map_type map;
};

/**
 * Manages all documentation tracking and generation.
 */
class Manager {
public:
    /**
     * Ctor.
     * @param config Path to a Zeekygen config file if documentation is to be
     * written to disk.
     * @param command The command used to invoke the Zeek process.
     * It's used when checking for out-of-date targets.  If the Zeek binary is
     * newer then a target, it needs to be rebuilt.
     */
    Manager(const std::string& config, const std::string& command);

    /**
     * Dtor.
     */
    ~Manager();

    /**
     * Do initialization that needs to happen before scripts are parsed.
     * Currently nothing outside of what's done in ctor is needed.
     */
    void InitPreScript();

    /**
     * Do initialization that needs to happen after scripts are parsed.
     * This is primarily dependency resolution/filtering.
     */
    void InitPostScript();

    /**
     * Builds all Zeekygen targets specified by config file and write out
     * documentation to disk.
     */
    void GenerateDocs() const;

    /**
     * Register Zeek script for which information/documentation will be gathered.
     * @param path Absolute path to Zeek script.
     */
    void Script(const std::string& path);

    /**
     * Register Zeek script dependency ("@load").
     * @param path Absolute path to a Zeek script.
     * @param dep Absolute path to a Zeek script being "@load"d from script given
     * by \a path.
     */
    void ScriptDependency(const std::string& path, const std::string& dep);

    /**
     * Register a module usage (script may export identifiers into the
     * module namespace).
     * @param path Absolute path to a Zeek script.
     * @param module The module which script given by \a path is using.
     */
    void ModuleUsage(const std::string& path, const std::string& module);

    /**
     * Signal that a record or enum type is now being parsed.
     * @param id The record or enum type identifier.
     */
    void StartType(zeek::detail::IDPtr id);

    /**
     * Register a script-level identifier for which information/documentation
     * will be gathered.
     * @param id The script-level identifier.
     * @param from_redef  The identifier was created from a redef (e.g. an enum).
     */
    void Identifier(zeek::detail::IDPtr id, bool from_redef = false);

    /**
     * Register a record-field for which information/documentation will be
     * gathered.
     * @param id The identifier of the record type which has the field.
     * @param field The field name/type information.
     * @param path Absolute path to a Zeek script in which this field is
     * declared.  This can be different from the place where the record type
     * is declared due to redefs.
     * @param from_redef  The field is from a record redefinition.
     */
    void RecordField(const zeek::detail::ID* id, const TypeDecl* field, const std::string& path, bool from_redef);

    /**
     * Register a redefinition of a particular identifier.
     * @param id The identifier being redef'd.
     * @param path Absolute path to a Zeek script doing the redef.
     * @param ic The initialization class that was used (e.g. =, +=, -=).
     * @param init_expr The initialization expression that was used.
     */
    void Redef(const zeek::detail::ID* id, const std::string& path, zeek::detail::InitClass ic,
               zeek::detail::ExprPtr init_expr);
    void Redef(const zeek::detail::ID* id, const std::string& path,
               zeek::detail::InitClass ic = zeek::detail::INIT_NONE);

    /**
     * Register a Spicy EVT module.
     * @param info the module information
     */
    void AddSpicyModule(std::unique_ptr<SpicyModuleInfo> info) {
        spicy_modules.map[info->Name()] = info.get();
        all_info.push_back(info.release()); // switch to manual memory mgmt like all other infos
    }

    const auto& SpicyModules() const { return spicy_modules.map; }

    /**
     * Register Zeekygen script summary content.
     * @param path Absolute path to a Zeek script.
     * @param comment Zeekygen-style summary comment ("##!") to associate with
     * script given by \a path.
     */
    void SummaryComment(const std::string& path, const std::string& comment);

    /**
     * Register a Zeekygen comment ("##") for an upcoming identifier (i.e.
     * this content is buffered and consumed by next identifier/field
     * declaration.
     * @param comment Content of the Zeekygen comment.
     */
    void PreComment(const std::string& comment);

    /**
     * Register a Zeekygen comment ("##<") for the last identifier seen.
     * @param comment Content of the Zeekygen comment.
     * @param identifier_hint Expected name of identifier with which to
     * associate \a comment.
     */
    void PostComment(const std::string& comment, const std::string& identifier_hint = "");

    /**
     * @param id Name of script-level enum identifier.
     * @return The name of the enum's type.
     */
    std::string GetEnumTypeName(const std::string& id) const;

    /**
     * @param name Name of a script-level identifier.
     * @return an identifier info object associated with \a name or a null
     * pointer if it's not a known identifier.
     */
    IdentifierInfo* GetIdentifierInfo(const std::string& name) const { return identifiers.GetInfo(name); }

    /**
     * @param name Name of a Zeek script ("normalized" to be a path relative
     * to a component within ZEEKPATH).
     * @return a script info object associated with \a name or a null pointer
     * if it's not a known script name.
     */
    ScriptInfo* GetScriptInfo(const std::string& name) const { return scripts.GetInfo(name); }

    /**
     * @param name Name of a Zeek script package ("normalized" to be a path
     * relative to a component within ZEEKPATH).
     * @return a package info object associated with \a name or a null pointer
     * if it's not a known package name.
     */
    PackageInfo* GetPackageInfo(const std::string& name) const { return packages.GetInfo(name); }

    /**
     * Check if a Zeekygen target is up-to-date.
     * @param target_file output file of a Zeekygen target.
     * @param dependencies all dependencies of the target.
     * @return true if modification time of \a target_file is newer than
     * modification time of Zeek binary, Zeekygen config file, and all
     * dependencies, else false.
     */
    template<class T>
    bool IsUpToDate(const std::string& target_file, const std::vector<T*>& dependencies) const;

    /** Returns true if Zeekygen is enabled. */
    bool IsEnabled() const { return ! disabled; }

private:
    using comment_buffer_t = std::vector<std::string>;
    using comment_buffer_map_t = std::map<std::string, comment_buffer_t>;

    void DbgAndWarn(const char* msg) const;
    void WarnMissingScript(const char* type, const zeek::detail::ID* id, const std::string& script) const;

    IdentifierInfo* CreateIdentifierInfo(zeek::detail::IDPtr id, ScriptInfo* script, bool from_redef = false);

    bool disabled = false;
    bool enable_warnings = false;
    comment_buffer_t comment_buffer;         // For whatever next identifier comes in.
    comment_buffer_map_t comment_buffer_map; // For a particular identifier.
    InfoMap<PackageInfo> packages;
    InfoMap<ScriptInfo> scripts;
    InfoMap<IdentifierInfo> identifiers;
    InfoMap<SpicyModuleInfo> spicy_modules;
    std::vector<Info*> all_info;
    IdentifierInfo* last_identifier_seen = nullptr;
    IdentifierInfo* incomplete_type = nullptr;
    std::map<std::string, std::string> enum_mappings; // enum id -> enum type id
    Config config;
    time_t mtime = 0;
};

template<class T>
bool Manager::IsUpToDate(const std::string& target_file, const std::vector<T*>& dependencies) const {
    struct stat s;

    if ( stat(target_file.c_str(), &s) < 0 ) {
        if ( errno == ENOENT )
            // Doesn't exist.
            return false;

        reporter->InternalError("Zeekygen failed to stat target file '%s': %s", target_file.c_str(), strerror(errno));
    }

    if ( difftime(mtime, s.st_mtime) > 0 )
        return false;

    if ( difftime(config.GetModificationTime(), s.st_mtime) > 0 )
        return false;

    for ( size_t i = 0; i < dependencies.size(); ++i )
        if ( difftime(dependencies[i]->GetModificationTime(), s.st_mtime) > 0 )
            return false;

    return true;
}

} // namespace zeekygen::detail

namespace detail {

ZEEK_EXTERN_DATA zeekygen::detail::Manager* zeekygen_mgr;

} // namespace detail
} // namespace zeek
