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

#include "zeek/ID.h"

#include "zeek/zeek-config.h"

#include "zeek/Attr.h"
#include "zeek/Desc.h"
#include "zeek/EventRegistry.h"
#include "zeek/Expr.h"
#include "zeek/File.h"
#include "zeek/Func.h"
#include "zeek/Scope.h"
#include "zeek/Traverse.h"
#include "zeek/Type.h"
#include "zeek/Val.h"
#include "zeek/module_util.h"
#include "zeek/script_opt/IDOptInfo.h"
#include "zeek/script_opt/UsageAnalyzer.h"
#include "zeek/zeekygen/IdentifierInfo.h"
#include "zeek/zeekygen/Manager.h"
#include "zeek/zeekygen/ScriptInfo.h"
#include "zeek/zeekygen/utils.h"

namespace zeek {

RecordTypePtr id::conn_id;
RecordTypePtr id::endpoint;
RecordTypePtr id::connection;
RecordTypePtr id::fa_file;
RecordTypePtr id::fa_metadata;
EnumTypePtr id::transport_proto;
TableTypePtr id::string_set;
TableTypePtr id::string_array;
TableTypePtr id::count_set;
VectorTypePtr id::string_vec;
VectorTypePtr id::index_vec;

const detail::IDPtr& id::find(std::string_view name) { return zeek::detail::global_scope()->Find(name); }

const TypePtr& id::find_type(std::string_view name) {
    auto id = zeek::detail::global_scope()->Find(name);

    if ( ! id )
        reporter->InternalError("Failed to find type named: %s", std::string(name).data());

    return id->GetType();
}

const ValPtr& id::find_val(std::string_view name) {
    auto id = zeek::detail::global_scope()->Find(name);

    if ( ! id )
        reporter->InternalError("Failed to find variable named: %s", std::string(name).data());

    return id->GetVal();
}

const ValPtr& id::find_const(std::string_view name) {
    auto id = zeek::detail::global_scope()->Find(name);

    if ( ! id )
        reporter->InternalError("Failed to find variable named: %s", std::string(name).data());

    if ( ! id->IsConst() )
        reporter->InternalError("Variable is not 'const', but expected to be: %s", std::string(name).data());

    return id->GetVal();
}

FuncPtr id::find_func(std::string_view name) {
    const auto& v = id::find_val(name);

    if ( ! v )
        return nullptr;

    if ( ! IsFunc(v->GetType()->Tag()) )
        reporter->InternalError("Expected variable '%s' to be a function", std::string(name).data());

    return v.get()->As<FuncVal*>()->AsFuncPtr();
}

void id::detail::init_types() {
    conn_id = id::find_type<RecordType>("conn_id");
    endpoint = id::find_type<RecordType>("endpoint");
    connection = id::find_type<RecordType>("connection");
    fa_file = id::find_type<RecordType>("fa_file");
    fa_metadata = id::find_type<RecordType>("fa_metadata");
    transport_proto = id::find_type<EnumType>("transport_proto");
    string_set = id::find_type<TableType>("string_set");
    string_array = id::find_type<TableType>("string_array");
    count_set = id::find_type<TableType>("count_set");
    string_vec = id::find_type<VectorType>("string_vec");
    index_vec = id::find_type<VectorType>("index_vec");
}

namespace detail {

ID::ID(const char* arg_name, IDScope arg_scope, bool arg_is_export) {
    name = util::copy_string(arg_name);
    scope = arg_scope;
    is_export = arg_is_export;
    offset = 0;

    if ( name && extract_var_name(name) == "_" ) {
        is_blank = true;
        SetType(base_type(TYPE_ANY));
    }

    opt_info = new IDOptInfo(this);

    SetLocationInfo(&start_location, &end_location);
}

ID::~ID() {
    ClearOptInfo();
    delete[] name;
}

std::string ID::ModuleName() const { return extract_module_name(name); }

void ID::SetType(TypePtr t) { type = std::move(t); }

void ID::ClearVal() { val = nullptr; }

void ID::SetVal(ValPtr v) {
    val = std::move(v);
    Modified();

#ifdef DEBUG
    UpdateValID();
#endif

    if ( type && val && type->Tag() == TYPE_FUNC && type->AsFuncType()->Flavor() == FUNC_FLAVOR_EVENT ) {
        EventHandler* handler = event_registry->Lookup(name);
        auto func = val.get()->As<FuncVal*>()->AsFuncPtr();
        if ( ! handler ) {
            handler = new EventHandler(name);
            handler->SetFunc(func);
            event_registry->Register(handler, true);

            if ( ! IsExport() )
                register_new_event({NewRef{}, this});
        }
        else {
            // Otherwise, internally defined events cannot
            // have local handler.
            handler->SetFunc(func);
        }
    }
}

void ID::SetVal(ValPtr v, InitClass c) {
    if ( c == INIT_NONE || c == INIT_FULL ) {
        SetVal(std::move(v));
        return;
    }

    if ( type->Tag() != TYPE_TABLE && (type->Tag() != TYPE_PATTERN || c == INIT_REMOVE) &&
         (type->Tag() != TYPE_VECTOR || c == INIT_REMOVE) ) {
        if ( c == INIT_EXTRA )
            Error("+= initializer only applies to tables, sets, vectors and patterns", v.get());
        else
            Error("-= initializer only applies to tables and sets", v.get());
    }

    else {
        if ( c == INIT_EXTRA ) {
            if ( ! val ) {
                SetVal(std::move(v));
                return;
            }
            else
                v->AddTo(val.get(), false);
        }
        else {
            if ( val )
                v->RemoveFrom(val.get());
        }
    }
}

void ID::SetVal(ExprPtr ev, InitClass c) {
    const auto& a = attrs->Find(c == INIT_EXTRA ? ATTR_ADD_FUNC : ATTR_DEL_FUNC);

    if ( ! a )
        Internal("no add/delete function in ID::SetVal");

    if ( ! val ) {
        Error(zeek::util::fmt("%s initializer applied to ID without value", c == INIT_EXTRA ? "+=" : "-="), this);
        return;
    }

    EvalFunc(a->GetExpr(), std::move(ev));
}

bool ID::IsRedefinable() const { return GetAttr(ATTR_REDEF) != nullptr; }

void ID::SetAttrs(AttributesPtr a) {
    attrs = nullptr;
    AddAttrs(std::move(a));
}

void ID::UpdateValAttrs() {
    if ( ! attrs )
        return;

    auto tag = GetType()->Tag();

    if ( tag == TYPE_FUNC ) {
        const auto& attr = attrs->Find(ATTR_ERROR_HANDLER);

        if ( attr )
            event_registry->SetErrorHandler(Name());
    }

    if ( tag == TYPE_RECORD ) {
        const auto& attr = attrs->Find(ATTR_LOG);

        if ( attr ) {
            // Apply &log to all record fields.
            RecordType* rt = GetType()->AsRecordType();
            for ( int i = 0; i < rt->NumFields(); ++i ) {
                TypeDecl* fd = rt->FieldDecl(i);

                if ( ! fd->attrs )
                    fd->attrs = make_intrusive<Attributes>(rt->GetFieldType(i), true, IsGlobal());

                fd->attrs->AddAttr(make_intrusive<Attr>(ATTR_LOG));
            }
        }
    }

    if ( ! val )
        return;

    auto vtag = val->GetType()->Tag();

    if ( vtag == TYPE_TABLE )
        val->AsTableVal()->SetAttrs(attrs);

    else if ( vtag == TYPE_FILE )
        val->AsFile()->SetAttrs(attrs.get());
}

const AttrPtr& ID::GetAttr(AttrTag t) const { return attrs ? attrs->Find(t) : Attr::nil; }

bool ID::IsDeprecated() const { return GetAttr(ATTR_DEPRECATED) != nullptr; }

void ID::MakeDeprecated(ExprPtr deprecation) {
    if ( IsDeprecated() )
        return;

    AddAttr(make_intrusive<Attr>(ATTR_DEPRECATED, std::move(deprecation)));
}

std::string ID::GetDeprecationWarning() const {
    std::string result;
    const auto& depr_attr = GetAttr(ATTR_DEPRECATED);

    if ( depr_attr )
        result = depr_attr->DeprecationMessage();

    if ( result.empty() )
        return util::fmt("deprecated (%s)", Name());
    else
        return util::fmt("deprecated (%s): %s", Name(), result.c_str());
}

void ID::AddAttr(AttrPtr a, bool is_redef) {
    std::vector<AttrPtr> attrv{std::move(a)};
    auto attrs = make_intrusive<Attributes>(std::move(attrv), GetType(), false, IsGlobal());
    AddAttrs(std::move(attrs), is_redef);
}

void ID::AddAttrs(AttributesPtr a, bool is_redef) {
    if ( attrs )
        attrs->AddAttrs(a, is_redef);
    else
        attrs = std::move(a);

    UpdateValAttrs();
}

void ID::RemoveAttr(AttrTag a) {
    if ( attrs )
        attrs->RemoveAttr(a);
}

void ID::SetOption() {
    if ( is_option )
        return;

    is_option = true;

    // option implied redefinable
    if ( ! IsRedefinable() )
        AddAttr(make_intrusive<Attr>(ATTR_REDEF));
}

void ID::EvalFunc(ExprPtr ef, ExprPtr ev) {
    auto arg1 = make_intrusive<detail::ConstExpr>(val);
    auto args = make_intrusive<detail::ListExpr>();
    args->Append(std::move(arg1));
    args->Append(std::move(ev));
    auto ce = make_intrusive<CallExpr>(std::move(ef), std::move(args));
    SetVal(ce->Eval(nullptr));
}

TraversalCode ID::Traverse(TraversalCallback* cb) const {
    TraversalCode tc = cb->PreID(this);
    HANDLE_TC_STMT_PRE(tc);

    if ( is_type ) {
        tc = cb->PreTypedef(this);
        HANDLE_TC_STMT_PRE(tc);

        tc = cb->PostTypedef(this);
        HANDLE_TC_STMT_PRE(tc);
    }

    // FIXME: Perhaps we should be checking at other than global scope.
    else if ( val && IsFunc(val->GetType()->Tag()) && cb->current_scope == detail::global_scope() ) {
        tc = val->AsFunc()->Traverse(cb);
        HANDLE_TC_STMT_PRE(tc);
    }

    else if ( ! is_enum_const ) {
        tc = cb->PreDecl(this);
        HANDLE_TC_STMT_PRE(tc);

        tc = cb->PostDecl(this);
        HANDLE_TC_STMT_PRE(tc);
    }

    tc = cb->PostID(this);
    HANDLE_TC_EXPR_POST(tc);
}

void ID::Error(const char* msg, const Obj* o2) {
    Obj::Error(msg, o2, true);
    SetType(error_type());
}

void ID::Describe(ODesc* d) const { d->Add(name); }

void ID::DescribeExtended(ODesc* d) const {
    d->Add(name);

    if ( type ) {
        d->Add(" : ");
        type->Describe(d);
    }

    if ( val ) {
        d->Add(" = ");
        val->Describe(d);
    }

    if ( attrs ) {
        d->Add(" ");
        attrs->Describe(d);
    }
}

void ID::DescribeReSTShort(ODesc* d) const {
    if ( is_type )
        d->Add(":zeek:type:`");
    else
        d->Add(":zeek:id:`");

    d->Add(name);
    d->Add("`");

    if ( type ) {
        d->Add(": ");
        d->Add(":zeek:type:`");

        if ( ! is_type && ! type->GetName().empty() )
            d->Add(type->GetName().c_str());
        else {
            TypeTag t = type->Tag();

            switch ( t ) {
                case TYPE_TABLE: d->Add(type->IsSet() ? "set" : type_name(t)); break;

                case TYPE_FUNC: d->Add(type->AsFuncType()->FlavorString().c_str()); break;

                case TYPE_ENUM:
                    if ( is_type )
                        d->Add(type_name(t));
                    else
                        d->Add(zeekygen_mgr->GetEnumTypeName(Name()).c_str());
                    break;

                default: d->Add(type_name(t)); break;
            }
        }

        d->Add("`");
    }

    if ( attrs ) {
        d->SP();
        attrs->DescribeReST(d, true);
    }
}

void ID::DescribeReST(ODesc* d, bool roles_only) const {
    if ( roles_only ) {
        if ( is_type )
            d->Add(":zeek:type:`");
        else
            d->Add(":zeek:id:`");
        d->Add(name);
        d->Add("`");
    }
    else {
        if ( is_type )
            d->Add(".. zeek:type:: ");
        else
            d->Add(".. zeek:id:: ");

        d->Add(name);

        if ( auto sc = zeek::zeekygen::detail::source_code_range(this) ) {
            d->PushIndent();
            d->Add(util::fmt(":source-code: %s", sc->data()));
            d->PopIndentNoNL();
        }
    }

    d->PushIndent();
    d->NL();

    if ( type ) {
        d->Add(":Type: ");

        if ( ! is_type && ! type->GetName().empty() ) {
            d->Add(":zeek:type:`");
            d->Add(type->GetName());
            d->Add("`");
        }
        else {
            type->DescribeReST(d, roles_only);

            if ( IsFunc(type->Tag()) ) {
                auto ft = type->AsFuncType();

                if ( ft->Flavor() == FUNC_FLAVOR_EVENT || ft->Flavor() == FUNC_FLAVOR_HOOK ) {
                    const auto& protos = ft->Prototypes();

                    if ( protos.size() > 1 ) {
                        auto first = true;

                        for ( const auto& proto : protos ) {
                            if ( first ) {
                                first = false;
                                continue;
                            }

                            d->NL();
                            d->Add(":Type: :zeek:type:`");
                            d->Add(ft->FlavorString());
                            d->Add("` (");
                            proto.args->DescribeFieldsReST(d, true);
                            d->Add(")");
                        }
                    }
                }
            }
        }

        d->NL();
    }

    if ( attrs ) {
        d->Add(":Attributes: ");
        attrs->DescribeReST(d);
        d->NL();
    }

    if ( val && type && type->Tag() != TYPE_FUNC && type->InternalType() != TYPE_INTERNAL_VOID &&
         // Values within Version module are likely to include a
         // constantly-changing version number and be a frequent
         // source of error/desynchronization, so don't include them.
         ModuleName() != "Version" ) {
        d->Add(":Default:");
        auto ii = zeekygen_mgr->GetIdentifierInfo(Name());
        auto redefs = ii->GetRedefs();
        const auto& iv = ! redefs.empty() && ii->InitialVal() ? ii->InitialVal() : val;

        if ( type->InternalType() == TYPE_INTERNAL_OTHER ) {
            switch ( type->Tag() ) {
                case TYPE_TABLE:
                    if ( iv->AsTable()->Length() == 0 ) {
                        d->Add(" ``{}``");
                        d->NL();
                        break;
                    }
                    // Fall-through.

                default:
                    d->NL();
                    d->PushIndent();
                    d->Add("::");
                    d->NL();
                    d->PushIndent();
                    iv->DescribeReST(d);
                    d->PopIndent();
                    d->PopIndent();
            }
        }

        else {
            d->SP();
            iv->DescribeReST(d);
            d->NL();
        }

        for ( auto& ir : redefs ) {
            if ( ! ir->init_expr )
                continue;

            if ( ir->ic == INIT_NONE )
                continue;

            std::string redef_str;
            ODesc expr_desc;
            ir->init_expr->Describe(&expr_desc);
            redef_str = expr_desc.Description();
            redef_str = util::strreplace(redef_str, "\n", " ");

            d->Add(":Redefinition: ");
            d->Add(util::fmt("from :doc:`/scripts/%s`", ir->from_script.data()));
            d->NL();
            d->PushIndent();

            if ( ir->ic == INIT_FULL )
                d->Add("``=``");
            else if ( ir->ic == INIT_EXTRA )
                d->Add("``+=``");
            else if ( ir->ic == INIT_REMOVE )
                d->Add("``-=``");
            else
                assert(false);

            d->Add("::");
            d->NL();
            d->PushIndent();
            d->Add(redef_str.data());
            d->PopIndent();
            d->PopIndent();
        }
    }
}

#ifdef DEBUG
void ID::UpdateValID() {
    if ( IsGlobal() && val && name && name[0] != '#' )
        val->SetID(this);
}
#endif

void ID::AddOptionHandler(FuncPtr callback, int priority) { option_handlers.emplace(priority, std::move(callback)); }

std::vector<Func*> ID::GetOptionHandlers() const {
    // multimap is sorted
    // It might be worth caching this if we expect it to be called
    // a lot...
    std::vector<Func*> v;
    for ( auto& element : option_handlers )
        v.push_back(element.second.get());
    return v;
}

void ID::ClearOptInfo() {
    delete opt_info;
    opt_info = nullptr;
}

} // namespace detail

} // namespace zeek
