/*
 *   JACK Rack
 *    
 *   Copyright (C) Robert Ham 2003 (node@users.sourceforge.net)
 *    
 *   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., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#define _GNU_SOURCE

#include "ac_config.h"

#include <stdarg.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <gtk/gtk.h>
#include <libxml/tree.h>
#include <libxml/xmlIO.h>
#include <libxml/xmlsave.h>
#include <zlib.h>
#include <math.h>

#include "file.h"
#include "jack_rack.h"
#include "globals.h"
#include "plugin_settings.h"
#include "control_message.h"

static int
xml_gzwrite_cb (void *context, const char *buffer, int len)
{
  gzFile zfile = (gzFile) context;

  return gzwrite (zfile, buffer, len);
}

static int
xml_gzread_cb (void *context, char *buffer, int len)
{
  gzFile zfile = (gzFile) context;

  return gzread (zfile, buffer, len);
}

static int
xml_gzclose_cb (void *context)
{
  gzFile zfile = (gzFile) context;

  if (gzclose (zfile) != Z_OK)
    return -1;

  return 0;
}

/* Cast a const string pointer to libxml2's string type (which is always
 * unsigned char *, so it's likely to be different from the native char). */
static const xmlChar *
to_s (const char *s)
{
  return (const xmlChar *) s;
}

/* Cast libxml2's string type to a const string pointer. */
static const char *
from_s (const xmlChar *s)
{
  return (const char *) s;
}

/* Create a new node with xmlNewChild, formatting the contents using printf.
 * If format is NULL then the content will be NULL; if format is "%b" then it
 * will format a single gboolean argument. */
xmlNodePtr
node_format_child (xmlNodePtr parent, const char *name, const char *format, ...)
{
  va_list ap;
  static char buf[256];
  const xmlChar *value = NULL;

  va_start (ap, format);
  if (!format)
    {
      value = NULL;
    }
  else if (strcmp (format, "%b") == 0)
    {
      /* printf doesn't have a bool formatter */
      gboolean b = va_arg(ap, gboolean);
      value = to_s (b ? "true" : "false");
    }
  else
    {
      vsnprintf (buf, sizeof buf, format, ap);
      value = to_s (buf);
    }
  va_end(ap);

  return xmlNewChild (parent, NULL, to_s (name), value);
}

xmlDocPtr
jack_rack_create_doc (jack_rack_t * jack_rack)
{
  xmlDocPtr doc;
  xmlNodePtr tree, jackrack, controlrow, wet_dry_values, midi_control;
  xmlDtdPtr dtd;
  plugin_slot_t * plugin_slot;
  GList * list;
  GSList *mlist;
  unsigned long control;
  unsigned long channel;
  gint copy;
#ifdef HAVE_ALSA
  midi_control_t *midi_ctrl;
#endif

  doc = xmlNewDoc (to_s (XML_DEFAULT_VERSION));

  /* dtd */
  dtd = xmlNewDtd (doc, to_s ("jackrack"), NULL, to_s ("http://jack-rack.sf.net/DTD/jack_rack_1.3.dtd"));
  doc->intSubset = dtd;
  xmlAddChild ((xmlNodePtr)doc, (xmlNodePtr)dtd);

  jackrack = xmlNewDocNode(doc, NULL, to_s ("jackrack"), NULL);
  xmlAddChild ((xmlNodePtr)doc, jackrack);

  /* channels */
  node_format_child (jackrack, "channels", "%lu", jack_rack->channels);

  /* samplerate */
  node_format_child (jackrack, "samplerate", "%lu", (unsigned long) sample_rate);

  for (list = jack_rack->slots; list; list = g_list_next (list))
    {
      plugin_slot = (plugin_slot_t *) list->data;

      tree = node_format_child (jackrack, "plugin", NULL);

      /* id */
      node_format_child (tree, "id", "%lu", plugin_slot->plugin->desc->id);

      /* enabled */
      node_format_child (tree, "enabled", "%b", settings_get_enabled (plugin_slot->settings));

      /* wet/dry stuff */
      node_format_child (tree, "wet_dry_enabled", "%b", settings_get_wet_dry_enabled (plugin_slot->settings));
      if (jack_rack->channels > 1)
        node_format_child (tree, "wet_dry_locked", "%b", settings_get_wet_dry_locked (plugin_slot->settings));

      /* wet/dry values */
      wet_dry_values = node_format_child (tree, "wet_dry_values", NULL);
      for (channel = 0; channel < jack_rack->channels; channel++)
        node_format_child (wet_dry_values, "value", "%f", settings_get_wet_dry_value (plugin_slot->settings, channel));

      /* lockall */
      if (plugin_slot->plugin->copies > 1 )
        node_format_child (tree, "lockall", "%b", settings_get_lock_all (plugin_slot->settings));

      /* control rows */
      for (control = 0; control < plugin_slot->plugin->desc->control_port_count; control++)
        {
          controlrow = node_format_child (tree, "controlrow", NULL);

          /* locked */
          if (plugin_slot->plugin->copies > 1)
            node_format_child (controlrow, "lock", "%b", settings_get_lock (plugin_slot->settings, control));

          /* plugin values */
          for (copy = 0; copy < plugin_slot->plugin->copies; copy++)
            node_format_child (controlrow, "value", "%f", settings_get_control_value (plugin_slot->settings, copy, control));
        }

      /* midi control */
#ifdef HAVE_ALSA
      for (mlist = plugin_slot->midi_controls; mlist; mlist = g_slist_next (mlist))
        {
          midi_ctrl = mlist->data;

          midi_control = node_format_child (tree, "midi_control", NULL);

          /* channel */
          node_format_child (midi_control, "midi_channel", "%u", midi_control_get_midi_channel (midi_ctrl));

          /* param */
          node_format_child (midi_control, "midi_param", "%u", midi_control_get_midi_param (midi_ctrl));

          /* minvalue */
          node_format_child (midi_control, "min_value", "%f", midi_control_get_min_value (midi_ctrl));

          /* maxvalue */
          node_format_child (midi_control, "max_value", "%f", midi_control_get_max_value (midi_ctrl));

          switch(midi_ctrl->ctrl_type)
            {
              case LADSPA_CONTROL:
                {
                  xmlNodePtr ladspa = node_format_child (midi_control, "ladspa", NULL);

                  /* copy */
                  node_format_child (ladspa, "copy", "%u", midi_ctrl->control.ladspa.copy);

                  /* control */
                  node_format_child (ladspa, "control", "%lu", midi_ctrl->control.ladspa.control);

                  break;
                }
              case WET_DRY_CONTROL:
                {
                  xmlNodePtr wet_dry = node_format_child (midi_control, "wet_dry", NULL);

                  /* channel */
                  node_format_child (wet_dry, "channel", "%lu", midi_ctrl->control.wet_dry.channel);

                  break;
                }
              case PLUGIN_ENABLE_CONTROL:
                node_format_child (midi_control, "enable", NULL);
                break;
            }
        }
#endif /* HAVE_ALSA */
    }

  return doc;
}

int
ui_save_file (ui_t * ui, const char * filename)
{
  gzFile zfile;
  xmlSaveCtxt *ctxt;
  xmlDocPtr doc;

  zfile = gzopen (filename, "wb9");
  if (!zfile)
    {
      ui_display_error (ui, E_ERROR, _("Could not save file '%s' (open failed)"), filename);
      return 1;
    }

  ctxt = xmlSaveToIO (xml_gzwrite_cb, xml_gzclose_cb, (void *) zfile, NULL, XML_SAVE_FORMAT);
  if (!ctxt)
    {
      ui_display_error (ui, E_ERROR, _("Could not save file '%s' (XML context failed)"), filename);
      gzclose (zfile);
      return 1;
    }

  doc = jack_rack_create_doc (ui->jack_rack);

  if (xmlSaveDoc (ctxt, doc) == -1)
    {
      ui_display_error (ui, E_ERROR, _("Could not save file '%s' (XML output failed)"), filename);
      xmlSaveClose (ctxt);
      xmlFreeDoc (doc);
      return 1;
    }

  if (xmlSaveClose (ctxt) == -1)
    {
      ui_display_error (ui, E_ERROR, _("Could not save file '%s' (closing failed)"), filename);
      xmlFreeDoc (doc);
      return 1;
    }

  xmlFreeDoc (doc);

  return 0;
}

static gboolean
node_name_is (const xmlNodePtr node, const char *name)
{
  return strcmp (from_s (node->name), name) == 0;
}

/* Parse node values as various data types. The names here correspond to the
 * formatting directives used with node_format_child. */

static gboolean
node_value_b (const xmlNodePtr node)
{
  gboolean value;
  xmlChar *content = xmlNodeGetContent (node);
  if (!content)
    return FALSE;

  value = (strcmp (from_s (content), "true") == 0) ? TRUE : FALSE;
  xmlFree (content);
  return value;
}

static float
node_value_f (const xmlNodePtr node)
{
  float value;
  xmlChar *content = xmlNodeGetContent (node);
  if (!content)
    return FALSE;

  value = strtof (from_s (content), NULL);
  xmlFree (content);
  return value;
}

static unsigned long
node_value_lu (const xmlNodePtr node)
{
  unsigned long value;
  xmlChar *content = xmlNodeGetContent (node);
  if (!content)
    return FALSE;

  value = strtoul (from_s (content), NULL, 10);
  xmlFree (content);
  return value;
}

static unsigned int
node_value_u (const xmlNodePtr node)
{
  return node_value_lu (node);
}

static gboolean
saved_rack_parse_plugin (saved_rack_t * saved_rack, saved_plugin_t * saved_plugin,
                         ui_t * ui, const char * filename, xmlNodePtr plugin)
{
  plugin_desc_t * desc;
  settings_t * settings = NULL;
  xmlNodePtr node;
  xmlNodePtr sub_node;
  unsigned long num;
  unsigned long control = 0;
  gboolean shown_ids_warning = FALSE;
  gboolean shown_order_warning = FALSE;
#ifdef HAVE_ALSA
  midi_control_t * midi_ctrl;
#endif


  for (node = plugin->children; node; node = node->next)
    {
      if (xmlNodeIsText (node))
        {
          /* Ignore text nodes */
        }
      else if (node_name_is (node, "id"))
        {
          if (settings)
            {
              /* We must have exactly one <id> per <plugin> */
              if (!shown_ids_warning)
                {
                  ui_display_error (ui, E_WARNING, _("The file '%s' contains multiple IDs in the same plugin; ignoring"), filename);
                  shown_ids_warning = TRUE;
                }
              continue;
            }

          num = node_value_lu (node);
          desc = plugin_mgr_get_any_desc (ui->plugin_mgr, num);
          if (!desc)
            {
              ui_display_error (ui, E_WARNING, _("The file '%s' contains an unknown plugin with ID '%ld'; skipping"), filename, num);
              return FALSE;
            }

          settings = settings_new (desc, saved_rack->channels, saved_rack->sample_rate);
        }
      else if (!settings)
        {
          /* We can't parse anything other than id until we've loaded a plugin */
          if (!shown_order_warning)
            {
              ui_display_error (ui, E_WARNING, _("The file '%s' contains plugin settings before the plugin ID; ignoring"), filename);
              shown_order_warning = TRUE;
            }
        }
      else if (node_name_is (node, "enabled"))
        settings_set_enabled (settings, node_value_b (node));
      else if (node_name_is (node, "wet_dry_enabled"))
        settings_set_wet_dry_enabled (settings, node_value_b (node));
      else if (node_name_is (node, "wet_dry_locked"))
        settings_set_wet_dry_locked (settings, node_value_b (node));
      else if (node_name_is (node, "wet_dry_values"))
        {
          unsigned long channel = 0;

          for (sub_node = node->children; sub_node; sub_node = sub_node->next)
            {
              if (node_name_is (sub_node, "value"))
                {
                  settings_set_wet_dry_value (settings, channel, node_value_f (sub_node));

                  channel++;
                }
            }
        }
      else if (node_name_is (node, "lockall"))
        settings_set_lock_all (settings, node_value_b (node));
      else if (node_name_is (node, "controlrow"))
        {
          gint copy = 0;

          for (sub_node = node->children; sub_node; sub_node = sub_node->next)
            {
              if (node_name_is (sub_node, "lock"))
                settings_set_lock (settings, control, node_value_b (sub_node));
              else if (node_name_is (sub_node, "value"))
                {
                  settings_set_control_value (settings, copy, control, node_value_f (sub_node));
                  copy++;
                }
            }

          control++;
        }
#ifdef HAVE_ALSA
      else if (node_name_is (node, "midi_control"))
        {
          xmlNodePtr control_node;

          midi_ctrl = g_malloc (sizeof (midi_control_t));
          midi_ctrl->min = -HUGE_VAL;
          midi_ctrl->max = HUGE_VAL;

          for (sub_node = node->children; sub_node; sub_node = sub_node->next)
            {
              if (node_name_is (sub_node, "midi_channel"))
                midi_ctrl->midi_channel = node_value_u (sub_node);
              else if (node_name_is (sub_node, "midi_param"))
                midi_ctrl->midi_param = node_value_u (sub_node);
              else if (node_name_is (sub_node, "min_value"))
                midi_ctrl->min = node_value_f (sub_node);
              else if (node_name_is (sub_node, "max_value"))
                midi_ctrl->max = node_value_f (sub_node);
              else if (node_name_is (sub_node, "ladspa"))
                {
                  midi_ctrl->ctrl_type = LADSPA_CONTROL;
                  for (control_node = sub_node->children;
                       control_node;
                       control_node = control_node->next)
                    {
                      if (node_name_is (control_node, "copy"))
                        midi_ctrl->control.ladspa.copy = node_value_u (control_node);
                      else if (node_name_is (control_node, "control"))
                        midi_ctrl->control.ladspa.control = node_value_lu (control_node);
                    }
                }
              else if (node_name_is (sub_node, "wet_dry"))
                {
                  midi_ctrl->ctrl_type = WET_DRY_CONTROL;
                  for (control_node = sub_node->children;
                       control_node;
                       control_node = control_node->next)
                    {
                      if (node_name_is (control_node, "channel"))
                        midi_ctrl->control.wet_dry.channel = node_value_lu (control_node);
                    }
                }
              else if (node_name_is (sub_node, "enable"))
                midi_ctrl->ctrl_type = PLUGIN_ENABLE_CONTROL;
           }

          saved_plugin->midi_controls =
            g_slist_append (saved_plugin->midi_controls, midi_ctrl);
        }
#endif /* HAVE_ALSA */
    }

  if (settings)
    {
      saved_plugin->settings = settings;
      return TRUE;
    }
  else
    {
      /* We never saw a plugin ID */
      return FALSE;
    }
}

static void
saved_rack_parse_jackrack (saved_rack_t * saved_rack, ui_t * ui, const char * filename, xmlNodePtr jackrack)
{
  xmlNodePtr node;
  saved_plugin_t * saved_plugin;

  for (node = jackrack->children; node; node = node->next)
    {
      if (node_name_is (node, "channels"))
        saved_rack->channels = node_value_lu (node);
      else if (node_name_is (node, "samplerate"))
        saved_rack->sample_rate = node_value_lu (node);
      else if (node_name_is (node, "plugin"))
        {
          saved_plugin = g_malloc0 (sizeof (saved_plugin_t));
          if (saved_rack_parse_plugin (saved_rack, saved_plugin, ui, filename, node))
            {
              saved_rack->plugins = g_slist_append (saved_rack->plugins, saved_plugin);
            }
          else
            {
              /* There is no content to clean up as no plugin was loaded */
              g_free (saved_plugin);
            }
        }
    }
}

static saved_rack_t *
saved_rack_new (ui_t * ui, const char * filename, xmlDocPtr doc)
{
  xmlNodePtr node;
  saved_rack_t *saved_rack;

  /* create the saved rack */
  saved_rack = g_malloc (sizeof (saved_rack_t));
  saved_rack->plugins = NULL;
  saved_rack->sample_rate = 48000;
  saved_rack->channels = 2;

  for (node = doc->children; node; node = node->next)
    {
      if (node_name_is (node, "jackrack"))
        saved_rack_parse_jackrack (saved_rack, ui, filename, node);
    }

  return saved_rack;
}

static void
saved_rack_destroy (saved_rack_t * saved_rack)
{
  g_free (saved_rack);
}

int
ui_open_file (ui_t * ui, const char * filename)
{
  gzFile zfile;
  xmlDocPtr doc;
  saved_rack_t * saved_rack;
  GSList * list;
  saved_plugin_t * saved_plugin;

  /*
   * In older versions of JACK Rack, rack files were written with libxml2 with
   * compression enabled, so they were gzip-compressed XML. However, more
   * recent versions of libxml2 now strongly recommend building without
   * compression support, in which case the compression level will be silently
   * ignored and JACK Rack will write uncompressed XML. So the file we're
   * reading may or may not be compressed. Use zlib's gzFile mechanism to
   * handle this.
   */
  zfile = gzopen (filename, "rb");
  if (!zfile)
    {
      ui_display_error (ui, E_ERROR, _("Could not read file '%s'"), filename);
      return 1;
    }

  doc = xmlReadIO (xml_gzread_cb, xml_gzclose_cb, (void *) zfile,
                   NULL, NULL, 0);
  if (!doc)
    {
      ui_display_error (ui, E_ERROR, _("Could not parse file '%s'"), filename);
      return 1;
    }

  if (strcmp (from_s (((xmlDtdPtr) doc->children)->name), "jackrack") != 0)
    {
      ui_display_error (ui, E_ERROR, _("The file '%s' is not a JACK Rack settings file"), filename);
      xmlFreeDoc (doc);
      return 1;
    }

  saved_rack = saved_rack_new (ui, filename, doc);
  xmlFreeDoc (doc);

  if (!saved_rack)
    return 1;

  if (ui->jack_rack->slots)
    {
      gboolean ok;

      ok = ui_get_ok (ui, _("The current rack will be cleared.\n\nAre you sure you want to continue?"));

      if (!ok)
        {
          saved_rack_destroy (saved_rack);
          return 1;
        }
    }

  if (saved_rack->channels != ui->jack_rack->channels)
    ui_set_channels (ui, saved_rack->channels);

  jack_rack_send_clear_plugins (ui->jack_rack);

  for (list = saved_rack->plugins; list; list = g_slist_next (list))
    {
      saved_plugin = list->data;

      settings_set_sample_rate (saved_plugin->settings, sample_rate);

      jack_rack_add_saved_plugin (ui->jack_rack, saved_plugin);
    }

  g_slist_free (saved_rack->plugins);
  g_free (saved_rack);

  return 0;
}

/* EOF */
