//=============================================================================
//
// Adventure Game Studio (AGS)
//
// Copyright (C) 1999-2011 Chris Jones and 2011-2026 various contributors
// The full list of copyright holders can be found in the Copyright.txt
// file, which is part of this source code distribution.
//
// The AGS source code is provided under the Artistic License 2.0.
// A copy of this license can be found in the file License.txt and at
// https://opensource.org/license/artistic-2-0/
//
//=============================================================================
#include <limits>
#include <inttypes.h>
#include <memory>
#include <stdio.h>
#include "core/platform.h"
#if AGS_PLATFORM_OS_WINDOWS
#include "platform/windows/windows.h"
#endif
#include <SDL.h>
#include "ac/common.h"
#include "ac/gamesetupstruct.h"
#include "ac/gamestate.h"
#include "ac/runtime_defines.h"
#include "debug/agseditordebugger.h"
#include "debug/debug_log.h"
#include "debug/debugger.h"
#include "debug/debugmanager.h"
#include "debug/out.h"
#include "debug/logfile.h"
#include "debug/messagebuffer.h"
#include "main/config.h"
#include "main/game_run.h"
#include "media/audio/audio_system.h"
#include "platform/base/agsplatformdriver.h"
#include "platform/base/sys_main.h"
#include "plugin/plugin_engine.h"
#include "script/script.h"
#include "script/cc_common.h"
#include "util/memory_compat.h"
#include "util/path.h"
#include "util/string_utils.h"
#include "util/textstreamwriter.h"

using namespace AGS::Common;
using namespace AGS::Engine;

extern char check_dynamic_sprites_at_exit;
extern int displayed_room;
extern RoomStruct thisroom;
extern volatile bool want_exit, abort_engine;
extern GameSetupStruct game;


int editor_debugging_enabled = 0;
int editor_debugging_initialized = 0;
char editor_debugger_instance_token[100];
IAGSEditorDebugger *editor_debugger = nullptr;
int break_on_next_script_step = 0;
volatile int game_paused_in_debugger = 0;

#if AGS_PLATFORM_OS_WINDOWS

#include "platform/windows/debug/namedpipesagsdebugger.h"

HWND editor_window_handle = 0;

IAGSEditorDebugger *GetEditorDebugger(const char *instanceToken)
{
    return new NamedPipesAGSDebugger(instanceToken);
}

#else   // AGS_PLATFORM_OS_WINDOWS

IAGSEditorDebugger *GetEditorDebugger(const char* /*instanceToken*/)
{
    return nullptr;
}

#endif

int debug_flags=0;

FPSDisplayMode display_fps = kFPS_Hide;

void send_message_to_debugger(IAGSEditorDebugger *ide_debugger,
    const std::vector<std::pair<String, String>>& tag_values, const String& command)
{
    String messageToSend = String::FromFormat(R"(<?xml version="1.0" encoding="Windows-1252"?><Debugger Command="%s">)", command.GetCStr());
#if AGS_PLATFORM_OS_WINDOWS
    messageToSend.Append(String::FromFormat("  <EngineWindow>%" PRIdPTR "</EngineWindow> ", sys_win_get_window()));
#endif

    for(const auto& tag_value : tag_values)
    {
        messageToSend.AppendFmt("  <%s><![CDATA[%s]]></%s> ",
                                tag_value.first.GetCStr(), tag_value.second.GetCStr(), tag_value.first.GetCStr());
    }

    messageToSend.Append("</Debugger>\n");

    ide_debugger->SendMessageToEditor(messageToSend.GetCStr());
}

class DebuggerLogOutputTarget : public AGS::Common::IOutputHandler
{
public:
    DebuggerLogOutputTarget(IAGSEditorDebugger *ide_debugger)
        : _ideDebugger(ide_debugger) {};
    virtual ~DebuggerLogOutputTarget() {};

    void OnRegister() override
    {
        // do nothing
    }

    void PrintMessage(const DebugMessage &msg) override
    {
        assert(_ideDebugger);
        std::vector<std::pair<String, String>> log_info =
                {
                        {"Text", msg.Text},
                        {"GroupID", StrUtil::IntToString(msg.GroupID)},
                        {"MTID", StrUtil::IntToString(msg.MT)}
                };
        send_message_to_debugger(_ideDebugger, log_info, "LOG");
    }
private:
    IAGSEditorDebugger *_ideDebugger = nullptr;
};

const String OutputFileID = "file";
const String OutputSystemID = "stdout";
const String OutputDebuggerLogID = "debugger";


// ----------------------------------------------------------------------------
// SDL log output
// ----------------------------------------------------------------------------

// SDL log priority names
static const char *SDL_priority[SDL_NUM_LOG_PRIORITIES] = {
    nullptr, "VERBOSE", "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL"
};
// SDL log category names
static const char *SDL_category[SDL_LOG_CATEGORY_RESERVED1] = {
    "APP", "ERROR", "SYSTEM", "AUDIO", "VIDEO", "RENDER", "INPUT"
};
// Conversion between SDL priorities and our MessageTypes
static MessageType SDL_to_MT[SDL_NUM_LOG_PRIORITIES] = {
    kDbgMsg_None, kDbgMsg_All, kDbgMsg_Debug, kDbgMsg_Info, kDbgMsg_Warn, kDbgMsg_Error, kDbgMsg_Alert
};
// Print SDL message through our own log
void SDL_Log_Output(void* /*userdata*/, int category, SDL_LogPriority priority, const char *message) {
    DbgMgr.Print(kDbgGroup_SDL, SDL_to_MT[priority],
        String::FromFormat("%s: %s: %s", SDL_category[category], SDL_priority[priority], message));
}

// ----------------------------------------------------------------------------
// Log configuration
// ----------------------------------------------------------------------------

// Create a new log output by ID
static std::unique_ptr<IOutputHandler> create_log_output(const String &name,
    const String &dir = "", const String &filename = "", LogFile::OpenMode open_mode = LogFile::kLogFile_Overwrite)
{
    if (name.CompareNoCase(OutputSystemID) == 0)
    {
        return platform->GetStdOut();
    }
    else if (name.CompareNoCase(OutputFileID) == 0)
    {
        auto log_file = std::make_unique<LogFile>();
        String logfile_dir = dir;
        if (dir.IsEmpty())
        {
            FSLocation fs = platform->GetAppOutputDirectory();
            CreateFSDirs(fs);
            logfile_dir = fs.FullDir;
        }
        else if (Path::IsRelativePath(dir) && platform->IsLocalDirRestricted())
        {
            FSLocation fs = GetGameUserDataDir();
            CreateFSDirs(fs);
            logfile_dir = fs.FullDir;
        }
        String logfilename = filename.IsEmpty() ? "ags.log" : filename;
        String logfile_path = Path::ConcatPaths(logfile_dir, logfilename);
        if (!log_file->OpenFile(logfile_path, open_mode))
            return nullptr;
        return std::move(log_file);
    }
    else if (name.CompareNoCase(OutputDebuggerLogID) == 0 &&
        editor_debugger != nullptr)
    {
        return std::make_unique<DebuggerLogOutputTarget>(editor_debugger);
    }
    return nullptr;
}

// Parses a string where each character defines a single log group; returns list of real group names.
std::vector<String> parse_log_multigroup(const String &group_str)
{
    std::vector<String> grplist;
    for (size_t i = 0; i < group_str.GetLength(); ++i)
    {
        switch (group_str[i])
        {
        case 'm': grplist.emplace_back("main"); break;
        case 'g': grplist.emplace_back("game"); break;
        case 's': grplist.emplace_back("script"); break;
        case 'c': grplist.emplace_back("sprcache"); break;
        case 'o': grplist.emplace_back("manobj"); break;
        case 'l': grplist.emplace_back("sdl"); break;
        case 'p': grplist.emplace_back("plugin"); break;
        }
    }
    return grplist;
}

MessageType get_messagetype_from_string(const String &option)
{
    if (option.CompareNoCase("all") == 0) return kDbgMsg_All;
    return StrUtil::ParseEnumAllowNum<MessageType>(option,
        CstrArr<kNumDbgMsg>{"", "alert", "fatal", "error", "warn", "info", "debug"},
        kDbgMsg_None, kDbgMsg_None);
}

typedef std::pair<CommonDebugGroup, MessageType> DbgGroupOption;

static void apply_log_config(const ConfigTree &cfg, const String &log_id,
                      bool def_enabled,
                      std::initializer_list<DbgGroupOption> def_opts)
{
    const String value = CfgReadString(cfg, "log", log_id);
    if (value.IsEmpty() && !def_enabled)
        return;

    // Setup message group filters
    MessageType def_verbosity = kDbgMsg_None;
    std::vector<std::pair<DebugGroupID, MessageType>> group_filters;
    if (value.IsEmpty() || value.CompareNoCase("default") == 0)
    {
        for (const auto &opt : def_opts)
            group_filters.push_back(std::make_pair(opt.first, opt.second));
    }
    else
    {
        const auto options = value.Split(',');
        for (const auto &opt : options)
        {
            String groupname = opt.LeftSection(':');
            MessageType msgtype = kDbgMsg_All;
            if (opt.GetLength() >= groupname.GetLength() + 1)
            {
                String msglevel = opt.Mid(groupname.GetLength() + 1);
                msglevel.Trim();
                if (msglevel.GetLength() > 0)
                    msgtype = get_messagetype_from_string(msglevel);
            }
            groupname.Trim();
            if (groupname.CompareNoCase("all") == 0 || groupname.IsEmpty())
            {
                def_verbosity = msgtype;
            }
            else if (groupname[0u] != '+')
            {
                group_filters.push_back(std::make_pair(groupname, msgtype));
            }
            else
            {
                const auto groups = parse_log_multigroup(groupname);
                for (const auto &g : groups)
                    group_filters.push_back(std::make_pair(g, msgtype));
            }
        }
    }

    // Test if already registered, if not then try create it,
    // if it exists, then reset the filter settings
    if (DbgMgr.HasOutput(log_id))
    {
        DbgMgr.SetOutputFilters(log_id, def_verbosity, &group_filters);
    }
    else
    {
        String path = CfgReadString(cfg, "log", String::FromFormat("%s-path", log_id.GetCStr()));
        auto dbgout = create_log_output(log_id, path);
        if (!dbgout)
            return;
        DbgMgr.RegisterOutput(log_id, std::move(dbgout), def_verbosity, &group_filters);
    }
}

void init_debug(const ConfigTree &cfg, bool stderr_only)
{
    // Setup SDL output
    SDL_LogSetOutputFunction(SDL_Log_Output, nullptr);
    String sdl_log = CfgReadString(cfg, "log", "sdl");
    SDL_LogPriority priority = StrUtil::ParseEnumAllowNum<SDL_LogPriority>(sdl_log,
        CstrArr<SDL_NUM_LOG_PRIORITIES>{"all", "verbose", "debug", "info", "warn", "error", "critical"},
        static_cast<SDL_LogPriority>(0), SDL_LOG_PRIORITY_INFO);
    SDL_LogSetAllPriority(priority);

    // Init platform's stdout setting
    platform->SetOutputToErr(stderr_only);

    // Register outputs
    apply_debug_config(cfg, false);
}

void apply_debug_config(const ConfigTree &cfg, bool finalize)
{
    apply_log_config(cfg, OutputSystemID, /* defaults */ true,
        { DbgGroupOption(kDbgGroup_Main, kDbgMsg_Info),
          DbgGroupOption(kDbgGroup_SDL, kDbgMsg_Info),
          DbgGroupOption(kDbgGroup_Plugin, kDbgMsg_Info)
        });
    bool legacy_log_enabled = CfgReadBoolInt(cfg, "misc", "log", false);

    apply_log_config(cfg, OutputFileID,
        /* defaults */
        legacy_log_enabled,
        { DbgGroupOption(kDbgGroup_Main, kDbgMsg_All),
          DbgGroupOption(kDbgGroup_SDL, kDbgMsg_Info),
          DbgGroupOption(kDbgGroup_Plugin, kDbgMsg_Info),
          DbgGroupOption(kDbgGroup_Game, kDbgMsg_Info),
          DbgGroupOption(kDbgGroup_Script, kDbgMsg_All),
#if DEBUG_SPRITECACHE
          DbgGroupOption(kDbgGroup_SprCache, kDbgMsg_All),
#else
          DbgGroupOption(kDbgGroup_SprCache, kDbgMsg_Info),
#endif
#if DEBUG_MANAGED_OBJECTS
          DbgGroupOption(kDbgGroup_ManObj, kDbgMsg_All),
#else
          DbgGroupOption(kDbgGroup_ManObj, kDbgMsg_Info),
#endif
        });

    // If the game was compiled in Debug mode *and* there's no regular file log,
    // then open "warnings.log" for printing script warnings.
    if (game.options[OPT_DEBUGMODE] != 0 && !DbgMgr.HasOutput(OutputFileID))
    {
        auto dbgout = create_log_output(OutputFileID, "./", "warnings.log", LogFile::kLogFile_OverwriteAtFirstMessage);
        if (dbgout)
        {
            std::vector<std::pair<DebugGroupID, MessageType>> group_filters;
            group_filters.push_back(std::make_pair(kDbgGroup_Game, kDbgMsg_Warn));
            group_filters.push_back(std::make_pair(kDbgGroup_Script, kDbgMsg_Warn));
            DbgMgr.RegisterOutput(OutputFileID, std::move(dbgout), kDbgMsg_None, &group_filters);
        }
    }

    // Engine -> editor logging
    if (editor_debugging_initialized)
    {
        apply_log_config(cfg, OutputDebuggerLogID, false, {});
    }

    // We don't need message buffer beyond this point
    if (finalize)
        DbgMgr.StopMessageBuffering();
}

void shutdown_debug()
{
    // Shutdown output subsystem
    DbgMgr.UnregisterAll();
}

// Prepends message text with current room number and running script info, then logs result
static void debug_script_print_impl(const String &msg, MessageType mt)
{
    String script_ref;
    ccInstance *curinst = ccInstance::GetCurrentInstance();
    if (curinst != nullptr)
    {
        String scriptname = curinst->GetScript()->GetSectionName(curinst->GetPC());
        script_ref.Format("[%s:%d] ", scriptname.GetCStr(), currentline);
    }

    Debug::Printf(kDbgGroup_Game, mt, "(room:%d) %s%s", displayed_room, script_ref.GetCStr(), msg.GetCStr());
}

void debug_script_print(MessageType mt, const char *msg, ...)
{
    va_list ap;
    va_start(ap, msg);
    String full_msg = String::FromFormatV(msg, ap);
    va_end(ap);
    debug_script_print_impl(full_msg, mt);
}

void debug_script_warn(const char *msg, ...)
{
    va_list ap;
    va_start(ap, msg);
    String full_msg = String::FromFormatV(msg, ap);
    va_end(ap);
    debug_script_print_impl(full_msg, kDbgMsg_Warn);
}

void debug_script_log(const char *msg, ...)
{
    va_list ap;
    va_start(ap, msg);
    String full_msg = String::FromFormatV(msg, ap);
    va_end(ap);
    debug_script_print_impl(full_msg, kDbgMsg_Debug);
}


struct Breakpoint
{
    char scriptName[80]{};
    int lineNumber = 0;
};

std::vector<Breakpoint> breakpoints;

bool send_state_to_debugger(const String& msg, const String& errorMsg)
{
    // Get either saved callstack from a script error, or current execution point
    String callStack = (!errorMsg.IsEmpty() && cc_has_error()) ?
        cc_get_error().CallStack : cc_get_callstack();
    if (callStack.IsEmpty())
        return false;

    std::vector<std::pair<String, String>> scipt_info = { { "ScriptState", callStack } };

    if (!errorMsg.IsEmpty())
    {
        scipt_info.emplace_back( "ErrorMessage", errorMsg );
    }

    send_message_to_debugger(editor_debugger, scipt_info, msg);
    return true;
}

bool send_state_to_debugger(const char *msg)
{
    return send_state_to_debugger(String(msg), String());
}

bool init_editor_debugging(const ConfigTree &cfg) 
{
#if AGS_PLATFORM_OS_WINDOWS
    editor_debugger = GetEditorDebugger(editor_debugger_instance_token);
#else
    // Editor isn't ported yet
    editor_debugger = nullptr;
#endif

    if (editor_debugger == nullptr)
        quit("editor_debugger is NULL but debugger enabled");

    if (editor_debugger->Initialize())
    {
        editor_debugging_initialized = 1;

        // Wait for the editor to send the initial breakpoints
        // and then its READY message
        while (check_for_messages_from_debugger() != kDbgResponse_Ready)
        {
            platform->Delay(10);
        }

        send_state_to_debugger("START");
        Debug::Printf(kDbgMsg_Info, "External debugger initialized");
        // Create and configure engine->editor log output
        apply_log_config(cfg, OutputDebuggerLogID, false, {});
        return true;
    }

    Debug::Printf(kDbgMsg_Error, "Failed to initialize external debugger");
    return false;
}

DebuggerResponse check_for_messages_from_debugger()
{
    if (editor_debugger->IsMessageAvailable())
    {
        char *msg = editor_debugger->GetNextMessage();
        if (msg == nullptr)
        {
            return kDbgResponse_Empty;
        }

        if (strncmp(msg, "<Engine Command=\"", 17) != 0) 
        {
            Debug::Printf(kDbgMsg_Warn, "Debugger: faulty message received:");
            Debug::Printf(kDbgMsg_Warn, "Debugger: %s", msg);
            free(msg);
            return kDbgResponse_Empty;
        }

        const char *msgPtr = &msg[17];
        DebuggerResponse response = kDbgResponse_Ok;

        if (strncmp(msgPtr, "START", 5) == 0)
        {
#if AGS_PLATFORM_OS_WINDOWS
            const char *windowHandle = strstr(msgPtr, "EditorWindow") + 14;
            editor_window_handle = (HWND)atoi(windowHandle);
#endif
            Debug::Printf(kDbgMsg_Info, "Debugger: session start");
        }
        else if (strncmp(msgPtr, "READY", 5) == 0)
        {
            response = kDbgResponse_Ready;
        }
        else if ((strncmp(msgPtr, "SETBREAK", 8) == 0) ||
            (strncmp(msgPtr, "DELBREAK", 8) == 0))
        {
            bool isDelete = (msgPtr[0] == 'D');
            // Format:  SETBREAK $scriptname$lineNumber$
            msgPtr += 10;
            char scriptNameBuf[sizeof(Breakpoint::scriptName)]{};
            for (size_t i = 0; msgPtr[0] != '$'; ++msgPtr, ++i)
            {
                if (i < sizeof(scriptNameBuf) - 1)
                    scriptNameBuf[i] = msgPtr[0];
            }
            msgPtr++;

            int lineNumber = atoi(msgPtr);

            if (isDelete) 
            {
                for (size_t i = 0; i < breakpoints.size(); ++i)
                {
                    if ((breakpoints[i].lineNumber == lineNumber) &&
                        (strcmp(breakpoints[i].scriptName, scriptNameBuf) == 0))
                    {
                        Debug::Printf("Debugger: breakpoint del at %s : %d", scriptNameBuf, lineNumber);
                        breakpoints.erase(breakpoints.begin() + i);
                        break;
                    }
                }
            }
            else 
            {
                Breakpoint bp;
                snprintf(bp.scriptName, sizeof(Breakpoint::scriptName), "%s", scriptNameBuf);
                bp.lineNumber = lineNumber;
                breakpoints.push_back(bp);
                Debug::Printf("Debugger: breakpoint set at %s : %d", scriptNameBuf, lineNumber);
            }
        }
        else if (strncmp(msgPtr, "RESUME", 6) == 0) 
        {
            Debug::Printf("Debugger: resume engine");
            game_paused_in_debugger = 0;
            response = kDbgResponse_Resume;
        }
        else if (strncmp(msgPtr, "STEP", 4) == 0) 
        {
            Debug::Printf("Debugger: step script");
            game_paused_in_debugger = 0;
            break_on_next_script_step = 1;
            response = kDbgResponse_Step;
        }
        else if (strncmp(msgPtr, "EXIT", 4) == 0) 
        {
            Debug::Printf(kDbgMsg_Info, "Debugger: shutdown engine");
            want_exit = true;
            abort_engine = true;
            check_dynamic_sprites_at_exit = 0;
            response = kDbgResponse_Exit;
        }

        free(msg);
        return response;
    }

    return kDbgResponse_Empty;
}




bool send_exception_to_debugger(const char *qmsg)
{
#if AGS_PLATFORM_OS_WINDOWS
    want_exit = false;
    // allow the editor to break with the error message
    if (editor_window_handle != NULL)
        SetForegroundWindow(editor_window_handle);

    if (!send_state_to_debugger("ERROR", qmsg))
        return false;

    for (DebuggerResponse resp = check_for_messages_from_debugger();
         resp != kDbgResponse_Resume && resp != kDbgResponse_Exit && (!want_exit);
         resp = check_for_messages_from_debugger())
    {
        platform->Delay(10);
    }
#else
    (void)qmsg;
#endif
    return true;
}


void break_into_debugger() 
{
#if AGS_PLATFORM_OS_WINDOWS

    if (!send_state_to_debugger("BREAK"))
        return;

    if (editor_window_handle != NULL)
        SetForegroundWindow(editor_window_handle);

    game_paused_in_debugger = 1;

    while (game_paused_in_debugger) 
    {
        update_polled_stuff();
        platform->YieldCPU();
    }

#endif
}

int scrDebugWait = 0;
extern int pluginsWantingDebugHooks;

// allow LShift to single-step,  RShift to pause flow
void scriptDebugHook (ccInstance *ccinst, int linenum) {

    if (pluginsWantingDebugHooks > 0) {
        // a plugin is handling the debugging
        const char *scname = ccinst ? ccinst->GetScript()->GetScriptName().c_str() : "(Not in script)";
        pl_run_plugin_debug_hooks(scname, linenum);
        return;
    }

    // no plugin, use built-in debugger

    if (ccinst == nullptr) 
    {
        // come out of script
        return;
    }

    if (break_on_next_script_step) 
    {
        break_on_next_script_step = 0;
        break_into_debugger();
        return;
    }

    String scriptName = ccinst->GetRunningInst()->GetScript()->GetSectionName(ccinst->GetPC());
    for (const auto & breakpoint : breakpoints)
    {
        if ((breakpoint.lineNumber == linenum) &&
            (scriptName.Compare(breakpoint.scriptName) == 0))
        {
            break_into_debugger();
            break;
        }
    }
}

int scrlockWasDown = 0;

void check_debug_keys() {
    if (play.debug_mode) {
        // do the run-time script debugging

        const Uint8 *ks = SDL_GetKeyboardState(nullptr);
        if ((!ks[SDL_SCANCODE_SCROLLLOCK]) && (scrlockWasDown))
            scrlockWasDown = 0;
        else if ((ks[SDL_SCANCODE_SCROLLLOCK]) && (!scrlockWasDown)) {

            break_on_next_script_step = 1;
            scrlockWasDown = 1;
        }

    }

}
