//  native jack backend
//  Copyright (C) 2009-2015 Tim Blechmann
//
//  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 2 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; see the file COPYING.  If not, write to
//  the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
//  Boston, MA 02111-1307, USA.

#pragma once

#include <iostream>
#include <string>
#include <vector>

#include <jack/jack.h>
#include <jack/thread.h>
#ifdef SC_JACK_USE_METADATA_API
#    include <jack/metadata.h>
#    include <jack/uuid.h>
#    include <jackey.h>
#endif

#include "utilities/branch_hints.hpp"

#include "audio_backend_common.hpp"
#include "cpu_time_info.hpp"

namespace nova {

/** jack backend
 *
 *  the jack callback thread is pinned to the first cpu of the system
 *
 *  \todo later it may be interesting to directly map the io busses to the jack port regions
 *  \todo rethink the use of output port lock
 */
template <typename engine_functor, typename sample_type = float, bool blocking = false>
class jack_backend : public detail::audio_backend_base<sample_type, jack_default_audio_sample_t, blocking, false>,
                     public detail::audio_settings_basic,
                     protected engine_functor {
    typedef detail::audio_backend_base<sample_type, jack_default_audio_sample_t, blocking, false> super;

public:
    jack_backend(void) = default;

    ~jack_backend(void) {
        if (audio_is_active())
            deactivate_audio();

        close_client();
    }

    void initialize(void) {}

    uint32_t get_audio_blocksize(void) const { return blocksize_; }

    uint32_t get_latency(void) const { return latency_; }

public:
    void open_client(std::string const& server_name, std::string const& name, uint32_t input_port_count,
                     uint32_t output_port_count, uint32_t blocksize) {
        blocksize_ = blocksize;

        /* open client */
        client = server_name.empty()
            ? jack_client_open(name.c_str(), JackNoStartServer, &status)
            : jack_client_open(name.c_str(), jack_options_t(JackNoStartServer | JackServerName), &status,
                               server_name.c_str());
        std::atomic_thread_fence(std::memory_order_release); // ensure visibility on other threads

        if (status & JackServerFailed)
            throw std::runtime_error("Unable to connect to JACK server");

        if (status & JackNameNotUnique) {
            const char* client_name = jack_get_client_name(client);
            std::cout << "unique client name: " << client_name << std::endl;
        }

        /* initialize callbacks */
        jack_set_thread_init_callback(client, jack_thread_init_callback, this);
        jack_set_process_callback(client, jack_process_callback, this);
        jack_set_xrun_callback(client, jack_xrun_callback, this);
        jack_set_graph_order_callback(client, jack_graph_order_callback, this);
        jack_on_info_shutdown(client, (JackInfoShutdownCallback)jack_on_info_shutdown_callback, nullptr);

        /* register ports */
        input_ports.clear();
        for (uint32_t i = 0; i != input_port_count; ++i) {
            std::string portname("input_");
            portname += std::to_string(i + 1);
            jack_port_t* port =
                jack_port_register(client, portname.c_str(), JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
            input_ports.push_back(port);

#ifdef SC_JACK_USE_METADATA_API
            jack_uuid_t uuid = jack_port_uuid(port);
            if (!jack_uuid_empty(uuid)) {
                std::string prettyname("Input ");
                prettyname += std::to_string(i + 1);
                jack_set_property(client, uuid, JACK_METADATA_PRETTY_NAME, prettyname.c_str(), "text/plain");

                std::string order("");
                order += std::to_string(i);
                jack_set_property(client, uuid, JACKEY_ORDER, order.c_str(),
                                  "http://www.w3.org/2001/XMLSchema#integer");
            }
#endif
        }
        input_channels = input_port_count;
        super::input_samples.resize(input_port_count);

        output_ports.clear();
        for (uint32_t i = 0; i != output_port_count; ++i) {
            std::string portname("output_");
            portname += std::to_string(i + 1);
            jack_port_t* port =
                jack_port_register(client, portname.c_str(), JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0);
            output_ports.push_back(port);

#ifdef SC_JACK_USE_METADATA_API
            jack_uuid_t uuid = jack_port_uuid(port);
            if (!jack_uuid_empty(uuid)) {
                std::string prettyname("Output ");
                prettyname += std::to_string(i + 1);
                jack_set_property(client, uuid, JACK_METADATA_PRETTY_NAME, prettyname.c_str(), "text/plain");

                std::string order("");
                order += std::to_string(input_port_count + i);
                jack_set_property(client, uuid, JACKEY_ORDER, order.c_str(),
                                  "http://www.w3.org/2001/XMLSchema#integer");
            }
#endif
        }
        output_channels = output_port_count;
        super::output_samples.resize(output_port_count);

        samplerate_ = jack_get_sample_rate(client);
        jack_frames = jack_get_buffer_size(client);

        if (jack_frames % blocksize_)
            throw std::runtime_error("Jack buffer size is not a multiple of blocksize");

        cpu_time_accumulator.resize(samplerate_, jack_frames, 1);
    }

    void close_client(void) {
        if (client) {
            jack_client_close(client);
            client = nullptr;
        }
    }

    bool audio_is_opened(void) { return client != nullptr; }

    bool audio_is_active(void) { return is_active; }

    void activate_audio(void) {
        is_active = true;
        jack_activate(client);
    }

    void deactivate_audio(void) {
        if (is_active) {
            jack_deactivate(client);
            is_active = false;
        }
    }

    void get_cpuload(float& peak, float& average) const { cpu_time_accumulator.get(peak, average); }

    int connect_input(size_t channel, const char* portname) {
        if (channel >= input_ports.size())
            return -1;
        return jack_connect(client, portname, jack_port_name(input_ports[channel]));
    }

    int connect_output(size_t channel, const char* portname) {
        if (channel >= output_ports.size())
            return -1;
        return jack_connect(client, jack_port_name(output_ports[channel]), portname);
    }

    int connect_all_inputs(const char* client_name) {
        const char** ports = jack_get_ports(client, client_name, nullptr, JackPortIsOutput);

        if (!ports)
            return -1;

        std::size_t i = 0;
        while (ports[i]) {
            if (i == input_ports.size())
                break;

            int err = jack_connect(client, ports[i], jack_port_name(input_ports[i]));
            if (err)
                return err;
            ++i;
        }

        free(ports);
        return 0;
    }

    int connect_all_outputs(const char* client_name) {
        const char** ports = jack_get_ports(client, client_name, nullptr, JackPortIsInput);

        if (!ports)
            return -1;

        std::size_t i = 0;
        while (ports[i]) {
            if (i == output_ports.size())
                break;

            int err = jack_connect(client, jack_port_name(output_ports[i]), ports[i]);
            if (err)
                return err;
            ++i;
        }

        free(ports);
        return 0;
    }

    int max_realtime_priority(void) const { return jack_client_max_real_time_priority(client); }

    int realtime_priority(void) const { return jack_client_real_time_priority(client); }

private:
    static void jack_thread_init_callback(void* arg) {
        std::atomic_thread_fence(std::memory_order_acquire);
        jack_backend* self = static_cast<jack_backend*>(arg);
        if (jack_client_thread_id(self->client) == pthread_self())
            engine_functor::init_thread();
        else
            name_thread("Jack Helper");
    }

    static int jack_process_callback(jack_nframes_t frames, void* arg) {
        return static_cast<jack_backend*>(arg)->perform(frames);
    }

    static int jack_on_info_shutdown_callback(jack_status_t code, const char* reason, void* arg) {
        std::cerr << "Jack server was shut down: " << reason << std::endl;
        std::cerr << "Exiting ..." << std::endl;
        exit(0); // TODO: later we may want to call a function
    }

    static int jack_xrun_callback(void* arg) { return static_cast<jack_backend*>(arg)->handle_xrun(); }

    static int jack_graph_order_callback(void* arg) { return static_cast<jack_backend*>(arg)->graph_order_changed(); }

    int handle_xrun(void) {
        time_is_synced = false;
        engine_functor::log_("Jack: xrun detected - resyncing clock\n");
        return 0;
    }

    int graph_order_changed() {
        time_is_synced = false;

        jack_nframes_t lat = 0;
        for (auto const& port : output_ports) {
            jack_latency_range_t range;
            jack_port_get_latency_range(port, JackPlaybackLatency, &range);
            jack_nframes_t portLat = range.max;
            if (portLat > lat)
                lat = portLat;
        }

        uint32_t latency = (uint32_t)lat;

        if (latency != latency_) {
            latency_ = latency;
            double latms = lat / samplerate_ * 1e3;
            engine_functor::log_printf_("JackDriver: max output latency %.1f ms\n", latms);
        }

        return true;
    }

    int perform(jack_nframes_t frames) {
        if (unlikely(!time_is_synced)) {
            engine_functor::sync_clock();
            time_is_synced = true;
        }

        /* get port regions */
        jack_default_audio_sample_t** inputs =
            (jack_default_audio_sample_t**)alloca(input_channels * sizeof(jack_default_audio_sample_t*));
        jack_default_audio_sample_t** outputs =
            (jack_default_audio_sample_t**)alloca(output_channels * sizeof(jack_default_audio_sample_t*));
        for (uint16_t i = 0; i != input_channels; ++i)
            inputs[i] = (jack_default_audio_sample_t*)jack_port_get_buffer(input_ports[i], frames);

        for (uint16_t i = 0; i != output_channels; ++i)
            outputs[i] = (jack_default_audio_sample_t*)jack_port_get_buffer(output_ports[i], frames);

        jack_nframes_t processed = 0;
        while (processed != frames) {
            super::fetch_inputs((const float**)inputs, blocksize_, input_channels);
            engine_functor::run_tick();
            super::deliver_outputs(outputs, blocksize_, output_channels);
            processed += blocksize_;
        }

        cpu_time_accumulator.update(jack_cpu_load(client));

        return 0;
    }

    static int jack_buffersize_callback(jack_nframes_t frames, void* arg) {
        return static_cast<jack_backend*>(arg)->buffer_size_callback(frames);
    }

    int buffer_size_callback(jack_nframes_t frames) {
        jack_frames = frames;
        if (jack_frames % blocksize_)
            /* we need a multiple of the blocksize */
            return 1;
        return 0;
    }

    jack_client_t* client = nullptr;
    jack_status_t status;

    bool is_active = false;
    bool time_is_synced = false;
    uint32_t blocksize_;

    std::vector<jack_port_t*> input_ports, output_ports;
    jack_nframes_t jack_frames;
    cpu_time_info cpu_time_accumulator;

    uint32_t latency_;
};

} /* namespace nova */
