#include "PluginEditor.h"
#include "SpectrumEngine.h"
#include "OscilloscopeEngine.h"
#include "VUMeterEngine.h"
#include "LUFSMeterEngine.h"
#include "VectorscopeEngine.h"
#include "SpectrogramEngine.h"
#include "WaveformEngine.h"
#include <cmath>
#include <cstdlib>
#include <cstring>

#ifndef PRISM_EMBED_WEBUI
 #define PRISM_EMBED_WEBUI 0
#endif

#ifndef PRISM_LINUX_UI_MODE_NAME
 #define PRISM_LINUX_UI_MODE_NAME "webview"
#endif

#ifndef PRISM_LINUX_UI_DIAGNOSTICS
 #define PRISM_LINUX_UI_DIAGNOSTICS 0
#endif

#if PRISM_EMBED_WEBUI
 #include "BinaryData.h"
#endif

#if JUCE_MAC
 #include "WebViewFrameRate.h"
#endif

namespace
{
    const juce::Identifier kRestoreSettingsEvent { "prismRestoreSettings" };
    constexpr int kDrainCapacity = 1 << 16;
#if JUCE_WINDOWS
    constexpr int kFallbackWindowsFrameRateHz = 60;
    constexpr int kMaxWindowsFrameRateHz = 240;

    int resolveWindowsFrameRateHz(const juce::Component& component)
    {
        const auto& displays = juce::Desktop::getInstance().getDisplays();
        if (const auto* display = displays.getDisplayForRect(component.getScreenBounds()))
        {
            const auto frequency = display->verticalFrequencyHz;
            if (frequency.has_value() && std::isfinite(*frequency) && *frequency > 0.0)
                return juce::jlimit(30, kMaxWindowsFrameRateHz, (int) std::lround(*frequency));
        }

        return kFallbackWindowsFrameRateHz;
    }
#endif

#if JUCE_LINUX
    constexpr int kLinuxFrameRateHz = 20;

#if PRISM_LINUX_UI_DIAGNOSTICS
    juce::String envValue(const char* name)
    {
        if (const auto* value = std::getenv(name); value != nullptr && value[0] != '\0')
            return value;

        return "<unset>";
    }

    juce::File linuxDiagnosticLogFile()
    {
        juce::File baseDir;

        if (const auto* xdgState = std::getenv("XDG_STATE_HOME"); xdgState != nullptr && xdgState[0] != '\0')
            baseDir = juce::File(juce::String(xdgState));
        else if (const auto* home = std::getenv("HOME"); home != nullptr && home[0] != '\0')
            baseDir = juce::File(juce::String(home)).getChildFile(".local").getChildFile("state");
        else
            baseDir = juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory);

        return baseDir.getChildFile("prism").getChildFile("plugin-ui-diagnostics.log");
    }

    void logLinuxDiagnostic(const juce::String& message)
    {
        const auto file = linuxDiagnosticLogFile();
        file.getParentDirectory().createDirectory();
        file.appendText(juce::Time::getCurrentTime().toISO8601(true) + " " + message + "\n",
                        false,
                        false,
                        "\n");
    }

    juce::String wrapperTypeName()
    {
        switch (juce::PluginHostType::getPluginLoadedAs())
        {
            case juce::AudioProcessor::wrapperType_VST3:       return "VST3";
            case juce::AudioProcessor::wrapperType_VST:        return "VST2";
            case juce::AudioProcessor::wrapperType_Standalone: return "Standalone";
            case juce::AudioProcessor::wrapperType_AudioUnit:  return "AudioUnit";
            case juce::AudioProcessor::wrapperType_AudioUnitv3:return "AudioUnitv3";
            case juce::AudioProcessor::wrapperType_AAX:        return "AAX";
            case juce::AudioProcessor::wrapperType_LV2:        return "LV2";
            case juce::AudioProcessor::wrapperType_Unity:      return "Unity";
            default:                                           return "Undefined";
        }
    }

    juce::String linuxHostDisplayContext()
    {
        const juce::PluginHostType host;
        juce::String context;
        context << "mode=" << PRISM_LINUX_UI_MODE_NAME
                << " host=\"" << host.getHostDescription() << "\""
                << " wrapper=" << wrapperTypeName()
                << " display=" << envValue("DISPLAY")
                << " waylandDisplay=" << envValue("WAYLAND_DISPLAY")
                << " sessionType=" << envValue("XDG_SESSION_TYPE")
                << " gdkBackend=" << envValue("GDK_BACKEND")
                << " desktop=" << envValue("XDG_CURRENT_DESKTOP");
        return context;
    }

    juce::String describeBounds(const juce::Rectangle<int>& bounds)
    {
        juce::String text;
        text << bounds.getX() << "," << bounds.getY()
             << " " << bounds.getWidth() << "x" << bounds.getHeight();
        return text;
    }
#endif
#endif

    std::unique_ptr<ScopeEngine> makeEngine()
    {
#if defined(PRISM_SCOPE_WAVEFORM) && PRISM_SCOPE_WAVEFORM
        return std::make_unique<WaveformEngine>();
#elif defined(PRISM_SCOPE_SPECTROGRAM) && PRISM_SCOPE_SPECTROGRAM
        return std::make_unique<SpectrogramEngine>();
#elif defined(PRISM_SCOPE_VECTORSCOPE) && PRISM_SCOPE_VECTORSCOPE
        return std::make_unique<VectorscopeEngine>();
#elif defined(PRISM_SCOPE_LUFSMETER) && PRISM_SCOPE_LUFSMETER
        return std::make_unique<LUFSMeterEngine>();
#elif defined(PRISM_SCOPE_VUMETER) && PRISM_SCOPE_VUMETER
        return std::make_unique<VUMeterEngine>();
#elif defined(PRISM_SCOPE_OSCILLOSCOPE) && PRISM_SCOPE_OSCILLOSCOPE
        return std::make_unique<OscilloscopeEngine>();
#else
        return std::make_unique<SpectrumEngine>();
#endif
    }

#if PRISM_USE_DEV_SERVER
    const juce::String kDevServerUrl { "http://localhost:5174" };
#elif PRISM_EMBED_WEBUI
    const char* getWebResourceData(const juce::String& name, int& dataSize)
    {
        dataSize = 0;

        for (int i = 0; i < BinaryData::namedResourceListSize; ++i)
        {
            if (name == juce::String(BinaryData::originalFilenames[i]))
                return BinaryData::getNamedResource(BinaryData::namedResourceList[i], dataSize);
        }

        return nullptr;
    }

    juce::String mimeForExtension(const juce::String& name)
    {
        if (name.endsWithIgnoreCase(".html"))  return "text/html";
        if (name.endsWithIgnoreCase(".js"))    return "text/javascript";
        if (name.endsWithIgnoreCase(".css"))   return "text/css";
        if (name.endsWithIgnoreCase(".svg"))   return "image/svg+xml";
        if (name.endsWithIgnoreCase(".json"))  return "application/json";
        if (name.endsWithIgnoreCase(".woff2")) return "font/woff2";
        if (name.endsWithIgnoreCase(".woff"))  return "font/woff";
        if (name.endsWithIgnoreCase(".png"))   return "image/png";
        return "application/octet-stream";
    }

    std::optional<juce::WebBrowserComponent::Resource> provideResource(const juce::String& url)
    {
        auto name = (url == "/") ? juce::String("index.html")
                                 : url.fromLastOccurrenceOf("/", false, false);
        name = name.upToFirstOccurrenceOf("?", false, false);

        int dataSize = 0;
        if (const char* data = getWebResourceData(name, dataSize))
        {
            std::vector<std::byte> bytes ((size_t) dataSize);
            std::memcpy (bytes.data(), data, (size_t) dataSize);
            return juce::WebBrowserComponent::Resource { std::move (bytes), mimeForExtension (name) };
        }

        return std::nullopt;
    }

    juce::String getResourceProviderOrigin()
    {
        return juce::WebBrowserComponent::getResourceProviderRoot().trimCharactersAtEnd("/");
    }

#if JUCE_LINUX
    juce::File writeLinuxWebViewIndexFile()
    {
        int dataSize = 0;
        const char* data = getWebResourceData("index.html", dataSize);

        if (data == nullptr || dataSize <= 0)
            return {};

        const auto webViewDir =
            juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory)
                .getChildFile("prism")
                .getChildFile("plugin-webview");

        if (! webViewDir.createDirectory())
            return {};

        auto indexFile = webViewDir.getChildFile("index.html");

        if (! indexFile.replaceWithData(data, (size_t) dataSize))
            return {};

        return indexFile;
    }
#endif
#endif

#if JUCE_LINUX
    juce::String makeWebSmokeUrl()
    {
        const juce::String html =
            "<!doctype html><html><head><meta charset=\"utf-8\">"
            "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
            "<style>html,body{width:100%;height:100%;margin:0;background:#000;color:rgba(255,255,255,.82);"
            "font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;}body{display:grid;place-items:center;}"
            "#status{padding:20px;text-align:center;white-space:pre-wrap;}</style></head>"
            "<body><div id=\"status\">Prism WebView smoke test\\nWaiting for JUCE bridge...</div>"
            "<script>"
            "function ready(){var s=document.getElementById('status');"
            "try{if(window.__JUCE__&&window.__JUCE__.backend){window.__JUCE__.backend.emitEvent('prismReady',{});"
            "if(s)s.textContent='Prism WebView smoke test\\nJUCE bridge emitted prismReady';return;}}"
            "catch(e){if(s)s.textContent='Prism WebView smoke test\\n'+String(e&&e.message||e);return;}"
            "setTimeout(ready,50);}ready();"
            "</script></body></html>";

        return "data:text/html;charset=utf-8," + juce::URL::addEscapeChars(html, false);
    }

    class FloatingWebViewWindow final : public juce::DocumentWindow
    {
    public:
        FloatingWebViewWindow()
            : juce::DocumentWindow("Prism WebView diagnostic",
                                   juce::Colours::black,
                                   juce::DocumentWindow::closeButton)
        {
            setUsingNativeTitleBar(true);
            setResizable(true, true);
        }

        void closeButtonPressed() override
        {
           #if PRISM_LINUX_UI_DIAGNOSTICS
            logLinuxDiagnostic("floating_webview closeButtonPressed");
           #endif
            setVisible(false);
        }
    };
#endif

    juce::WebBrowserComponent::Options makeWebOptions(PrismSpectrumEditor& editor, const char* scopeId)
    {
        auto options = juce::WebBrowserComponent::Options{}
            .withNativeIntegrationEnabled()
            .withInitialisationData("prismScope", juce::String(scopeId))
            .withEventListener("prismConfig", [&editor](juce::var v) { editor.onPrismConfig(std::move(v)); })
            .withEventListener("prismReady",  [&editor](juce::var)   { editor.onPrismReady(); })
            .withEventListener("prismSpectrogramConfig", [&editor](juce::var v) { editor.onScopeNativeConfig(std::move(v)); })
            .withEventListener("prismScopeMeasurement", [&editor](juce::var v) { editor.onScopeMeasurement(std::move(v)); })
            .withEventListener("prismSettingsPanel", [&editor](juce::var v) { editor.onSettingsPanel(std::move(v)); });

#if JUCE_WINDOWS
        options = options.withBackend(juce::WebBrowserComponent::Options::Backend::webview2);

        // WebView2's default user-data folder is created next to the host executable.
        // For plugins installed under Program Files\Common Files\VST3\..., that path
        // is read-only to the (non-admin) DAW process; WebView2 may fail to
        // initialise, leaving a blank webview or canceled navigation page.
        // Point it at a writable per-user folder instead (JUCE's docs flag this
        // explicitly for plugin projects).
        const auto webView2DataDir =
            juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory)
                .getChildFile("prism")
                .getChildFile("WebView2");
        webView2DataDir.createDirectory();
        options = options.withWinWebView2Options(
            juce::WebBrowserComponent::Options::WinWebView2{}.withUserDataFolder(webView2DataDir));
#endif

#if PRISM_EMBED_WEBUI
        options = options.withResourceProvider(
            [](const auto& url) { return provideResource(url); },
            getResourceProviderOrigin());
#endif
        return options;
    }
}

PrismSpectrumEditor::PrismSpectrumEditor(PrismSpectrumProcessor& p)
    : juce::AudioProcessorEditor(&p),
      processorRef(p),
      engine(makeEngine())
{
    drainLeft.assign((size_t) kDrainCapacity, 0.0f);
    drainRight.assign((size_t) kDrainCapacity, 0.0f);

#if JUCE_LINUX
 #if PRISM_LINUX_UI_DIAGNOSTICS
    logLinuxDiagnostic("editor constructed scope=" + juce::String(engine->scopeId())
                       + " " + linuxHostDisplayContext()
                       + " logFile=" + linuxDiagnosticLogFile().getFullPathName());
 #endif
#endif

    loadUi();

    // Per-scope sizing: open at the scope's preferred default, with a per-scope min.
    const auto pref = engine->preferredSize();
    setResizable(true, true);
    setResizeLimits(pref.minWidth, pref.minHeight, 4096, 4096);
    setSize(pref.defaultWidth, pref.defaultHeight);
}

PrismSpectrumEditor::~PrismSpectrumEditor()
{
#if JUCE_WINDOWS || JUCE_LINUX
    stopTimer();
#endif
#if JUCE_LINUX
 #if PRISM_LINUX_UI_DIAGNOSTICS
    logLinuxDiagnostic("editor destructed scope=" + juce::String(engine->scopeId())
                       + " mode=" + juce::String(PRISM_LINUX_UI_MODE_NAME));
 #endif
    floatingWebViewWindow.reset();
#endif
    webViewReady = false;
}

void PrismSpectrumEditor::resized()
{
    if (webView != nullptr && webView->getParentComponent() == this)
        webView->setBounds(getLocalBounds());

    webViewFallback.setBounds(getLocalBounds());

#if JUCE_LINUX
 #if PRISM_LINUX_UI_DIAGNOSTICS
    logLinuxDiagnostic("editor resized local=" + describeBounds(getLocalBounds())
                       + " screen=" + describeBounds(getScreenBounds())
                       + " mode=" + juce::String(PRISM_LINUX_UI_MODE_NAME));
 #endif
#endif
}

void PrismSpectrumEditor::loadUi()
{
#if JUCE_LINUX
 #if PRISM_LINUX_UI_DIAGNOSTICS
    logLinuxDiagnostic("loadUi " + linuxHostDisplayContext());
 #endif

   #if defined(PRISM_LINUX_UI_MODE_NATIVE_SMOKE) && PRISM_LINUX_UI_MODE_NATIVE_SMOKE
    loadNativeSmoke();
    return;
   #elif defined(PRISM_LINUX_UI_MODE_WEB_SMOKE) && PRISM_LINUX_UI_MODE_WEB_SMOKE
    loadWebSmoke();
    return;
   #elif defined(PRISM_LINUX_UI_MODE_FLOATING_WEBVIEW) && PRISM_LINUX_UI_MODE_FLOATING_WEBVIEW
    loadFloatingWebView();
    return;
   #endif
#endif

    loadEmbeddedWebView();
}

bool PrismSpectrumEditor::createWebView()
{
    auto options = makeWebOptions(*this, engine->scopeId());

#if JUCE_WINDOWS
    if (! juce::WebBrowserComponent::areOptionsSupported(options))
    {
        showWebViewFallback();
        return false;
    }
#endif

#if JUCE_LINUX
 #if PRISM_LINUX_UI_DIAGNOSTICS
    logLinuxDiagnostic("creating WebBrowserComponent mode=" + juce::String(PRISM_LINUX_UI_MODE_NAME));
 #endif
#endif

    webView = std::make_unique<juce::WebBrowserComponent>(options);
    return true;
}

void PrismSpectrumEditor::loadEmbeddedWebView()
{
    if (! createWebView())
        return;

    addAndMakeVisible(*webView);

#if PRISM_USE_DEV_SERVER
   #if JUCE_LINUX
    #if PRISM_LINUX_UI_DIAGNOSTICS
    logLinuxDiagnostic("navigating embedded WebView url=" + kDevServerUrl);
    #endif
   #endif
    webView->goToURL(kDevServerUrl);
#elif PRISM_EMBED_WEBUI
   #if JUCE_LINUX
    const auto indexFile = writeLinuxWebViewIndexFile();

    if (indexFile.existsAsFile())
    {
        const auto url = juce::URL(indexFile).toString(false);
       #if PRISM_LINUX_UI_DIAGNOSTICS
        logLinuxDiagnostic("navigating embedded WebView url=" + url);
       #endif
        webView->goToURL(url);
        return;
    }

   #if PRISM_LINUX_UI_DIAGNOSTICS
    logLinuxDiagnostic("navigating embedded WebView url=" + juce::WebBrowserComponent::getResourceProviderRoot());
   #endif
   #endif
    webView->goToURL(juce::WebBrowserComponent::getResourceProviderRoot());
#endif
}

#if JUCE_LINUX
void PrismSpectrumEditor::showLinuxDiagnosticPlaceholder(const juce::String& text)
{
    webViewFallback.setText(text, juce::dontSendNotification);
    webViewFallback.setJustificationType(juce::Justification::centred);
    webViewFallback.setColour(juce::Label::backgroundColourId, juce::Colours::black);
    webViewFallback.setColour(juce::Label::textColourId, juce::Colour(0xffe5e7eb));
    webViewFallback.setMinimumHorizontalScale(0.7f);
    addAndMakeVisible(webViewFallback);
}

void PrismSpectrumEditor::loadNativeSmoke()
{
   #if PRISM_LINUX_UI_DIAGNOSTICS
    logLinuxDiagnostic("native_smoke loaded");
   #endif
    webViewReady = true;
    showLinuxDiagnosticPlaceholder("Prism native smoke test\n"
                                   "Root JUCE editor rendered without WebView\n"
                                   "Mode: native_smoke");
}

void PrismSpectrumEditor::loadWebSmoke()
{
    if (! createWebView())
        return;

    addAndMakeVisible(*webView);

    const auto url = makeWebSmokeUrl();
   #if PRISM_LINUX_UI_DIAGNOSTICS
    logLinuxDiagnostic("navigating web_smoke WebView url=data:text/html;charset=utf-8,<inline smoke page>");
   #endif
    webView->goToURL(url);
}

void PrismSpectrumEditor::loadFloatingWebView()
{
    showLinuxDiagnosticPlaceholder("Prism floating WebView diagnostic\n"
                                   "Embedded native placeholder is visible\n"
                                   "The real Prism WebView should open in a separate window");

    if (! createWebView())
        return;

    floatingWebViewWindow = std::make_unique<FloatingWebViewWindow>();
    floatingWebViewWindow->setContentNonOwned(webView.get(), false);

    const auto pref = engine->preferredSize();
    floatingWebViewWindow->setSize(pref.defaultWidth, pref.defaultHeight);
    floatingWebViewWindow->centreWithSize(pref.defaultWidth, pref.defaultHeight);
    floatingWebViewWindow->setVisible(true);
    floatingWebViewWindow->toFront(true);

   #if PRISM_USE_DEV_SERVER
   #if PRISM_LINUX_UI_DIAGNOSTICS
    logLinuxDiagnostic("navigating floating WebView url=" + kDevServerUrl);
   #endif
    webView->goToURL(kDevServerUrl);
   #elif PRISM_EMBED_WEBUI
    const auto indexFile = writeLinuxWebViewIndexFile();

    if (indexFile.existsAsFile())
    {
        const auto url = juce::URL(indexFile).toString(false);
       #if PRISM_LINUX_UI_DIAGNOSTICS
        logLinuxDiagnostic("navigating floating WebView url=" + url);
       #endif
        webView->goToURL(url);
        return;
    }

   #if PRISM_LINUX_UI_DIAGNOSTICS
    logLinuxDiagnostic("navigating floating WebView url=" + juce::WebBrowserComponent::getResourceProviderRoot());
   #endif
    webView->goToURL(juce::WebBrowserComponent::getResourceProviderRoot());
   #endif
}
#endif

void PrismSpectrumEditor::showWebViewFallback()
{
    webViewFallback.setText(
        "Prism needs the Microsoft Edge WebView2 Runtime to show this plugin on Windows.\n"
        "Install the Evergreen WebView2 Runtime from Microsoft, then reopen the plugin.",
        juce::dontSendNotification);
    webViewFallback.setJustificationType(juce::Justification::centred);
    webViewFallback.setColour(juce::Label::backgroundColourId, juce::Colour(0xff111216));
    webViewFallback.setColour(juce::Label::textColourId, juce::Colour(0xfff4f6f8));
    webViewFallback.setMinimumHorizontalScale(0.75f);
    addAndMakeVisible(webViewFallback);
}

#if JUCE_WINDOWS || JUCE_LINUX
void PrismSpectrumEditor::timerCallback()
{
    renderFrame();
}
#endif

void PrismSpectrumEditor::startFrameDriver()
{
    if (webView == nullptr || ! webViewReady || frameDriverStarted)
        return;

    frameDriverStarted = true;

#if JUCE_WINDOWS
    startTimerHz(resolveWindowsFrameRateHz(*this));
#elif JUCE_LINUX
    startTimerHz(kLinuxFrameRateHz);
#else
    // Drive frames at the display's refresh rate (adapts to 60/120/144 Hz).
    vblank = juce::VBlankAttachment(this, [this] { renderFrame(); });
#endif
}

void PrismSpectrumEditor::onSettingsPanel(juce::var payload)
{
    const int height = juce::jmax(0, (int) payload.getProperty("height", 0));
    const int delta = height - settingsPanelHeight;
    if (delta == 0)
        return;

    settingsPanelHeight = height;
    // Grow/shrink the window by exactly the panel height so the scope area is
    // unchanged. Deliberately NOT touching resize limits — doing so makes JUCE's
    // constrainer snap the window and double the applied size.
    setSize(getWidth(), getHeight() + delta);
}

void PrismSpectrumEditor::onPrismConfig(juce::var payload)
{
    const auto settings = payload.getProperty("settings", juce::var());

    // Persist only genuine per-instance overrides (persist=true). App-default /
    // restore-driven updates carry persist=false so a non-overridden instance
    // keeps re-reading the app's current settings on reopen.
    if ((bool) payload.getProperty("persist", false))
        processorRef.setSettingsJson(juce::JSON::toString(settings));

    engine->configure(settings);
}

void PrismSpectrumEditor::onPrismReady()
{
#if JUCE_LINUX
 #if PRISM_LINUX_UI_DIAGNOSTICS
    logLinuxDiagnostic("prismReady received mode=" + juce::String(PRISM_LINUX_UI_MODE_NAME)
                       + " scope=" + juce::String(engine->scopeId()));
 #endif
#endif
    webViewReady = true;
    pushRestoreSettings();
    sendAppDefaults();
    startFrameDriver();
}

void PrismSpectrumEditor::onScopeNativeConfig(juce::var payload)
{
    engine->configureNative(payload);
}

void PrismSpectrumEditor::onScopeMeasurement(juce::var payload)
{
    engine->setMeasurementActive((bool) payload.getProperty("active", false));
}

void PrismSpectrumEditor::pushRestoreSettings()
{
    if (webView == nullptr)
        return;

    auto* obj = new juce::DynamicObject();
    obj->setProperty("json", processorRef.getSettingsJson());
    webView->emitEventIfBrowserIsVisible(kRestoreSettingsEvent, juce::var(obj));
}

void PrismSpectrumEditor::sendAppDefaults()
{
    if (webView == nullptr)
        return;

    // Resolve Prism's app-data dir to match Electron's userData per platform.
    // macOS: userApplicationDataDirectory == ~/Library, so the Electron path is
    //   ~/Library/Application Support/prism (the extra subfolder only exists on mac).
    // Windows/Linux: JUCE's userApplicationDataDirectory already points at the right
    //   per-platform config root (%APPDATA% / ~/.config), so no extra append needed.
   #if JUCE_MAC
    const auto appData = juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory)
                             .getChildFile("Application Support")
                             .getChildFile("prism");
   #else
    const auto appData = juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory)
                             .getChildFile("prism");
   #endif
    const auto docs = juce::File::getSpecialLocation(juce::File::userDocumentsDirectory);
    const auto themesDir = docs.getChildFile("Prism Themes");
    const auto profilesDir = docs.getChildFile("Prism Profiles");

    juce::String themeId, themeFile, profileJson;

    if (const auto themeState = juce::JSON::parse(appData.getChildFile("theme-state.json"));
        auto* obj = themeState.getDynamicObject())
        themeId = obj->getProperty("activeThemeId").toString();

    if (themeId.isNotEmpty())
    {
        const auto file = themesDir.getChildFile(themeId + ".iro");
        if (file.existsAsFile())
            themeFile = file.loadFileAsString();
    }

    juce::String activeProfileId;
    if (const auto profileState = juce::JSON::parse(appData.getChildFile("profile-state.json"));
        auto* obj = profileState.getDynamicObject())
        activeProfileId = obj->getProperty("activeProfileId").toString();

    if (activeProfileId.isNotEmpty() && profilesDir.isDirectory())
    {
        for (const auto& file : profilesDir.findChildFiles(juce::File::findFiles, false, "*.prsm"))
        {
            const auto content = file.loadFileAsString();
            if (const auto parsed = juce::JSON::parse(content); auto* o = parsed.getDynamicObject())
            {
                if (o->getProperty("id").toString() == activeProfileId)
                {
                    profileJson = content;
                    break;
                }
            }
        }
    }

    auto* payload = new juce::DynamicObject();
    payload->setProperty("themeId", themeId);
    payload->setProperty("themeFile", themeFile);
    payload->setProperty("profileJson", profileJson);
    webView->emitEventIfBrowserIsVisible(juce::Identifier("prismAppDefaults"), juce::var(payload));
}

void PrismSpectrumEditor::renderFrame()
{
    if (webView == nullptr || ! webViewReady)
        return;

#if JUCE_MAC
    if (! frameRateUncapped && uncapAttempts < 300)
    {
        ++uncapAttempts;
        if (auto* peer = getPeer())
            frameRateUncapped = prismUncapWebViewFrameRate(peer->getNativeHandle());
    }
#endif

    const double sampleRate = processorRef.getSampleRateHz();
    if (sampleRate > 0.0 && sampleRate != lastSampleRate)
    {
        engine->setSampleRate(sampleRate);
        lastSampleRate = sampleRate;
    }

    const int drained = processorRef.drainStereo(drainLeft.data(), drainRight.data(), (int) drainLeft.size());

#if JUCE_LINUX
    if (drained <= 0)
        return;
#endif

    engine->process(drainLeft.data(), drainRight.data(), drained);

    webView->emitEventIfBrowserIsVisible(engine->frameEventId(), engine->buildFrame(sampleRate));
}
