//  nova server
//  Copyright (C) 2008, 2009, 2010 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 <atomic>

#include "buffer_manager.hpp"
#include "memory_pool.hpp"
#include "node_graph.hpp"
#include "../sc/sc_osc_handler.hpp"
#include "server_args.hpp"
#include "server_scheduler.hpp"
#include "synth_factory.hpp"
#include "../sc/sc_ugen_factory.hpp"
#include "../utilities/callback_interpreter.hpp"
#include "../utilities/static_pooled_class.hpp"
#include "../utilities/asynchronous_log.hpp"
#include "../../common/server_shm.hpp"

#ifdef PORTAUDIO_BACKEND
#    include "audio_backend/portaudio_backend.hpp"
#elif defined(JACK_BACKEND)
#    include "audio_backend/jack_backend.hpp"
#endif

#include "../../scsynth/SC_TimeDLL.hpp"
#include "../../scsynth/SC_Time.hpp"
namespace nova {

struct realtime_engine_functor {
    static inline void sync_clock(void);
    static void init_thread(void);
    static inline void run_tick(void);
    static void log_(const char*);
    static void log_printf_(const char*, ...);
};

extern class nova_server* instance;

/** callback class for audio-thread to system-thread communcation
 *
 *  the system_callback uses an internal memory pool for real-time safe
 *  memory management. objects are only allowed to be allocated from
 *  the real-time thread.
 * */

class system_callback : public static_pooled_class<system_callback, 1 << 20, /* 1mb pool of realtime memory */
                                                   false, 5> {
public:
    virtual ~system_callback(void) = default;

    virtual void run(void) = 0;
};

/** system_callback to delete object in the system thread. useful to avoid hitting the memory allocator
 *  from within the real-time threads
 */
template <typename T> class delete_callback : public system_callback {
public:
    delete_callback(T* ptr): ptr_(ptr) {}

private:
    virtual void run(void) override { delete ptr_; }

    const T* const ptr_;
};


/** dsp thread init functor
 *
 *  for the real-time use, it should acquire real-time scheduling and pin the thread to a certain CPU
 *
 * */
struct thread_init_functor {
    thread_init_functor(bool real_time): rt(real_time) {}

    void operator()(int thread_index);

private:
    const bool rt;
};

struct io_thread_init_functor {
    void operator()() const;
};

namespace detail {
#if defined(PORTAUDIO_BACKEND)
typedef portaudio_backend<realtime_engine_functor, float, false> audio_backend;
#elif defined(JACK_BACKEND)
typedef jack_backend<realtime_engine_functor, float, false> audio_backend;
#else
#    error "no audio backend selected"
#endif

} // detail

class nova_server : public asynchronous_log_thread,
                    public node_graph,
                    public server_shared_memory_creator,
                    public scheduler<thread_init_functor>,
                    public detail::audio_backend,
                    public synth_factory,
                    public buffer_manager,
                    public sc_osc_handler {
public:
    SC_TimeDLL mDLL;
    bool use_system_clock;
    bool non_rt;
    double smooth_samplerate;

    typedef detail::audio_backend audio_backend;

    /* main nova_server function */
    nova_server(server_arguments const& args);

    ~nova_server(void);

    void prepare_backend(void);

    /* @{ */
    /** io interpreter */
    void add_io_callback(system_callback* cb) { io_interpreter.add_callback(cb); }
    /* @} */

    /* @{ */
    /** system interpreter
     * \note: should only be called from the main audio thread
     */
    void add_system_callback(system_callback* cb) { system_interpreter.add_callback(cb); }

    void run(void) {
        start_dsp_threads();
        system_interpreter.run();
    }

    void prepare_to_terminate() { server_shared_memory_creator::disconnect(); }

    void terminate(void) {
        system_interpreter.terminate();
        quit_requested_ = true;
    }
    /* @} */

    /** non-rt synthesis */
    void run_nonrt_synthesis(server_arguments const& arguments);

public:
    /* @{ */
    /** node control */
    abstract_synth* add_synth(const char* name, int id, node_position_constraint const& constraints);
    group* add_group(int id, node_position_constraint const& constraints);
    parallel_group* add_parallel_group(int id, node_position_constraint const& constraints);

    void free_node(server_node* node);
    void group_free_all(abstract_group* group);
    void group_free_deep(abstract_group* group);

    void node_pause(server_node* node) {
        node->pause();
        notification_node_turned_off(node);
    }

    void node_resume(server_node* node) {
        node->resume();
        notification_node_turned_on(node);
    }

    void set_node_slot(int node_id, slot_index_t slot, float value);
    void set_node_slot(int node_id, const char* slot, float value);
    /* @} */

    void cpu_load(float& peak, float& average) const { audio_backend::get_cpuload(peak, average); }

    void increment_logical_time(void) { sc_osc_handler::increment_logical_time(time_per_tick); }

    void set_last_now(time_tag const& lasts, time_tag const& nows) { sc_osc_handler::set_last_now(lasts, nows); }

    void compensate_latency(void) {
        sc_osc_handler::add_last_now(
            time_tag::from_samples(audio_backend::get_latency(), audio_backend::get_samplerate()));
    }

public:
    HOT void tick() {
        sc_factory->apply_done_actions();
        run_callbacks();
        execute_scheduled_bundles();

        if (unlikely(dsp_queue_dirty))
            rebuild_dsp_queue();

        compute_audio();
    }

    void rebuild_dsp_queue(void);

    void request_dsp_queue_update(void) { dsp_queue_dirty = true; }

    bool quit_requested() { return quit_requested_.load(); }

private:
    void perform_node_add(server_node* node, node_position_constraint const& constraints, bool update_dsp_queue);
    void finalize_node(server_node& node);
    std::atomic<bool> quit_requested_ = { false };
    bool dsp_queue_dirty = false;

    callback_interpreter<system_callback, false> system_interpreter; // rt to system thread
    threaded_callback_interpreter<system_callback, io_thread_init_functor> io_interpreter; // for network IO
};

inline void run_scheduler_tick(void) {
    const int blocksize = sc_factory->world.mBufLength;
    // FIXME/TODO
    // server may have fewer input channels than sc_factory
    const int input_channels = instance->audio_backend::get_input_count();
    const int output_channels = sc_factory->world.mNumOutputs;
    const int buf_counter = ++sc_factory->world.mBufCounter;

    /* touch all input buffers */
    for (int channel = 0; channel != input_channels; ++channel)
        sc_factory->world.mAudioBusTouched[output_channels + channel] = buf_counter;

    instance->tick();

    /* wipe all untouched output buffers */
    for (int channel = 0; channel != output_channels; ++channel) {
        if (sc_factory->world.mAudioBusTouched[channel] != buf_counter)
            zerovec(sc_factory->world.mAudioBus + blocksize * channel, blocksize);
    }
}

inline void realtime_engine_functor::sync_clock(void) {
    if (instance->use_system_clock) {
        double nows = (uint64)(OSCTime(std::chrono::system_clock::now())) * kOSCtoSecs;
        instance->mDLL.Reset(sc_factory->world.mSampleRate, sc_factory->world.mBufLength, SC_TIME_DLL_BW, nows);
        time_tag oscTime = time_tag((uint64)((instance->mDLL.PeriodTime()) * kSecondsToOSCunits + .5));
        time_tag oscInc = time_tag((uint64)((instance->mDLL.Period()) * kSecondsToOSCunits + .5));
        instance->smooth_samplerate = instance->mDLL.SampleRate();
        instance->set_last_now(oscTime, oscTime + oscInc);
    } else
        instance->update_time_from_system();

    instance->compensate_latency();
}


inline void realtime_engine_functor::run_tick(void) {
    /* // debug timedll
    static int count = 0;
    if(count >= 44100/64){
        count = 0;
                log_printf("DLL: t %.6f p %.9f sr %.6f e %.9f avg(e) %.9f \n",
                 instance->mDLL.PeriodTime(), instance->mDLL.Period(), instance->mDLL.SampleRate(),
                 instance->mDLL.Error(), instance->mDLL.AvgError());
    }
    count++;
    */
    run_scheduler_tick();

    if (instance->use_system_clock) {
        // time_tag nows =
        // time_tag::from_ptime(boost::date_time::microsec_clock<boost::posix_time::ptime>::universal_time());
        double nows = (uint64)(OSCTime(std::chrono::system_clock::now())) * kOSCtoSecs;
        instance->mDLL.Update(nows);
        time_tag oscTime = time_tag((uint64)((instance->mDLL.PeriodTime()) * kSecondsToOSCunits + .5));
        time_tag oscInc = time_tag((uint64)((instance->mDLL.Period()) * kSecondsToOSCunits + .5));
        instance->smooth_samplerate = instance->mDLL.SampleRate();
        instance->set_last_now(oscTime, oscTime + oscInc);
    } else
        instance->increment_logical_time();
}

inline bool log(const char* string) {
    if (instance->get_error_posting())
        return instance->log(string);
    return true;
}

inline bool log(const char* string, size_t length) {
    if (instance->get_error_posting())
        return instance->log(string, length);
    return true;
}

inline bool log_printf(const char* fmt, ...) {
    if (instance->get_error_posting()) {
        va_list vargs;
        va_start(vargs, fmt);
        auto result = instance->log_printf(fmt, vargs);
        va_end(vargs);
        return result;
    }
    return true;
}

inline bool log_printf(const char* fmt, va_list vargs) {
    if (instance->get_error_posting()) {
        return instance->log_printf(fmt, vargs);
    }
    return true;
}

} /* namespace nova */
