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

#include <lilv/lilv.h>
#include <lv2/core/lv2.h>
#include <lv2/event/event.h>
#include <lv2/port-groups/port-groups.h>
#include <lv2/presets/presets.h>

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

static LilvNode* applies_to_pred     = NULL;
static LilvNode* control_class       = NULL;
static LilvNode* event_class         = NULL;
static LilvNode* group_pred          = NULL;
static LilvNode* label_pred          = NULL;
static LilvNode* preset_class        = NULL;
static LilvNode* designation_pred    = NULL;
static LilvNode* supports_event_pred = NULL;

static void
print_port(const LilvPlugin* p,
           uint32_t          index,
           const float*      mins,
           const float*      maxes,
           const float*      defaults)
{
  const LilvPort* port = lilv_plugin_get_port_by_index(p, index);

  printf("\n\tPort %u:\n", index);

  if (!port) {
    printf("\t\tERROR: Illegal/nonexistent port\n");
    return;
  }

  bool first = true;

  const LilvNodes* classes = lilv_port_get_classes(p, port);
  printf("\t\tType:        ");
  LILV_FOREACH (nodes, i, classes) {
    const LilvNode* value = lilv_nodes_get(classes, i);
    if (!first) {
      printf("\n\t\t             ");
    }
    printf("%s", lilv_node_as_uri(value));
    first = false;
  }

  if (lilv_port_is_a(p, port, event_class)) {
    LilvNodes* supported = lilv_port_get_value(p, port, supports_event_pred);
    if (lilv_nodes_size(supported) > 0) {
      printf("\n\t\tSupported events:\n");
      LILV_FOREACH (nodes, i, supported) {
        const LilvNode* value = lilv_nodes_get(supported, i);
        printf("\t\t\t%s\n", lilv_node_as_uri(value));
      }
    }
    lilv_nodes_free(supported);
  }

  LilvScalePoints* points = lilv_port_get_scale_points(p, port);
  if (points) {
    printf("\n\t\tScale Points:\n");
  }
  LILV_FOREACH (scale_points, i, points) {
    const LilvScalePoint* point = lilv_scale_points_get(points, i);
    printf("\t\t\t%s = \"%s\"\n",
           lilv_node_as_string(lilv_scale_point_get_value(point)),
           lilv_node_as_string(lilv_scale_point_get_label(point)));
  }
  lilv_scale_points_free(points);

  const LilvNode* sym = lilv_port_get_symbol(p, port);
  printf("\n\t\tSymbol:      %s\n", lilv_node_as_string(sym));

  LilvNode* name = lilv_port_get_name(p, port);
  printf("\t\tName:        %s\n", lilv_node_as_string(name));
  lilv_node_free(name);

  LilvNodes* groups = lilv_port_get_value(p, port, group_pred);
  if (lilv_nodes_size(groups) > 0) {
    printf("\t\tGroup:       %s\n",
           lilv_node_as_string(lilv_nodes_get_first(groups)));
  }
  lilv_nodes_free(groups);

  LilvNodes* designations = lilv_port_get_value(p, port, designation_pred);
  if (lilv_nodes_size(designations) > 0) {
    printf("\t\tDesignation: %s\n",
           lilv_node_as_string(lilv_nodes_get_first(designations)));
  }
  lilv_nodes_free(designations);

  if (lilv_port_is_a(p, port, control_class)) {
    if (!isnan(mins[index])) {
      printf("\t\tMinimum:     %f\n", mins[index]);
    }
    if (!isnan(maxes[index])) {
      printf("\t\tMaximum:     %f\n", maxes[index]);
    }
    if (!isnan(defaults[index])) {
      printf("\t\tDefault:     %f\n", defaults[index]);
    }
  }

  LilvNodes* properties = lilv_port_get_properties(p, port);
  if (lilv_nodes_size(properties) > 0) {
    printf("\t\tProperties:  ");
  }
  first = true;
  LILV_FOREACH (nodes, i, properties) {
    if (!first) {
      printf("\t\t             ");
    }
    printf("%s\n", lilv_node_as_uri(lilv_nodes_get(properties, i)));
    first = false;
  }
  if (lilv_nodes_size(properties) > 0) {
    printf("\n");
  }
  lilv_nodes_free(properties);
}

static void
print_plugin(LilvWorld* world, const LilvPlugin* p)
{
  LilvNode* val = NULL;

  printf("%s\n\n", lilv_node_as_uri(lilv_plugin_get_uri(p)));

  val = lilv_plugin_get_name(p);
  if (val) {
    printf("\tName:              %s\n", lilv_node_as_string(val));
    lilv_node_free(val);
  }

  const LilvPluginClass* pclass      = lilv_plugin_get_class(p);
  const LilvNode*        class_label = lilv_plugin_class_get_label(pclass);
  if (class_label) {
    printf("\tClass:             %s\n", lilv_node_as_string(class_label));
  }

  val = lilv_plugin_get_author_name(p);
  if (val) {
    printf("\tAuthor:            %s\n", lilv_node_as_string(val));
    lilv_node_free(val);
  }

  val = lilv_plugin_get_author_email(p);
  if (val) {
    printf("\tAuthor Email:      %s\n", lilv_node_as_uri(val));
    lilv_node_free(val);
  }

  val = lilv_plugin_get_author_homepage(p);
  if (val) {
    printf("\tAuthor Homepage:   %s\n", lilv_node_as_uri(val));
    lilv_node_free(val);
  }

  if (lilv_plugin_has_latency(p)) {
    uint32_t latency_port = lilv_plugin_get_latency_port_index(p);
    printf("\tHas latency:       yes, reported by port %u\n", latency_port);
  } else {
    printf("\tHas latency:       no\n");
  }

  printf("\tBundle:            %s\n",
         lilv_node_as_uri(lilv_plugin_get_bundle_uri(p)));

  const LilvNode* binary_uri = lilv_plugin_get_library_uri(p);
  if (binary_uri) {
    printf("\tBinary:            %s\n",
           lilv_node_as_uri(lilv_plugin_get_library_uri(p)));
  }

  LilvUIs* uis = lilv_plugin_get_uis(p);
  if (lilv_nodes_size(uis) > 0) {
    printf("\tUIs:\n");
    LILV_FOREACH (uis, i, uis) {
      const LilvUI* ui = lilv_uis_get(uis, i);
      printf("\t\t%s\n", lilv_node_as_uri(lilv_ui_get_uri(ui)));

      const char* binary = lilv_node_as_uri(lilv_ui_get_binary_uri(ui));

      const LilvNodes* types = lilv_ui_get_classes(ui);
      LILV_FOREACH (nodes, t, types) {
        printf("\t\t\tClass:  %s\n",
               lilv_node_as_uri(lilv_nodes_get(types, t)));
      }

      if (binary) {
        printf("\t\t\tBinary: %s\n", binary);
      }

      printf("\t\t\tBundle: %s\n",
             lilv_node_as_uri(lilv_ui_get_bundle_uri(ui)));
    }
  }
  lilv_uis_free(uis);

  printf("\tData URIs:         ");
  const LilvNodes* data_uris = lilv_plugin_get_data_uris(p);
  bool             first     = true;
  LILV_FOREACH (nodes, i, data_uris) {
    if (!first) {
      printf("\n\t                   ");
    }
    printf("%s", lilv_node_as_uri(lilv_nodes_get(data_uris, i)));
    first = false;
  }
  printf("\n");

  /* Required Features */

  LilvNodes* features = lilv_plugin_get_required_features(p);
  if (features) {
    printf("\tRequired Features: ");
  }
  first = true;
  LILV_FOREACH (nodes, i, features) {
    if (!first) {
      printf("\n\t                   ");
    }
    printf("%s", lilv_node_as_uri(lilv_nodes_get(features, i)));
    first = false;
  }
  if (features) {
    printf("\n");
  }
  lilv_nodes_free(features);

  /* Optional Features */

  features = lilv_plugin_get_optional_features(p);
  if (features) {
    printf("\tOptional Features: ");
  }
  first = true;
  LILV_FOREACH (nodes, i, features) {
    if (!first) {
      printf("\n\t                   ");
    }
    printf("%s", lilv_node_as_uri(lilv_nodes_get(features, i)));
    first = false;
  }
  if (features) {
    printf("\n");
  }
  lilv_nodes_free(features);

  /* Extension Data */

  LilvNodes* data = lilv_plugin_get_extension_data(p);
  if (data) {
    printf("\tExtension Data:    ");
  }
  first = true;
  LILV_FOREACH (nodes, i, data) {
    if (!first) {
      printf("\n\t                   ");
    }
    printf("%s", lilv_node_as_uri(lilv_nodes_get(data, i)));
    first = false;
  }
  if (data) {
    printf("\n");
  }
  lilv_nodes_free(data);

  /* Presets */

  LilvNodes* presets = lilv_plugin_get_related(p, preset_class);
  if (presets) {
    printf("\tPresets: \n");
  }
  LILV_FOREACH (nodes, i, presets) {
    const LilvNode* preset = lilv_nodes_get(presets, i);
    lilv_world_load_resource(world, preset);
    LilvNodes* titles = lilv_world_find_nodes(world, preset, label_pred, NULL);
    if (titles) {
      const LilvNode* title = lilv_nodes_get_first(titles);
      printf("\t         %s\n", lilv_node_as_string(title));
      lilv_nodes_free(titles);
    } else {
      fprintf(stderr,
              "Preset <%s> has no rdfs:label\n",
              lilv_node_as_string(lilv_nodes_get(presets, i)));
    }
  }
  lilv_nodes_free(presets);

  /* Ports */

  const uint32_t num_ports = lilv_plugin_get_num_ports(p);
  float*         mins      = (float*)calloc(num_ports, sizeof(float));
  float*         maxes     = (float*)calloc(num_ports, sizeof(float));
  float*         defaults  = (float*)calloc(num_ports, sizeof(float));
  lilv_plugin_get_port_ranges_float(p, mins, maxes, defaults);

  for (uint32_t i = 0; i < num_ports; ++i) {
    print_port(p, i, mins, maxes, defaults);
  }

  free(mins);
  free(maxes);
  free(defaults);
}

static void
print_version(void)
{
  printf("lv2info (lilv) " LILV_VERSION "\n");
}

static int
print_usage(const char* const name, const int status)
{
  FILE* const out = status ? stderr : stdout;
  fprintf(out, "Usage: %s [OPTION]... PLUGIN_URI\n", name);
  fprintf(out,
          "Print information about an installed LV2 plugin.\n\n"
          "  -V, --version  Print version information and exit\n"
          "  -h, --help     Print this help and exit\n"
          "  -m FILE        Add record of plugin to manifest FILE\n"
          "  -p FILE        Write description of plugin to FILE\n\n"
          "Data is loaded from directories in LV2_PATH.\n"
          "For -p and -m, Turtle files are created or appended to, and\n"
          "@prefix directives are only written if the file was empty.\n");
  return status;
}

int
main(int argc, char** argv)
{
  const char* plugin_file   = NULL;
  const char* manifest_file = NULL;
  const char* plugin_uri    = NULL;
  for (int i = 1; i < argc; ++i) {
    if (!strcmp(argv[i], "-V") || !strcmp(argv[i], "--version")) {
      print_version();
      return 0;
    }

    if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) {
      return print_usage(argv[0], 0);
    }

    if (!strcmp(argv[i], "-p")) {
      plugin_file = argv[++i];
    } else if (!strcmp(argv[i], "-m")) {
      manifest_file = argv[++i];
    } else if (argv[i][0] != '-' && (i == argc - 1)) {
      plugin_uri = argv[i];
    } else {
      return print_usage(argv[0], 1);
    }
  }

  if (!plugin_uri) {
    return print_usage(argv[0], 1);
  }

  int ret = 0;

  LilvWorld* world = lilv_world_new();
  lilv_world_set_option(world, LILV_OPTION_OBJECT_INDEX, NULL);
  lilv_world_load_all(world);

  LilvNode* uri = lilv_new_uri(world, plugin_uri);
  if (!uri) {
    fprintf(stderr, "Invalid plugin URI\n");
    lilv_world_free(world);
    return 1;
  }

  applies_to_pred     = lilv_new_uri(world, LV2_CORE__appliesTo);
  control_class       = lilv_new_uri(world, LILV_URI_CONTROL_PORT);
  event_class         = lilv_new_uri(world, LILV_URI_EVENT_PORT);
  group_pred          = lilv_new_uri(world, LV2_PORT_GROUPS__group);
  label_pred          = lilv_new_uri(world, LILV_NS_RDFS "label");
  preset_class        = lilv_new_uri(world, LV2_PRESETS__Preset);
  designation_pred    = lilv_new_uri(world, LV2_CORE__designation);
  supports_event_pred = lilv_new_uri(world, LV2_EVENT__supportsEvent);

  const LilvPlugins* plugins = lilv_world_get_all_plugins(world);
  const LilvPlugin*  p       = lilv_plugins_get_by_uri(plugins, uri);

  if (p && plugin_file) {
    LilvNode* base = lilv_new_file_uri(world, NULL, plugin_file);

    FILE* plugin_fd = fopen(plugin_file, "a");
    if (plugin_fd) {
      lilv_plugin_write_description(world, p, base, plugin_fd);
      fclose(plugin_fd);
    } else {
      fprintf(stderr, "error: Failed to open %s\n", plugin_file);
    }

    if (manifest_file) {
      FILE* manifest_fd = fopen(manifest_file, "a");
      if (manifest_fd) {
        lilv_plugin_write_manifest_entry(
          world, p, base, manifest_fd, plugin_file);
        fclose(manifest_fd);
      } else {
        fprintf(stderr, "error: Failed to open %s\n", manifest_file);
      }
    }
    lilv_node_free(base);
  } else if (p) {
    print_plugin(world, p);
  } else {
    fprintf(stderr, "Plugin not found.\n");
  }

  ret = (p != NULL ? 0 : -1);

  lilv_node_free(uri);

  lilv_node_free(supports_event_pred);
  lilv_node_free(designation_pred);
  lilv_node_free(preset_class);
  lilv_node_free(label_pred);
  lilv_node_free(group_pred);
  lilv_node_free(event_class);
  lilv_node_free(control_class);
  lilv_node_free(applies_to_pred);

  lilv_world_free(world);
  return ret;
}
