/*
 * Copyright (C) 2026 Linux Studio Plugins Project <https://lsp-plug.in/>
 *           (C) 2026 Vladimir Sadovnikov <sadko4u@gmail.com>
 *
 * This file is part of lsp-plugin-fw
 * Created on: 31 янв. 2022 г.
 *
 * lsp-plugin-fw is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 *
 * lsp-plugin-fw is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with lsp-plugin-fw. If not, see <https://www.gnu.org/licenses/>.
 */

#ifndef LSP_PLUG_IN_PLUG_FW_WRAP_STANDALONE_IMPL_UI_WRAPPER_H_
#define LSP_PLUG_IN_PLUG_FW_WRAP_STANDALONE_IMPL_UI_WRAPPER_H_

#include <lsp-plug.in/plug-fw/version.h>
#include <lsp-plug.in/plug-fw/core/SamplePlayer.h>
#include <lsp-plug.in/plug-fw/wrap/standalone/ui_wrapper.h>

#define BACKEND_STATUS_OFF          "PluginWindow::StatusBar::Label::FAIL"
#define BACKEND_STATUS_ON           "PluginWindow::StatusBar::Label::OK"

namespace lsp
{
    namespace standalone
    {
        UIWrapper::UIWrapper(standalone::Wrapper *wrapper, resource::ILoader *loader, ui::Module *ui) : ui::IWrapper(ui, loader)
        {
            pPlugin             = wrapper->pPlugin;
            pWrapper            = wrapper;

            nPosition           = 0;
            pConnBackend        = NULL;
            bConnConnected      = false;
            pConnStatus         = NULL;
            pConnName           = NULL;
            pConnIndicatorPanel = NULL;
        }

        UIWrapper::~UIWrapper()
        {
            do_destroy();
        }

        status_t UIWrapper::init(void *root_widget)
        {
        #ifdef LSP_TRACE
            lsp_trace("Begin UI initialization");
            const system::time_millis_t start = system::get_time_millis();
            lsp_finally {
                const system::time_millis_t end = system::get_time_millis();
                lsp_trace("UI initialization time: %d ms", int(end - start));
            };
        #endif /* LSP_TRACE */

            status_t res = STATUS_OK;

            // Force position sync at startup
            nPosition   = atomic_load(&pWrapper->nPosition) - 1;
            const meta::plugin_t *meta = pUI->metadata();
            if (pUI->metadata() == NULL)
                return STATUS_BAD_STATE;

            // Create list of ports and sort it in ascending order by the identifier
            lsp_trace("Creating ports for %s - %s", meta->name, meta->description);
            for (const meta::port_t *port = meta->ports ; port->id != NULL; ++port) {
                if ((res = create_port(port, NULL)) != STATUS_OK)
                    return res;
            }

            // Initialize parent
            if ((res = IWrapper::init(root_widget)) != STATUS_OK)
                return res;

            // Initialize the UI
            if ((res = pUI->init(this)) != STATUS_OK)
                return res;

            // Build the UI
            if (meta->ui_resource != NULL)
            {
                if ((res = build_ui(meta->ui_resource)) != STATUS_OK)
                {
                    lsp_error("Error building UI for resource %s: code=%d", meta->ui_resource, int(res));
                    return res;
                }
            }

            // Call the post-initialization routine and show the audio backend connection state indicator
            if ((res = pUI->post_init()) == STATUS_OK)
            {
                pConnStatus = controller()->widgets()->get<tk::Label>("backend_status");
                pConnName = controller()->widgets()->get<tk::Label>("backend_name");
                pConnIndicatorPanel = controller()->widgets()->get<tk::Widget>("backend_indicator");
                update_connection_name();
                sync_connection_status(bConnConnected);
            }

            tk::Window *root    = window();
            if (root == NULL)
            {
                lsp_error("No root window present!\n");
                return STATUS_BAD_STATE;
            }

            // Bind events to the root window
            root->slot(tk::SLOT_SHOW)->bind(slot_ui_show, this);
            root->slot(tk::SLOT_HIDE)->bind(slot_ui_hide, this);

            return res;
        }

        void UIWrapper::do_destroy()
        {
            pConnStatus = NULL;

            // Call the parent class for destroy
            IWrapper::destroy();

            // Destroy ports
            vSyncPorts.flush();

            // Cleanup generated metadata
            for (size_t i=0, n=vGenMetadata.size(); i<n; ++i)
            {
                meta::port_t *port = vGenMetadata.uget(i);
                lsp_trace("destroy generated UI port metadata %p", port);
                meta::drop_port_metadata(port);
            }
            vGenMetadata.flush();

            // Destroy the display
            if (pDisplay != NULL)
            {
                pDisplay->destroy();
                delete pDisplay;
                pDisplay    = NULL;
            }

            pPlugin         = NULL;
            pWrapper        = NULL;
        }

        void UIWrapper::destroy()
        {
            do_destroy();
        }

        core::KVTStorage *UIWrapper::kvt_lock()
        {
            return pWrapper->kvt_lock();
        }

        core::KVTStorage *UIWrapper::kvt_trylock()
        {
            return pWrapper->kvt_trylock();
        }

        bool UIWrapper::kvt_release()
        {
            return pWrapper->kvt_release();
        }

        const meta::package_t *UIWrapper::package() const
        {
            return pWrapper->package();
        }

        meta::plugin_format_t UIWrapper::plugin_format() const
        {
            return meta::PLUGIN_STANDALONE;
        }

        void UIWrapper::dump_state_request()
        {
            atomic_add(&pWrapper->nDumpReq, 1);
        }

        void UIWrapper::main_iteration()
        {
            IWrapper::main_iteration();

            // Always call main iteration for the underlying display
            if (pDisplay != NULL)
                pDisplay->main_iteration();
        }

        status_t UIWrapper::play_file(const char *file, wsize_t position, bool release)
        {
            core::SamplePlayer *p = pWrapper->sample_player();
            if (p != NULL)
            {
                // Trigger playback and force the position to become out-of-sync
                p->play_sample(file, position, release);
            }
            return STATUS_OK;
        }

        status_t UIWrapper::create_port(const meta::port_t *port, const char *postfix)
        {
            // Find the matching port for the backend
            standalone::Port *jp    = pWrapper->port_by_id(port->id);
            if (jp == NULL)
                return STATUS_OK;

            // Create UI port
            standalone::UIPort *jup = NULL;

            switch (port->role)
            {
                case meta::R_AUDIO_IN:
                case meta::R_AUDIO_OUT:
                case meta::R_AUDIO_SEND:
                case meta::R_AUDIO_RETURN:
                    // Stub port
                    jup     = new standalone::UIPort(jp);
                    break;

                case meta::R_MESH:
                    jup     = new standalone::UIMeshPort(jp);
                    if (meta::is_out_port(port))
                        vSyncPorts.add(jup);
                    break;

                case meta::R_FBUFFER:
                    jup     = new standalone::UIFrameBufferPort(jp);
                    if (meta::is_out_port(port))
                        vSyncPorts.add(jup);
                    break;

                case meta::R_STREAM:
                    jup     = new standalone::UIStreamPort(jp);
                    if (meta::is_out_port(port))
                        vSyncPorts.add(jup);
                    break;

                case meta::R_OSC_OUT:
                    jup     = new standalone::UIOscPortIn(jp);
                    vSyncPorts.add(jup);
                    break;
                case meta::R_OSC_IN:
                    jup     = new standalone::UIOscPortOut(jp);
                    break;

                case meta::R_PATH:
                    jup     = new standalone::UIPathPort(jp, this);
                    break;

                case meta::R_STRING:
                case meta::R_SEND_NAME:
                case meta::R_RETURN_NAME:
                    jup     = new standalone::UIStringPort(jp, this);
                    vSyncPorts.add(jup);
                    break;

                case meta::R_CONTROL:
                    jup     = new standalone::UIControlPort(jp, this);
                    break;

                case meta::R_BYPASS:
                    jup     = new standalone::UIControlPort(jp, NULL);
                    break;

                case meta::R_METER:
                    jup     = new standalone::UIMeterPort(jp);
                    vSyncPorts.add(jup);
                    break;

                case meta::R_PORT_SET:
                {
                    LSPString postfix_str;
                    standalone::PortGroup *pg       = static_cast<standalone::PortGroup *>(jp);
                    standalone::UIPortGroup *upg    = new standalone::UIPortGroup(pg, this);
                    vPorts.add(upg);

                    for (size_t row=0; row<pg->rows(); ++row)
                    {
                        // Generate postfix
                        postfix_str.fmt_ascii("%s_%d", (postfix != NULL) ? postfix : "", int(row));
                        const char *port_post = postfix_str.get_ascii();

                        // Clone port metadata
                        meta::port_t *cm        = clone_port_metadata(port->members, port_post);
                        if (cm != NULL)
                        {
                            vGenMetadata.add(cm);

                            for (; cm->id != NULL; ++cm)
                            {
                                if (meta::is_growing_port(cm))
                                    cm->start    = cm->min + ((cm->max - cm->min) * row) / float(pg->rows());
                                else if (meta::is_lowering_port(cm))
                                    cm->start    = cm->max - ((cm->max - cm->min) * row) / float(pg->rows());

                                create_port(cm, port_post);
                            }
                        }
                    }

                    break;
                }

                default:
                    break;
            }

            if (jup != NULL)
                vPorts.add(jup);

            return STATUS_OK;
        }

        void UIWrapper::sync_kvt(core::KVTStorage *kvt)
        {
            // Synchronize DSP -> UI transfer
            size_t sync;
            const char *kvt_name;
            const core::kvt_param_t *kvt_value;

            do
            {
                sync = 0;

                core::KVTIterator *it = kvt->enum_tx_pending();
                while (it->next() == STATUS_OK)
                {
                    kvt_name = it->name();
                    if (kvt_name == NULL)
                        break;
                    status_t res = it->get(&kvt_value);
                    if (res != STATUS_OK)
                        break;
                    if ((res = it->commit(core::KVT_TX)) != STATUS_OK)
                        break;

                    kvt_dump_parameter("TX kvt param (DSP->UI): %s = ", kvt_value, kvt_name);
                    notify_write_to_kvt(kvt, kvt_name, kvt_value);
                    ++sync;
                }
            } while (sync > 0);

            // Synchronize UI -> DSP transfer
            #ifdef LSP_DEBUG
            {
                core::KVTIterator *it = kvt->enum_rx_pending();
                while (it->next() == STATUS_OK)
                {
                    kvt_name = it->name();
                    if (kvt_name == NULL)
                        break;
                    status_t res = it->get(&kvt_value);
                    if (res != STATUS_OK)
                        break;
                    if ((res = it->commit(core::KVT_RX)) != STATUS_OK)
                        break;

                    core::kvt_dump_parameter("RX kvt param (UI->DSP): %s = ", kvt_value, kvt_name);
                }
            }
            #else
                kvt->commit_all(core::KVT_RX);    // Just clear all RX queue for non-debug version
            #endif
        }

        bool UIWrapper::sync(ws::timestamp_t ts)
        {
            // Initialize DSP state
            dsp::context_t ctx;
            dsp::start(&ctx);

            // Check that position has been updated and sync it's state
            atomic_t pos    = atomic_load(&pWrapper->nPosition);
            if (nPosition != pos)
            {
                position_updated(pWrapper->position());
                nPosition       = pos;
            }

            // Transfer the values of the ports to the UI
            if (pWrapper->lock_meters_soft())
            {
                lsp_finally { pWrapper->unlock_meters(); };

                size_t sync = vSyncPorts.size();
                for (size_t i=0; i<sync; ++i)
                {
                    standalone::UIPort *jup = vSyncPorts.uget(i);
                    do {
                        if (jup->sync())
                            jup->notify_all(ui::PORT_NONE);
                    } while (jup->sync_again());
                }
            }

            // Synchronize KVT state
            core::KVTStorage *kvt = pWrapper->kvt_trylock();
            if (kvt != NULL)
            {
                sync_kvt(kvt);

                // Call garbage collection and release KVT storage
                kvt->gc();
                pWrapper->kvt_release();
            }

            // Notify sample listeners if something has changed
            core::SamplePlayer *sp = pWrapper->sample_player();
            if (sp != NULL)
                notify_play_position(sp->position(), sp->sample_length());

            dsp::finish(&ctx);

            return true;
        }

        void UIWrapper::sync_inline_display()
        {
            // Check that window is present
            if (wWindow == NULL)
                return;

            // Initialize DSP state
            dsp::context_t ctx;
            dsp::start(&ctx);

            // Check if inline display is present
            plug::canvas_data_t *data = NULL;
            if (pWrapper->test_display_draw())
                data = pWrapper->render_inline_display(
                    STANDALONE_INLINE_DISPLAY_SIZE,
                    STANDALONE_INLINE_DISPLAY_SIZE);

            // Check that returned data is valid
            if ((data != NULL) && (data->pData != NULL) && (data->nWidth > 0) && (data->nHeight > 0))
            {
                size_t row_size = data->nWidth * sizeof(uint32_t);

                if (data->nStride > row_size)
                {
                    // Compress image data if stride is greater than row size
                    uint8_t *dst = data->pData;
                    for (size_t row = 0; row < data->nHeight; ++row)
                    {
                        uint8_t *p  = &data->pData[row * data->nStride];
                        memmove(dst, p, row_size);
                    }
                }

                wWindow->set_icon(data->pData, data->nWidth, data->nHeight);
            }

            dsp::finish(&ctx);
        }

        status_t UIWrapper::slot_ui_hide(tk::Widget *sender, void *ptr, void *data)
        {
            UIWrapper *_this = static_cast<UIWrapper *>(ptr);
            if (_this->pWrapper != NULL)
                _this->pWrapper->set_ui_active(false);
            return STATUS_OK;
        }

        status_t UIWrapper::slot_ui_show(tk::Widget *sender, void *ptr, void *data)
        {
            UIWrapper *_this = static_cast<UIWrapper *>(ptr);
            if (_this->pWrapper != NULL)
                _this->pWrapper->set_ui_active(true);
            return STATUS_OK;
        }

        void UIWrapper::set_connection_status(const core::AudioBackendInfo * backend, bool connected)
        {
            if (bConnConnected != connected)
            {
                bConnConnected = connected;
                sync_connection_status(bConnConnected);
            }
            if (pConnBackend != backend)
            {
                pConnBackend = backend;
                update_connection_name();
            }
        }

        void UIWrapper::sync_connection_status(bool connected)
        {
            if (pConnStatus)
            {
                ctl::revoke_style(pConnStatus, BACKEND_STATUS_OFF);
                ctl::revoke_style(pConnStatus, BACKEND_STATUS_ON);
                ctl::inject_style(pConnStatus, (connected) ? BACKEND_STATUS_ON : BACKEND_STATUS_OFF);
                pConnStatus->text()->set((connected) ? "statuses.backend.on" : "statuses.backend.off");
            }

            if (pConnIndicatorPanel)
                pConnIndicatorPanel->visibility()->set(true);
        }

        void UIWrapper::update_connection_name()
        {
            if (pConnName)
            {
                LSPString tmp;
                tk::String * const text = pConnName->text();

                if (pConnBackend == NULL)
                    text->set("statuses.backend.label.unknown");
                else if (!pConnBackend->lc_key.is_empty())
                {
                    bool set = tmp.set_ascii("statuses.backend.label.");
                    if (set)
                        set = tmp.append(&pConnBackend->lc_key);
                    if (set)
                        text->set_key(&tmp);
                }
                else if (!pConnBackend->display.is_empty())
                    text->set_raw(&pConnBackend->display);
                else
                {
                    bool set = tmp.set_ascii("statuses.backend.label.");
                    if (set)
                        set = tmp.append(&pConnBackend->uid);
                    if (set)
                        text->set_key(&tmp);
                }
            }

            if (pConnIndicatorPanel)
                pConnIndicatorPanel->visibility()->set(true);
        }

        void UIWrapper::visual_schema_reloaded(const tk::StyleSheet *sheet)
        {
            update_connection_name();
            set_connection_status(pConnBackend, bConnConnected);
        }

        status_t UIWrapper::export_settings(config::Serializer *s, size_t flags, const io::Path *basedir)
        {
            // Notify the plugin the state is about to be saved
            pPlugin->before_state_save();

            // Synchronize KVT state as before_state_save() can change it
            core::KVTStorage *kvt = pWrapper->kvt_trylock();
            if (kvt != NULL)
            {
                lsp_finally { pWrapper->kvt_release(); };
                sync_kvt(kvt);
                kvt->gc();
            }

            // Do the usual stuff
            status_t res = ui::IWrapper::export_settings(s, flags, basedir);

            // Notify the plugin that the state has been just saved
            if (res == STATUS_OK)
                pPlugin->state_saved();

            return res;
        }

        status_t UIWrapper::import_settings(config::PullParser *parser, size_t flags, const io::Path *basedir)
        {
            // Notify the plugin the state is about to be load
            pPlugin->before_state_load();

            // Do the usual stuff
            status_t res = ui::IWrapper::import_settings(parser, flags, basedir);

            // Synchronize KVT state as there can be changes
            core::KVTStorage *kvt = pWrapper->kvt_trylock();
            if (kvt != NULL)
            {
                lsp_finally { pWrapper->kvt_release(); };
                sync_kvt(kvt);
                kvt->gc();
            }

            // Notify the plugin that the state has been just loaded
            if (res == STATUS_OK)
            {
                pPlugin->state_loaded();
                pWrapper->bUpdateSettings = true;
            }

            return res;
        }

        const core::ShmState *UIWrapper::shm_state()
        {
            return (pWrapper != NULL) ? pWrapper->shm_state() : NULL;
        }

        status_t UIWrapper::select_ui_schema(const char *name)
        {
            if (name == NULL)
                return STATUS_BAD_ARGUMENTS;
            LSPString tmp;
            if (!tmp.set_native(name))
                return STATUS_NO_MEM;

            return select_ui_schema(tmp);
        }

        status_t UIWrapper::select_ui_schema(const LSPString *name)
        {
            if (name == NULL)
                return STATUS_BAD_ARGUMENTS;
            return select_ui_schema(*name);
        }

        status_t UIWrapper::select_ui_schema(const LSPString & name)
        {
            ctl::PluginWindow * const pwnd = ctl::ctl_cast<ctl::PluginWindow>(pWindow);
            if (pwnd == NULL)
                return STATUS_NOT_IMPLEMENTED;

            // Form the right path to the schema
            LSPString path;
            size_t offset = 0;
            if (!name.starts_with_ascii(LSP_BUILTIN_PREFIX, offset))
            {
                if (!path.append_ascii(LSP_BUILTIN_PREFIX))
                    return STATUS_NO_MEM;
            }
            else
                offset     += strlen(LSP_BUILTIN_PREFIX);
            if ((!name.starts_with_ascii("schema/", offset)) &&
                (!name.starts_with_ascii("schema\\", offset)))
            {
                if (!path.append_ascii("schema/"))
                    return STATUS_NO_MEM;
            }
            if (!path.append(&name))
                return STATUS_NO_MEM;
            if (!path.ends_with_ascii_nocase(".xml"))
            {
                if (!path.append_ascii(".xml"))
                    return STATUS_NO_MEM;
            }

            // Schema differs from current?
            if (sVisualSchema.equals(&path))
                return STATUS_OK;

            // Load new visual schema
            status_t res = pwnd->select_ui_schema(path);
            if (res == STATUS_OK)
                sVisualSchema.swap(path);

            return res;
        }

        status_t UIWrapper::enumerate_backends(core::AudioBackendInfoList & list)
        {
            return (pWrapper != NULL) ? pWrapper->enumerate_backends(list) : STATUS_BAD_STATE;
        }

        status_t UIWrapper::select_backend(const LSPString & name)
        {
            status_t res = pWrapper->select_backend(name);
            if (res != STATUS_OK)
                return res;

            // Notify plugin window about selection of backend
            ctl::PluginWindow * const pwnd = ctl::ctl_cast<ctl::PluginWindow>(pWindow);
            if (pwnd != NULL)
                pwnd->audio_backend_selected(name);

            // Update connection status
            set_connection_status(
                pWrapper->selected_backend(),
                pWrapper->connected());

            return STATUS_OK;
        }

    } /* namespace standalone */
} /* namespace lsp */

#endif /* LSP_PLUG_IN_PLUG_FW_WRAP_STANDALONE_IMPL_UI_WRAPPER_H_ */
