// ------------------------------------------------------------------------
// eca-engine.cpp: Main processing engine
// Copyright (C) 1999-2009,2012,2015,2020 Kai Vehmanen
// Copyright (C) 2005 Stuart Allie
//
// Attributes:
//     eca-style-version: 3 (see Ecasound Programmer's Guide)
//
// 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; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
// ------------------------------------------------------------------------

#include <iostream>
#include <string>
#include <vector>
#include <ctime>
#include <cmath>
#include <utility>

#include <assert.h>
#include <unistd.h>
#include <pthread.h>
#include <errno.h>
#include <sys/time.h> /* gettimeofday() */

#include <kvu_dbc.h>
#include <kvu_numtostr.h>
#include <kvu_procedure_timer.h>
#include <kvu_rtcaps.h>
#include <kvu_threads.h>

#include "samplebuffer.h"
#include "audioio.h"
#include "audioio-buffered.h"
#include "audioio-device.h"
#include "audioio-db-client.h"
#include "audioio-loop.h"
#include "audioio-barrier.h"
#include "audioio-mp3.h"
#include "midi-server.h"
#include "eca-chain.h"
#include "eca-chainop.h"
#include "eca-error.h"
#include "eca-logger.h"
#include "eca-chainsetup-edit.h"
#include "eca-engine.h"
#include "eca-engine_impl.h"

using std::cerr;
using std::endl;
using std::vector;

/**
 * Enable and disable features
 */

/* Profile callback execution */
// #define PROFILE_ENGINE

/**
 * Local macro definitions
 */

#ifdef PROFILE_ENGINE_EXECUTION
#define PROFILE_ENGINE_STATEMENT(x) (x)
#else
#define PROFILE_ENGINE_STATEMENT(x) ((void)0)
#endif

/**
 * Prototypes of static functions
 */

static void mix_to_outputs_divide_helper(const SAMPLE_BUFFER *from, SAMPLE_BUFFER *to, int divide_by, bool first_time);
static void mix_to_outputs_sum_helper(const SAMPLE_BUFFER *from, SAMPLE_BUFFER *to, bool first_time);

/**
 * Implementations of non-static functions
 */

/**********************************************************************
 * Driver implementation
 **********************************************************************/

int ECA_ENGINE_DEFAULT_DRIVER::exec(ECA_ENGINE* engine, ECA_CHAINSETUP* csetup)
{
  bool drain_at_end = false;

  engine_repp = engine;

  exit_request_rep = false;
  engine->init_engine_state();

  while(true) {

    engine_repp->check_command_queue();

    /* case 1: external exit request */
    if (exit_request_rep == true) break;

    /* case 2: engine running, execute one loop iteration */
    if (engine_repp->status() == ECA_ENGINE::engine_status_running) {
      engine_repp->engine_iteration();
    }
    else {
      /* case 3a-i: engine finished and in batch mode -> exit */
      if (engine_repp->status() == ECA_ENGINE::engine_status_finished &&
          engine_repp->batch_mode() == true) {
        ECA_LOG_MSG(ECA_LOGGER::system_objects, "batch finished in exec, exit");
        drain_at_end = true;
        break;
      }

      /* case 3a-ii: engine error occured -> exit */
      else if (engine_repp->status() == ECA_ENGINE::engine_status_error) {
        ECA_LOG_MSG(ECA_LOGGER::system_objects, "engine error, exit");
        break;
      }

      /* case 3b: engine not running, wait for commands */
      engine_repp->wait_for_commands();
    }

    engine_repp->update_engine_state();
  }

  if (engine_repp->is_prepared() == true)
    engine_repp->stop_operation(drain_at_end);

  return 0;
}

void ECA_ENGINE_DEFAULT_DRIVER::start(void)
{
  if (engine_repp->is_prepared() != true) engine_repp->prepare_operation();
  engine_repp->start_operation();
}

void ECA_ENGINE_DEFAULT_DRIVER::stop(bool drain)
{
  if (engine_repp->is_prepared() == true) engine_repp->stop_operation(drain);
}

void ECA_ENGINE_DEFAULT_DRIVER::exit(void)
{
  if (engine_repp->is_prepared() == true) engine_repp->stop_operation();
  exit_request_rep = true;

  /* no need to block here as the next 
   * function will be exec()
   */
}

/**********************************************************************
 * Engine implementation - Public functions
 **********************************************************************/

/**
 * Context help:
 *  J = originates from driver callback
 *  E = ----- " ------- engine thread (exec())
 *  C = ----- " ------- control thread (external)
 *
 *  X-level-Y -> Y = steps from originating functions
 */

/**
 * Class constructor. A pointer to an enabled 
 * ECA_CHAINSETUP object must be given as argument. 
 *
 * @pre csetup != 0
 * @pre csetup->is_enabled() == true
 * @post status() == ECA_ENGINE::engine_status_stopped
 */
ECA_ENGINE::ECA_ENGINE(ECA_CHAINSETUP* csetup) 
  : prepared_rep(false),
    running_rep(false),
    finished_rep(false),
    outputs_finished_rep(0),
    driver_errors_rep(0),
    csetup_repp(csetup),
    mixslot_repp(0)
{
  // --
  DBC_REQUIRE(csetup != 0);
  DBC_REQUIRE(csetup->is_enabled() == true);
  // --

  ECA_LOG_MSG(ECA_LOGGER::system_objects, "ECA_ENGINE constructor");

  csetup_repp->toggle_locked_state(true);

  impl_repp = new ECA_ENGINE_impl;
  mixslot_repp = new SAMPLE_BUFFER(buffersize(), 0);

  init_variables();
  init_connection_to_chainsetup();

  PROFILE_ENGINE_STATEMENT(init_profiling());

  csetup_repp->toggle_locked_state(false);

  // --
  DBC_ENSURE(status() == ECA_ENGINE::engine_status_stopped);
  // --
}

/**
 * Class destructor.
 */
ECA_ENGINE::~ECA_ENGINE(void)
{
  ECA_LOG_MSG(ECA_LOGGER::system_objects, "ECA_ENGINE destructor");

  if (csetup_repp != 0) {
    command(ECA_ENGINE::ep_exit, 0.0f);
    wait_for_exit(5);
    if (csetup_repp != 0) {
      cleanup();
    }
  }
  
  PROFILE_ENGINE_STATEMENT(dump_profile_info());

  if (driver_local == true) {
    delete driver_repp;
    driver_repp = 0;
  }

  for(size_t n = 0; n < cslots_rep.size(); n++) {
    delete cslots_rep[n];
  }

  delete mixslot_repp;
  delete impl_repp;

  ECA_LOG_MSG(ECA_LOGGER::subsystems, "Engine exiting");
}

/**
 * Launches the engine. This function will block 
 * until processing is finished.
 *
 * Note that a exec() is a one-shot function.
 * It's not possible to call it multiple times.
 *
 * @param batch_mode if true, once engine is started 
 *                   it will continue processing until 
 *                   'status() == engine_status_finished' 
 *                   condition is reached and then exit;
 *                   if false, engine will react to 
 *                   commands until explicit 'exit' is
 *                   given
 *
 * @see command()
 * @see status()
 *
 * @pre is_valid() == true
 * @post status() == ECA_ENGINE::engine_status_notready
 * @post is_valid() != true
 */
int ECA_ENGINE::exec(bool batch_mode)
{
  // --
  DBC_REQUIRE(csetup_repp != 0);
  DBC_REQUIRE(csetup_repp->is_enabled() == true);
  // --

  int result = 0;

  csetup_repp->toggle_locked_state(true);

  batchmode_enabled_rep = batch_mode;

  ECA_LOG_MSG(ECA_LOGGER::subsystems, "Engine - Driver start");

  int res = driver_repp->exec(this, csetup_repp);
  if (res < 0) {
    ++driver_errors_rep;
    ECA_LOG_MSG(ECA_LOGGER::info, 
                "WARNING: Engine has raised an error! "
                "Possible causes: connection lost to system services, unable to adapt "
                "to changes in operating environment, etc.");
  }

  csetup_repp->toggle_locked_state(false);

  signal_exit();

  if (outputs_finished_rep > 0) {
    ECA_LOG_MSG(ECA_LOGGER::info, "WARNING: An output object has raised an error! "
                "Possible causes: Out of disk space, permission denied, unable to launch external "
                "applications needed in processing, etc.");
  }

  DBC_CHECK(status() == ECA_ENGINE::engine_status_stopped ||
            status() == ECA_ENGINE::engine_status_finished ||
            status() == ECA_ENGINE::engine_status_error);

  if (status() == ECA_ENGINE::engine_status_error) {
    result = -1;
  }

  cleanup();

  ECA_LOG_MSG(ECA_LOGGER::user_objects, 
              "Engine state when finishing: " +
              kvu_numtostr(static_cast<int>(status())));

  // --
  DBC_ENSURE(status() == ECA_ENGINE::engine_status_notready);
  // --

  return result;
}

/**
 * Sends 'cmd' to engines command queue. Commands are 
 * processed in the server's main loop. 
 *
 * context: C-level-0
 *          must no be called from exec() context
 */
void ECA_ENGINE::command(Engine_command_t cmd, double arg)
{
  ECA_ENGINE::complex_command_t item;
  item.type = cmd;
  item.m.legacy.value = arg;
  impl_repp->command_queue_rep.push_back(item);
}

/**
 * Sends 'ccmd' to engines command queue. Commands are 
 * processed in the server's main loop. Passing a complex
 * command allows to address objects regardless of
 * the state of ECA_CHAINSETUP iterators (i.e. currently
 * selected objects).
 *
 * context: C-level-0
 *          must no be called from exec() context
 */
void ECA_ENGINE::command(complex_command_t ccmd)
{
  impl_repp->command_queue_rep.push_back(ccmd);
}

/**
 * Wait for a stop signal. Functions blocks until 
 * the signal is received or 'timeout' seconds
 * has elapsed.
 * 
 * context: C-level-0
 *          must not be run from the engine 
 *          driver context
 *
 * @see signal_stop()
 */
void ECA_ENGINE::wait_for_stop(int timeout)
{
  struct timespec timeoutspec;

  // FIXME: start to use MONOTONIC, needs pthread_condattr_setclock
  int ret = kvu_pthread_cond_timeout(timeout, &timeoutspec, false);
  DBC_CHECK(ret == 0);

  pthread_mutex_lock(&impl_repp->ecasound_stop_mutex_repp);
  while(running_rep == true) {
    ret = pthread_cond_timedwait(&impl_repp->ecasound_stop_cond_repp, 
                                 &impl_repp->ecasound_stop_mutex_repp,
                                 &timeoutspec);
    ECA_LOG_MSG(ECA_LOGGER::system_objects, 
                kvu_pthread_timed_wait_result(ret, "wait_for_stop"));
  }
  pthread_mutex_unlock(&impl_repp->ecasound_stop_mutex_repp);
}

/**
 * Wait for an exit signal. Function blocks until 
 * the signal is received or 'timeout' seconds
 * has elapsed.
 * 
 * context: C-level-0
 *
 * @see signal_exit()
 */
void ECA_ENGINE::wait_for_exit(int timeout)
{

  int ret = kvu_pthread_timed_wait(&impl_repp->ecasound_exit_mutex_repp, 
                                   &impl_repp->ecasound_exit_cond_repp, 
                                   timeout);
  ECA_LOG_MSG(ECA_LOGGER::info, 
              kvu_pthread_timed_wait_result(ret, "(eca_main) wait_for_exit"));
}

/**********************************************************************
 * Engine implementation - Public functions for observing engine 
 *                         status information
 **********************************************************************/

/**
 * Returns true engine's internal 
 * state is valid for processing.
 *
 * context: C-level-0
 *          no limitations
 */
bool ECA_ENGINE::is_valid(void) const
{
  if (csetup_repp == 0 ||
      csetup_repp->is_enabled() != true ||
      csetup_repp->is_valid() != true ||
      chains_repp == 0 ||
      chains_repp->size() == 0 ||
      inputs_repp == 0 ||
      inputs_repp->size() == 0 ||
      outputs_repp == 0 ||
      outputs_repp->size() == 0) {

    return false;
  }
  
  return true;
}

/**
 * Whether current setup has finite length.
 *
 * context: C-level-0
 */
bool ECA_ENGINE::is_finite_length(void) const
{
  DBC_CHECK(csetup_repp != 0);

  if (csetup_repp->max_length_set() == true ||
      csetup_repp->number_of_realtime_inputs() == 0) {
    return true;
  }

  return false;
}

/**
 * Returns engine's current status.
 *
 * context: C-level-0
 *          no limitations
 */
ECA_ENGINE::Engine_status_t ECA_ENGINE::status(void) const
{ 
  if (csetup_repp == 0) 
    return ECA_ENGINE::engine_status_notready;

  /* calculated in update_engine_status() */
  if (finished_rep == true)
    return ECA_ENGINE::engine_status_finished;
  
  if (outputs_finished_rep > 0 ||
      driver_errors_rep > 0) 
    return ECA_ENGINE::engine_status_error;

  if (is_running() == true) 
    return ECA_ENGINE::engine_status_running;

  if (is_prepared() == true) 
    return ECA_ENGINE::engine_status_stopped;

  return ECA_ENGINE::engine_status_stopped;
}

/**********************************************************************
 * Engine implementation - API for engine driver objects
 **********************************************************************/

/**
 * Processes available new commands. If no
 * messages are available, function will return
 * immediately without blocking.
 *
 * context: E-level-1
 *          can be run at the same time as engine_iteration(); 
 *          note! this is called with the engine lock held
 *          so no long operations allowed!
 */
void ECA_ENGINE::check_command_queue(void)
{
  while(impl_repp->command_queue_rep.is_empty() != true) {
    ECA_ENGINE::complex_command_t item;
    int popres = impl_repp->command_queue_rep.pop_front(&item);

    if (popres <= 0) {
      /* queue is empty or temporarily unavailable, unable to continue 
       * processing messages without blocking */
      break;
    }

    switch(item.type) 
      {
        // ---
        // Basic commands.
        // ---            
      case ep_exit:
        {
          // FIXME: is clear the right thing or should remaining cmds
          //        be still processed? OTOH, client app should know...
          impl_repp->command_queue_rep.clear();
          ECA_LOG_MSG(ECA_LOGGER::system_objects,"ecasound_queue: exit!");
          driver_repp->exit();
          return;
        }

        // ---
        // Chain operators (stateless addressing)
        // ---
      case ep_exec_edit:
        {
          csetup_repp->execute_edit(item.cs);
          if (item.cs.need_chain_reinit) {
            reinit_chains(true);
          }
          break;
        }
      case ep_prepare:
        {
          if (is_prepared() != true)
            prepare_operation();
          break;
        }
      case ep_start:
        {
          if (status() != engine_status_running)
            request_start();
          break;
        }
      case ep_stop:
        {
          if (status() == engine_status_running ||
              status() == engine_status_finished) request_stop(false);
          break;
        }
      case ep_stop_with_drain:
        {
          if (status() == engine_status_running ||
              status() == engine_status_finished)
            request_stop(true);
          break;
        }

        // ---
        // Global position
        // ---
      case ep_rewind: { change_position(- item.m.legacy.value); break; }
      case ep_forward: { change_position(item.m.legacy.value); break; }
      case ep_setpos: { set_position(item.m.legacy.value); break; }
      case ep_setpos_samples: { set_position_samples(static_cast<SAMPLE_SPECS::sample_pos_t>(item.m.legacy.value)); break; }
      case ep_setpos_live_samples: { set_position_samples_live(static_cast<SAMPLE_SPECS::sample_pos_t>(item.m.legacy.value)); break; }

      case ep_debug: break;

      } /* switch */
    
  }
}

/**
 * Waits for new commands to arrive. Function
 * will block until at least one new message
 * is available or until a timeout occurs.
 *
 * context: E-level-1
 *          can be run at the same time as 
 *          engine_iteration()
 */
void ECA_ENGINE::wait_for_commands(void)
{
  impl_repp->command_queue_rep.poll(5, 0);
}

/**
 * Intializes internal state variables.
 *
 * context: E-level-1
 *          must not be run at the same 
 *          time as engine_iteration()
 *
 * @see update_engine_state()
 */
void ECA_ENGINE::init_engine_state(void)
{
  finished_rep = false;
  inputs_not_finished_rep = 1; // for the 1st iteration
  outputs_finished_rep = 0;
  mixslot_repp->event_tag_set(SAMPLE_BUFFER::tag_end_of_stream, false);
  for(size_t n = 0; n < cslots_rep.size(); n++) {
    cslots_rep[n]->event_tag_set(SAMPLE_BUFFER::tag_end_of_stream, false);
  }
}

/**
 * Updates engine state to match current 
 * situation.
 *
 * context: J-level-0
 *          must not be run at the same 
 *          time as engine_iteration()
 *
 * @see init_engine_state()
 */
void ECA_ENGINE::update_engine_state(void)
{
  // --
  // Check whether all inputs have finished
  // (note: as update_engine_state() is 
  // guaranteed to not to be called at the same 
  // time as engine_iteration(), this is the 
  // only safe place to calculate finished state
  
  if (inputs_not_finished_rep == 0 && 
      outputs_finished_rep == 0 && 
      finished_rep != true) {
    if (is_running() == true) {
      ECA_LOG_MSG(ECA_LOGGER::system_objects,"all inputs finished - stop");
      // FIXME: this is still wrong, command() is not fully rt-safe
      // we are not allowed to call request_stop here
      command(ECA_ENGINE::ep_stop_with_drain, 0.0f);
    }

    state_change_to_finished();
  }
  
  // --
  // Check whether some output has raised an error

  if (status() == ECA_ENGINE::engine_status_error) {
    if (is_running() == true) {
      ECA_LOG_MSG(ECA_LOGGER::system_objects,"output error - stop");
      // FIXME: this is still wrong, command() is not fully rt-safe
      // we are not allowed to call request_stop here
      command(ECA_ENGINE::ep_stop, 0.0f);
    }
  }
}

/**
 * Executes one engine loop iteration. It is critical
 * that this function is only called when engine is 
 * running.
 *
 * @pre is_running() == true
 *
 * context: J-level-0
 */
void ECA_ENGINE::engine_iteration(void)
{
  DBC_CHECK(is_running() == true);
  
  PROFILE_ENGINE_STATEMENT(impl_repp->looptimer_rep.start(); impl_repp->looptimer_range_rep.start());
  
  inputs_not_finished_rep = 0;
  prehandle_control_position();
  inputs_to_chains();
  process_chains();
  // FIXME: add support for sub-buffersize offsets
  if (preroll_samples_rep >= recording_offset_rep) {
    /* record material to non-real-time outputs */
    mix_to_outputs(false);
  }
  else {
    /* skip slave targets */
    mix_to_outputs(true);
    preroll_samples_rep += buffersize();
  }
  posthandle_control_position();
  
  PROFILE_ENGINE_STATEMENT(impl_repp->looptimer_rep.stop(); impl_repp->looptimer_range_rep.stop());
}

/**
 * Reinitialize chains
 *
 * Flush any existing state from the chain operators (e.g.
 * old audio data in buffers, changes in configuration, and
 * so forth).
 *
 * @arg force force reinitialization even if chain itself
 *            reports it is properly initialized
 */
void ECA_ENGINE::reinit_chains(bool force)
{
  for(size_t i = 0; i != chains_repp->size(); i++) {
    if (force == true ||
        (*chains_repp)[i]->is_initialized() != true) {
      (*chains_repp)[i]->init(0, 0, 0);
    }
  }
}

/**
 * Prepares engine for operation. Prepares all 
 * realtime devices and starts servers.
 * 
 * This function should be called by the
 * driver before it starts iterating the 
 * engine's main loop.
 *
 * context: E-level-1/3
 *          must not be run at the same time
 *          as engine_iteration()
 *
 * @pre is_running() != true
 * @pre is_prepared() != true
 * @post is_prepared() == true
 * @post status() == ECA_ENGINE::engine_status_running
 */
void ECA_ENGINE::prepare_operation(void)
{
  // ---
  DBC_REQUIRE(is_running() != true);
  DBC_REQUIRE(is_prepared() != true);
  // ---

  /* 1. acquire rt-lock for chainsetup and samplebuffers */
  csetup_repp->toggle_locked_state(true);

  for(size_t n = 0; n < cslots_rep.size(); n++) {
    cslots_rep[n]->set_rt_lock(true);
  }
  mixslot_repp->set_rt_lock(true);

  /* 2. reinitialize chains if necessary */
  reinit_chains(true);

  /* 3. start subsystem servers and forked audio objects */
  start_forked_objects();
  start_servers();

  /* 4. prepare rt objects */
  prepare_realtime_objects();

  /* ... initial offset is needed because preroll is 
   * incremented only after checking whether we are 
   * still in preroll mode */
  preroll_samples_rep = buffersize(); 

  /* 6. enable rt-scheduling */
  if (csetup_repp->raised_priority() == true) {
    if (kvu_set_thread_scheduling(SCHED_FIFO, csetup_repp->get_sched_priority()) != 0)
      ECA_LOG_MSG(ECA_LOGGER::system_objects, "Unable to change scheduling policy!");
    else
      ECA_LOG_MSG(ECA_LOGGER::user_objects, 
                  std::string("Using realtime-scheduling (SCHED_FIFO:")
                  + kvu_numtostr(csetup_repp->get_sched_priority()) + ").");
  }

  /* 7. change engine to active and running */
  prepared_rep = true;
  init_engine_state();
  ECA_LOG_MSG(ECA_LOGGER::system_objects, "engine prepared");

  // ---
  DBC_ENSURE(is_prepared() == true);
  DBC_ENSURE(status() == ECA_ENGINE::engine_status_stopped);
  // ---
}

/**
 * Starts engine operation.
 * 
 * This function should be called by the
 * driver just before it starts iterating the 
 * engine's main loop.
 *
 * context: E-level-1/3
 *          must not be run at the same time
 *          as engine_iteration()
 *
 * @pre is_prepared() == true
 * @pre is_running() != true
 * @post is_running() == true
 * @post status() == ECA_ENGINE::engine_status_running
 */
void ECA_ENGINE::start_operation(void)
{
  // ---
  DBC_REQUIRE(is_prepared() == true);
  DBC_REQUIRE(is_running() != true);
  // ---

  ECA_LOG_MSG(ECA_LOGGER::system_objects, "starting engine operation!");

  start_realtime_objects();
  running_rep = true;

  // ---
  DBC_ENSURE(is_running() == true);
  DBC_ENSURE(status() == ECA_ENGINE::engine_status_running);
  // ---
}

/**
 * Stops all realtime devices and servers.
 * 
 * This function should be called by the
 * driver when it stops iterating the 
 * engine's main loop.
 *
 * context: E-level-1/3
 *          must not be run at the same time
 *          as engine_iteration()
 *
 * @param drain whether to block until all queued data is processed
 *              by realtime devices before stopping
 *
 * @pre is_running() == true
 * @post is_running() != true
 * @post is_prepared() != true
 */
void ECA_ENGINE::stop_operation(bool drain)
{
  // ---
  DBC_REQUIRE(is_prepared() == true);
  // ---

  ECA_LOG_MSG(ECA_LOGGER::system_objects, "stopping engine operation!");

  /* stop realtime devices */
  for (unsigned int adev_sizet = 0; adev_sizet != realtime_objects_rep.size(); adev_sizet++) {
    if (realtime_objects_rep[adev_sizet]->is_running() == true)
      realtime_objects_rep[adev_sizet]->stop(drain);
  }

  prepared_rep = false;

  /* release samplebuffer rt-locks */
  for(size_t n = 0; n < cslots_rep.size(); n++) {
    cslots_rep[n]->set_rt_lock(false);
  }
  mixslot_repp->set_rt_lock(false);

  stop_servers();
  stop_forked_objects();

  /* lower priority back to normal */
  if (csetup_repp->raised_priority() == true) {
    if (kvu_set_thread_scheduling(SCHED_OTHER, 0) != 0)
      ECA_LOG_MSG(ECA_LOGGER::info, "Unable to change scheduling back to SCHED_OTHER!");
    else
      ECA_LOG_MSG(ECA_LOGGER::system_objects, "Changed back to non-realtime scheduling SCHED_OTHER.");
  }

  /* release chainsetup lock */
  csetup_repp->toggle_locked_state(false);

  /* signals wait_for_stop() that engine operation has stopped */
  signal_stop();

  // ---
  DBC_ENSURE(is_running() != true);
  DBC_ENSURE(is_prepared() != true);
  // ---
}

/**
 * Whether engine has been actived 
 * with prepare_operation().
 *
 * context: no limitations
 */
bool ECA_ENGINE::is_prepared(void) const
{
  return prepared_rep;
}

/**
 * Whether engine has been started
 * with start_operation().
 *
 * context: no limitations
 */
bool ECA_ENGINE::is_running(void) const
{
  return running_rep;
}

/**********************************************************************
 * Engine implementation - Attribute functions
 **********************************************************************/

long int ECA_ENGINE::buffersize(void) const
{
  DBC_CHECK(csetup_repp != 0);
  return csetup_repp->buffersize();
}

int ECA_ENGINE::max_channels(void) const
{
  int result = 0;
  for(unsigned int n = 0; n < csetup_repp->inputs.size(); n++) {
    if (csetup_repp->inputs[n]->channels() > result)
      result = csetup_repp->inputs[n]->channels();
  }
  for(unsigned int n = 0; n < csetup_repp->outputs.size(); n++) {
    if (csetup_repp->outputs[n]->channels() > result)
      result = csetup_repp->outputs[n]->channels();
  }
  return result;
}

/**********************************************************************
 * Engine implementation - Private functions for transport control
 **********************************************************************/

/**
 * Requests the engine driver to start operation.
 *
 * This function should only be called from 
 * check_command_queue().
 * 
 * @pre status() != engine_status_running
 *
 * context: E-level-2
 */
void ECA_ENGINE::request_start(void)
{
  // ---
  DBC_REQUIRE(status() != engine_status_running);
  // ---

  ECA_LOG_MSG(ECA_LOGGER::user_objects, "Request start");

  // --
  // start the driver
  driver_repp->start();
}

/**
 * Requests the engine driver to stop operation.
 *
 * This function should only be called from 
 * check_command_queue().
 *
 * @pre status() == ECA_ENGINE::engine_status_running ||
 *      status() == ECA_ENGINE::engine_status_finished
 *
 * context: E-level-2
 */
void ECA_ENGINE::request_stop(bool drain)
{ 
  // ---
  DBC_REQUIRE(status() == engine_status_running ||
              status() == engine_status_finished);
  // ---

  ECA_LOG_MSG(ECA_LOGGER::user_objects,
              std::string("Request stop (") + 
              (drain ? std::string("drain") : std::string("no-drain")) + ")");

  driver_repp->stop(drain);
}

/**
 * Sends a stop signal indicating that engine
 * state has changed to stopped.
 *
 * context: E-level-1/4 
 *
 * @see wait_for_stop()
 */
void ECA_ENGINE::signal_stop(void)
{
  pthread_mutex_lock(&impl_repp->ecasound_stop_mutex_repp);
  ECA_LOG_MSG(ECA_LOGGER::system_objects, "Signaling stop");
  running_rep = false;
  pthread_cond_broadcast(&impl_repp->ecasound_stop_cond_repp);
  pthread_mutex_unlock(&impl_repp->ecasound_stop_mutex_repp);
}

/**
 * Sends an exit signal indicating that engine
 * driver has exited.
 *
 * context: E-level-1/4 
 * 
 * @see wait_for_exit()
 */
void ECA_ENGINE::signal_exit(void)
{
  pthread_mutex_lock(&impl_repp->ecasound_exit_mutex_repp);
  ECA_LOG_MSG(ECA_LOGGER::system_objects, "Signaling exit");
  pthread_cond_broadcast(&impl_repp->ecasound_exit_cond_repp);
  pthread_mutex_unlock(&impl_repp->ecasound_exit_mutex_repp);
}

/**
 * Processing is start If and only if processing 
 * previously stopped with conditional_stop().
 *
 * context: E-level-3
 */
void ECA_ENGINE::conditional_start(void)
{
  if (was_running_rep == true) {
    // don't call request_start(), as it would signal that we are 
    // starting from completely halted state
    if (is_prepared() != true) prepare_operation();
    start_operation();
  }
}

/**
 * Processing is stopped.
 *
 * context: E-level-3
 *
 * @see conditional_stop()
 * @see request_stop()
 */
void ECA_ENGINE::conditional_stop(void)
{
  if (status() == ECA_ENGINE::engine_status_running) {
    ECA_LOG_MSG(ECA_LOGGER::system_objects,"conditional stop");
    was_running_rep = true;
    // don't call request_stop(), as it would signal that we are 
    // stopping completely (JACK transport stop will be sent  to all)
    if (is_prepared() == true) stop_operation(false);
  }
  else was_running_rep = false;
}

void ECA_ENGINE::start_servers(void)
{
  if (csetup_repp->double_buffering() == true) {
    csetup_repp->pserver_repp->start();
    ECA_LOG_MSG(ECA_LOGGER::user_objects, "prefilling i/o buffers.");
    csetup_repp->pserver_repp->wait_for_full();
    ECA_LOG_MSG(ECA_LOGGER::user_objects, "i/o buffers prefilled.");
  }
  
  if (use_midi_rep == true) {
    csetup_repp->midi_server_repp->start();
  }
}

void ECA_ENGINE::stop_servers(void)
{ 
  if (csetup_repp->double_buffering() == true) {
    csetup_repp->pserver_repp->stop();
    csetup_repp->pserver_repp->wait_for_stop();
  }

  if (use_midi_rep == true) {
    csetup_repp->midi_server_repp->stop();
  }
}

/**
 * Goes through all input/outputs in 'vec', checks whether they 
 * implement the AUDIO_IO_BARRIER interface, and if yes, 
 * issues either 'start_io()' or 'stop_io()' on them.
 */
static void priv_toggle_forked_objects(bool start, std::vector<AUDIO_IO*>* vec)
{
  unsigned int n;
  for(n = 0; n < vec->size(); n++) {
    AUDIO_IO_BARRIER *barrier
      = dynamic_cast<AUDIO_IO_BARRIER*>((*vec)[n]);

    if (barrier) {
      if (start)
        barrier->start_io();
      else
        barrier->stop_io();
    }
  }
}

void ECA_ENGINE::start_forked_objects(void)
{
  priv_toggle_forked_objects(true, inputs_repp);
  priv_toggle_forked_objects(true, outputs_repp);
}

void ECA_ENGINE::stop_forked_objects(void)
{
  priv_toggle_forked_objects(false, inputs_repp);
  priv_toggle_forked_objects(false, outputs_repp);
}

void ECA_ENGINE::state_change_to_finished(void)
{
  if (finished_rep != true) {
    ECA_LOG_MSG_NOPREFIX(ECA_LOGGER::info, "");
    ECA_LOG_MSG(ECA_LOGGER::subsystems, "Engine - Processing finished");
  }
  finished_rep = true;
}

void ECA_ENGINE::prepare_realtime_objects(void)
{
  /* 1. prepare objects */
  for (unsigned int n = 0; n < realtime_objects_rep.size(); n++) {
    realtime_objects_rep[n]->prepare();
  }

  /* 2. prefill rt output objects with silence */
  mixslot_repp->make_silent();
  for (unsigned int n = 0; n < realtime_outputs_rep.size(); n++) {
    if (realtime_outputs_rep[n]->prefill_space() > 0) {
      if (realtime_outputs_rep[n]->prefill_space() < 
          prefill_threshold_rep * buffersize()) {

        ECA_LOG_MSG(ECA_LOGGER::user_objects,
                    "audio output '" + 
                    realtime_outputs_rep[n]->name() + 
                    "' only offers " +
                    kvu_numtostr(realtime_outputs_rep[n]->prefill_space()) +
                    " frames of prefill space. Decreasing amount of prefill.");
        
        prefill_threshold_rep = realtime_outputs_rep[n]->prefill_space() / buffersize();
      }
      
      ECA_LOG_MSG(ECA_LOGGER::user_objects,
                  "prefilling rt-outputs with " +
                  kvu_numtostr(prefill_threshold_rep) +
                  " blocks.");

      for (int m = 0; m < prefill_threshold_rep; m++) {
        realtime_outputs_rep[n]->write_buffer(mixslot_repp);
      }
    }
  }
}

void ECA_ENGINE::start_realtime_objects(void)
{
  /* 1. start all realtime devices */
  for (unsigned int n = 0; n < realtime_objects_rep.size(); n++)
    realtime_objects_rep[n]->start();
}

/**
 * Performs a close-open cycle for all realtime 
 * devices.
 */
void ECA_ENGINE::reset_realtime_devices(void)
{
  for (size_t n = 0; n < realtime_objects_rep.size(); n++) {
    if (realtime_objects_rep[n]->is_open() == true) {
      ECA_LOG_MSG(ECA_LOGGER::user_objects, 
                  "Reseting rt-object " + 
                  realtime_objects_rep[n]->label());
      realtime_objects_rep[n]->close();
    }
  }
  for (size_t n = 0; n < realtime_objects_rep.size(); n++) {
    realtime_objects_rep[n]->open();
  }  
}

/**********************************************************************
 * Engine implementation - Private functions for observing and
 *                         modifying position
 **********************************************************************/

SAMPLE_SPECS::sample_pos_t ECA_ENGINE::current_position_in_samples(void) const
{
  return csetup_repp->position_in_samples();
}

double ECA_ENGINE::current_position_in_seconds_exact(void) const
{
  return csetup_repp->position_in_seconds_exact();
}

// FIXME: remove
#if 0
double ECA_ENGINE::current_position_chain(void) const
{
  AUDIO_IO* ptr = (*inputs_repp)[(*chains_repp)[csetup_repp->active_chain_index_rep]->connected_input()]; 
    return ptr->position_in_seconds_exact();
  return 0.0f;
}
#endif

/**
 * Seeks to position 'seconds'. Affects all input and 
 * outputs objects, and the chainsetup object position.
 *
 * context: E-level-2
 */
void ECA_ENGINE::set_position(double seconds)
{
  conditional_stop();
  csetup_repp->seek_position_in_seconds(seconds);
  // reinit chains to flush out any stale audio data related
  // to the old position
  reinit_chains(true);
  // FIXME: calling init_engine_state() may lead to races
  init_engine_state();
  conditional_start();
}

/**
 * Seeks to position 'samples'. Affects all input and 
 * outputs objects, and the chainsetup object position.
 *
 * context: E-level-2
 */
void ECA_ENGINE::set_position_samples(SAMPLE_SPECS::sample_pos_t samples)
{
  conditional_stop();
  csetup_repp->seek_position_in_samples(samples);
  // reinit chains to flush out any stale audio data related
  // to the old position
  reinit_chains(true);
  // FIXME: calling init_engine_state() may lead to races
  init_engine_state();
  conditional_start();
}

/**
 * Seeks to position 'samples' without stopping the engine.
 * Affects all input and outputs objects, and the chainsetup 
 * object position.
 *
 * context: E-level-2
 */
void ECA_ENGINE::set_position_samples_live(SAMPLE_SPECS::sample_pos_t samples)
{
  csetup_repp->seek_position_in_samples(samples);
  // FIXME: calling init_engine_state() may lead to races
  init_engine_state();
}

/**
 * Seeks to position 'current+seconds'. Affects all input and 
 * outputs objects, and the chainsetup object position.
 *
 * context: E-level-2
 */
void ECA_ENGINE::change_position(double seconds)
{
  double curpos = csetup_repp->position_in_seconds_exact();
  conditional_stop();
  csetup_repp->seek_position_in_seconds(curpos + seconds);
  conditional_start();
}

/**
 * Calculates how much data we need to process and sets the
 * buffersize accordingly for all non-real-time inputs.
 *
 * context: J-level-1
 */
void ECA_ENGINE::prehandle_control_position(void)
{
  csetup_repp->change_position_in_samples(buffersize());
  if (csetup_repp->max_length_set() == true &&
      csetup_repp->is_over_max_length() == true) {
    int extra_tail = csetup_repp->position_in_samples() -
                     csetup_repp->max_length_in_samples();
    int buffer_remain = buffersize() - extra_tail;
    if (buffer_remain < 0)
      buffer_remain = 0;
    else if (buffer_remain > buffersize())
      buffer_remain = buffersize();
    for(unsigned int adev_sizet = 0; adev_sizet < non_realtime_inputs_rep.size(); adev_sizet++) {
      non_realtime_inputs_rep[adev_sizet]->set_buffersize(buffer_remain);
    }
  }
}

/**
 * If we've processed all the data that was requested, stop or rewind. 
 * Also resets buffersize to its default value. If finished
 * state is reached, engine status is set to 
 * 'ECA_ENGINE::engine_status_finished'.
 */
void ECA_ENGINE::posthandle_control_position(void)
{
  if (csetup_repp->max_length_set() == true &&
      csetup_repp->is_over_max_length() == true) {
    if (csetup_repp->looping_enabled() == true) {
      ECA_LOG_MSG(ECA_LOGGER::system_objects,"loop point reached");
      inputs_not_finished_rep = 1;
      csetup_repp->seek_position_in_samples(0);
      for(unsigned int adev_sizet = 0; adev_sizet < non_realtime_inputs_rep.size(); adev_sizet++) {
        non_realtime_inputs_rep[adev_sizet]->set_buffersize(buffersize());
      }
    }
    else {
      ECA_LOG_MSG(ECA_LOGGER::system_objects,"posthandle_c_p over_max - stop");
      if (status() == ECA_ENGINE::engine_status_running ||
          status() == ECA_ENGINE::engine_status_finished) {
        command(ECA_ENGINE::ep_stop_with_drain, 0.0f);
      }
      state_change_to_finished();
    }
  }
}

/**********************************************************************
 * Engine implementation - Private functions for setup and cleanup
 **********************************************************************/

/**
 * Called only from class constructor.
 */
void ECA_ENGINE::init_variables(void)
{
  use_midi_rep = false;
  batchmode_enabled_rep = false;
  driver_local = false;

  pthread_cond_init(&impl_repp->ecasound_stop_cond_repp, NULL);
  pthread_mutex_init(&impl_repp->ecasound_stop_mutex_repp, NULL);
  pthread_cond_init(&impl_repp->ecasound_exit_cond_repp, NULL);
  pthread_mutex_init(&impl_repp->ecasound_exit_mutex_repp, NULL);
}

/**
 * Called only from class constructor.
 */
void ECA_ENGINE::init_connection_to_chainsetup(void)
{
  inputs_repp = &(csetup_repp->inputs);
  outputs_repp = &(csetup_repp->outputs);
  chains_repp = &(csetup_repp->chains);

  init_engine_state();
  init_driver();
  init_prefill();
  init_servers();
  init_chains();
  create_cache_object_lists();
  update_cache_chain_connections();
  update_cache_latency_values();
}

/**
 * Initializes the engine driver object.
 */
void ECA_ENGINE::init_driver(void)
{
  if (csetup_repp->engine_driver_repp != 0) {
    driver_repp = csetup_repp->engine_driver_repp;
    driver_local = false;
  }
  else {
    driver_repp = new ECA_ENGINE_DEFAULT_DRIVER();
    driver_local = true;
  }
}

/** 
 * Initializes prefill variables.
 */
void ECA_ENGINE::init_prefill(void)
{
  int channels = (max_channels() > 0 ? max_channels() : 1);
  prefill_threshold_rep = 0;

  if (csetup_repp->max_buffers() == true) 
    prefill_threshold_rep = ECA_ENGINE::prefill_threshold_constant / buffersize() / channels;

  if (prefill_threshold_rep < ECA_ENGINE::prefill_blocks_constant)
    prefill_threshold_rep = ECA_ENGINE::prefill_blocks_constant;
  
  ECA_LOG_MSG(ECA_LOGGER::system_objects,
              "Prefill loops: " +
              kvu_numtostr(prefill_threshold_rep) +
              " (blocksize " + 
              kvu_numtostr(buffersize()) + ").");
}

/**
 * 
 * Called only from init_connection_to_chainsetup().
 */
void ECA_ENGINE::init_servers(void)
{
  if (csetup_repp->midi_devices.size() > 0) {
    use_midi_rep = true;
    ECA_LOG_MSG(ECA_LOGGER::info, "Initializing MIDI-server.");
    csetup_repp->midi_server_repp->init();
  }
}

/**
 * Called only from init_connection_to_chainsetup().
 */
void ECA_ENGINE::init_chains(void)
{
  mixslot_repp->number_of_channels(max_channels());
  mixslot_repp->event_tag_set(SAMPLE_BUFFER::tag_mixed_content);
  mixslot_repp->event_tag_set(SAMPLE_BUFFER::tag_var_length, false);

  cslots_rep.resize(chains_repp->size());
  for(size_t n = 0; n < cslots_rep.size(); n++) {
    cslots_rep[n] = new SAMPLE_BUFFER(buffersize(), max_channels());
    cslots_rep[n]->event_tag_set(SAMPLE_BUFFER::tag_var_length, false);
  }

  for (unsigned int c = 0; c != chains_repp->size(); c++) {
    int inch = (*inputs_repp)[(*chains_repp)[c]->connected_input()]->channels();
    int outch = (*outputs_repp)[(*chains_repp)[c]->connected_output()]->channels();
    (*chains_repp)[c]->init(cslots_rep[c], inch, outch);
  }
}

/**
 * Frees all reserved resources.
 *
 * @post status() == ECA_ENGINE::engine_status_notready
 * @post is_valid() != true
 */
void ECA_ENGINE::cleanup(void)
{
  if (csetup_repp != 0) {
    csetup_repp->toggle_locked_state(true);
    vector<CHAIN*>::iterator q = csetup_repp->chains.begin();
    while(q != csetup_repp->chains.end()) {
      if (*q != 0) {
        (*q)->disconnect_buffer();
      }
      ++q;
    }
    csetup_repp->toggle_locked_state(false);
  }

  csetup_repp = 0;

  // --
  DBC_ENSURE(status() == ECA_ENGINE::engine_status_notready);
  DBC_ENSURE(is_valid() != true);
  // --
}

/**
 * Updates 'input_chain_count_rep' and
 * 'output_chain_count_rep'.
 */
void ECA_ENGINE::update_cache_chain_connections(void)
{
  input_chain_count_rep.resize(inputs_repp->size());
  for(unsigned int n = 0; n < inputs_repp->size(); n++) {
    input_chain_count_rep[n] =
      csetup_repp->number_of_attached_chains_to_input(csetup_repp->inputs[n]);
  }
  
  output_chain_count_rep.resize(outputs_repp->size());
  for(unsigned int n = 0; n < outputs_repp->size(); n++) {
    output_chain_count_rep[n] =
      csetup_repp->number_of_attached_chains_to_output(csetup_repp->outputs[n]);
  }
}

/**
 * Update system latency values for multitrack
 * recording.
 */
void ECA_ENGINE::update_cache_latency_values(void)
{
  if (csetup_repp->multitrack_mode() == true &&
      csetup_repp->multitrack_mode_offset() == -1) {
    long int in_latency = -1;
    for(unsigned int n = 0; n < realtime_inputs_rep.size(); n++) {
      if (in_latency == -1) {
        in_latency = realtime_inputs_rep[n]->latency();
      }
      else {
        if (in_latency != realtime_inputs_rep[n]->latency()) {
          ECA_LOG_MSG(ECA_LOGGER::info, 
                      "WARNING: Latency mismatch between input objects!");
        }
      }

      ECA_LOG_MSG(ECA_LOGGER::user_objects,
                  "Input latency for '" +
                  realtime_inputs_rep[n]->name() + 
                  "' is " + kvu_numtostr(in_latency) + ".");

    }
    
    long int out_latency = -1;
    for(unsigned int n = 0; n < realtime_outputs_rep.size(); n++) {
      if (out_latency == -1) {
        if (realtime_outputs_rep[n]->prefill_space() > 0) {
          long int max_prefill = prefill_threshold_rep * buffersize();
          if (max_prefill > realtime_outputs_rep[n]->prefill_space()) {
            max_prefill = realtime_outputs_rep[n]->prefill_space();
          }
          out_latency = max_prefill + realtime_outputs_rep[n]->latency();
        }
        else
          out_latency = realtime_outputs_rep[n]->latency();
      }
      else {
        if ((realtime_outputs_rep[n]->prefill_space() > 0 && 
             out_latency != (prefill_threshold_rep * buffersize()) + realtime_outputs_rep[n]->latency()) || 
            (realtime_outputs_rep[n]->prefill_space() == 0 &&
             out_latency != realtime_outputs_rep[n]->latency())) {
          ECA_LOG_MSG(ECA_LOGGER::info, 
                      "WARNING: Latency mismatch between output objects!");
        }
      }

      ECA_LOG_MSG(ECA_LOGGER::user_objects,
                  "Output latency for '" +
                  realtime_outputs_rep[n]->name() + 
                  "' is " + kvu_numtostr(out_latency) + ".");
    }

    recording_offset_rep = (out_latency > in_latency ? 
                            out_latency : in_latency);
    
    if (recording_offset_rep % buffersize()) {
      ECA_LOG_MSG(ECA_LOGGER::info, 
                  "WARNING: Recording offset not divisible with chainsetup buffersize.");
    }
    
    ECA_LOG_MSG(ECA_LOGGER::user_objects,
                "recording offset is " +
                kvu_numtostr(recording_offset_rep) +
                " samples.");
  }
  else if (csetup_repp->multitrack_mode() == true) {
    /* multitrack_mode_offset() explicitly given (not -1) */
    recording_offset_rep = csetup_repp->multitrack_mode_offset();
  }
  else {
    recording_offset_rep = 0;
  }
}

/**
 * Assigns input and output objects in lists of realtime
 * and nonrealtime objects.
 */
void ECA_ENGINE::create_cache_object_lists(void)
{
  for(unsigned int n = 0; n < inputs_repp->size(); n++) {
    if (AUDIO_IO_DEVICE::is_realtime_object((*inputs_repp)[n]) == true) {
      realtime_inputs_rep.push_back(static_cast<AUDIO_IO_DEVICE*>((*inputs_repp)[n]));
      realtime_objects_rep.push_back(static_cast<AUDIO_IO_DEVICE*>((*inputs_repp)[n]));
    }
    else {
      non_realtime_inputs_rep.push_back((*inputs_repp)[n]);
      non_realtime_objects_rep.push_back((*inputs_repp)[n]);
    }
  }
  DBC_CHECK(static_cast<int>(realtime_inputs_rep.size()) == csetup_repp->number_of_realtime_inputs());

  for(unsigned int n = 0; n < outputs_repp->size(); n++) {
    if (AUDIO_IO_DEVICE::is_realtime_object((*outputs_repp)[n]) == true) {
      realtime_outputs_rep.push_back(static_cast<AUDIO_IO_DEVICE*>((*outputs_repp)[n]));
      realtime_objects_rep.push_back(static_cast<AUDIO_IO_DEVICE*>((*outputs_repp)[n]));
    }
    else {
      non_realtime_outputs_rep.push_back((*outputs_repp)[n]);
      non_realtime_objects_rep.push_back((*outputs_repp)[n]);
    }
  }
  DBC_CHECK(static_cast<int>(realtime_outputs_rep.size()) == csetup_repp->number_of_realtime_outputs());
}

/**
 * Called only from class constructor.
 */
void ECA_ENGINE::init_profiling(void)
{
  impl_repp->looptimer_low_rep = static_cast<double>(buffersize()) / csetup_repp->samples_per_second();
  impl_repp->looptimer_mid_rep = static_cast<double>(buffersize() * 2) / csetup_repp->samples_per_second();
  impl_repp->looptimer_high_rep = static_cast<double>(buffersize()) * prefill_threshold_rep / csetup_repp->samples_per_second();

  impl_repp->looptimer_rep.set_lower_bound_seconds(impl_repp->looptimer_low_rep);
  impl_repp->looptimer_rep.set_upper_bound_seconds(impl_repp->looptimer_high_rep);
  impl_repp->looptimer_range_rep.set_lower_bound_seconds(impl_repp->looptimer_mid_rep);
  impl_repp->looptimer_range_rep.set_upper_bound_seconds(impl_repp->looptimer_mid_rep);
}

/**
 * Prints  profiling information to stderr.
 */
void ECA_ENGINE::dump_profile_info(void)
{
  long int slower_than_rt = impl_repp->looptimer_rep.event_count() -
                            impl_repp->looptimer_rep.events_under_lower_bound() -
                            impl_repp->looptimer_rep.events_over_upper_bound();

  cerr << "*** profile begin ***" << endl;
  cerr << "Loops faster than realtime: "  << kvu_numtostr(impl_repp->looptimer_rep.events_under_lower_bound());
  cerr << " (<" << kvu_numtostr(impl_repp->looptimer_low_rep * 1000, 1) << " msec)" << endl;
  cerr << "Loops slower than realtime: "  << kvu_numtostr(slower_than_rt);
  cerr << " (>=" << kvu_numtostr(impl_repp->looptimer_low_rep * 1000, 1) << " msec)" << endl;
  cerr << "Loops slower than realtime: "  << kvu_numtostr(impl_repp->looptimer_range_rep.events_over_upper_bound());
  cerr << " (>" << kvu_numtostr(impl_repp->looptimer_mid_rep * 1000, 1) << " msec)" << endl;
  cerr << "Loops exceeding all buffering: " << kvu_numtostr(impl_repp->looptimer_rep.events_over_upper_bound());
  cerr << " (>" << kvu_numtostr(impl_repp->looptimer_high_rep * 1000, 1) << " msec)" << endl;
  cerr << "Total loops: " << kvu_numtostr(impl_repp->looptimer_rep.event_count()) << endl;
  cerr << "Fastest/slowest/average loop time: ";
  cerr << kvu_numtostr(impl_repp->looptimer_rep.min_duration_seconds() * 1000, 1);
  cerr << "/";
  cerr << kvu_numtostr(impl_repp->looptimer_rep.max_duration_seconds() * 1000, 1);
  cerr << "/";
  cerr << kvu_numtostr(impl_repp->looptimer_rep.average_duration_seconds() * 1000, 1);
  cerr << " msec." << endl;
  cerr << "*** profile end   ***" << endl;
}

/**********************************************************************
 * Engine implementation - Private functions for signal routing
 **********************************************************************/

/**
 * Reads audio data from input objects.
 *
 * context: J-level-1 (see 
 */
void ECA_ENGINE::inputs_to_chains(void)
{
  /**
   * - go through all inputs
   * - depending on connectivity, read either to a mixdown slot, or 
   *   directly to a per-chain slot
   */

  for(size_t inputnum = 0; inputnum < inputs_repp->size(); inputnum++) {

    if (input_chain_count_rep[inputnum] > 1) {
      /* case-1a: read buffer from input 'inputnum' to 'mixslot';
       *          later (1b) the data is copied to each per-chain slow
       *          to which input is connected to */

      mixslot_repp->length_in_samples(buffersize());

      if ((*inputs_repp)[inputnum]->finished() != true) {
        (*inputs_repp)[inputnum]->read_buffer(mixslot_repp);
        if ((*inputs_repp)[inputnum]->finished() != true) {
          inputs_not_finished_rep++;
        }
      }
      else {
        /* note: no more input data for this change (N:1 input-chain case) */
        mixslot_repp->make_empty();
      }
    }
    for (size_t c = 0; c != chains_repp->size(); c++) {
      if ((*chains_repp)[c]->connected_input() == static_cast<int>(inputnum)) {
        if (input_chain_count_rep[inputnum] == 1) {
          /* case-2: read buffer from input 'inputnum' to chain 'c' */
          cslots_rep[c]->length_in_samples(buffersize());

          if ((*inputs_repp)[inputnum]->finished() != true) {
            (*inputs_repp)[inputnum]->read_buffer(cslots_rep[c]);
            if ((*inputs_repp)[inputnum]->finished() != true) {
              inputs_not_finished_rep++;
            }
          }
          else {
            /* note: no more input data for this change (1:1 input-chain case) */
            cslots_rep[c]->make_empty();
          }

          /* note: input connected to only one chain, so no need to
             iterate through the other chains */
          break; 
        }
        else {
          /* case-1b: input connected to chain 'n', copy 'mixslot' to 
           *          the matching per-chain slot */
          cslots_rep[c]->copy_all_content(*mixslot_repp);
        }
      }
    }
  }
}

/**
 * context: J-level-1
 */
void ECA_ENGINE::process_chains(void)
{
  vector<CHAIN*>::const_iterator p = chains_repp->begin();
  while(p != chains_repp->end()) {
    (*p)->process();
    ++p;
  }
}

void mix_to_outputs_divide_helper(const SAMPLE_BUFFER *from, SAMPLE_BUFFER *to, int divide_by, bool first_time)
{
  if (first_time == true) {
    // this is the first output connected to this chain
    if (from->number_of_channels() < to->number_of_channels()) {
      to->make_silent();
    }
    to->copy_matching_channels(*from);
    to->divide_by(divide_by);
  }
  else {
    to->add_with_weight(*from, divide_by);
  }
}

void mix_to_outputs_sum_helper(const SAMPLE_BUFFER *from, SAMPLE_BUFFER *to, bool first_time)
{
  if (first_time == true) {
    // this is the first output connected to this chain
    if (from->number_of_channels() < to->number_of_channels()) {
      to->make_silent();
    }
    to->copy_matching_channels(*from);
  }
  else {
    to->add_matching_channels(*from);
  }
}

/**
 * context: J-level-1
 */
void ECA_ENGINE::mix_to_outputs(bool skip_realtime_target_outputs)
{
  for(size_t outputnum = 0; outputnum < outputs_repp->size(); outputnum++) {
    if (skip_realtime_target_outputs == true) {
      if (csetup_repp->is_realtime_target_output(outputnum) == true) {
        ECA_LOG_MSG(ECA_LOGGER::system_objects,
                    "Skipping rt-target output " +
                    (*outputs_repp)[outputnum]->label() + ".");
        continue;
      }
    }

    int count = 0;

    /* FIXME: number_of_channels() may end up allocating memory! */
    mixslot_repp->number_of_channels((*outputs_repp)[outputnum]->channels());
    
    for(size_t n = 0; n != chains_repp->size(); n++) {
      // --
      // if chain is already released, skip
      // --
      if ((*chains_repp)[n]->connected_output() == -1) {
        // --
        // skip, if chain is not connected
        // --
        continue;
      }

      if ((*chains_repp)[n]->connected_output() == static_cast<int>(outputnum)) {
        // --
        // output is connected to this chain
        // --
        if (output_chain_count_rep[outputnum] == 1) {
          // --
          // there's only one output connected to this chain,
          // so we don't need to mix anything
          // --
          (*outputs_repp)[outputnum]->write_buffer(cslots_rep[n]);
          if ((*outputs_repp)[outputnum]->finished() == true) 
            /* note: loop devices always connected both as inputs as
             *       outputs, so their finished status must not be
             *       counted as an error (like for other output types) */
            if (dynamic_cast<LOOP_DEVICE*>((*outputs_repp)[outputnum]) == 0)
              outputs_finished_rep++;
          break;
        }
        else {
          ++count;

          if (csetup_repp->mix_mode() == ECA_CHAINSETUP::cs_mmode_avg)
            mix_to_outputs_divide_helper(cslots_rep[n], mixslot_repp, output_chain_count_rep[outputnum], (count == 1));
          else
            mix_to_outputs_sum_helper(cslots_rep[n], mixslot_repp, (count == 1));

          mixslot_repp->event_tags_add(*cslots_rep[n]);

          if (count == output_chain_count_rep[outputnum]) {
            (*outputs_repp)[outputnum]->write_buffer(mixslot_repp);
            if ((*outputs_repp)[outputnum]->finished() == true) 
              /* note: loop devices always connected both as inputs as
               *       outputs, so their finished status must not be
               *       counted as an error (like for other output types) */
              if (dynamic_cast<LOOP_DEVICE*>((*outputs_repp)[outputnum]) == 0)
                outputs_finished_rep++;
          }
        }
      }
    }
  } 
}

/**********************************************************************
 * Engine implementation - Obsolete functions
 **********************************************************************/
