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

// Support routines to help deal with Zeek debugging commands and
// implementation of most commands.

#include "zeek/DebugCmds.h"

#include <regex.h>
#include <sys/types.h>
#include <cassert>
#include <cstring>

#include "zeek/DbgBreakpoint.h"
#include "zeek/Debug.h"
#include "zeek/DebugCmdInfoConstants.cc" // NOLINT(bugprone-suspicious-include)
#include "zeek/Desc.h"
#include "zeek/Frame.h"
#include "zeek/Func.h"
#include "zeek/ID.h"
#include "zeek/PolicyFile.h"
#include "zeek/Reporter.h"
#include "zeek/Scope.h"
#include "zeek/Val.h"
#include "zeek/util.h"

using namespace std;

namespace zeek::detail {

DebugCmdInfoQueue g_DebugCmdInfos;

//
// Helper routines
//
static bool string_is_regex(const string& s) { return strpbrk(s.data(), "?*\\+"); }

static void lookup_global_symbols_regex(const string& orig_regex, vector<ID*>& matches, bool func_only = false) {
    if ( util::streq(orig_regex.c_str(), "") )
        return;

    string regex = "^";
    int len = orig_regex.length();
    for ( int i = 0; i < len; ++i ) {
        if ( orig_regex[i] == '*' )
            regex.push_back('.');
        regex.push_back(orig_regex[i]);
    }
    regex.push_back('$');

    regex_t re;
    if ( regcomp(&re, regex.c_str(), REG_EXTENDED | REG_NOSUB) ) {
        debug_msg("Invalid regular expression: %s\n", regex.c_str());
        return;
    }

    auto global = global_scope();
    const auto& syms = global->Vars();

    ID* nextid;
    for ( const auto& sym : syms ) {
        ID* nextid = sym.second.get();
        if ( ! func_only || nextid->GetType()->Tag() == TYPE_FUNC )
            if ( ! regexec(&re, nextid->Name(), 0, nullptr, 0) )
                matches.push_back(nextid);
    }
}

static void choose_global_symbols_regex(const string& regex, vector<ID*>& choices, bool func_only = false) {
    lookup_global_symbols_regex(regex, choices, func_only);

    if ( choices.size() <= 1 )
        return;

    while ( true ) {
        debug_msg("There were multiple matches, please choose:\n");

        for ( size_t i = 0; i < choices.size(); i++ )
            debug_msg("[%zu] %s\n", i + 1, choices[i]->Name());

        debug_msg("[a] All of the above\n");
        debug_msg("[n] None of the above\n");
        debug_msg("Enter your choice: ");

        char charinput[256];
        if ( ! fgets(charinput, sizeof(charinput) - 1, stdin) ) {
            choices.clear();
            return;
        }
        if ( charinput[strlen(charinput) - 1] == '\n' )
            charinput[strlen(charinput) - 1] = 0;

        string input = charinput;
        if ( input == "a" )
            return;

        if ( input == "n" ) {
            choices.clear();
            return;
        }
        int option = atoi(input.c_str());
        if ( option > 0 && option <= (int)choices.size() ) {
            ID* choice = choices[option - 1];
            choices.clear();
            choices.push_back(choice);
            return;
        }
    }
}

//
// DebugCmdInfo implementation
//

DebugCmdInfo::DebugCmdInfo(const DebugCmdInfo& info) : cmd(info.cmd), helpstring(nullptr) {
    num_names = info.num_names;
    names = info.names;
    resume_execution = info.resume_execution;
    repeatable = info.repeatable;
}

DebugCmdInfo::DebugCmdInfo(DebugCmd arg_cmd, const char* const* arg_names, int arg_num_names, bool arg_resume_execution,
                           const char* const arg_helpstring, bool arg_repeatable)
    : cmd(arg_cmd), helpstring(arg_helpstring) {
    num_names = arg_num_names;
    resume_execution = arg_resume_execution;
    repeatable = arg_repeatable;

    for ( int i = 0; i < num_names; ++i )
        names.push_back(arg_names[i]);
}

const DebugCmdInfo* get_debug_cmd_info(DebugCmd cmd) {
    if ( (int)cmd < g_DebugCmdInfos.size() )
        return g_DebugCmdInfos[(int)cmd];
    else
        return nullptr;
}

int find_all_matching_cmds(const string& prefix, const char* array_of_matches[]) {
    // Trivial implementation for now (### use hashing later).

    unsigned int arglen = prefix.length();
    int matches = 0;

    for ( int i = 0; i < num_debug_cmds(); ++i ) {
        array_of_matches[g_DebugCmdInfos[i]->Cmd()] = nullptr;

        for ( int j = 0; j < g_DebugCmdInfos[i]->NumNames(); ++j ) {
            const char* curr_name = g_DebugCmdInfos[i]->Names()[j];
            if ( strncmp(curr_name, prefix.c_str(), arglen) != 0 )
                continue;

            // If exact match, then only return that one.
            if ( ! prefix.compare(curr_name) ) {
                for ( int k = 0; k < num_debug_cmds(); ++k )
                    array_of_matches[k] = nullptr;

                array_of_matches[g_DebugCmdInfos[i]->Cmd()] = curr_name;
                return 1;
            }

            array_of_matches[g_DebugCmdInfos[i]->Cmd()] = curr_name;
            ++matches;
        }
    }

    return matches;
}

//
// ------------------------------------------------------------
// Implementation of some debugger commands

// Start, end bounds of which frame numbers to print
static int dbg_backtrace_internal(int start, int end) {
    if ( start < 0 || end < 0 || (unsigned)start >= g_frame_stack.size() || (unsigned)end >= g_frame_stack.size() )
        reporter->InternalError("Invalid stack frame index in DbgBacktraceInternal\n");

    if ( start < end ) {
        int temp = start;
        start = end;
        end = temp;
    }

    for ( int i = start; i >= end; --i ) {
        const Frame* f = g_frame_stack[i];
        const Stmt* stmt = f ? f->GetNextStmt() : nullptr;

        string context = get_context_description(stmt, f);
        debug_msg("#%d  %s\n", int(g_frame_stack.size() - 1 - i), context.c_str());
    };

    return 1;
}

// Returns 0 for illegal arguments, or 1 on success.
int dbg_cmd_backtrace(DebugCmd cmd, const vector<string>& args) {
    assert(cmd == dcBacktrace);
    assert(g_frame_stack.size() > 0);

    unsigned int start_iter;
    int end_iter;

    if ( args.size() > 0 ) {
        int how_many; // determines how we traverse the frames
        int valid_arg = sscanf(args[0].c_str(), "%i", &how_many);
        if ( ! valid_arg ) {
            debug_msg("Argument to backtrace '%s' invalid: must be an integer\n", args[0].c_str());
            return 0;
        }

        if ( how_many > 0 ) { // innermost N frames
            start_iter = g_frame_stack.size() - 1;
            end_iter = start_iter - how_many + 1;
            if ( end_iter < 0 )
                end_iter = 0;
        }
        else { // outermost N frames
            start_iter = how_many - 1;
            if ( start_iter + 1 > g_frame_stack.size() )
                start_iter = g_frame_stack.size() - 1;
            end_iter = 0;
        }
    }
    else {
        start_iter = g_frame_stack.size() - 1;
        end_iter = 0;
    }

    return dbg_backtrace_internal(start_iter, end_iter);
}

// Returns 0 if invalid args, else 1.
int dbg_cmd_frame(DebugCmd cmd, const vector<string>& args) {
    assert(cmd == dcFrame || cmd == dcUp || cmd == dcDown);

    if ( cmd == dcFrame ) {
        int idx = 0;

        if ( args.size() > 0 ) {
            if ( args.size() > 1 ) {
                debug_msg("Too many arguments: expecting frame number 'n'\n");
                return 0;
            }

            if ( ! sscanf(args[0].c_str(), "%d", &idx) ) {
                debug_msg("Argument to frame must be a positive integer\n");
                return 0;
            }

            if ( idx < 0 || (unsigned int)idx >= g_frame_stack.size() ) {
                debug_msg("No frame %d", idx);
                return 0;
            }
        }

        g_debugger_state.curr_frame_idx = idx;
    }

    else if ( cmd == dcDown ) {
        if ( g_debugger_state.curr_frame_idx == 0 ) {
            debug_msg("Innermost frame already selected\n");
            return 0;
        }

        g_debugger_state.curr_frame_idx--;
    }

    else if ( cmd == dcUp ) {
        if ( (unsigned int)(g_debugger_state.curr_frame_idx + 1) == g_frame_stack.size() ) {
            debug_msg("Outermost frame already selected\n");
            return 0;
        }

        ++g_debugger_state.curr_frame_idx;
    }

    int user_frame_number = g_frame_stack.size() - 1 - g_debugger_state.curr_frame_idx;

    // Set the current location to the new frame being looked at
    // for 'list', 'break', etc.
    const Stmt* stmt = g_frame_stack[user_frame_number]->GetNextStmt();
    if ( ! stmt )
        reporter->InternalError("Assertion failed: stmt is null");

    const Location loc = *stmt->GetLocationInfo();
    g_debugger_state.last_loc = loc;
    g_debugger_state.already_did_list = false;

    return dbg_backtrace_internal(user_frame_number, user_frame_number);
}

int dbg_cmd_help(DebugCmd cmd, const vector<string>& args) {
    assert(cmd == dcHelp);

    debug_msg("Help summary: \n\n");
    for ( int i = 1; i < num_debug_cmds(); ++i ) {
        const DebugCmdInfo* info = get_debug_cmd_info(DebugCmd(i));
        debug_msg("%s -- %s\n", info->Names()[0], info->Helpstring());
    }

    return -1;
}

int dbg_cmd_break(DebugCmd cmd, const vector<string>& args) {
    assert(cmd == dcBreak);

    vector<DbgBreakpoint*> bps;

    int cond_index = -1; // at which argument pos. does bp condition start?

    if ( args.empty() || args[0] == "if" ) { // break on next stmt
        int user_frame_number = g_frame_stack.size() - 1 - g_debugger_state.curr_frame_idx;

        Stmt* stmt = g_frame_stack[user_frame_number]->GetNextStmt();
        if ( ! stmt )
            reporter->InternalError("Assertion failed: stmt is null");

        DbgBreakpoint* bp = new DbgBreakpoint();
        bp->SetID(g_debugger_state.NextBPID());

        if ( ! bp->SetLocation(stmt) ) {
            debug_msg("Breakpoint not set.\n");
            delete bp;
            return 0;
        }

        if ( args.size() > 0 && args[0] == "if" )
            cond_index = 1;

        bps.push_back(bp);
    }

    else {
        vector<string> locstrings;
        if ( string_is_regex(args[0]) ) {
            vector<ID*> choices;
            choose_global_symbols_regex(args[0], choices, true);
            for ( const auto& choice : choices )
                locstrings.emplace_back(choice->Name());
        }
        else
            locstrings.push_back(args[0]);

        for ( const auto& loc_str : locstrings ) {
            debug_msg("Setting breakpoint on %s:\n", loc_str.c_str());
            vector<ParseLocationRec> plrs = parse_location_string(loc_str);
            for ( const auto& plr : plrs ) {
                DbgBreakpoint* bp = new DbgBreakpoint();
                bp->SetID(g_debugger_state.NextBPID());
                if ( ! bp->SetLocation(plr, loc_str) ) {
                    debug_msg("Breakpoint not set.\n");
                    delete bp;
                }
                else
                    bps.push_back(bp);
            }
        }

        if ( args.size() > 1 && args[1] == "if" )
            cond_index = 2;
    }

    // Is there a condition specified?
    if ( cond_index >= 0 && ! bps.empty() ) {
        // ### Implement conditions
        string cond;
        for ( const auto& arg : args ) {
            cond += arg;
            cond += "    ";
        }
        bps[0]->SetCondition(cond);
    }

    for ( auto& bp : bps ) {
        bp->SetTemporary(false);
        g_debugger_state.breakpoints[bp->GetID()] = bp;
    }

    return 0;
}

// Set a condition on an existing breakpoint.
int dbg_cmd_break_condition(DebugCmd cmd, const vector<string>& args) {
    assert(cmd == dcBreakCondition);

    if ( args.size() < 2 ) {
        debug_msg("Arguments must specify breakpoint number and condition.\n");
        return 0;
    }

    int idx = atoi(args[0].c_str());
    DbgBreakpoint* bp = g_debugger_state.breakpoints[idx];

    string expr;
    for ( int i = 1; i < int(args.size()); ++i ) {
        expr += args[i];
        expr += " ";
    }
    bp->SetCondition(expr);

    return 1;
}

// Change the state of a breakpoint.
int dbg_cmd_break_set_state(DebugCmd cmd, const vector<string>& args) {
    assert(cmd == dcDeleteBreak || cmd == dcClearBreak || cmd == dcDisableBreak || cmd == dcEnableBreak ||
           cmd == dcIgnoreBreak);

    if ( cmd == dcClearBreak || cmd == dcIgnoreBreak ) {
        debug_msg("'clear' and 'ignore' commands not currently supported\n");
        return 0;
    }

    if ( g_debugger_state.breakpoints.empty() ) {
        debug_msg("No breakpoints currently set.\n");
        return -1;
    }

    vector<int> bps_to_change;

    if ( args.empty() ) {
        BPIDMapType::iterator iter;
        for ( iter = g_debugger_state.breakpoints.begin(); iter != g_debugger_state.breakpoints.end(); ++iter )
            bps_to_change.push_back(iter->second->GetID());
    }
    else {
        for ( const auto& arg : args )
            if ( int idx = atoi(arg.c_str()) )
                bps_to_change.push_back(idx);
    }

    for ( auto bp_change : bps_to_change ) {
        BPIDMapType::iterator result = g_debugger_state.breakpoints.find(bp_change);

        if ( result != g_debugger_state.breakpoints.end() ) {
            switch ( cmd ) {
                case dcDisableBreak:
                    g_debugger_state.breakpoints[bp_change]->SetEnable(false);
                    debug_msg("Breakpoint %d disabled\n", bp_change);
                    break;

                case dcEnableBreak:
                    g_debugger_state.breakpoints[bp_change]->SetEnable(true);
                    debug_msg("Breakpoint %d enabled\n", bp_change);
                    break;

                case dcDeleteBreak:
                    delete g_debugger_state.breakpoints[bp_change];
                    g_debugger_state.breakpoints.erase(bp_change);
                    debug_msg("Breakpoint %d deleted\n", bp_change);
                    break;

                default: reporter->InternalError("Invalid command in DbgCmdBreakSetState\n");
            }
        }

        else
            debug_msg("Breakpoint %d does not exist\n", bp_change);
    }

    return -1;
}

// Evaluate an expression and print the result.
int dbg_cmd_print(DebugCmd cmd, const vector<string>& args) {
    assert(cmd == dcPrint);

    // ### TODO: add support for formats

    // Just concatenate all the 'args' into one expression.
    string expr;
    for ( size_t i = 0; i < args.size(); ++i ) {
        expr += args[i];
        if ( i < args.size() - 1 )
            expr += " ";
    }

    auto val = dbg_eval_expr(expr.c_str());

    if ( val ) {
        ODesc d;
        val->Describe(&d);
        debug_msg("%s\n", d.Description());
    }
    else {
        debug_msg("<expression has no value>\n");
    }

    return 1;
}

// Get the debugger's state.
// Allowed arguments are: break (breakpoints), watch, display, source.
int dbg_cmd_info(DebugCmd cmd, const vector<string>& args) {
    assert(cmd == dcInfo);

    if ( args.empty() ) {
        debug_msg("Syntax: info info-command\n");
        debug_msg("List of info-commands:\n");
        debug_msg("info breakpoints -- List of breakpoints and watches\n");
        return 1;
    }

    if ( ! strncmp(args[0].c_str(), "breakpoints", args[0].size()) ||
         ! strncmp(args[0].c_str(), "watch", args[0].size()) ) {
        debug_msg("Num Type           Disp Enb What\n");

        BPIDMapType::iterator iter;
        for ( iter = g_debugger_state.breakpoints.begin(); iter != g_debugger_state.breakpoints.end(); ++iter ) {
            DbgBreakpoint* bp = (*iter).second;
            debug_msg("%-4d%-15s%-5s%-4s%s\n", bp->GetID(), "breakpoint", bp->IsTemporary() ? "del" : "keep",
                      bp->IsEnabled() ? "y" : "n", bp->Description());
        }
    }

    else
        debug_msg("I don't have info for that yet.\n");

    return 1;
}

int dbg_cmd_list(DebugCmd cmd, const vector<string>& args) {
    assert(cmd == dcList);

    // The constant 4 is to match the GDB behavior.
    const unsigned int CENTER_IDX = 4; // 5th line is the 'interesting' one

    int pre_offset = 0;
    if ( args.size() > 1 ) {
        debug_msg("Syntax: list [file:]line  OR  list function_name\n");
        return 0;
    }

    if ( args.empty() ) {
        // Special case: if we just hit a breakpoint, then show
        // that line without advancing first.
        if ( g_debugger_state.already_did_list )
            pre_offset = 10;
    }

    else if ( args[0] == "-" )
        // Why -10 ?  Because that's what GDB does.
        pre_offset = -10;

    else if ( args[0][0] == '-' || args[0][0] == '+' ) {
        int offset;
        if ( ! sscanf(args[0].c_str(), "%d", &offset) ) {
            debug_msg("Offset must be a number\n");
            return false;
        }

        pre_offset = offset;
    }

    else {
        vector<ParseLocationRec> plrs = parse_location_string(args[0]);
        ParseLocationRec plr = plrs[0];
        if ( plr.type == PLR_UNKNOWN ) {
            debug_msg("Invalid location specifier\n");
            return false;
        }

        g_debugger_state.last_loc.SetFile(plr.filename);
        g_debugger_state.last_loc.SetFirstLine(plr.line);
        pre_offset = 0;
    }

    if ( (int)pre_offset + (int)g_debugger_state.last_loc.FirstLine() - (int)CENTER_IDX < 0 )
        pre_offset = CENTER_IDX - g_debugger_state.last_loc.FirstLine();

    g_debugger_state.last_loc.IncrementLine(pre_offset);

    int last_line_in_file = how_many_lines_in(g_debugger_state.last_loc.FileName());

    if ( g_debugger_state.last_loc.FirstLine() > last_line_in_file )
        g_debugger_state.last_loc.SetLine(last_line_in_file);

    PrintLines(g_debugger_state.last_loc.FileName(), g_debugger_state.last_loc.FirstLine() - CENTER_IDX, 10, true);

    g_debugger_state.already_did_list = true;

    return 1;
}

int dbg_cmd_trace(DebugCmd cmd, const vector<string>& args) {
    assert(cmd == dcTrace);

    if ( args.empty() ) {
        debug_msg("Execution tracing is %s.\n", g_trace_state.DoTrace() ? "on" : "off");
        return 1;
    }

    if ( args[0] == "on" ) {
        g_trace_state.TraceOn();
        return 1;
    }

    if ( args[0] == "off" ) {
        g_trace_state.TraceOff();
        return 1;
    }

    debug_msg("Invalid argument");
    return 0;
}

int num_debug_cmds() { return static_cast<int>(g_DebugCmdInfos.size()); }

} // namespace zeek::detail
