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

#include "zeek/zeekygen/Manager.h"

#include <cstdlib>
#include <utility>

#include "zeek/DebugLogger.h"
#include "zeek/Expr.h"
#include "zeek/util.h"
#include "zeek/zeekygen/IdentifierInfo.h"
#include "zeek/zeekygen/Info.h"
#include "zeek/zeekygen/PackageInfo.h"
#include "zeek/zeekygen/ScriptInfo.h"
#include "zeek/zeekygen/utils.h"

using namespace std;

namespace zeek::zeekygen::detail
	{

static void DbgAndWarn(const char* msg)
	{
	if ( reporter->Errors() )
		// We've likely already reported to real source of the problem
		// as an error, avoid adding an additional warning which may
		// be confusing.
		return;

	reporter->Warning("%s", msg);
	DBG_LOG(DBG_ZEEKYGEN, "%s", msg);
	}

static void WarnMissingScript(const char* type, const zeek::detail::ID* id, const string& script)
	{
	if ( script == "<command line>" )
		return;

	DbgAndWarn(util::fmt("Can't generate Zeekygen doumentation for %s %s, "
	                     "lookup of %s failed",
	                     type, id->Name(), script.c_str()));
	}

static string RemoveLeadingSpace(const string& s)
	{
	if ( s.empty() || s[0] != ' ' )
		return s;

	// Treat "##Text" and "## Text" the same, so that a single space doesn't
	// cause reST formatting to think the later is indented a level.
	string rval = s;
	rval.erase(0, 1);
	return rval;
	}

Manager::Manager(const string& arg_config, const string& bro_command)
	: disabled(), comment_buffer(), comment_buffer_map(), packages(), scripts(), identifiers(),
	  all_info(), last_identifier_seen(), incomplete_type(), enum_mappings(), config(arg_config),
	  bro_mtime()
	{
	if ( getenv("ZEEK_DISABLE_ZEEKYGEN") )
		disabled = true;

	// If running bro without the "-X" option, then we don't need bro_mtime.
	if ( disabled || arg_config.empty() )
		return;

	// Find the absolute or relative path to bro by checking each PATH
	// component and also the current directory (so that this works if
	// bro_command is a relative path).
	const char* env_path = getenv("PATH");
	string path = env_path ? string(env_path) + ":." : ".";
	string path_to_bro = util::find_file(bro_command, path);
	struct stat s;

	// One way that find_file() could fail is when bro is located in
	// a PATH component that starts with a tilde (such as "~/bin").  A simple
	// workaround is to just run bro with a relative or absolute path.
	if ( path_to_bro.empty() || stat(path_to_bro.c_str(), &s) < 0 )
		reporter->InternalError("Zeekygen can't get mtime of zeek binary %s (try again by "
		                        "specifying the absolute or relative path to Zeek): %s",
		                        path_to_bro.c_str(), strerror(errno));

	// Internal error will abort above in the case that stat isn't initialized
	// NOLINTNEXTLINE(clang-analyzer-core.uninitialized.Assign)
	bro_mtime = s.st_mtime;
	}

Manager::~Manager()
	{
	for ( size_t i = 0; i < all_info.size(); ++i )
		delete all_info[i];
	}

void Manager::InitPreScript()
	{
	if ( disabled )
		return;
	}

void Manager::InitPostScript()
	{
	if ( disabled )
		return;

	for ( size_t i = 0; i < all_info.size(); ++i )
		all_info[i]->InitPostScript();

	config.FindDependencies(all_info);
	}

void Manager::GenerateDocs() const
	{
	if ( disabled )
		return;

	config.GenerateDocs();
	}

void Manager::Script(const string& path)
	{
	if ( disabled )
		return;

	string name = normalize_script_path(path);

	if ( scripts.GetInfo(name) )
		{
		DbgAndWarn(util::fmt("Duplicate Zeekygen script documentation: %s", name.c_str()));
		return;
		}

	ScriptInfo* info = new ScriptInfo(name, path);
	scripts.map[name] = info;
	all_info.push_back(info);
	DBG_LOG(DBG_ZEEKYGEN, "Made ScriptInfo %s", name.c_str());

	if ( ! info->IsPkgLoader() )
		return;

	name = util::SafeDirname(name).result;

	if ( packages.GetInfo(name) )
		{
		DbgAndWarn(util::fmt("Duplicate Zeekygen package documentation: %s", name.c_str()));
		return;
		}

	PackageInfo* pkginfo = new PackageInfo(name);
	packages.map[name] = pkginfo;
	all_info.push_back(pkginfo);
	DBG_LOG(DBG_ZEEKYGEN, "Made PackageInfo %s", name.c_str());
	}

void Manager::ScriptDependency(const string& path, const string& dep)
	{
	if ( disabled )
		return;

	if ( dep.empty() )
		{
		DbgAndWarn(util::fmt("Empty Zeekygen script doc dependency: %s", path.c_str()));
		return;
		}

	string name = normalize_script_path(path);
	string depname = normalize_script_path(dep);
	ScriptInfo* script_info = scripts.GetInfo(name);

	if ( ! script_info )
		{
		DbgAndWarn(util::fmt("Failed to add Zeekygen script doc dependency %s "
		                     "for %s",
		                     depname.c_str(), name.c_str()));
		return;
		}

	script_info->AddDependency(depname);
	DBG_LOG(DBG_ZEEKYGEN, "Added script dependency %s for %s", depname.c_str(), name.c_str());

	for ( size_t i = 0; i < comment_buffer.size(); ++i )
		DbgAndWarn(
			util::fmt("Discarded extraneous Zeekygen comment: %s", comment_buffer[i].c_str()));
	}

void Manager::ModuleUsage(const string& path, const string& module)
	{
	if ( disabled )
		return;

	string name = normalize_script_path(path);
	ScriptInfo* script_info = scripts.GetInfo(name);

	if ( ! script_info )
		{
		DbgAndWarn(util::fmt("Failed to add Zeekygen module usage %s in %s", module.c_str(),
		                     name.c_str()));
		return;
		}

	script_info->AddModule(module);
	DBG_LOG(DBG_ZEEKYGEN, "Added module usage %s in %s", module.c_str(), name.c_str());
	}

IdentifierInfo* Manager::CreateIdentifierInfo(zeek::detail::IDPtr id, ScriptInfo* script,
                                              bool from_redef)
	{
	const auto& id_name = id->Name();
	auto prev = identifiers.GetInfo(id_name);
	IdentifierInfo* rval = prev ? prev : new IdentifierInfo(std::move(id), script, from_redef);

	rval->AddComments(comment_buffer);
	comment_buffer.clear();

	comment_buffer_map_t::iterator it = comment_buffer_map.find(id_name);

	if ( it != comment_buffer_map.end() )
		{
		rval->AddComments(it->second);
		comment_buffer_map.erase(it);
		}

	if ( ! prev )
		{
		all_info.push_back(rval);
		identifiers.map[id_name] = rval;
		}

	last_identifier_seen = rval;

	if ( script )
		script->AddIdentifierInfo(rval);

	return rval;
	}

void Manager::StartType(zeek::detail::IDPtr id)
	{
	if ( disabled )
		return;

	if ( id->GetLocationInfo() == &zeek::detail::no_location )
		{
		DbgAndWarn(util::fmt("Can't generate zeekygen doumentation for %s, "
		                     "no location available",
		                     id->Name()));
		return;
		}

	string script = normalize_script_path(id->GetLocationInfo()->filename);
	ScriptInfo* script_info = scripts.GetInfo(script);

	if ( ! script_info )
		{
		WarnMissingScript("identifier", id.get(), script);
		return;
		}

	DBG_LOG(DBG_ZEEKYGEN, "Making IdentifierInfo (incomplete) %s, in %s", id->Name(),
	        script.c_str());
	incomplete_type = CreateIdentifierInfo(std::move(id), script_info);
	}

static bool IsEnumType(zeek::detail::ID* id)
	{
	return id->IsType() ? id->GetType()->Tag() == TYPE_ENUM : false;
	}

void Manager::Identifier(zeek::detail::IDPtr id, bool from_redef)
	{
	if ( disabled )
		return;

	if ( incomplete_type )
		{
		if ( incomplete_type->Name() == id->Name() )
			{
			DBG_LOG(DBG_ZEEKYGEN, "Finished document for type %s", id->Name());
			incomplete_type->CompletedTypeDecl();
			incomplete_type = nullptr;
			return;
			}

		if ( IsEnumType(incomplete_type->GetID()) )
			enum_mappings[id->Name()] = incomplete_type->GetID()->Name();
		}

	IdentifierInfo* id_info = identifiers.GetInfo(id->Name());

	if ( id_info )
		{
		if ( IsFunc(id_info->GetID()->GetType()->Tag()) )
			{
			// Function may already been seen (declaration versus body).
			id_info->AddComments(comment_buffer);
			comment_buffer.clear();
			return;
			}

		DbgAndWarn(util::fmt("Duplicate identifier documentation: %s", id->Name()));
		return;
		}

	if ( id->GetLocationInfo() == &zeek::detail::no_location )
		{
		// Internally-created identifier (e.g. file/proto analyzer enum tags).
		// Handled specially since they don't have a script location.
		DBG_LOG(DBG_ZEEKYGEN, "Made internal IdentifierInfo %s", id->Name());
		CreateIdentifierInfo(id, nullptr, from_redef);
		return;
		}

	string script = normalize_script_path(id->GetLocationInfo()->filename);
	ScriptInfo* script_info = scripts.GetInfo(script);

	if ( ! script_info )
		{
		WarnMissingScript("identifier", id.get(), script);
		return;
		}

	DBG_LOG(DBG_ZEEKYGEN, "Making IdentifierInfo %s, in script %s", id->Name(), script.c_str());
	CreateIdentifierInfo(std::move(id), script_info, from_redef);
	}

void Manager::RecordField(const zeek::detail::ID* id, const TypeDecl* field, const string& path,
                          bool from_redef)
	{
	if ( disabled )
		return;

	IdentifierInfo* idd = identifiers.GetInfo(id->Name());

	if ( ! idd )
		{
		DbgAndWarn(util::fmt("Can't generate zeekygen doumentation for "
		                     "record field %s, unknown record: %s",
		                     field->id, id->Name()));
		return;
		}

	string script = normalize_script_path(path);
	idd->AddRecordField(field, script, comment_buffer, from_redef);
	comment_buffer.clear();
	DBG_LOG(DBG_ZEEKYGEN, "Document record field %s, identifier %s, script %s", field->id,
	        id->Name(), script.c_str());
	}

void Manager::Redef(const zeek::detail::ID* id, const string& path, zeek::detail::InitClass ic,
                    zeek::detail::ExprPtr init_expr)
	{
	if ( disabled )
		return;

	if ( path == "<params>" )
		// This is a redef defined on the command line.
		return;

	IdentifierInfo* id_info = identifiers.GetInfo(id->Name());

	if ( ! id_info )
		{
		DbgAndWarn(util::fmt("Can't generate zeekygen doumentation for "
		                     "redef of %s, identifier lookup failed",
		                     id->Name()));
		return;
		}

	string from_script = normalize_script_path(path);
	ScriptInfo* script_info = scripts.GetInfo(from_script);

	if ( ! script_info )
		{
		WarnMissingScript("redef", id, from_script);
		return;
		}

	id_info->AddRedef(from_script, ic, std::move(init_expr), comment_buffer);
	script_info->AddRedef(id_info);
	comment_buffer.clear();
	last_identifier_seen = id_info;
	DBG_LOG(DBG_ZEEKYGEN, "Added redef of %s from %s", id->Name(), from_script.c_str());
	}

void Manager::Redef(const zeek::detail::ID* id, const std::string& path, zeek::detail::InitClass ic)
	{
	Redef(id, path, ic, nullptr);
	}

void Manager::SummaryComment(const string& script, const string& comment)
	{
	if ( disabled )
		return;

	string name = normalize_script_path(script);
	ScriptInfo* info = scripts.GetInfo(name);

	if ( info )
		info->AddComment(RemoveLeadingSpace(comment));
	else
		DbgAndWarn(util::fmt("Lookup of script %s failed for summary comment %s", name.c_str(),
		                     comment.c_str()));
	}

void Manager::PreComment(const string& comment)
	{
	if ( disabled )
		return;

	comment_buffer.push_back(RemoveLeadingSpace(comment));
	}

void Manager::PostComment(const string& comment, const string& id_hint)
	{
	if ( disabled )
		return;

	if ( id_hint.empty() )
		{
		if ( last_identifier_seen )
			last_identifier_seen->AddComment(RemoveLeadingSpace(comment));
		else
			DbgAndWarn(util::fmt("Discarded unassociated Zeekygen comment %s", comment.c_str()));

		return;
		}

	if ( last_identifier_seen && last_identifier_seen->Name() == id_hint )
		last_identifier_seen->AddComment(RemoveLeadingSpace(comment));
	else
		// Assume identifier it's associated w/ is coming later.
		comment_buffer_map[id_hint].push_back(RemoveLeadingSpace(comment));
	}

string Manager::GetEnumTypeName(const string& id) const
	{
	map<string, string>::const_iterator it = enum_mappings.find(id);
	return it == enum_mappings.end() ? "" : it->second;
	}

	} // namespace zeek::zeekygen::detail
