#ifdef _MSC_VER
#  pragma warning( disable: 4267 )  // conversion from don't care to whatever, possible loss of data
#endif

#ifdef __MINGW32__
#  define __STDC_FORMAT_MACROS
#endif
#include <algorithm>
#include <assert.h>
#include <inttypes.h>
#include <mutex>

#include "imgui.h"

#include "TracyConfig.hpp"
#include "TracyFileRead.hpp"
#include "TracyFilesystem.hpp"
#include "TracyImGui.hpp"
#include "TracyManualData.hpp"
#include "TracyPrint.hpp"
#include "TracySourceView.hpp"
#include "TracyTexture.hpp"
#include "TracyView.hpp"
#include "../server/TracySysUtil.hpp"
#include "../public/common/TracyStackFrames.hpp"
#include "../Fonts.hpp"

#include "imgui_internal.h"
#include "IconsFontAwesome7.h"

namespace tracy
{

double s_time = 0;

View::View( void(*cbMainThread)(const std::function<void()>&, bool), const char* addr, uint16_t port, SetTitleCallback stcb, SetScaleCallback sscb, AttentionCallback acb, AchievementsMgr* amgr )
    : m_worker( addr, port, s_config.memoryLimit == 0 ? -1 : ( s_config.memoryLimitPercent * tracy::GetPhysicalMemorySize() / 100 ) )
    , m_staticView( false )
    , m_viewMode( ViewMode::LastFrames )
    , m_viewModeHeuristicTry( true )
    , m_totalMemory( GetPhysicalMemorySize() )
    , m_forceConnectionPopup( true, true )
    , m_tc( *this, m_worker, s_config.threadedRendering )
    , m_frames( nullptr )
    , m_messagesScrollBottom( true )
    , m_reactToCrash( true )
    , m_reactToLostConnection( true )
    , m_stcb( stcb )
    , m_sscb( sscb )
    , m_acb( acb )
    , m_cbMainThread( cbMainThread )
    , m_achievementsMgr( amgr )
    , m_achievements( s_config.achievements )
    , m_horizontalScrollMultiplier( s_config.horizontalScrollMultiplier )
    , m_verticalScrollMultiplier( s_config.verticalScrollMultiplier )
    , m_manualData( std::make_shared<TracyManualData>() )
    , m_markdown( this, &m_worker )
#ifdef __EMSCRIPTEN__
    , m_td( 2, "ViewMt" )
#else
    , m_td( std::thread::hardware_concurrency(), "ViewMt" )
    , m_llm( m_worker, *this, *m_manualData )
#endif
{
    InitTextEditor();
    SetupConfig();
    SetupRanges();
}

View::View( void(*cbMainThread)(const std::function<void()>&, bool), FileRead& f, SetTitleCallback stcb, SetScaleCallback sscb, AttentionCallback acb, AchievementsMgr* amgr )
    : m_worker( f )
    , m_filename( f.GetFilename() )
    , m_staticView( true )
    , m_viewMode( ViewMode::Paused )
    , m_totalMemory( GetPhysicalMemorySize() )
    , m_tc( *this, m_worker, s_config.threadedRendering )
    , m_frames( m_worker.GetFramesBase() )
    , m_messagesScrollBottom( false )
    , m_stcb( stcb )
    , m_sscb( sscb )
    , m_acb( acb )
    , m_userData( m_worker.GetCaptureProgram().c_str(), m_worker.GetCaptureTime(), f.GetFilename().c_str() )
    , m_cbMainThread( cbMainThread )
    , m_achievementsMgr( amgr )
    , m_achievements( s_config.achievements )
    , m_horizontalScrollMultiplier( s_config.horizontalScrollMultiplier )
    , m_verticalScrollMultiplier( s_config.verticalScrollMultiplier )
    , m_manualData( std::make_shared<TracyManualData>() )
    , m_markdown( this, &m_worker )
#ifdef __EMSCRIPTEN__
    , m_td( 2, "ViewMt" )
#else
    , m_td( std::thread::hardware_concurrency(), "ViewMt" )
    , m_llm( m_worker, *this, *m_manualData )
#endif
{
    m_notificationTime = 4;
    m_notificationText = std::string( "Trace loaded in " ) + TimeToString( m_worker.GetLoadTime() );

    InitTextEditor();
    SetupConfig();
    SetupRanges();

    m_vd.zvStart = m_worker.GetFirstTime();
    m_vd.zvEnd = m_worker.GetLastTime();
    m_userData.StateShouldBePreserved();
    m_userData.LoadState( m_vd );
    m_userData.LoadAnnotations( m_annotations );
    m_userData.LoadSourceSubstitutions( m_sourceSubstitutions );
    ValidateSourceRegex();

    if( m_worker.GetCallstackSampleCount() == 0 ) m_showAllSymbols = true;

    Achieve( "loadTrace" );
}

View::~View()
{
    m_worker.Shutdown();
    SaveUserData();

    if( m_compare.loadThread.joinable() ) m_compare.loadThread.join();
    if( m_saveThread.joinable() ) m_saveThread.join();

    if( m_FrameTextureCache.textureId ) FreeTexture( m_FrameTextureCache.textureId, m_cbMainThread );
    if( m_FrameTextureCacheConnection.textureId ) FreeTexture( m_FrameTextureCacheConnection.textureId, m_cbMainThread );

    if( m_playback.texture ) FreeTexture( m_playback.texture, m_cbMainThread );
}

void View::InitTextEditor()
{
    m_sourceView = std::make_unique<SourceView>();
    m_sourceViewFile = nullptr;
}

void View::SetupConfig()
{
    // Keep in sync with TracyView_Options.cpp View::DrawOptions(), bottom of the file.
    m_vd.frameTarget = s_config.targetFps;
    m_vd.drawFrameTargets = s_config.drawFrameTargets;
    m_vd.dynamicColors = s_config.dynamicColors;
    m_vd.forceColors = s_config.forceColors;
    m_vd.ghostZones = s_config.ghostZones;
    m_vd.shortenName = (ShortenName)s_config.shortenName;
    m_vd.drawSamples = s_config.drawSamples;
    m_vd.drawContextSwitches = s_config.drawContextSwitches;
    m_vd.plotHeight = s_config.plotHeight;
}

void View::Achieve( const char* id )
{
    if( !m_achievements || !m_achievementsMgr ) return;
    m_achievementsMgr->Achieve( id );
}

void View::SaveUserData()
{
    m_userData.StoreState( m_vd );
    m_userData.StoreAnnotations( m_annotations );
    m_userData.StoreSourceSubstitutions( m_sourceSubstitutions );
    m_userData.Save();
}

void View::ViewSource( const char* fileName, int line )
{
    assert( fileName );
    m_sourceViewFile = fileName;
    m_sourceView->OpenSource( fileName, line, *this, m_worker );
}

void View::ViewSource( const char* fileName, int line, const char* functionName )
{
    assert( functionName );

    uint64_t addr = 0;
    uint64_t base = 0;
    const auto fnsz = strlen( functionName );
    auto& symMap = m_worker.GetSymbolMap();
    for( auto& sym : symMap )
    {
        const auto name = m_worker.GetString( sym.second.name );
        const auto ptr = strstr( name, functionName );
        if( ptr &&
            ( ptr[fnsz] == 0 || ptr[fnsz] == '(' || ptr[fnsz] == '<' ) &&
            ( ptr == name || ( ptr[-1] == ' ' || ptr[-1] == ':' ) ) )
        {
            if( addr != 0 )
            {
                // Ambiguous function name. Bail out.
                ViewSource( fileName, line );
                return;
            }
            else
            {
                addr = sym.first;
                if( sym.second.isInline )
                {
                    base = m_worker.GetSymbolForAddress( addr );
                    if( base == 0 )
                    {
                        addr = 0;
                    }
                }
                else
                {
                    base = addr;
                }
            }
        }
    }
    if( addr != 0 && base != 0 )
    {
        ViewSymbol( fileName, line, base, addr );
    }
    else
    {
        ViewSource( fileName, line );
    }
}

void View::ViewSourceCheckKeyMod( const char* fileName, int line, const char* functionName )
{
    if( ImGui::GetIO().KeyCtrl )
    {
        ViewSource( fileName, line );
    }
    else
    {
        ViewSource( fileName, line, functionName );
    }
}

void View::ViewSymbolSource( const char* fileName, int line )
{
    assert( fileName );
    if( m_sourceView->SwitchTo( fileName, line, m_worker, *this ) )
    {
        m_sourceViewFile = fileName;
    }
    else
    {
        ViewSource( fileName, line );
    }
}

void View::ViewSymbol( const char* fileName, int line, uint64_t baseAddr, uint64_t symAddr )
{
    assert( fileName || symAddr );
    m_sourceViewFile = fileName ? fileName : (const char*)~uint64_t( 0 );
    m_sourceView->OpenSymbol( fileName, line, baseAddr, symAddr, m_worker, *this );
}

void View::UpdateThreadOrder()
{
    const auto& threadData = m_worker.GetThreadData();
    if( threadData.size() == m_threadOrder.size() ) return;

    m_threadOrder.reserve( threadData.size() );
    // Only new threads are in the end of the worker's ThreadData vector.
    // Threads which get reordered by received thread hints are not new, yet removed from m_threadOrder.
    // Therefore, those are kept in m_threadReinsert and are gathered before the remaining new threads.
    const size_t numReinsert = m_threadReinsert.size();
    const size_t numNew = threadData.size() - m_threadOrder.size() - numReinsert;
    for( size_t i = 0; i < numReinsert + numNew; i++ )
    {
        const ThreadData* td = i < numReinsert ? m_threadReinsert[i] : threadData[m_threadOrder.size()];
        auto it = std::find_if( m_threadOrder.begin(), m_threadOrder.end(), [td]( const auto t ) { return td->groupHint < t->groupHint; } );
        m_threadOrder.insert( it, td );
    }
    m_threadReinsert.clear();
}

bool View::ViewDispatch( const char* fileName, int line, uint64_t symAddr )
{
    if( line == 0 )
    {
        fileName = nullptr;
    }
    else
    {
        if( !SourceFileValid( fileName, m_worker.GetCaptureTime(), *this, m_worker ) )
        {
            fileName = nullptr;
            line = 0;
        }
    }
    if( symAddr == 0 )
    {
        if( line != 0 )
        {
            ViewSource( fileName, line );
            return true;
        }
        return false;
    }
    else
    {
        uint64_t baseAddr = 0;
        if( m_worker.HasSymbolCode( symAddr ) )
        {
            baseAddr = symAddr;
        }
        else
        {
            const auto parentAddr = m_worker.GetSymbolForAddress( symAddr );
            if( parentAddr != 0 && m_worker.HasSymbolCode( parentAddr ) )
            {
                baseAddr = parentAddr;
            }
        }
        if( baseAddr != 0 || line != 0 )
        {
            ViewSymbol( fileName, line, baseAddr, symAddr );
            return true;
        }
        return false;
    }
}

static const char* CompressionName[] = {
    "LZ4",
    "LZ4 HC",
    "LZ4 HC extreme",
    "Zstd",
    nullptr
};

static const char* CompressionDesc[] = {
    "Fastest save, fast load time, big file size",
    "Slow save, fastest load time, reasonable file size",
    "Very slow save, fastest load time, file smaller than LZ4 HC",
    "Configurable save time (fast-slowest), reasonable load time, smallest file size",
    nullptr
};

static_assert( sizeof( CompressionName ) == sizeof( CompressionDesc ), "Unmatched compression names and descriptions" );


bool View::Draw()
{
    Worker::MainThreadDataLockGuard lock = m_worker.ObtainLockForMainThread();
    HandshakeStatus status = (HandshakeStatus)m_worker.GetHandshakeStatus();
    switch( status )
    {
    case HandshakeProtocolMismatch:
        Attention( m_attnProtoMismatch );
        ImGui::OpenPopup( "Protocol mismatch" );
        break;
    case HandshakeNotAvailable:
        Attention( m_attnNotAvailable );
        ImGui::OpenPopup( "Client not ready" );
        break;
    case HandshakeDropped:
        Attention( m_attnDropped );
        ImGui::OpenPopup( "Client disconnected" );
        break;
    default:
        break;
    }

    const auto& failure = m_worker.GetFailureType();
    if( failure != Worker::Failure::None )
    {
        Attention( m_attnFailure );
        ImGui::OpenPopup( "Instrumentation failure" );
    }

    if( ImGui::BeginPopupModal( "Protocol mismatch", nullptr, ImGuiWindowFlags_AlwaysAutoResize ) )
    {
        ImGui::PushFont( g_fonts.normal, FontBig );
        TextCentered( ICON_FA_TRIANGLE_EXCLAMATION );
        ImGui::PopFont();
        ImGui::TextUnformatted( "The client you are trying to connect to uses incompatible protocol version.\nMake sure you are using the same Tracy version on both client and server." );
        ImGui::Separator();
        if( ImGui::Button( "My bad" ) )
        {
            ImGui::CloseCurrentPopup();
            ImGui::EndPopup();
            m_attnProtoMismatch = false;
            return false;
        }
        ImGui::SameLine();
        if( ImGui::Button( "Reconnect" ) )
        {
            ImGui::CloseCurrentPopup();
            ImGui::EndPopup();
            m_reconnectRequested = true;
            m_attnProtoMismatch = false;
            return false;
        }
        ImGui::EndPopup();
    }

    if( ImGui::BeginPopupModal( "Client not ready", nullptr, ImGuiWindowFlags_AlwaysAutoResize ) )
    {
        ImGui::PushFont( g_fonts.normal, FontBig );
        TextCentered( ICON_FA_LIGHTBULB );
        ImGui::PopFont();
        ImGui::TextUnformatted( "The client you are trying to connect to is no longer able to sent profiling data,\nbecause another server was already connected to it.\nYou can do the following:\n\n  1. Restart the client application.\n  2. Rebuild the client application with on-demand mode enabled." );
        ImGui::Separator();
        if( ImGui::Button( "I understand" ) )
        {
            ImGui::CloseCurrentPopup();
            ImGui::EndPopup();
            m_attnNotAvailable = false;
            return false;
        }
        ImGui::SameLine();
        if( ImGui::Button( "Reconnect" ) )
        {
            ImGui::CloseCurrentPopup();
            ImGui::EndPopup();
            m_reconnectRequested = true;
            m_attnNotAvailable = false;
            return false;
        }
        ImGui::EndPopup();
    }

    if( ImGui::BeginPopupModal( "Client disconnected", nullptr, ImGuiWindowFlags_AlwaysAutoResize ) )
    {
        ImGui::PushFont( g_fonts.normal, FontBig );
        TextCentered( ICON_FA_HANDSHAKE );
        ImGui::PopFont();
        ImGui::TextUnformatted( "The client you are trying to connect to has disconnected during the initial\nconnection handshake. Please check your network configuration." );
        ImGui::Separator();
        if( ImGui::Button( "Will do" ) )
        {
            ImGui::CloseCurrentPopup();
            ImGui::EndPopup();
            m_attnDropped = false;
            return false;
        }
        ImGui::SameLine();
        if( ImGui::Button( "Reconnect" ) )
        {
            ImGui::CloseCurrentPopup();
            ImGui::EndPopup();
            m_reconnectRequested = true;
            m_attnDropped = false;
            return false;
        }
        ImGui::EndPopup();
    }

    if( ImGui::BeginPopupModal( "Instrumentation failure", nullptr, ImGuiWindowFlags_AlwaysAutoResize ) )
    {
        const auto& data = m_worker.GetFailureData();
        ImGui::PushFont( g_fonts.normal, FontBig );
        TextCentered( ICON_FA_SKULL );
        ImGui::PopFont();
        ImGui::TextUnformatted( "Profiling session terminated due to improper instrumentation.\nPlease correct your program and try again." );
        ImGui::TextUnformatted( "Reason:" );
        ImGui::SameLine();
        ImGui::TextUnformatted( Worker::GetFailureString( failure ) );
        ImGui::Separator();
        if( data.srcloc != 0 )
        {
            const auto& srcloc = m_worker.GetSourceLocation( data.srcloc );
            if( srcloc.name.active )
            {
                TextFocused( "Zone name:", m_worker.GetString( srcloc.name ) );
            }
            TextFocused( "Function:", m_worker.GetString( srcloc.function ) );
            TextDisabledUnformatted( "Location:" );
            ImGui::SameLine();
            ImGui::TextUnformatted( LocationToString( m_worker.GetString( srcloc.file ), srcloc.line ) );
        }
        if( data.thread != 0 )
        {
            TextFocused( "Thread:", m_worker.GetThreadName( data.thread ) );
            ImGui::SameLine();
            ImGui::TextDisabled( "(%s)", RealToString( data.thread ) );
            if( m_worker.IsThreadFiber( data.thread ) )
            {
                ImGui::SameLine();
                TextColoredUnformatted( ImVec4( 0.2f, 0.6f, 0.2f, 1.f ), "Fiber" );
            }
        }
        if( !data.message.empty() )
        {
            TextFocused( "Context:", data.message.c_str() );
        }
        if( data.callstack != 0 )
        {
            if( ImGui::TreeNode( "Call stack" ) )
            {
                ImGui::BeginChild( "##callstackFailure", ImVec2( 1200, 500 ) );
                if( ImGui::BeginTable( "##callstack", 4, ImGuiTableFlags_Resizable | ImGuiTableFlags_Borders ) )
                {
                    ImGui::TableSetupColumn( "Frame", ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoResize );
                    ImGui::TableSetupColumn( "Function" );
                    ImGui::TableSetupColumn( "Location" );
                    ImGui::TableSetupColumn( "Image" );
                    ImGui::TableHeadersRow();

                    auto& cs = m_worker.GetCallstack( data.callstack );
                    int fidx = 0;
                    for( auto& entry : cs )
                    {
                        auto frameData = m_worker.GetCallstackFrame( entry );
                        if( !frameData )
                        {
                            ImGui::TableNextRow();
                            ImGui::TableNextColumn();
                            ImGui::Text( "%i", fidx++ );
                            ImGui::TableNextColumn();
                            char buf[32];
                            sprintf( buf, "%p", (void*)m_worker.GetCanonicalPointer( entry ) );
                            ImGui::TextUnformatted( buf );
                            if( ImGui::IsItemHovered() )
                            {
                                ImGui::BeginTooltip();
                                ImGui::TextUnformatted( "Click on entry to copy it to clipboard." );
                                ImGui::EndTooltip();
                                if( ImGui::IsItemClicked() )
                                {
                                    ImGui::SetClipboardText( buf );
                                }
                            }
                        }
                        else
                        {
                            const auto fsz = frameData->size;
                            for( uint8_t f=0; f<fsz; f++ )
                            {
                                const auto& frame = frameData->data[f];
                                auto txt = m_worker.GetString( frame.name );

                                if( fidx == 0 && f != fsz-1 )
                                {
                                    auto test = s_tracyStackFrames;
                                    bool match = false;
                                    do
                                    {
                                        if( strcmp( txt, *test ) == 0 )
                                        {
                                            match = true;
                                            break;
                                        }
                                    }
                                    while( *++test );
                                    if( match ) continue;
                                }

                                ImGui::TableNextRow();
                                ImGui::TableNextColumn();
                                if( f == fsz-1 )
                                {
                                    ImGui::Text( "%i", fidx++ );
                                }
                                else
                                {
                                    TextDisabledUnformatted( "inline" );
                                }
                                ImGui::TableNextColumn();
                                {
                                    ImGui::PushTextWrapPos( 0.0f );
                                    if( txt[0] == '[' )
                                    {
                                        TextDisabledUnformatted( txt );
                                    }
                                    else
                                    {
                                        ImGui::TextUnformatted( txt );
                                    }
                                    ImGui::PopTextWrapPos();
                                }
                                if( ImGui::IsItemHovered() )
                                {
                                    ImGui::BeginTooltip();
                                    ImGui::TextUnformatted( "Click on entry to copy it to clipboard." );
                                    ImGui::EndTooltip();
                                    if( ImGui::IsItemClicked() )
                                    {
                                        ImGui::SetClipboardText( txt );
                                    }
                                }
                                ImGui::TableNextColumn();
                                ImGui::PushTextWrapPos( 0.0f );
                                txt = m_worker.GetString( frame.file );
                                TextDisabledUnformatted( LocationToString( txt, frame.line ) );
                                if( ImGui::IsItemHovered() )
                                {
                                    ImGui::BeginTooltip();
                                    ImGui::TextUnformatted( "Click on entry to copy it to clipboard." );
                                    ImGui::EndTooltip();
                                    if( ImGui::IsItemClicked() )
                                    {
                                        ImGui::SetClipboardText( txt );
                                    }
                                }
                                ImGui::PopTextWrapPos();
                                ImGui::TableNextColumn();
                                if( frameData->imageName.Active() )
                                {
                                    TextDisabledUnformatted( m_worker.GetString( frameData->imageName ) );
                                }
                            }
                        }
                    }
                    ImGui::EndTable();
                }
                ImGui::EndChild();
                ImGui::TreePop();
            }
        }
        ImGui::Separator();
        if( ImGui::Button( "I understand" ) )
        {
            ImGui::CloseCurrentPopup();
            m_worker.ClearFailure();
            m_attnFailure = false;
        }
        ImGui::EndPopup();
    }

    static FileCompression comp = FileCompression::Zstd;
    static int zlvl = 3;
    static bool buildDict = false;
    static int streams = 4;

    bool saveFailed = false;
    if( !m_filenameStaging.empty() )
    {
        ImGui::OpenPopup( "Save trace" );
    }
    if( ImGui::BeginPopupModal( "Save trace", nullptr, ImGuiWindowFlags_AlwaysAutoResize ) )
    {
        assert( !m_filenameStaging.empty() );
        auto fn = m_filenameStaging.c_str();
        ImGui::PushFont( g_fonts.normal, FontBig );
        TextFocused( "Path:", fn );
        ImGui::PopFont();
        ImGui::Separator();

        if( ImGui::TreeNode( ICON_FA_FILE_ZIPPER " Trace compression" ) )
        {
            TextDisabledUnformatted( "Can be changed later with the upgrade utility" );
            ImGui::Indent();
            int idx = 0;
            while( CompressionName[idx] )
            {
                if( ImGui::RadioButton( CompressionName[idx], (int)comp == idx ) ) comp = (FileCompression)idx;
                ImGui::SameLine();
                TextDisabledUnformatted( CompressionDesc[idx] );
                idx++;
            }
            ImGui::Unindent();
            ImGui::TextUnformatted( "Zstd level" );
            ImGui::SameLine();
            TextDisabledUnformatted( "Increasing level decreases file size, but increases save and load times" );
            ImGui::Indent();
            if( ImGui::SliderInt( "##zstd", &zlvl, 1, 22, "%d", ImGuiSliderFlags_AlwaysClamp ) )
            {
                comp = FileCompression::Zstd;
            }
            ImGui::Unindent();

            ImGui::TextUnformatted( ICON_FA_SHUFFLE " Compression streams" );
            ImGui::SameLine();
            TextDisabledUnformatted( "Parallelize save and load at the cost of file size" );
            ImGui::Indent();
            ImGui::SliderInt( "##streams", &streams, 1, 64, "%d", ImGuiSliderFlags_AlwaysClamp );
            ImGui::Unindent();

            if( m_worker.GetFrameImageCount() != 0 )
            {
                ImGui::Separator();
                ImGui::Checkbox( "Build frame images dictionary", &buildDict );
                ImGui::SameLine();
                TextDisabledUnformatted( "Decreases run-time memory requirements" );
            }
            ImGui::TreePop();
        }

        ImGui::Separator();
        if( ImGui::Button( ICON_FA_FLOPPY_DISK " Save trace" ) )
        {
            saveFailed = !Save( fn, comp, zlvl, buildDict, streams );
            m_filenameStaging.clear();
            ImGui::CloseCurrentPopup();
            Achieve( "saveTrace" );
        }
        ImGui::SameLine();
        if( ImGui::Button( "Cancel" ) )
        {
            m_filenameStaging.clear();
            ImGui::CloseCurrentPopup();
        }
        ImGui::EndPopup();
    }

    if( saveFailed ) ImGui::OpenPopup( "Save failed" );
    if( ImGui::BeginPopupModal( "Save failed", nullptr, ImGuiWindowFlags_AlwaysAutoResize ) )
    {
        ImGui::PushFont( g_fonts.normal, FontBig );
        TextCentered( ICON_FA_TRIANGLE_EXCLAMATION );
        ImGui::PopFont();
        ImGui::TextUnformatted( "Could not save trace at the specified location. Try again somewhere else." );
        ImGui::Separator();
        if( ImGui::Button( "Oh well" ) ) ImGui::CloseCurrentPopup();
        ImGui::EndPopup();
    }

    if( !m_staticView &&
        ( ImGui::IsKeyDown( ImGuiKey_LeftCtrl ) || ImGui::IsKeyDown( ImGuiKey_RightCtrl ) ) &&
        ( ImGui::IsKeyDown( ImGuiKey_LeftShift ) || ImGui::IsKeyDown( ImGuiKey_RightShift ) ) &&
        ( ImGui::IsKeyDown( ImGuiKey_LeftAlt ) || ImGui::IsKeyDown( ImGuiKey_RightAlt ) ) &&
        ImGui::IsKeyPressed( ImGuiKey_R ) )
    {
        m_reconnectRequested = true;
        return false;
    }

    s_time += ImGui::GetIO().DeltaTime;
    return DrawImpl();
}

static const char* MainWindowButtons[] = {
    ICON_FA_PLAY " Resume",
    ICON_FA_PAUSE " Pause",
    ICON_FA_SQUARE " Stopped"
};

constexpr size_t MainWindowButtonsCount = sizeof( MainWindowButtons ) / sizeof( *MainWindowButtons );

bool View::DrawImpl()
{
    if( !m_worker.HasData() )
    {
        bool keepOpen = true;
        char tmp[2048];
        sprintf( tmp, "%s###Connection", m_worker.GetAddr().c_str() );
        ImGui::Begin( tmp, &keepOpen, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse );
        ImGui::PushFont( g_fonts.normal, FontNormal * 2.f );
        ImGui::Spacing();
        TextCentered( ICON_FA_WIFI );
        ImGui::Spacing();
        ImGui::PopFont();
        ImGui::TextUnformatted( "Waiting for connection…" );
        DrawWaitingDotsCentered( s_time );
        ImGui::End();
        return keepOpen;
    }

    if( m_achievements )
    {
        if( m_worker.IsConnected() ) Achieve( "connectToClient" );
        if( m_worker.GetZoneCount() > 0 ) Achieve( "instrumentationIntro" );
        if( m_worker.GetZoneCount() > 100 * 1000 * 1000 ) Achieve( "100million" );
        if( m_worker.GetCallstackSampleCount() > 0 ) Achieve( "samplingIntro" );
        if( m_worker.AreFramesUsed() ) Achieve( "instrumentFrames" );
    }

    Attention( m_attnWorking );

    if( !m_uarchSet )
    {
        m_uarchSet = true;
        m_sourceView->SetCpuId( m_worker.GetCpuId() );
    }
    if( !m_userData.Valid() ) m_userData.Init( m_worker.GetCaptureProgram().c_str(), m_worker.GetCaptureTime(), nullptr );
    if( m_saveThreadState.load( std::memory_order_acquire ) == SaveThreadState::NeedsJoin )
    {
        m_saveThread.join();
        m_saveThreadState.store( SaveThreadState::Inert, std::memory_order_release );
        const auto src = m_srcFileBytes.load( std::memory_order_relaxed );
        const auto dst = m_dstFileBytes.load( std::memory_order_relaxed );
        m_notificationTime = 4;
        char buf[1024];
        sprintf( buf, "Trace size %s (%.2f%% ratio)", MemSizeToString( dst ), 100.f * dst / src );
        m_notificationText = buf;
        m_acb();
    }

    const auto& io = ImGui::GetIO();
    m_wasActive.store( false, std::memory_order_release );

    assert( m_shortcut == ShortcutAction::None );
    if( io.KeyCtrl )
    {
        if( ImGui::IsKeyPressed( ImGuiKey_F ) )
        {
            m_findZone.show = true;
            m_shortcut = ShortcutAction::OpenFind;
        }
    }

    if( !m_frames ) m_frames = m_worker.GetFramesBase();

    const auto th = ImGui::GetTextLineHeight();
    float bw = 0;
    for( int i=0; i<MainWindowButtonsCount; i++ )
    {
        bw = std::max( bw, ImGui::CalcTextSize( MainWindowButtons[i] ).x );
    }
    bw += th;

    bool keepOpen = true;
    bool* keepOpenPtr = nullptr;
    (void)keepOpenPtr;
    if( m_staticView )
    {
        keepOpenPtr = &keepOpen;
    }

#ifndef TRACY_NO_ROOT_WINDOW
    if( !m_titleSet && m_stcb )
    {
        m_titleSet = true;
        UpdateTitle();
    }

    ImGuiViewport* viewport = ImGui::GetMainViewport();
    {
        auto& style = ImGui::GetStyle();
        const auto wrPrev = style.WindowRounding;
        const auto wbsPrev = style.WindowBorderSize;
        const auto wpPrev = style.WindowPadding;
        style.WindowRounding = 0.f;
        style.WindowBorderSize = 0.f;
        style.WindowPadding = ImVec2( 4.f, 4.f );
        style.Colors[ImGuiCol_WindowBg] = ImVec4( 0.129f, 0.137f, 0.11f, 1.f );

        ImGui::SetNextWindowPos( viewport->Pos );
        ImGui::SetNextWindowSize( ImVec2( m_rootWidth, m_rootHeight ) );
        ImGui::SetNextWindowViewport( viewport->ID );
        ImGui::Begin( "Timeline view###Profiler", nullptr, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoNavFocus );

        style.WindowRounding = wrPrev;
        style.WindowBorderSize = wbsPrev;
        style.WindowPadding = wpPrev;
        style.Colors[ImGuiCol_WindowBg] = ImVec4( 0.11f, 0.11f, 0.08f, 1.f );
    }
#else
    char tmp[2048];
    sprintf( tmp, "%s###Profiler", m_worker.GetCaptureName().c_str() );
    ImGui::SetNextWindowSize( ImVec2( 1550, 800 ), ImGuiCond_FirstUseEver );
    ImGui::Begin( tmp, keepOpenPtr, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoBringToFrontOnFocus );
    ImGuiViewport* viewport = ImGui::GetMainViewport();
#endif

    if( !m_staticView )
    {
        if( ImGui::Button( ICON_FA_WIFI ) || m_forceConnectionPopup )
        {
            if( m_forceConnectionPopup )
            {
                m_forceConnectionPopup.Decay( false );
                ImGui::SetNextWindowPos( viewport->Pos + ImGui::GetCursorPos() );
            }
            ImGui::OpenPopup( "TracyConnectionPopup" );
        }
        ImGui::SameLine();
        if( ImGui::BeginPopup( "TracyConnectionPopup" ) )
        {
            const bool wasDisconnectIssued = m_disconnectIssued;
            const bool discardData = !DrawConnection();
            const bool disconnectIssuedJustNow = m_disconnectIssued != wasDisconnectIssued;
            if( discardData ) keepOpen = false;
            if( disconnectIssuedJustNow || discardData ) ImGui::CloseCurrentPopup();
            ImGui::EndPopup();
        }
    }
    auto& threadHints = m_worker.GetPendingThreadHints();
    if( !threadHints.empty() )
    {
        m_threadReinsert.reserve( threadHints.size()  );
        for( auto v : threadHints )
        {
            auto it = std::find_if( m_threadOrder.begin(), m_threadOrder.end(), [v]( const auto& t ) { return t->id == v; } );
            if( it != m_threadOrder.end() )
            {
                // Will be reinserted in the correct place later.
                // A separate list is kept of threads that were already known to avoid having to figure out which one is missing in m_threadOrder.
                m_threadReinsert.push_back( *it );
                m_threadOrder.erase( it );
            }
        }
        m_worker.ClearPendingThreadHints();
    }
    m_worker.DoPostponedWork();
    if( !m_worker.IsDataStatic() )
    {
        if( m_worker.IsConnected() )
        {
            if( ImGui::Button( m_viewMode == ViewMode::Paused ? MainWindowButtons[0] : MainWindowButtons[1], ImVec2( bw, 0 ) ) )
            {
                if( m_viewMode != ViewMode::Paused )
                {
                    m_viewMode = ViewMode::Paused;
                    m_viewModeHeuristicTry = false;
                }
                else
                {
                    ImGui::OpenPopup( "viewMode" );
                }
            }
        }
        else
        {
            Attention( m_attnDisconnected );
            ImGui::BeginDisabled();
            ImGui::ButtonEx( MainWindowButtons[2], ImVec2( bw, 0 ) );
            ImGui::EndDisabled();
        }
        if( ImGui::BeginPopup( "viewMode" ) )
        {
            if( ImGui::Selectable( ICON_FA_MAGNIFYING_GLASS_PLUS " Newest three frames" ) )
            {
                m_viewMode = ViewMode::LastFrames;
            }
            if( ImGui::Selectable( ICON_FA_RULER_HORIZONTAL " Use current zoom level" ) )
            {
                m_viewMode = ViewMode::LastRange;
            }
            ImGui::EndPopup();
        }
        else if( m_viewModeHeuristicTry )
        {
            const auto lastTime = m_worker.GetLastTime() - m_worker.GetFirstTime();
            if( lastTime > 5*1000*1000*1000ll )
            {
                if( m_viewMode == ViewMode::LastFrames && m_worker.GetFrameCount( *m_worker.GetFramesBase() ) <= ( m_worker.IsOnDemand() ? 3 : 2 ) )
                {
                    m_viewMode = ViewMode::LastRange;
                    ZoomToRange( lastTime - 5*1000*1000*1000ll, lastTime, false );
                }
                else
                {
                    m_viewModeHeuristicTry = false;
                }
            }
        }
    }
    else
    {
        ImGui::PushStyleColor( ImGuiCol_Button, (ImVec4)ImColor::HSV( 0.f, 0.6f, 0.6f) );
        ImGui::PushStyleColor( ImGuiCol_ButtonHovered, (ImVec4)ImColor::HSV( 0.f, 0.7f, 0.7f) );
        ImGui::PushStyleColor( ImGuiCol_ButtonActive, (ImVec4)ImColor::HSV( 0.f, 0.8f, 0.8f) );
        if( ImGui::Button( ICON_FA_POWER_OFF ) ) keepOpen = false;
        ImGui::PopStyleColor( 3 );
    }
    ImGui::SameLine();
    ToggleButton( ICON_FA_GEAR, m_showOptions );
    ImGui::SameLine();
    ToggleButton( ICON_FA_COMMENT " Messages", m_showMessages );
    ImGui::SameLine();
    ToggleButton( ICON_FA_MAGNIFYING_GLASS " Find", m_findZone.show );
    ImGui::SameLine();
    ToggleButton( ICON_FA_ARROW_UP_WIDE_SHORT " Statistics", m_showStatistics );
    ImGui::SameLine();
    ToggleButton( ICON_FA_FIRE_FLAME_CURVED " Flame", m_showFlameGraph );
    ImGui::SameLine();
    ToggleButton( ICON_FA_MEMORY " Memory", m_memInfo.show );
    ImGui::SameLine();
    ToggleButton( ICON_FA_SCALE_BALANCED " Compare", m_compare.show );
    ImGui::SameLine();
    ToggleButton( ICON_FA_FINGERPRINT " Info", m_showInfo );
    ImGui::SameLine();
    if( ImGui::Button( ICON_FA_SCREWDRIVER_WRENCH ) ) ImGui::OpenPopup( "ToolsPopup" );
    if( ImGui::BeginPopup( "ToolsPopup" ) )
    {
        const auto ficnt = m_worker.GetFrameImageCount();
        if( ButtonDisablable( ICON_FA_PLAY " Playback", ficnt == 0 ) )
        {
            m_showPlayback = true;
        }
        const auto& ctd = m_worker.GetCpuThreadData();
        if( ButtonDisablable( ICON_FA_SLIDERS " CPU data", ctd.empty() ) )
        {
            m_showCpuDataWindow = true;
        }
        ToggleButton( ICON_FA_NOTE_STICKY " Annotations", m_showAnnotationList );
        ToggleButton( ICON_FA_RULER " Limits", m_showRanges );
        const auto cscnt = m_worker.GetContextSwitchSampleCount();
        if( ButtonDisablable( ICON_FA_HOURGLASS_HALF " Wait stacks", cscnt == 0 ) )
        {
            m_showWaitStacks = true;
        }
        if( ButtonDisablable( ICON_FA_IMAGES " Frame statistics", !m_worker.AreFramesUsed() ) )
        {
            m_showFrameStatistics = true;
        }
        ImGui::EndPopup();
    }
    ImGui::SameLine();
    ToggleButton( ICON_FA_BOOK, m_showManual );
    if( m_sscb )
    {
        ImGui::SameLine();
        if( ImGui::Button( ICON_FA_MAGNIFYING_GLASS_PLUS ) ) ImGui::OpenPopup( "ZoomPopup" );
        if( ImGui::BeginPopup( "ZoomPopup" ) )
        {
            if( ImGui::Button( "50%" ) )  m_sscb( 1.f/2 );
            if( ImGui::Button( "57%" ) )  m_sscb( 1.f/1.75f );
            if( ImGui::Button( "66%" ) )  m_sscb( 1.f/1.5f );
            if( ImGui::Button( "80%" ) )  m_sscb( 1.f/1.25f );
            if( ImGui::Button( "100%" ) ) m_sscb( 1.f );
            if( ImGui::Button( "125%" ) ) m_sscb( 1.25f );
            if( ImGui::Button( "150%" ) ) m_sscb( 1.5f );
            if( ImGui::Button( "175%" ) ) m_sscb( 1.75f );
            if( ImGui::Button( "200%" ) ) m_sscb( 2.f );
            if( ImGui::Button( "225%" ) ) m_sscb( 2.25f );
            if( ImGui::Button( "250%" ) ) m_sscb( 2.5f );
            if( ImGui::Button( "275%" ) ) m_sscb( 2.75f );
            if( ImGui::Button( "300%" ) ) m_sscb( 3.f );
            ImGui::EndPopup();
        }
    }
#ifndef __EMSCRIPTEN__
    if( s_config.llm )
    {
        ImGui::SameLine();
        ToggleButton( ICON_FA_ROBOT, m_llm.m_show );
    }
#endif
    if( m_worker.AreFramesUsed() )
    {
        ImGui::SameLine();
        if( ImGui::SmallButton( " " ICON_FA_CARET_LEFT " " ) ) ZoomToPrevFrame();
        ImGui::SameLine();
        {
            const auto vis = Vis( m_frames );
            if( !vis )
            {
                ImGui::PushStyleColor( ImGuiCol_Text, GImGui->Style.Colors[ImGuiCol_TextDisabled] );
            }
            ImGui::Text( "%s: %s", GetFrameSetName( *m_frames ), RealToString( m_worker.GetFrameCount( *m_frames ) ) );
            if( !vis )
            {
                ImGui::PopStyleColor();
            }
            if( ImGui::IsItemClicked() ) ImGui::OpenPopup( "GoToFramePopup" );
        }
        ImGui::SameLine();
        if( ImGui::SmallButton( " " ICON_FA_CARET_RIGHT " " ) ) ZoomToNextFrame();
        ImGui::SameLine();
        if( ImGui::BeginCombo( "##frameCombo", nullptr, ImGuiComboFlags_NoPreview ) )
        {
            auto& frames = m_worker.GetFrames();
            for( auto& fd : frames )
            {
                bool isSelected = m_frames == fd;
                if( ImGui::Selectable( GetFrameSetName( *fd ), isSelected ) )
                {
                    m_frames = fd;
                }
                if( isSelected )
                {
                    ImGui::SetItemDefaultFocus();
                }
                ImGui::SameLine();
                ImGui::TextDisabled( "(%s)", RealToString( fd->frames.size() ) );
            }
            ImGui::EndCombo();
        }
        if( ImGui::BeginPopup( "GoToFramePopup" ) )
        {
            static int frameNum = 1;
            const bool mainFrameSet = m_frames->name == 0;
            const auto numFrames = mainFrameSet ? m_frames->frames.size() - 1 : m_frames->frames.size();
            const auto frameOffset = mainFrameSet ? 0 : 1;
            ImGui::SetNextItemWidth( 120 * GetScale() );
            const bool clicked = ImGui::InputInt( "##goToFrame", &frameNum, 1, 100, ImGuiInputTextFlags_EnterReturnsTrue );
            frameNum = std::min( std::max( frameNum, 1 ), int( numFrames ) );
            if( clicked ) ZoomToRange( m_worker.GetFrameBegin( *m_frames, frameNum - frameOffset ), m_worker.GetFrameEnd( *m_frames, frameNum - frameOffset ) );
            ImGui::EndPopup();
        }
    }

    {
        ImGui::SameLine();
        ImGui::Spacing();
        ImGui::SameLine();

        auto targetLabelSize = ImGui::CalcTextSize( ICON_FA_EYE " 12345.67 ms" ).x;
        auto cx = ImGui::GetCursorPosX();
        ImGui::Text( ICON_FA_EYE " %s", TimeToString( m_vd.zvEnd - m_vd.zvStart ) );
        TooltipIfHovered( "View span" );
        ImGui::SameLine();
        auto dx = ImGui::GetCursorPosX() - cx;
        if( dx < targetLabelSize ) ImGui::SameLine( cx + targetLabelSize );

        const auto firstTime = m_worker.GetFirstTime();
        const auto lastTime = m_worker.GetLastTime();
        cx = ImGui::GetCursorPosX();
        ImGui::Text( ICON_FA_DATABASE " %s", TimeToString( lastTime - firstTime ) );
        if( ImGui::IsItemHovered() )
        {
            ImGui::BeginTooltip();
            if( firstTime == 0 )
            {
                ImGui::Text( "Time span" );
            }
            else
            {
                TextFocused( "Total time span:", TimeToString( lastTime ) );
            }
            ImGui::EndTooltip();
            if( ImGui::IsItemClicked( 2 ) )
            {
                ZoomToRange( firstTime, lastTime );
            }
        }
        ImGui::SameLine();
        dx = ImGui::GetCursorPosX() - cx;
        if( dx < targetLabelSize ) ImGui::SameLine( cx + targetLabelSize );

        targetLabelSize = ImGui::CalcTextSize( ICON_FA_MEMORY " 1234.56 MB (123.45 %%)" ).x;
        cx = ImGui::GetCursorPosX();
        const auto mem = memUsage.load( std::memory_order_relaxed );
        ImGui::Text( ICON_FA_MEMORY " %s", MemSizeToString( mem ) );
        TooltipIfHovered( "Profiler memory usage" );
        if( m_totalMemory != 0 )
        {
            ImGui::SameLine();
            const auto memUse = float( mem ) / m_totalMemory * 100;
            if( memUse < 80 )
            {
                ImGui::TextDisabled( "(%.2f%%)", memUse );
            }
            else
            {
                ImGui::TextColored( ImVec4( 1.f, 0.25f, 0.25f, 1.f ), "(%.2f%%)", memUse );
            }
        }
        ImGui::SameLine();
        dx = ImGui::GetCursorPosX() - cx;
        if( dx < targetLabelSize ) ImGui::SameLine( cx + targetLabelSize );
        ImGui::Spacing();

        const auto memoryLimit = m_worker.GetMemoryLimit();
        if( memoryLimit > 0 )
        {
            ImGui::SameLine();
            if( memUsage.load( std::memory_order_relaxed ) > memoryLimit )
            {
                TextColoredUnformatted( 0xFF2222FF, ICON_FA_TRIANGLE_EXCLAMATION );
            }
            else
            {
                TextColoredUnformatted( 0xFF00FFFF, ICON_FA_TRIANGLE_EXCLAMATION );
            }
            if( ImGui::IsItemHovered() )
            {
                ImGui::BeginTooltip();
                ImGui::Text( "Memory limit: %s", MemSizeToString( memoryLimit ) );
                ImGui::EndTooltip();
            }
        }
    }
    DrawNotificationArea();

    m_frameHover = -1;

    DrawFrames();

    const auto dockspaceId = ImGui::GetID( "tracyDockspace" );
    ImGui::DockSpace( dockspaceId, ImVec2( 0, 0 ), ImGuiDockNodeFlags_NoDockingOverCentralNode );
    if( ImGuiDockNode* node = ImGui::DockBuilderGetCentralNode( dockspaceId ) )
    {
        node->LocalFlags |= ImGuiDockNodeFlags_NoTabBar;
    }
    ImGui::SetNextWindowDockID( dockspaceId );
    {
        auto& style = ImGui::GetStyle();
        const auto wpPrev = style.WindowPadding;
        style.WindowPadding = ImVec2( 1, 0 );
#ifndef TRACY_NO_ROOT_WINDOW
        style.Colors[ImGuiCol_WindowBg] = ImVec4( 0.129f, 0.137f, 0.11f, 1.f );
#endif

        ImGui::Begin( "Work area", nullptr, ImGuiWindowFlags_NoNavFocus );

        style.WindowPadding = wpPrev;
#ifndef TRACY_NO_ROOT_WINDOW
        style.Colors[ImGuiCol_WindowBg] = ImVec4( 0.11f, 0.11f, 0.08f, 1.f );
#endif
    }

    DrawTimeline();

    ImGui::End();
    ImGui::End();

    m_zoneHighlight = nullptr;
    m_gpuHighlight = nullptr;

    DrawInfoWindow();

    if( m_showOptions ) DrawOptions();
    if( m_showMessages ) DrawMessages();
    if( m_showFlameGraph ) DrawFlameGraph();
    if( m_findZone.show ) DrawFindZone();
    if( m_showStatistics ) DrawStatistics();
    if( m_memInfo.show ) DrawMemory();
    if( m_memInfo.showAllocList ) DrawAllocList();
    if( m_compare.show ) DrawCompare();
    if( m_callstackView.id != 0 ) DrawCallstackWindow();
    if( m_memoryAllocInfoWindow >= 0 ) DrawMemoryAllocWindow();
    if( m_showInfo ) DrawInfo();
    if( m_sourceViewFile ) DrawTextEditor();
    if( m_lockInfoWindow != InvalidId ) DrawLockInfoWindow();
    if( m_showPlayback ) DrawPlayback();
    if( m_showCpuDataWindow ) DrawCpuDataWindow();
    if( m_selectedAnnotation ) DrawSelectedAnnotation();
    if( m_showAnnotationList ) DrawAnnotationList();
    if( m_sampleParents.symAddr != 0 ) DrawSampleParents();
    if( m_showRanges ) DrawRanges();
    if( m_showWaitStacks ) DrawWaitStacks();
    if( m_showManual ) DrawManual();
    if( m_showFrameStatistics ) DrawFrameStatistics();
#ifndef __EMSCRIPTEN__
    if( m_llm.m_show ) m_llm.Draw( m_llmConstraint );
#endif

    if( m_setRangePopup.active )
    {
        m_setRangePopup.active = false;
        ImGui::OpenPopup( "SetZoneRange" );
    }
    if( ImGui::BeginPopup( "SetZoneRange" ) )
    {
        const auto s = std::min( m_setRangePopup.min, m_setRangePopup.max );
        const auto e = std::max( m_setRangePopup.min, m_setRangePopup.max );
        TextDisabledUnformatted( ICON_FA_RULER " Set time range for:" );
        ImGui::Indent();
        for( auto& r : m_ranges )
        {
            if( ImGui::Selectable( r.name ) )
            {
                r.range->active = true;
                r.range->min = s;
                r.range->max = e;
                m_showRanges = true;
            }
        }
        ImGui::Unindent();
        ImGui::Separator();
        if( ImGui::Selectable( ICON_FA_NOTE_STICKY " Add annotation" ) )
        {
            AddAnnotation( s, e );
        }
        ImGui::EndPopup();
    }
    m_setRangePopupOpen = ImGui::IsPopupOpen( "SetZoneRange" );

    if( m_zoomAnim.active )
    {
        if( m_viewMode == ViewMode::LastRange )
        {
            const auto delta = m_worker.GetLastTime() - m_vd.zvEnd;
            if( delta != 0 )
            {
                m_zoomAnim.start0 += delta;
                m_zoomAnim.start1 += delta;
                m_zoomAnim.end0 += delta;
                m_zoomAnim.end1 += delta;
            }
        }
        UpdateZoomAnimation( m_zoomAnim, m_vd.zvStart, m_vd.zvEnd, io.DeltaTime );
    }

    bool active = m_wasActive.load( std::memory_order_acquire );

    active |= m_callstackBuzzAnim.Update( io.DeltaTime );
    active |= m_sampleParentBuzzAnim.Update( io.DeltaTime );
    active |= m_callstackTreeBuzzAnim.Update( io.DeltaTime );
    active |= m_zoneinfoBuzzAnim.Update( io.DeltaTime );
    active |= m_findZoneBuzzAnim.Update( io.DeltaTime );
    active |= m_optionsLockBuzzAnim.Update( io.DeltaTime );
    active |= m_lockInfoAnim.Update( io.DeltaTime );
    active |= m_statBuzzAnim.Update( io.DeltaTime );

    m_wasActive.store( active, std::memory_order_release );

    if( m_firstFrame )
    {
        const auto now = std::chrono::high_resolution_clock::now();
        if( m_firstFrameTime.time_since_epoch().count() == 0 )
        {
            m_firstFrameTime = now;
        }
        else
        {
            if( std::chrono::duration_cast<std::chrono::milliseconds>( now - m_firstFrameTime ).count() > 500 )
            {
                m_firstFrame = false;
                m_tc.FirstFrameExpired();
            }
        }
    }

    if( m_reactToCrash )
    {
        auto& crash = m_worker.GetCrashEvent();
        if( crash.thread != 0 )
        {
            m_reactToCrash = false;
            ImGui::OpenPopup( "Application crashed!" );
        }
    }
    if( ImGui::BeginPopupModal( "Application crashed!", nullptr, ImGuiWindowFlags_AlwaysAutoResize ) )
    {
        auto& crash = m_worker.GetCrashEvent();
        assert( crash.thread != 0 );
        ImGui::TextUnformatted( ICON_FA_SKULL );
        ImGui::SameLine();
        TextColoredUnformatted( 0xFF4444FF, "Application has crashed" );
        ImGui::SameLine();
        ImGui::TextUnformatted( ICON_FA_SKULL );
        ImGui::Separator();
        TextFocused( "Time:", TimeToString( crash.time ) );
        TextFocused( "Thread:", m_worker.GetThreadName( crash.thread ) );
        ImGui::SameLine();
        ImGui::TextDisabled( "(%s)", RealToString( crash.thread ) );
        if( m_worker.IsThreadFiber( crash.thread ) )
        {
            ImGui::SameLine();
            TextColoredUnformatted( ImVec4( 0.2f, 0.6f, 0.2f, 1.f ), "Fiber" );
        }
        TextFocused( "Reason:", m_worker.GetString( crash.message ) );
        if( crash.callstack != 0 )
        {
            bool hilite = m_callstackView.id == crash.callstack;
            if( hilite )
            {
                SetButtonHighlightColor();
            }
            if( ImGui::Button( ICON_FA_ALIGN_JUSTIFY " Call stack" ) )
            {
                m_callstackView = {
                    .id = crash.callstack,
                    .thread = crash.thread
                };
            }
            if( hilite )
            {
                ImGui::PopStyleColor( 3 );
            }
            if( ImGui::IsItemHovered() )
            {
                CallstackTooltip( crash.callstack );
            }
        }
        ImGui::Separator();
        if( ImGui::Button( ICON_FA_MICROSCOPE " Focus" ) ) CenterAtTime( crash.time );
        ImGui::SameLine();
        if( ImGui::Button( "Dismiss" ) ) ImGui::CloseCurrentPopup();
        ImGui::EndPopup();
    }

    if( m_reactToLostConnection && !m_worker.IsConnected() )
    {
        m_reactToLostConnection = false;
        const auto inFlight = m_worker.GetSendInFlight();
        if( inFlight > 1 || ( inFlight == 1 && !m_worker.WasDisconnectIssued() ) )
        {
            ImGui::OpenPopup( "Connection lost!" );
        }
    }
    if( ImGui::BeginPopupModal( "Connection lost!", nullptr, ImGuiWindowFlags_AlwaysAutoResize ) )
    {
        ImGui::PushFont( g_fonts.normal, FontBig );
        TextCentered( ICON_FA_PLUG );
        ImGui::PopFont();
        ImGui::TextUnformatted(
            "Connection to the profiled application was lost\n"
            "before all required profiling data could be retrieved.\n"
            "This will result in missing source locations,\n"
            "unresolved stack frames, etc." );
        ImGui::Separator();
        if( ImGui::Button( "Dismiss" ) ) ImGui::CloseCurrentPopup();
        ImGui::EndPopup();
    }

    return keepOpen;
}

void View::DrawFrameImage( FrameImageCache& cache, const FrameImage& fi, float scale )
{
    if ( fi.ptr != cache.dataPtr )
    {
        if( !cache.textureId ) cache.textureId = MakeTexture();
        UpdateTexture( cache.textureId, m_worker.UnpackFrameImage( fi ), fi.w, fi.h );
        cache.dataPtr = fi.ptr;
    }
    if( fi.flip )
    {
        ImGui::Image( cache.textureId, ImVec2( fi.w * scale, fi.h * scale ), ImVec2( 0, 1 ), ImVec2( 1, 0 ) );
    }
    else
    {
        ImGui::Image( cache.textureId, ImVec2( fi.w * scale, fi.h * scale ) );
    }
}

void View::DrawTextEditor()
{
    const auto scale = GetScale();
    ImGui::SetNextWindowSize( ImVec2( 1800 * scale, 800 * scale ), ImGuiCond_FirstUseEver );
    m_sourceViewConstraint.Constrain();
    bool show = true;
    ImGui::Begin( "Source view", &show, ImGuiWindowFlags_NoScrollbar );
    if( !ImGui::GetCurrentWindowRead()->SkipItems ) m_sourceView->Render( m_worker, *this, m_sourceViewConstraint );
    ImGui::End();
    if( !show ) m_sourceViewFile = nullptr;
}

void View::CrashTooltip()
{
    auto& crash = m_worker.GetCrashEvent();
    ImGui::BeginTooltip();
    TextFocused( "Time:", TimeToString( crash.time ) );
    TextFocused( "Reason:", m_worker.GetString( crash.message ) );
    ImGui::EndTooltip();
}

bool View::DrawSourceTooltip( const char* filename, uint32_t lineStart, uint32_t lineEnd, int before, int after, bool separateTooltip )
{
    if( !filename ) return false;
    if( !SourceFileValid( filename, m_worker.GetCaptureTime(), *this, m_worker ) ) return false;
    m_srcHintCache.Parse( filename, m_worker, *this );
    if( m_srcHintCache.empty() ) return false;
    ImGui::PushStyleVar( ImGuiStyleVar_ItemSpacing, ImVec2( 0, 0 ) );
    if( separateTooltip ) ImGui::BeginTooltip();
    ImGui::PushFont( g_fonts.mono, FontNormal );
    auto& lines = m_srcHintCache.get();
    bool first = true;
    int bottomEmpty = 0;
    constexpr int MaxRenderedLines = 40;
    const int start = std::max( 0, (int)lineStart - ( before+1 ) );
    const int end = std::min<int>( m_srcHintCache.get().size(), (int)lineEnd + after );
    const int endLoop = std::min( end, start + MaxRenderedLines );
    for( int i=start; i<endLoop; i++ )
    {
        auto& line = lines[i];
        const auto lineNum = uint32_t( i+1 );
        const bool inRange = lineNum >= lineStart && lineNum <= lineEnd;
        if( line.begin == line.end )
        {
            if( !first ) bottomEmpty++;
        }
        else
        {
            first = false;
            while( bottomEmpty > 0 )
            {
                ImGui::TextUnformatted( "" );
                bottomEmpty--;
            }

            auto ptr = line.begin;
            auto it = line.tokens.begin();
            while( ptr < line.end )
            {
                if( it == line.tokens.end() )
                {
                    ImGui::TextUnformatted( ptr, line.end );
                    ImGui::SameLine( 0, 0 );
                    break;
                }
                if( ptr < it->begin )
                {
                    ImGui::TextUnformatted( ptr, it->begin );
                    ImGui::SameLine( 0, 0 );
                }
                auto color = SyntaxColors[(int)it->color];
                if( !inRange ) color = ( color & 0xFFFFFF ) | 0x99000000;
                TextColoredUnformatted( color, it->begin, it->end );
                ImGui::SameLine( 0, 0 );
                ptr = it->end;
                ++it;
            }
            ImGui::ItemSize( ImVec2( 0, 0 ), 0 );
        }
    }
    if( end != endLoop ) ImGui::TextDisabled( "… (+%i more lines)", end - endLoop );
    ImGui::PopFont();
    if( separateTooltip ) ImGui::EndTooltip();
    ImGui::PopStyleVar();
    return true;
}

bool View::Save( const char* fn, FileCompression comp, int zlevel, bool buildDict, int streams )
{
    std::unique_ptr<FileWrite> f( FileWrite::Open( fn, comp, zlevel, streams ) );
    if( !f ) return false;

    m_filename = fn;
    if( m_stcb ) UpdateTitle();

    m_userData.StateShouldBePreserved();
    m_userData.SetFilePath( fn );
    m_saveThreadState.store( SaveThreadState::Saving, std::memory_order_relaxed );
    m_saveThread = std::thread( [this, f{std::move( f )}, buildDict] {
        Worker::MainThreadDataLockGuard lock = m_worker.ObtainLockForMainThread();
        m_worker.Write( *f, buildDict );
        f->Finish();
        const auto stats = f->GetCompressionStatistics();
        m_srcFileBytes.store( stats.first, std::memory_order_relaxed );
        m_dstFileBytes.store( stats.second, std::memory_order_relaxed );
        m_saveThreadState.store( SaveThreadState::NeedsJoin, std::memory_order_release );
    } );

    return true;
}

void View::HighlightThread( uint64_t thread )
{
    m_drawThreadMigrations = thread;
    m_drawThreadHighlight = thread;
}

void View::SelectThread( uint64_t thread )
{
    m_selectedThread = thread;
}

bool View::WasActive() const
{
    return m_wasActive.load( std::memory_order_acquire ) ||
        m_zoomAnim.active ||
        ( m_showFlameGraph && m_flameGraphZoomAnim.active ) ||
        m_notificationTime > 0 ||
        !m_playback.pause ||
        m_worker.IsConnected() ||
        !m_worker.IsBackgroundDone();
}

void View::DpiScaleChanged()
{
    m_flameGraphConstraint.Reset();
    m_messagesConstraint.Reset();
    m_findZoneConstraint.Reset();
    m_statisticsConstraint.Reset();
    m_memoryConstraint.Reset();
    m_compareConstraint.Reset();
    m_llmConstraint.Reset();
    m_waitStacksConstraint.Reset();
    m_frameStatsConstraint.Reset();
    m_sampleEntryConstraint.Reset();
    m_callstackConstraint.Reset();
    m_zoneInfoConstraint.Reset();
    m_gpuZoneInfoConstraint.Reset();
    m_sourceViewConstraint.Reset();
}

void View::AddLlmAttachment( const nlohmann::json& json )
{
#ifndef __EMSCRIPTEN__
    m_llm.AddAttachmentLocking( json.dump(), "user" );
    m_llm.m_show = true;
#endif
}

void View::AddLlmQuery( const char* query )
{
#ifndef __EMSCRIPTEN__
    std::string str( query );
    m_llm.AddMessageLocking( std::move( str ), "user" );
    m_llm.m_show = true;
    m_llm.QueueSendMessageLocking();
#endif
}

void View::ViewCallstack( uint32_t callstack, uint32_t thread, int64_t waitTime, const char* waitReason, const char* waitReasonCode, const char* waitState, const char* waitStateCode )
{
    m_callstackView = {
        .id = callstack,
        .thread = thread,
        .wait = {
            .time = waitTime,
            .reason = waitReason,
            .reasonCode = waitReasonCode,
            .state = waitState,
            .stateCode = waitStateCode
        }
    };
}

}
