// Copyright 2007-2024 David Robillard <d@drobilla.net>
// SPDX-License-Identifier: ISC

#include "backend.h"

#include "comm.h"
#include "jack_impl.h"
#include "jalv_config.h"
#include "log.h"
#include "lv2_evbuf.h"
#include "process.h"
#include "process_setup.h"
#include "settings.h"
#include "string_utils.h"
#include "types.h"
#include "urids.h"

#include <lilv/lilv.h>
#include <lv2/atom/atom.h>
#include <lv2/atom/forge.h>
#include <lv2/urid/urid.h>
#include <zix/sem.h>

#include <jack/jack.h>
#include <jack/midiport.h>
#include <jack/transport.h>
#include <jack/types.h>

#if USE_JACK_METADATA
#  include <jack/metadata.h>
#endif

#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#ifdef __clang__
#  define REALTIME __attribute__((annotate("realtime")))
#else
#  define REALTIME
#endif

/// Maximum supported latency in frames (at most 2^24 so all integers work)
static const float max_latency = 16777216.0f;

/// Jack buffer size callback
static int
buffer_size_cb(jack_nframes_t nframes, void* data)
{
  JalvBackend* const  backend  = (JalvBackend*)data;
  JalvSettings* const settings = backend->settings;
  JalvProcess* const  proc     = backend->process;

  settings->block_length = nframes;
#if USE_JACK_PORT_TYPE_GET_BUFFER_SIZE
  settings->midi_buf_size =
    jack_port_type_get_buffer_size(backend->client, JACK_DEFAULT_MIDI_TYPE);
#endif
  if (proc->run_state == JALV_RUNNING) {
    jalv_process_activate(proc, backend->urids, proc->instance, settings);
  }
  return 0;
}

/// Jack shutdown callback
static void
shutdown_cb(void* data)
{
  JalvBackend* const backend = (JalvBackend*)data;
  zix_sem_post(backend->done);
}

static void
forge_position(LV2_Atom_Forge* const        forge,
               const JalvURIDs* const       urids,
               const jack_transport_state_t state,
               const jack_position_t        pos)
{
  LV2_Atom_Forge_Frame frame;
  lv2_atom_forge_object(forge, &frame, 0, urids->time_Position);
  lv2_atom_forge_key(forge, urids->time_frame);
  lv2_atom_forge_long(forge, pos.frame);
  lv2_atom_forge_key(forge, urids->time_speed);
  lv2_atom_forge_float(forge, (state == JackTransportRolling) ? 1.0 : 0.0);
  if ((pos.valid & JackPositionBBT)) {
    lv2_atom_forge_key(forge, urids->time_barBeat);
    lv2_atom_forge_float(forge, pos.beat - 1 + (pos.tick / pos.ticks_per_beat));
    lv2_atom_forge_key(forge, urids->time_bar);
    lv2_atom_forge_long(forge, pos.bar - 1);
    lv2_atom_forge_key(forge, urids->time_beatUnit);
    lv2_atom_forge_int(forge, pos.beat_type);
    lv2_atom_forge_key(forge, urids->time_beatsPerBar);
    lv2_atom_forge_float(forge, pos.beats_per_bar);
    lv2_atom_forge_key(forge, urids->time_beatsPerMinute);
    lv2_atom_forge_float(forge, pos.beats_per_minute);
  }
}

static int
process_silent(JalvProcess* const proc, const jack_nframes_t nframes)
{
  for (uint32_t p = 0U; p < proc->num_ports; ++p) {
    JalvProcessPort* const port  = &proc->ports[p];
    jack_port_t* const     jport = (jack_port_t*)proc->ports[p].sys_port;
    if (jport && port->flow == FLOW_OUTPUT) {
      void* const buf = jack_port_get_buffer(jport, nframes);
      if (port->type == TYPE_EVENT) {
        jack_midi_clear_buffer(buf);
      } else {
        memset(buf, '\0', nframes * sizeof(float));
      }
    }
  }

  return jalv_bypass(proc, nframes);
}

static bool
process_transport(JalvProcess* const           proc,
                  const jack_transport_state_t state,
                  const jack_position_t        pos,
                  const jack_nframes_t         nframes)
{
  // If transport state is not as expected, then something has changed
  const bool rolling = state == JackTransportRolling;
  const bool has_bbt = (pos.valid & JackPositionBBT);
  const bool xport_changed =
    (rolling != proc->rolling || pos.frame != proc->position ||
     (has_bbt && pos.beats_per_minute != proc->bpm));

  // Update transport state to expected values for next cycle
  proc->position = rolling ? pos.frame + nframes : pos.frame;
  proc->bpm      = has_bbt ? pos.beats_per_minute : proc->bpm;
  proc->rolling  = rolling;

  return xport_changed;
}

/// Jack process callback
static REALTIME int
process_cb(jack_nframes_t nframes, void* data)
{
  JalvBackend* const     backend     = (JalvBackend*)data;
  const JalvURIDs* const urids       = backend->urids;
  JalvProcess* const     proc        = backend->process;
  jack_client_t* const   client      = backend->client;
  uint64_t               pos_buf[64] = {0U};
  LV2_Atom* const        lv2_pos     = (LV2_Atom*)pos_buf;

  // If execution is paused, emit silence and return
  if (proc->run_state == JALV_PAUSED) {
    return process_silent(proc, nframes);
  }

  // Get transport state and position
  jack_position_t              pos   = {0U};
  const jack_transport_state_t state = jack_transport_query(client, &pos);

  // Check if transport is discontinuous from last time and update state
  const bool xport_changed = process_transport(proc, state, pos, nframes);
  if (xport_changed) {
    // Build an LV2 position object to report change to plugin
    lv2_atom_forge_set_buffer(&proc->forge, (uint8_t*)pos_buf, sizeof(pos_buf));
    forge_position(&proc->forge, urids, state, pos);
  }

  // Prepare port buffers
  for (uint32_t p = 0; p < proc->num_ports; ++p) {
    JalvProcessPort* const port = &proc->ports[p];
    if (port->sys_port && (port->type == TYPE_AUDIO || port->type == TYPE_CV)) {
      // Connect plugin port directly to Jack port buffer
      lilv_instance_connect_port(
        proc->instance, p, jack_port_get_buffer(port->sys_port, nframes));
    } else if (port->type == TYPE_EVENT && port->flow == FLOW_INPUT) {
      lv2_evbuf_reset(port->evbuf, true);
      LV2_Evbuf_Iterator iter = lv2_evbuf_begin(port->evbuf);

      if (port->is_primary && xport_changed) {
        // Write new transport position
        lv2_evbuf_write(
          &iter, 0, 0, lv2_pos->type, lv2_pos->size, LV2_ATOM_BODY(lv2_pos));
      }

      if (port->sys_port) {
        // Write Jack MIDI input
        void* buf = jack_port_get_buffer(port->sys_port, nframes);
        for (uint32_t i = 0; i < jack_midi_get_event_count(buf); ++i) {
          jack_midi_event_t ev;
          jack_midi_event_get(&ev, buf, i);
          lv2_evbuf_write(
            &iter, ev.time, 0, urids->midi_MidiEvent, ev.size, ev.buffer);
        }
      }
    } else if (port->type == TYPE_EVENT) {
      // Clear event output for plugin to write to
      lv2_evbuf_reset(port->evbuf, false);
    }
  }

  // Run plugin for this cycle
  const bool send_ui_updates = jalv_run(proc, nframes);

  // Deliver MIDI output and UI events
  for (uint32_t p = 0; p < proc->num_ports; ++p) {
    JalvProcessPort* const port = &proc->ports[p];
    if (port->flow == FLOW_OUTPUT && port->type == TYPE_CONTROL &&
        port->reports_latency) {
      // Get the latency in frames from the control output truncated to integer
      const float    value = proc->controls_buf[p];
      const uint32_t frames =
        (value >= 0.0f && value <= max_latency) ? (uint32_t)value : 0U;

      if (proc->plugin_latency != frames) {
        // Update the cached value and notify the UI if the latency changed
        proc->plugin_latency = frames;

        const JalvLatencyChange body   = {frames};
        const JalvMessageHeader header = {LATENCY_CHANGE, sizeof(body)};
        jalv_write_split_message(
          proc->plugin_to_ui, &header, sizeof(header), &body, sizeof(body));
      }
    } else if (port->flow == FLOW_OUTPUT && port->type == TYPE_EVENT) {
      void* buf = NULL;
      if (port->sys_port) {
        buf = jack_port_get_buffer(port->sys_port, nframes);
        jack_midi_clear_buffer(buf);
      }

      for (LV2_Evbuf_Iterator i = lv2_evbuf_begin(port->evbuf);
           lv2_evbuf_is_valid(i);
           i = lv2_evbuf_next(i)) {
        // Get event from LV2 buffer
        uint32_t frames    = 0;
        uint32_t subframes = 0;
        LV2_URID type      = 0;
        uint32_t size      = 0;
        void*    body      = NULL;
        lv2_evbuf_get(i, &frames, &subframes, &type, &size, &body);

        if (buf && type == urids->midi_MidiEvent) {
          // Write MIDI event to Jack output
          jack_midi_event_write(buf, frames, body, size);
        }

        if (proc->has_ui) {
          // Forward event to UI
          jalv_write_event(proc->plugin_to_ui, p, size, type, body);
        }
      }
    } else if (send_ui_updates && port->flow == FLOW_OUTPUT &&
               port->type == TYPE_CONTROL) {
      jalv_write_control(proc->plugin_to_ui, p, proc->controls_buf[p]);
    }
  }

  return 0;
}

/// Jack latency callback
static void
latency_cb(const jack_latency_callback_mode_t mode, void* const data)
{
  // Calculate latency assuming all ports depend on each other

  JalvBackend* const       backend = (JalvBackend*)data;
  const JalvProcess* const proc    = backend->process;
  const PortFlow           flow =
    ((mode == JackCaptureLatency) ? FLOW_INPUT : FLOW_OUTPUT);

  // First calculate the min/max latency of all feeding ports
  uint32_t             ports_found = 0;
  jack_latency_range_t range       = {UINT32_MAX, 0};
  for (uint32_t p = 0; p < proc->num_ports; ++p) {
    JalvProcessPort* const port = &proc->ports[p];
    if (port->sys_port && port->flow == flow) {
      jack_latency_range_t r;
      jack_port_get_latency_range(port->sys_port, mode, &r);
      if (r.min < range.min) {
        range.min = r.min;
      }
      if (r.max > range.max) {
        range.max = r.max;
      }
      ++ports_found;
    }
  }

  if (ports_found == 0) {
    range.min = 0;
  }

  // Add the plugin's own latency
  range.min += proc->plugin_latency;
  range.max += proc->plugin_latency;

  // Tell Jack about it
  for (uint32_t p = 0; p < proc->num_ports; ++p) {
    const JalvProcessPort* const port = &proc->ports[p];
    if (port->sys_port && port->flow == flow) {
      jack_port_set_latency_range(port->sys_port, mode, &range);
    }
  }
}

static jack_client_t*
create_client(const char* const name, const bool exact_name)
{
  char* const jack_name = jalv_strdup(name);

  // Truncate client name to suit JACK if necessary
  if (strlen(jack_name) >= (unsigned)jack_client_name_size() - 1) {
    jack_name[jack_client_name_size() - 1] = '\0';
  }

  // Connect to JACK
  jack_client_t* const client = jack_client_open(
    jack_name, (exact_name ? JackUseExactName : JackNullOption), NULL);

  free(jack_name);

  return client;
}

JalvBackend*
jalv_backend_allocate(void)
{
  return (JalvBackend*)calloc(1, sizeof(JalvBackend));
}

void
jalv_backend_free(JalvBackend* const backend)
{
  free(backend);
}

int
jalv_backend_open(JalvBackend* const     backend,
                  const JalvURIDs* const urids,
                  JalvSettings* const    settings,
                  JalvProcess* const     process,
                  ZixSem* const          done,
                  const char* const      name,
                  const bool             exact_name)
{
  jack_client_t* const client =
    backend->client ? backend->client : create_client(name, exact_name);

  if (!client) {
    return 1;
  }

  jalv_log(JALV_LOG_INFO, "JACK name:    %s\n", jack_get_client_name(client));

  // Set audio engine properties
  settings->sample_rate   = (float)jack_get_sample_rate(client);
  settings->block_length  = jack_get_buffer_size(client);
  settings->midi_buf_size = 4096;
#if USE_JACK_PORT_TYPE_GET_BUFFER_SIZE
  settings->midi_buf_size =
    jack_port_type_get_buffer_size(client, JACK_DEFAULT_MIDI_TYPE);
#endif

  // Set JACK callbacks
  void* const arg = (void*)backend;
  jack_set_process_callback(client, &process_cb, arg);
  jack_set_buffer_size_callback(client, &buffer_size_cb, arg);
  jack_on_shutdown(client, &shutdown_cb, arg);
  jack_set_latency_callback(client, &latency_cb, arg);

  backend->urids              = urids;
  backend->settings           = settings;
  backend->process            = process;
  backend->done               = done;
  backend->client             = client;
  backend->is_internal_client = false;
  return 0;
}

void
jalv_backend_close(JalvBackend* const backend)
{
  if (backend && backend->client && !backend->is_internal_client) {
    jack_client_close(backend->client);
  }
}

void
jalv_backend_activate(JalvBackend* const backend)
{
  jack_activate(backend->client);
}

void
jalv_backend_deactivate(JalvBackend* const backend)
{
  if (!backend->is_internal_client && backend->client) {
    jack_deactivate(backend->client);
  }
}

void
jalv_backend_activate_port(JalvBackend* const backend,
                           JalvProcess* const proc,
                           const uint32_t     port_index)
{
  jack_client_t* const   client = backend->client;
  JalvProcessPort* const port   = &proc->ports[port_index];

  // Connect unsupported ports to NULL (known to be optional by this point)
  if (port->flow == FLOW_UNKNOWN || port->type == TYPE_UNKNOWN) {
    lilv_instance_connect_port(proc->instance, port_index, NULL);
    return;
  }

  // Build Jack flags for port
  enum JackPortFlags jack_flags =
    (port->flow == FLOW_INPUT) ? JackPortIsInput : JackPortIsOutput;

  // Connect the port based on its type
  switch (port->type) {
  case TYPE_UNKNOWN:
    break;
  case TYPE_CONTROL:
    lilv_instance_connect_port(
      proc->instance, port_index, &proc->controls_buf[port_index]);
    break;
  case TYPE_AUDIO:
    port->sys_port = jack_port_register(
      client, port->symbol, JACK_DEFAULT_AUDIO_TYPE, jack_flags, 0);
    break;
#if USE_JACK_METADATA
  case TYPE_CV:
    port->sys_port = jack_port_register(
      client, port->symbol, JACK_DEFAULT_AUDIO_TYPE, jack_flags, 0);
    if (port->sys_port) {
      jack_set_property(client,
                        jack_port_uuid(port->sys_port),
                        "http://jackaudio.org/metadata/signal-type",
                        "CV",
                        "text/plain");
    }
    break;
#endif
  case TYPE_EVENT:
    if (port->supports_midi) {
      port->sys_port = jack_port_register(
        client, port->symbol, JACK_DEFAULT_MIDI_TYPE, jack_flags, 0);
    }
    break;
  }

#if USE_JACK_METADATA
  if (port->sys_port) {
    // Set port order to index
    char index_str[16];
    snprintf(index_str, sizeof(index_str), "%u", port_index);
    jack_set_property(client,
                      jack_port_uuid(port->sys_port),
                      "http://jackaudio.org/metadata/order",
                      index_str,
                      "http://www.w3.org/2001/XMLSchema#integer");

    // Set port pretty name to label
    if (port->label) {
      jack_set_property(client,
                        jack_port_uuid(port->sys_port),
                        JACK_METADATA_PRETTY_NAME,
                        port->label,
                        "text/plain");
    }
  }
#endif
}

void
jalv_backend_recompute_latencies(JalvBackend* const backend)
{
  jack_recompute_total_latencies(backend->client);
}