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

#include "zeek/zeekygen/utils.h"

#include <errno.h>
#include <sys/stat.h>

#include "zeek/Func.h"
#include "zeek/ID.h"
#include "zeek/Reporter.h"
#include "zeek/Scope.h"
#include "zeek/Val.h"
#include "zeek/plugin/Manager.h"

using namespace std;

namespace zeek::zeekygen::detail
	{

bool prettify_params(string& s)
	{
	size_t identifier_start_pos = 0;
	bool in_identifier = false;
	string identifier;

	for ( size_t i = 0; i < s.size(); ++i )
		{
		char next = s[i];

		if ( ! in_identifier )
			{
			// Pass by leading whitespace.
			if ( isspace(next) )
				continue;

			// Only allow alphabetic and '_' as first char of identifier.
			if ( isalpha(next) || next == '_' )
				{
				identifier_start_pos = i;
				identifier += next;
				in_identifier = true;
				continue;
				}

			// Don't need to change anything.
			return false;
			}

		// All other characters of identifier are alphanumeric or '_'.
		if ( isalnum(next) || next == '_' )
			{
			identifier += next;
			continue;
			}

		if ( next == ':' )
			{
			if ( i + 1 < s.size() && s[i + 1] == ':' )
				{
				// It's part of an identifier's namespace scoping.
				identifier += next;
				identifier += s[i + 1];
				++i;
				continue;
				}

			// Prettify function param/return value reST markup.
			string subst;

			if ( identifier == "Returns" )
				subst = ":returns";
			else
				subst = ":" + identifier;

			s.replace(identifier_start_pos, identifier.size(), subst);
			return true;
			}

		// Don't need to change anything.
		return false;
		}

	return false;
	}

bool is_public_api(const zeek::detail::ID* id)
	{
	return (id->Scope() == zeek::detail::SCOPE_GLOBAL) ||
	       (id->Scope() == zeek::detail::SCOPE_MODULE && id->IsExport());
	}

time_t get_mtime(const string& filename)
	{
	struct stat s;

	if ( stat(filename.c_str(), &s) < 0 )
		reporter->InternalError("Zeekygen failed to stat file '%s': %s", filename.c_str(),
		                        strerror(errno));

	return s.st_mtime;
	}

string make_heading(const string& heading, char underline)
	{
	return heading + "\n" + string(heading.size(), underline) + "\n";
	}

size_t end_of_first_sentence(const string& s)
	{
	size_t rval = 0;

	while ( (rval = s.find_first_of('.', rval)) != string::npos )
		{
		if ( rval == s.size() - 1 )
			// Period is at end of string.
			return rval;

		if ( isspace(s[rval + 1]) )
			// Period has a space after it.
			return rval;

		// Period has some non-space character after it, keep looking.
		++rval;
		}

	return rval;
	}

bool is_all_whitespace(const string& s)
	{
	for ( size_t i = 0; i < s.size(); ++i )
		if ( ! isspace(s[i]) )
			return false;

	return true;
	}

string redef_indication(const string& from_script)
	{
	return util::fmt("(present if :doc:`/scripts/%s` is loaded)", from_script.c_str());
	}

std::string normalize_script_path(std::string_view path)
	{
	if ( auto p = plugin_mgr->LookupPluginByPath(path) )
		{
		auto rval = util::detail::normalize_path(path);
		auto prefix = util::SafeBasename(p->PluginDirectory()).result;
		return prefix + "/" + rval.substr(p->PluginDirectory().size() + 1);
		}

	return util::detail::without_zeekpath_component(path);
	}

std::optional<std::string> source_code_range(const zeek::detail::ID* id)
	{
	const auto& type = id->GetType();

	if ( ! type )
		return {};

	// Some object locations won't end up capturing concrete syntax of closing
	// braces on subsequent line -- of course that doesn't have to always be
	// case, but it's true for current code style and the possibility of
	// capturing an extra line of context is not harmful (human reader shouldn't
	// be too confused by it).
	int extra_lines = 0;
	const zeek::detail::Location* loc = &zeek::detail::no_location;

	switch ( type->Tag() )
		{
		case TYPE_FUNC:
			{
			const auto& v = id->GetVal();

			if ( v && v->AsFunc()->GetBodies().size() == 1 )
				{
				// Either a function or an event/hook with single body can
				// report that single, continuous range.
				loc = v->AsFunc()->GetBodies()[0].stmts->GetLocationInfo();
				++extra_lines;
				}
			else
				loc = id->GetLocationInfo();
			}
			break;
		case TYPE_ENUM:
			// Fallthrough
		case TYPE_RECORD:
			if ( id->IsType() )
				{
				loc = type->GetLocationInfo();

				if ( zeek::util::ends_with(loc->filename, ".bif.zeek") )
					// Source code won't be available to reference, so fall back
					// to identifier location which may actually be in a regular
					// .zeek script.
					loc = id->GetLocationInfo();
				else
					++extra_lines;
				}
			else
				loc = id->GetLocationInfo();

			break;
		default:
			loc = id->GetLocationInfo();
			break;
		}

	if ( loc == &zeek::detail::no_location )
		return {};

	return util::fmt("%s %d %d", normalize_script_path(loc->filename).data(), loc->first_line,
	                 loc->last_line + extra_lines);
	}

	} // namespace zeek::zeekygen::detail
