/*
  Copyright 2007-2022 David Robillard <d@drobilla.net>

  Permission to use, copy, modify, and/or distribute this software for any
  purpose with or without fee is hereby granted, provided that the above
  copyright notice and this permission notice appear in all copies.

  THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

#include "jalv_config.h"
#include "jalv_internal.h"
#include "lv2_evbuf.h"

#include "lilv/lilv.h"
#include "lv2/atom/atom.h"
#include "lv2/atom/forge.h"
#include "sratom/sratom.h"
#include "zix/ring.h"
#include "zix/sem.h"

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

#ifdef HAVE_JACK_METADATA
#  include <jack/metadata.h>
#endif

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

struct JalvBackend {
  jack_client_t* client;             ///< Jack client
  bool           is_internal_client; ///< Running inside jackd
};

/// Internal Jack client initialization entry point
int
jack_initialize(jack_client_t* client, const char* load_init);

/// Internal Jack client finalization entry point
void
jack_finish(void* arg);

/// Jack buffer size callback
static int
jack_buffer_size_cb(jack_nframes_t nframes, void* data)
{
  Jalv* const jalv   = (Jalv*)data;
  jalv->block_length = nframes;
  jalv->buf_size_set = true;
#ifdef HAVE_JACK_PORT_TYPE_GET_BUFFER_SIZE
  jalv->midi_buf_size = jack_port_type_get_buffer_size(jalv->backend->client,
                                                       JACK_DEFAULT_MIDI_TYPE);
#endif
  jalv_allocate_port_buffers(jalv);
  return 0;
}

/// Jack shutdown callback
static void
jack_shutdown_cb(void* data)
{
  Jalv* const jalv = (Jalv*)data;
  jalv_close_ui(jalv);
  zix_sem_post(&jalv->done);
}

/// Jack process callback
static REALTIME int
jack_process_cb(jack_nframes_t nframes, void* data)
{
  Jalv* const    jalv   = (Jalv*)data;
  jack_client_t* client = jalv->backend->client;

  // Get Jack transport position
  jack_position_t pos;
  const bool      rolling =
    (jack_transport_query(client, &pos) == JackTransportRolling);

  // If transport state is not as expected, then something has changed
  const bool xport_changed =
    (rolling != jalv->rolling || pos.frame != jalv->position ||
     pos.beats_per_minute != jalv->bpm);

  uint8_t   pos_buf[256];
  LV2_Atom* lv2_pos = (LV2_Atom*)pos_buf;
  if (xport_changed) {
    // Build an LV2 position object to report change to plugin
    lv2_atom_forge_set_buffer(&jalv->forge, pos_buf, sizeof(pos_buf));
    LV2_Atom_Forge*      forge = &jalv->forge;
    LV2_Atom_Forge_Frame frame;
    lv2_atom_forge_object(forge, &frame, 0, jalv->urids.time_Position);
    lv2_atom_forge_key(forge, jalv->urids.time_frame);
    lv2_atom_forge_long(forge, pos.frame);
    lv2_atom_forge_key(forge, jalv->urids.time_speed);
    lv2_atom_forge_float(forge, rolling ? 1.0 : 0.0);
    if (pos.valid & JackPositionBBT) {
      lv2_atom_forge_key(forge, jalv->urids.time_barBeat);
      lv2_atom_forge_float(forge,
                           pos.beat - 1 + (pos.tick / pos.ticks_per_beat));
      lv2_atom_forge_key(forge, jalv->urids.time_bar);
      lv2_atom_forge_long(forge, pos.bar - 1);
      lv2_atom_forge_key(forge, jalv->urids.time_beatUnit);
      lv2_atom_forge_int(forge, pos.beat_type);
      lv2_atom_forge_key(forge, jalv->urids.time_beatsPerBar);
      lv2_atom_forge_float(forge, pos.beats_per_bar);
      lv2_atom_forge_key(forge, jalv->urids.time_beatsPerMinute);
      lv2_atom_forge_float(forge, pos.beats_per_minute);
    }

    if (jalv->opts.dump) {
      char* str = sratom_to_turtle(jalv->sratom,
                                   &jalv->unmap,
                                   "time:",
                                   NULL,
                                   NULL,
                                   lv2_pos->type,
                                   lv2_pos->size,
                                   LV2_ATOM_BODY(lv2_pos));
      jalv_ansi_start(stdout, 36);
      printf("\n## Position ##\n%s\n", str);
      jalv_ansi_reset(stdout);
      free(str);
    }
  }

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

  switch (jalv->play_state) {
  case JALV_PAUSE_REQUESTED:
    jalv->play_state = JALV_PAUSED;
    zix_sem_post(&jalv->paused);
    break;
  case JALV_PAUSED:
    for (uint32_t p = 0; p < jalv->num_ports; ++p) {
      jack_port_t* jport = jalv->ports[p].sys_port;
      if (jport && jalv->ports[p].flow == FLOW_OUTPUT) {
        void* buf = jack_port_get_buffer(jport, nframes);
        if (jalv->ports[p].type == TYPE_EVENT) {
          jack_midi_clear_buffer(buf);
        } else {
          memset(buf, '\0', nframes * sizeof(float));
        }
      }
    }
    return 0;
  default:
    break;
  }

  // Prepare port buffers
  for (uint32_t p = 0; p < jalv->num_ports; ++p) {
    struct Port* port = &jalv->ports[p];
    if (port->type == TYPE_AUDIO && port->sys_port) {
      // Connect plugin port directly to Jack port buffer
      lilv_instance_connect_port(
        jalv->instance, p, jack_port_get_buffer(port->sys_port, nframes));
#ifdef HAVE_JACK_METADATA
    } else if (port->type == TYPE_CV && port->sys_port) {
      // Connect plugin port directly to Jack port buffer
      lilv_instance_connect_port(
        jalv->instance, p, jack_port_get_buffer(port->sys_port, nframes));
#endif
    } else if (port->type == TYPE_EVENT && port->flow == FLOW_INPUT) {
      lv2_evbuf_reset(port->evbuf, true);

      // Write transport change event if applicable
      LV2_Evbuf_Iterator iter = lv2_evbuf_begin(port->evbuf);
      if (xport_changed) {
        lv2_evbuf_write(&iter,
                        0,
                        0,
                        lv2_pos->type,
                        lv2_pos->size,
                        (const uint8_t*)LV2_ATOM_BODY(lv2_pos));
      }

      if (jalv->request_update) {
        // Plugin state has changed, request an update
        const LV2_Atom_Object get = {
          {sizeof(LV2_Atom_Object_Body), jalv->urids.atom_Object},
          {0, jalv->urids.patch_Get}};
        lv2_evbuf_write(&iter,
                        0,
                        0,
                        get.atom.type,
                        get.atom.size,
                        (const uint8_t*)LV2_ATOM_BODY_CONST(&get));
      }

      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, jalv->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);
    }
  }
  jalv->request_update = false;

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

  // Deliver MIDI output and UI events
  for (uint32_t p = 0; p < jalv->num_ports; ++p) {
    struct Port* const port = &jalv->ports[p];
    if (port->flow == FLOW_OUTPUT && port->type == TYPE_CONTROL &&
        lilv_port_has_property(
          jalv->plugin, port->lilv_port, jalv->nodes.lv2_reportsLatency)) {
      if (jalv->plugin_latency != port->control) {
        jalv->plugin_latency = port->control;
        jack_recompute_total_latencies(client);
      }
    } 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;
        uint32_t type      = 0;
        uint32_t size      = 0;
        uint8_t* body      = NULL;
        lv2_evbuf_get(i, &frames, &subframes, &type, &size, &body);

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

        if (jalv->has_ui) {
          // Forward event to UI
          jalv_send_to_ui(jalv, p, type, size, body);
        }
      }
    } else if (send_ui_updates && port->flow == FLOW_OUTPUT &&
               port->type == TYPE_CONTROL) {
      char           buf[sizeof(ControlChange) + sizeof(float)];
      ControlChange* ev = (ControlChange*)buf;
      ev->index         = p;
      ev->protocol      = 0;
      ev->size          = sizeof(float);
      *(float*)(ev + 1) = port->control;
      if (zix_ring_write(jalv->plugin_events, buf, sizeof(buf)) < sizeof(buf)) {
        fprintf(stderr, "Plugin => UI buffer overflow!\n");
      }
    }
  }

  return 0;
}

/// Calculate latency assuming all ports depend on each other
static void
jack_latency_cb(jack_latency_callback_mode_t mode, void* data)
{
  Jalv* const         jalv = (Jalv*)data;
  const enum 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 < jalv->num_ports; ++p) {
    struct Port* port = &jalv->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 += jalv->plugin_latency;
  range.max += jalv->plugin_latency;

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

static jack_client_t*
jack_create_client(Jalv* jalv)
{
  jack_client_t* client = NULL;

  // Determine the name of the JACK client
  char* jack_name = NULL;
  if (jalv->opts.name) {
    // Name given on command line
    jack_name = jalv_strdup(jalv->opts.name);
  } else {
    // Use plugin name
    LilvNode* name = lilv_plugin_get_name(jalv->plugin);
    jack_name      = jalv_strdup(lilv_node_as_string(name));
    lilv_node_free(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
  if (!client) {
    client = jack_client_open(
      jack_name,
      (jalv->opts.name_exact ? JackUseExactName : JackNullOption),
      NULL);
  }

  free(jack_name);

  return client;
}

JalvBackend*
jalv_backend_init(Jalv* jalv)
{
  jack_client_t* const client =
    jalv->backend ? jalv->backend->client : jack_create_client(jalv);

  if (!client) {
    return NULL;
  }

  printf("JACK Name:    %s\n", jack_get_client_name(client));

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

  // Set JACK callbacks
  void* const arg = (void*)jalv;
  jack_set_process_callback(client, &jack_process_cb, arg);
  jack_set_buffer_size_callback(client, &jack_buffer_size_cb, arg);
  jack_on_shutdown(client, &jack_shutdown_cb, arg);
  jack_set_latency_callback(client, &jack_latency_cb, arg);

  if (jalv->backend) {
    /* Internal JACK client, jalv->backend->is_internal_client was already set
       in jack_initialize() when allocating the backend. */
    return jalv->backend;
  }

  // External JACK client, allocate and return opaque backend
  JalvBackend* backend        = (JalvBackend*)calloc(1, sizeof(JalvBackend));
  backend->client             = client;
  backend->is_internal_client = false;
  return backend;
}

void
jalv_backend_close(Jalv* jalv)
{
  if (jalv->backend) {
    if (!jalv->backend->is_internal_client) {
      jack_client_close(jalv->backend->client);
    }

    free(jalv->backend);
    jalv->backend = NULL;
  }
}

void
jalv_backend_activate(Jalv* jalv)
{
  jack_activate(jalv->backend->client);
}

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

void
jalv_backend_activate_port(Jalv* jalv, uint32_t port_index)
{
  jack_client_t*     client = jalv->backend->client;
  struct Port* const port   = &jalv->ports[port_index];

  const LilvNode* sym = lilv_port_get_symbol(jalv->plugin, port->lilv_port);

  // 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(jalv->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_CONTROL:
    lilv_instance_connect_port(jalv->instance, port_index, &port->control);
    break;
  case TYPE_AUDIO:
    port->sys_port = jack_port_register(
      client, lilv_node_as_string(sym), JACK_DEFAULT_AUDIO_TYPE, jack_flags, 0);
    break;
#ifdef HAVE_JACK_METADATA
  case TYPE_CV:
    port->sys_port = jack_port_register(
      client, lilv_node_as_string(sym), 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 (lilv_port_supports_event(
          jalv->plugin, port->lilv_port, jalv->nodes.midi_MidiEvent)) {
      port->sys_port = jack_port_register(client,
                                          lilv_node_as_string(sym),
                                          JACK_DEFAULT_MIDI_TYPE,
                                          jack_flags,
                                          0);
    }
    break;
  default:
    break;
  }

#ifdef HAVE_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
    LilvNode* name = lilv_port_get_name(jalv->plugin, port->lilv_port);
    jack_set_property(client,
                      jack_port_uuid(port->sys_port),
                      JACK_METADATA_PRETTY_NAME,
                      lilv_node_as_string(name),
                      "text/plain");
    lilv_node_free(name);
  }
#endif
}

int
jack_initialize(jack_client_t* const client, const char* const load_init)
{
  const size_t args_len = strlen(load_init);
  if (args_len > JACK_LOAD_INIT_LIMIT) {
    fprintf(stderr, "error: Too many arguments given\n");
    return -1;
  }

  Jalv* const jalv = (Jalv*)calloc(1, sizeof(Jalv));
  if (!jalv) {
    return -1;
  }

  if (!(jalv->backend = (JalvBackend*)calloc(1, sizeof(JalvBackend)))) {
    free(jalv);
    return -1;
  }

  jalv->backend->client             = client;
  jalv->backend->is_internal_client = true;

  // Build full command line with "program" name for building argv
  const size_t cmd_len = strlen("jalv ") + args_len;
  char* const  cmd     = (char*)calloc(cmd_len + 1, 1);
  snprintf(cmd, cmd_len + 1, "jalv %s", load_init);

  // Build argv
  int    argc = 0;
  char** argv = NULL;
  char*  tok  = cmd;
  for (size_t i = 0; i <= cmd_len; ++i) {
    if (isspace(cmd[i]) || !cmd[i]) {
      argv           = (char**)realloc(argv, sizeof(char*) * ++argc);
      cmd[i]         = '\0';
      argv[argc - 1] = tok;
      tok            = cmd + i + 1;
    }
  }

  const int err = jalv_open(jalv, &argc, &argv);
  if (err) {
    jalv_backend_close(jalv);
    free(jalv);
  }

  free(argv);
  free(cmd);
  return err;
}

void
jack_finish(void* const arg)
{
  Jalv* const jalv = (Jalv*)arg;
  if (jalv) {
    if (jalv_close(jalv)) {
      fprintf(stderr, "Failed to close Jalv\n");
    }

    free(jalv);
  }
}