// yabridge: a Wine plugin bridge
// Copyright (C) 2020-2024 Robbert van der Helm
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

#pragma once

#include <string>
#include <vector>

#include <sys/mman.h>

/**
 * A shared memory object that allows audio buffers to be shared between the
 * native plugin and the Wine plugin host. This is intended as an optimization,
 * and it is used alongside yabridge's usual socket based messages. Normally
 * audio buffers would have to be copied from the host to the native plugin,
 * sent to the Wine plugin host, and then copied to a buffer on the Wine plugin
 * host side for them to be processed by the plugin. The results then have to be
 * sent back to the native plugin, where they finally have to be copied back to
 * the host's buffers. While this wouldn't be an issue for small amounts of
 * data, it also increases the overhead of bridging plugins considerably since
 * there's not much else going on. So to prevent unnecessary copies, we'll
 * communicate the audio buffer data through shared memory objects so we can
 * reduce all of the operations described above to one copy from the host to the
 * shared memory region, and another copy from the shared memory region back to
 * the host. And since we're still using messages alongside this, we also don't
 * need any locks.
 *
 * This approach introduces a few additional moving parts that we'd rather not
 * have to deal with, but the benefits likely outweigh the costs. The buffer is
 * set up on the Wine side after the VST2 or VST3 plugin has finished preparing
 * for audio processing. The configuration (e.g. name, and dimensions) for this
 * shared memory object are then sent back to the plugin so the plugin can map
 * the same shared memory region.
 */
class AudioShmBuffer {
   public:
    /**
     * The parameters needed for creating, configuring and connecting to a
     * shared audio buffer object. This is done on the Wine plugin host. For
     * this we need to know the plugin's bus/channel configuration, whether the
     * host is going to ask the plugin to process 32-bit or 64-bit floating
     * point audio, and the maximum size of the samples per audio buffer. The
     * bus/channel configuration can be queried directly from the plugin. For
     * VST2 plugins the other information is passed before the call to
     * `effMainsChanged` through `effSetProcessPrecision` and `effSetBlockSize`,
     * which would thus need to be kept track of. For VST3 plugins this is all
     * sent as part of the `Steinberg::Vst::ProcessSetup` object.
     */
    struct Config {
        /**
         * The unique identifier for this shared memory object. The backing file
         * will be created in `/dev/shm` by the operating system.
         */
        std::string name;

        /**
         * The size of the shared memory object **in bytes** (so not samples).
         * This should be large enough to hold all input and output buffers, and
         * it depends on whether the host is going to pass 32-bit single
         * precision or 64-bit double precision audio to the plugin.
         */
        uint32_t size;

        /**
         * Offsets **in bytes** within the shared memory object for an input
         * audio channel, indexed by `[bus][channel]`. For VST2 plugins the bus
         * will always be 0. This can be used later to retrieve a pointer to the
         * audio channel.
         *
         * The offsets are in bytes rather than in samples to accommodate mixed
         * 32-bit and 64-bit IO. This is allowed in CLAP. If a port can do
         * either 32-bit or 64-bit audio, we'll reserve enough space for 64-bit
         * samples and then just not use the latter half if the host ends up
         * using 32-bit samples.
         */
        std::vector<std::vector<uint32_t>> input_offsets;
        /**
         * Offsets **in bytes** within the shared memory object for an output
         * audio channel, indexed by `[bus][channel]`. See `input_offsets` for
         * more details.
         */
        std::vector<std::vector<uint32_t>> output_offsets;

        template <typename S>
        void serialize(S& s) {
            s.text1b(name, 1024);
            s.value4b(size);
            s.container(input_offsets, 8192, [](S& s, auto& offsets) {
                s.container4b(offsets, 8192);
            });
            s.container(output_offsets, 8192, [](S& s, auto& offsets) {
                s.container4b(offsets, 8192);
            });
        }
    };

    /**
     * Connect to or create the shared memory object and map it to this
     * process's memory. The configuration is created on the Wine side using the
     * process described in `Config`'s docstring.
     *
     * @throw std::system_error If the shared memory object could not be
     *   created or mapped.
     */
    AudioShmBuffer(const Config& config);

    /**
     * Destroy the shared memory object. Either side dropping the object will
     * cause the object to get destroyed in an effort to avoid memory leaks
     * caused by crashing plugins or hosts.
     */
    ~AudioShmBuffer() noexcept;

    AudioShmBuffer(const AudioShmBuffer&) = delete;
    AudioShmBuffer& operator=(const AudioShmBuffer&) = delete;

    AudioShmBuffer(AudioShmBuffer&&) noexcept;
    AudioShmBuffer& operator=(AudioShmBuffer&&) noexcept;

    /**
     * Adapt to a new buffer size or channel layout. The name of the buffer
     * needs to remain the same.
     *
     * @throw `std::invalid_argument` If the config is for a buffer with a
     *   different name.
     * @throw std::system_error If the shared memory object could not be mapped.
     */
    void resize(const Config& new_config);

    inline size_t num_input_channels(const uint32_t bus) const {
        return config_.input_offsets[bus].size();
    }

    inline size_t num_output_channels(const uint32_t bus) const {
        return config_.output_offsets[bus].size();
    }

    /**
     * Get a pointer to the part of the buffer where this input audio channel is
     * stored in. Both the bus and the channel indices start at zero. These
     * addresses might change after a call to `resize()`.
     */
    template <typename T>
    // NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
    T* input_channel_ptr(const uint32_t bus, const uint32_t channel) noexcept {
        return reinterpret_cast<T*>(shm_bytes_ +
                                    config_.input_offsets[bus][channel]);
    }

    template <typename T>
    // NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
    const T* input_channel_ptr(const uint32_t bus,
                               const uint32_t channel) const noexcept {
        return reinterpret_cast<const T*>(shm_bytes_ +
                                          config_.input_offsets[bus][channel]);
    }

    /**
     * Get a pointer to the part of the buffer where this output audio channel
     * is stored in. Both the bus and the channel indices start at zero. These
     * addresses might change after a call to `resize()`.
     */
    template <typename T>
    // NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
    T* output_channel_ptr(const uint32_t bus, const uint32_t channel) noexcept {
        return reinterpret_cast<T*>(shm_bytes_ +
                                    config_.output_offsets[bus][channel]);
    }

    template <typename T>
    // NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
    const T* output_channel_ptr(const uint32_t bus,
                                const uint32_t channel) const noexcept {
        return reinterpret_cast<const T*>(shm_bytes_ +
                                          config_.output_offsets[bus][channel]);
    }

    Config config_;

   private:
    /**
     * Resize the shared memory object, and set up the memory mapping.
     *
     * @throw std::system_error If the shared memory object could not be mapped.
     */
    void setup_mapping();

    /**
     * The file descriptor for our shared memory object.
     */
    int shm_fd_ = 0;
    /**
     * A pointer to our mapped shared memory region.
     */
    uint8_t* shm_bytes_ = nullptr;
    /**
     * The size of the mapped shared memory area, used for remapping.
     */
    size_t shm_size_ = 0;

    bool is_moved_ = false;
};
