/*
 *   elektroid-cli.c
 *   Copyright (C) 2019 David García Goñi <dagargo@gmail.com>
 *
 *   This file is part of Elektroid.
 *
 *   Elektroid 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 3 of the License, or
 *   (at your option) any later version.
 *
 *   Elektroid 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 Elektroid. If not, see <http://www.gnu.org/licenses/>.
 */

#include "audio.h"
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#if defined(__linux__)
#include <signal.h>
#endif
#include <stdint.h>
#include <inttypes.h>
#include <stddef.h>
#include <glib.h>
#include <glib/gstdio.h>
#include "../config.h"
#include "backend.h"
#include "regconn.h"
#include "regpref.h"
#include "sample.h"
#include "utils.h"

#define CLI_SLEEP_US 200000

#define ERR_MSG_CMD_NOT_IN_SYSTEM_FS "Command not available in system backend"
#define ERR_MSG_REMOTE_PATH_MISSING "Remote path missing"
#define ERR_MSG_REMOTE_PATH_SRC_MISSING "Remote path source missing"

#define GET_FS_OPS_OFFSET(member) offsetof(struct fs_operations, member)
#define GET_FS_OPS_FUNC(type,fs,offset) (*(((type *) (((gchar *) fs) + offset))))
#define RETURN_IF_NULL(f) if (!(f)) {error_print ("Function not implemented"); return EXIT_FAILURE;}

static struct backend backend;
static struct task_control task_control;
static struct controllable controllable;	//Used for CLI control for operations that do not use task_control or sysex_transfer.
static gchar *connector, *fs, *op;

static const struct fs_operations *fs_ops;
static const gchar *current_path_progress;
static gboolean connected_to_tty;
static gboolean same_line_progress;
static gboolean use_audio;

gboolean
elektroid_ask_user_to_continue (const gchar *msg,
				struct controllable *controllable)
{
  gchar buf, *remain;
  gsize bytes_read;
  GIOChannel *channel = g_io_channel_unix_new (STDIN_FILENO);

  printf ("%s\n", msg);
  while (1)
    {
      printf ("Do you want to continue? [y/n]\n");
      g_io_channel_read_chars (channel, &buf, 1, &bytes_read, NULL);
      if (buf == 'y' || buf == 'n')
	{
	  break;
	}
      g_io_channel_read_line (channel, &remain, &bytes_read, NULL, NULL);
      g_free (remain);
    }
  g_io_channel_unref (channel);

  controllable_set_active (controllable, buf == 'y');

  return controllable->active;
}

static void
complete_progress (gint err)
{
  if (same_line_progress && !err)
    {
      fprintf (stderr, "\n");
    }
}

static void
print_progress (struct task_control *task_control)
{
  gint progress = task_control->progress * 100;
  const gchar *end = same_line_progress ? "\r" : "\n";
  fprintf (stderr, "%s: %3d %%%s", current_path_progress, progress, end);
  if (same_line_progress)
    {
      fflush (stderr);
    }
}

static void
setup ()
{
  connected_to_tty = isatty (fileno (stderr));
  same_line_progress = !debug_level && connected_to_tty;
}

static const gchar *
cli_get_path (const gchar *device_path)
{
  gint len = strlen (device_path);
  const gchar *path = device_path;
  gint i = 0;

  while (*path != G_SEARCHPATH_SEPARATOR && i < len)
    {
      path++;
      i++;
    }
  if (*path == G_SEARCHPATH_SEPARATOR)
    {
      path++;
    }

  return path;
}

static gint
cli_ld ()
{
  struct backend_device device;
  GArray *devices = backend_get_devices ();

  for (gint i = 0; i < devices->len; i++)
    {
      device = g_array_index (devices, struct backend_device, i);
      printf ("%d: id: %s; name: %s\n", i, device.id, device.name);
    }

  g_array_free (devices, TRUE);

  return EXIT_SUCCESS;
}

static gint
cli_connect (const gchar *device_path)
{
  struct backend_device device;
  GArray *devices = backend_get_devices ();
  gint err, id;
  gchar *rem;

  if (!devices->len)
    {
      error_print ("No devices found");
      err = EXIT_FAILURE;
      goto end;
    }

  errno = 0;
  id = (gint) g_ascii_strtoll (device_path, &rem, 10);
  if (errno || device_path == rem)
    {
      error_print ("Device not provided properly in '%s'", device_path);
      err = EXIT_FAILURE;
      goto end;
    }

  if (id >= devices->len)
    {
      error_print ("Invalid device '%d'", id);
      err = EXIT_FAILURE;
      goto end;
    }

  device = g_array_index (devices, struct backend_device, id);
  err = backend_init_connector (&backend, &device, connector, NULL);

  if (!err && fs)
    {
      fs_ops = backend_get_fs_operations_by_name (&backend, fs);
      if (!fs_ops)
	{
	  error_print ("Invalid filesystem '%s'", fs);
	  err = EXIT_FAILURE;
	}
    }

end:
  g_array_free (devices, TRUE);
  return err;
}

static void
cli_print_item (struct item_iterator *iter, struct backend *backend,
		const struct fs_operations *fs_ops)
{
  gchar id[LABEL_MAX];
  gchar hsize[LABEL_MAX];
  gboolean slot = fs_ops->options & FS_OPTION_SLOT_STORAGE;
  gboolean info = (fs_ops->options & FS_OPTION_SHOW_INFO_COLUMN) &&
    *iter->item.object_info;

  if (iter->item.size > -1)
    {
      get_human_size (iter->item.size, FALSE, hsize, LABEL_MAX);
    }
  else
    {
      snprintf (hsize, LABEL_MAX, "?B");
    }
  snprintf (id, LABEL_MAX, "%d ", iter->item.id);
  printf ("%c %*s %s%*s%*s%s%-*s%s%s%s\n", iter->item.type,
	  DEFAULT_PRINT_COL_SIZE_LEN, hsize,
	  slot ? " " : "",
	  slot ? DEFAULT_PRINT_COL_ID_LEN : 0,
	  slot ? id : "",
	  slot ? DEFAULT_PRINT_COL_SLOT_LEN : 0,
	  slot ? iter->item.slot : "",
	  slot ? " " : "",
	  DEFAULT_PRINT_COL_NAME_LEN, iter->item.name,
	  info ? " [ " : "",
	  info ? iter->item.object_info : "", info ? " ]" : "");
}

static gint
cli_list (int argc, gchar *argv[], int *optind)
{
  gint err;
  const gchar *path;
  struct item_iterator iter;
  const gchar *device_path;

  if (*optind == argc)
    {
      error_print (ERR_MSG_REMOTE_PATH_MISSING);
      return EXIT_FAILURE;
    }
  else
    {
      device_path = argv[*optind];
      (*optind)++;
    }

  err = cli_connect (device_path);
  if (err)
    {
      return err;
    }

  RETURN_IF_NULL (fs_ops->readdir);
  RETURN_IF_NULL (fs_ops->get_exts);

  path = cli_get_path (device_path);
  err = fs_ops->readdir (&backend, &iter, path,
			 fs_ops->get_exts (&backend, fs_ops));
  if (err)
    {
      return err;
    }

  while (!item_iterator_next (&iter) &&
	 controllable_is_active (&controllable))
    {
      if (fs_ops->print_item)
	{
	  fs_ops->print_item (&iter, &backend, fs_ops);
	}
      else
	{
	  cli_print_item (&iter, &backend, fs_ops);
	}
    }

  item_iterator_free (&iter);

  return controllable_is_active (&controllable) ? EXIT_SUCCESS : EXIT_FAILURE;
}

static gint
cli_command_path (int argc, gchar *argv[], int *optind, gssize member_offset)
{
  const gchar *path;
  const gchar *device_path;
  gint err;
  fs_path_func f;

  if (*optind == argc)
    {
      error_print (ERR_MSG_REMOTE_PATH_MISSING);
      return EXIT_FAILURE;
    }
  else
    {
      device_path = argv[*optind];
      (*optind)++;
    }

  err = cli_connect (device_path);
  if (err)
    {
      return err;
    }

  f = GET_FS_OPS_FUNC (fs_path_func, fs_ops, member_offset);
  RETURN_IF_NULL (f);

  path = cli_get_path (device_path);
  return f (&backend, path);
}

static gint
cli_command_src_dst (int argc, gchar *argv[], int *optind,
		     gssize member_offset)
{
  const gchar *src_path, *dst_path;
  gchar *device_src_path, *device_dst_path;
  gint src_card, dst_card, err;
  fs_src_dst_func f;

  if (*optind == argc)
    {
      error_print (ERR_MSG_REMOTE_PATH_SRC_MISSING);
      return EXIT_FAILURE;
    }
  else
    {
      device_src_path = argv[*optind];
      (*optind)++;
    }

  if (*optind == argc)
    {
      error_print ("Remote path destination missing");
      return EXIT_FAILURE;
    }
  else
    {
      device_dst_path = argv[*optind];
      (*optind)++;
    }

  src_card = atoi (device_src_path);
  dst_card = atoi (device_dst_path);
  if (src_card != dst_card)
    {
      error_print ("Source and destination device must be the same");
      return EXIT_FAILURE;
    }

  err = cli_connect (device_src_path);
  if (err)
    {
      return err;
    }

  f = GET_FS_OPS_FUNC (fs_src_dst_func, fs_ops, member_offset);
  RETURN_IF_NULL (f);

  src_path = cli_get_path (device_src_path);
  dst_path = cli_get_path (device_dst_path);
  return f (&backend, src_path, dst_path);
}

static gint
cli_command_mv_rename (int argc, gchar *argv[], int *optind)
{
  const gchar *src_path, *dst_path;
  gchar *device_src_path, *device_dst_path;
  gint src_card, dst_card, err;
  fs_src_dst_func f;

  if (*optind == argc)
    {
      error_print (ERR_MSG_REMOTE_PATH_SRC_MISSING);
      return EXIT_FAILURE;
    }
  else
    {
      device_src_path = argv[*optind];
      (*optind)++;
    }

  if (*optind == argc)
    {
      error_print ("Remote path destination missing");
      return EXIT_FAILURE;
    }
  else
    {
      device_dst_path = argv[*optind];
      (*optind)++;
    }

  err = cli_connect (device_src_path);
  if (err)
    {
      return err;
    }

  src_card = atoi (device_src_path);
  src_path = cli_get_path (device_src_path);

  f = fs_ops->move;
  // If move is implemented, rename must behave the same way.
  if (f)
    {
      dst_card = atoi (device_dst_path);
      if (src_card != dst_card)
	{
	  error_print ("Source and destination device must be the same");
	  return EXIT_FAILURE;
	}
      dst_path = cli_get_path (device_dst_path);
    }
  else
    {
      f = fs_ops->rename;
      RETURN_IF_NULL (f);

      dst_path = device_dst_path;
    }

  return f (&backend, src_path, dst_path);
}

static const gchar *
cli_get_backend_type_name ()
{
  switch (backend.type)
    {
    case BE_TYPE_SYSTEM:
      return "SYSTEM";
    case BE_TYPE_MIDI:
      return "MIDI";
    case BE_TYPE_NO_MIDI:
      return "NO-MIDI";
    default:
      return "UNKNOWN";
    }
}

static gint
cli_fs_compare (gconstpointer a, gconstpointer b)
{
  const struct fs_operations *fsa = a;
  const struct fs_operations *fsb = b;
  return strcmp (fsa->name, fsb->name);
}

static gint
cli_info (int argc, gchar *argv[], int *optind)
{
  const gchar *device_path;
  gint err;
  gboolean first;
  GSList *e, *sorted;

  if (*optind == argc)
    {
      error_print ("Device missing");
      return EXIT_FAILURE;
    }
  else
    {
      device_path = argv[*optind];
      (*optind)++;
    }

  err = cli_connect (device_path);
  if (err)
    {
      return err;
    }

  printf ("Type: %s\n", cli_get_backend_type_name ());
  printf ("Device name: %s\n", backend.name);
  printf ("Device version: %s\n", backend.version);
  printf ("Device description: %s\n", backend.description);
  printf ("Connector: %s\n", backend.conn_name);
  printf ("Filesystems: ");

  sorted = g_slist_copy (backend.fs_ops);
  sorted = g_slist_sort (sorted, cli_fs_compare);

  e = sorted;
  first = TRUE;
  while (e)
    {
      const struct fs_operations *fs_ops = e->data;
      gboolean cli_only = fs_ops->gui_name == NULL;
      printf ("%s%s%s", first ? "" : ", ", fs_ops->name,
	      cli_only ? " (CLI only)" : "");
      first = FALSE;
      e = e->next;
    }
  printf ("\n");

  g_slist_free (g_steal_pointer (&sorted));

  return EXIT_SUCCESS;
}

static gint
cli_df (int argc, gchar *argv[], int *optind)
{
  const gchar *device_path;
  const gchar *path;
  gchar size[LABEL_MAX];
  gchar diff[LABEL_MAX];
  gchar free[LABEL_MAX];
  gint err;
  struct backend_storage_stats statfs;

  if (*optind == argc)
    {
      error_print ("Device missing");
      return EXIT_FAILURE;
    }
  else
    {
      device_path = argv[*optind];
      (*optind)++;
    }

  err = cli_connect (device_path);
  if (err)
    {
      return err;
    }

  if (!backend.get_storage_stats)
    {
      error_print ("Storage stats not implemented");
      return EXIT_FAILURE;
    }

  path = cli_get_path (device_path);
  if (!strlen (path))
    {
      error_print ("Path missing");
      return EXIT_FAILURE;
    }

  printf ("%-20.20s%16.16s%16.16s%16.16s%11.10s\n", "Storage", "Size",
	  "Used", "Available", "Use%");

  err = 0;
  for (guint i = 1; i < G_MAXUINT8; i <<= 1)
    {
      gint v = backend.get_storage_stats (&backend, i, &statfs, path);
      if (v >= 0)
	{
	  get_human_size (statfs.bsize, FALSE, size, LABEL_MAX);
	  get_human_size (statfs.bsize - statfs.bfree, FALSE, diff,
			  LABEL_MAX);
	  get_human_size (statfs.bfree, FALSE, free, LABEL_MAX);
	  printf ("%-20.20s%16s%16s%16s%10.2f%%\n",
		  statfs.name, size, diff, free,
		  backend_get_storage_stats_percent (&statfs));
	}

      if (!v)
	{
	  break;
	}
    }

  return err;
}

static gint
cli_upgrade_os (int argc, gchar *argv[], int *optind)
{
  gint err;
  const gchar *src_path;
  const gchar *device_path;
  struct idata idata;
  struct sysex_transfer sysex_transfer;

  if (*optind == argc)
    {
      error_print ("Local path missing");
      return EXIT_FAILURE;
    }
  else
    {
      src_path = argv[*optind];
      (*optind)++;
    }

  if (*optind == argc)
    {
      error_print (ERR_MSG_REMOTE_PATH_MISSING);
      return EXIT_FAILURE;
    }
  else
    {
      device_path = argv[*optind];
      (*optind)++;
    }

  err = cli_connect (device_path);
  if (err)
    {
      return err;
    }
  if (backend.type == BE_TYPE_SYSTEM)
    {
      error_print (ERR_MSG_CMD_NOT_IN_SYSTEM_FS);
      return EXIT_FAILURE;
    }

  RETURN_IF_NULL (backend.upgrade_os);

  err = file_load (src_path, &idata, NULL);
  if (err)
    {
      error_print ("Error while loading '%s'.", src_path);
    }
  else
    {
      sysex_transfer_init_tx (&sysex_transfer, idata_steal (&idata));
      err = backend.upgrade_os (&backend, &sysex_transfer, &controllable);
      sysex_transfer_clear (&sysex_transfer);
    }

  return err;
}

static gint
cli_download_item (const gchar *src_path, const gchar *dst_path)
{
  gint err;
  gchar *download_path;
  struct idata idata;

  RETURN_IF_NULL (fs_ops->download);
  RETURN_IF_NULL (fs_ops->get_download_path);
  RETURN_IF_NULL (fs_ops->save);

  controllable_set_active (&task_control.controllable, TRUE);
  task_control.callback = print_progress;
  current_path_progress = src_path;

  err = fs_ops->download (&backend, src_path, &idata, &task_control);
  if (err)
    {
      return err;
    }

  download_path = fs_ops->get_download_path (&backend, fs_ops, dst_path,
					     src_path, &idata);
  if (!download_path)
    {
      err = EXIT_FAILURE;
      goto cleanup;
    }

  err = fs_ops->save (&backend, download_path, &idata, &task_control);
  g_free (download_path);

cleanup:
  idata_clear (&idata);

  complete_progress (err);

  return err;
}

static gint
cli_download_dir (const gchar *src_path, const gchar *dst_path)
{
  gint err;
  struct item_iterator iter;

  RETURN_IF_NULL (fs_ops->readdir);

  err = fs_ops->readdir (&backend, &iter, src_path, NULL);
  if (err)
    {
      return err;
    }

  while (!item_iterator_next (&iter) && controllable_is_active (&controllable)
	 && !err)
    {
      gchar *rsrc_path;
      gchar *filename = item_get_filename (&iter.item, fs_ops->options);
      rsrc_path = path_chain (PATH_INTERNAL, src_path, filename);
      g_free (filename);

      if (iter.item.type == ITEM_TYPE_FILE && iter.item.size != 0)	//File and non empty slot
	{
	  err = cli_download_item (rsrc_path, dst_path);
	}
      else if (iter.item.type == ITEM_TYPE_DIR)
	{
	  gchar *rdst_path = path_chain (PATH_SYSTEM, dst_path,
					 iter.item.name);

	  err = g_mkdir (rdst_path, 0755);
	  if (err)
	    {
	      error_print
		("Error while creating directory '%s'. Continuing...",
		 rdst_path);
	    }
	  else
	    {
	      err = cli_download_dir (rsrc_path, rdst_path);
	    }

	  g_free (rdst_path);
	}

      g_free (rsrc_path);
    }

  item_iterator_free (&iter);

  return controllable_is_active (&controllable) ? err : EXIT_FAILURE;
}

static gint
cli_download (int argc, gchar *argv[], int *optind, gint recursive)
{
  const gchar *src_path;
  const gchar *dst_path;
  gchar *device_src_path;
  gint err;

  if (*optind == argc)
    {
      error_print (ERR_MSG_REMOTE_PATH_MISSING);
      return EXIT_FAILURE;
    }
  else
    {
      device_src_path = argv[*optind];
      (*optind)++;
    }

  err = cli_connect (device_src_path);
  if (err)
    {
      return err;
    }

  src_path = cli_get_path (device_src_path);

  if (*optind == argc)
    {
      dst_path = ".";
    }
  else
    {
      dst_path = argv[*optind];

      debug_print (1, "Creating directory '%s'...", dst_path);
      err = g_mkdir_with_parents (dst_path, 0755);
      if (err)
	{
	  error_print ("Error while creating directory '%s'", dst_path);
	  return err;
	}
    }

  if (recursive)
    {
      if (strcmp (src_path, "/"))
	{
	  gchar *new_dir = g_path_get_basename (src_path);
	  gchar *full_dst_path = path_chain (PATH_SYSTEM, dst_path, new_dir);
	  debug_print (1, "Creating directory '%s'...", full_dst_path);
	  err = g_mkdir (full_dst_path, 0755);
	  if (err)
	    {
	      error_print ("Error while creating directory '%s'",
			   full_dst_path);
	    }
	  else
	    {
	      err = cli_download_dir (src_path, full_dst_path);
	    }

	  g_free (full_dst_path);
	  g_free (new_dir);
	  return err;
	}
      else
	{
	  return cli_download_dir (src_path, dst_path);
	}
    }
  else
    {
      return cli_download_item (src_path, dst_path);
    }
}

static gint
cli_upload_item (const gchar *src_path, const gchar *dst_path)
{
  gint err;
  gchar *upload_path;
  struct idata idata;

  RETURN_IF_NULL (fs_ops->load);
  RETURN_IF_NULL (fs_ops->get_upload_path);
  RETURN_IF_NULL (fs_ops->upload);

  controllable_set_active (&task_control.controllable, TRUE);
  task_control.callback = print_progress;
  current_path_progress = src_path;

  err = fs_ops->load (&backend, src_path, &idata, &task_control);
  if (err)
    {
      return err;
    }

  upload_path = fs_ops->get_upload_path (&backend, fs_ops, dst_path,
					 src_path, &idata);

  err = fs_ops->upload (&backend, upload_path, &idata, &task_control);
  idata_clear (&idata);

  g_free (upload_path);

  complete_progress (err);

  return err;
}

static gint
cli_upload (int argc, gchar *argv[], int *optind)
{
  gint err;
  const gchar *dst_path;
  gchar *src_path, *device_dst_path;

  if (*optind == argc)
    {
      error_print ("Local path missing");
      return EXIT_FAILURE;
    }
  else
    {
      src_path = argv[*optind];
      (*optind)++;
    }

  if (*optind == argc)
    {
      error_print (ERR_MSG_REMOTE_PATH_MISSING);
      return EXIT_FAILURE;
    }
  else
    {
      device_dst_path = argv[*optind];
      (*optind)++;
    }

  err = cli_connect (device_dst_path);
  if (err)
    {
      return err;
    }

  dst_path = cli_get_path (device_dst_path);

  return cli_upload_item (src_path, dst_path);
}

static gint
cli_send (int argc, gchar *argv[], int *optind)
{
  gint err;
  const gchar *device_dst_path, *src_file;
  struct idata idata;
  struct sysex_transfer sysex_transfer;

  if (*optind == argc)
    {
      error_print ("Source file missing");
      return EXIT_FAILURE;
    }
  else
    {
      src_file = argv[*optind];
      (*optind)++;
    }

  if (*optind == argc)
    {
      error_print ("Remote device missing");
      return EXIT_FAILURE;
    }
  else
    {
      device_dst_path = argv[*optind];
      (*optind)++;
    }

  connector = "default";
  err = cli_connect (device_dst_path);
  if (err)
    {
      return err;
    }
  if (backend.type == BE_TYPE_SYSTEM)
    {
      error_print (ERR_MSG_CMD_NOT_IN_SYSTEM_FS);
      return EXIT_FAILURE;
    }

  err = file_load (src_file, &idata, NULL);
  if (err)
    {
      error_print ("Error while loading '%s'.", src_file);
    }
  else
    {
      sysex_transfer_init_tx (&sysex_transfer, idata_steal (&idata));
      err = backend_tx_sysex (&backend, &sysex_transfer, &controllable);
      sysex_transfer_clear (&sysex_transfer);
    }

  return err;
}

static gint
cli_receive (int argc, gchar *argv[], int *optind)
{
  gint err;
  const gchar *device_src_path, *dst_file;
  struct idata idata;
  struct sysex_transfer sysex_transfer;

  if (*optind == argc)
    {
      error_print ("Remote device missing");
      return EXIT_FAILURE;
    }
  else
    {
      device_src_path = argv[*optind];
      (*optind)++;
    }

  if (*optind == argc)
    {
      error_print ("Destination file missing");
      return EXIT_FAILURE;
    }
  else
    {
      dst_file = argv[*optind];
      (*optind)++;
    }

  connector = "default";
  err = cli_connect (device_src_path);
  if (err)
    {
      return err;
    }
  if (backend.type == BE_TYPE_SYSTEM)
    {
      error_print (ERR_MSG_CMD_NOT_IN_SYSTEM_FS);
      return EXIT_FAILURE;
    }

  backend_rx_drain (&backend);

  //This doesn't need to be synchronized because the CLI is not multithreaded.
  sysex_transfer_init_rx (&sysex_transfer, BE_SYSEX_TIMEOUT_MS, TRUE);
  err = backend_rx_sysex (&backend, &sysex_transfer, &controllable);
  if (err)
    {
      error_print ("Error while downloading.");
    }
  else
    {
      idata_init (&idata, sysex_transfer_steal (&sysex_transfer), NULL, NULL,
		  NULL);
      err = file_save (dst_file, &idata, NULL);
      idata_clear (&idata);
    }

  return err;
}

static gint
cli_play (int argc, gchar *argv[], int *optind)
{
  gint err;
  struct idata sample;
  const gchar *audio_file;
  struct sample_info sample_info_src;
  struct sample_load_opts sample_load_opts;

  if (*optind == argc)
    {
      error_print ("Source file missing");
      return EXIT_FAILURE;
    }
  else
    {
      audio_file = argv[*optind];
      (*optind)++;
    }

  controllable_set_active (&task_control.controllable, TRUE);
  task_control.callback = print_progress;
  current_path_progress = audio_file;

  task_control_reset (&task_control, 1);

  sample_load_opts_init (&sample_load_opts, 2, audio.rate,
			 sample_get_internal_format (), FALSE);

  err = sample_load_from_file (audio_file, &sample, NULL, &sample_load_opts,
			       &sample_info_src);
  if (err)
    {
      error_print ("Error while loading '%s'.", audio_file);
    }
  else
    {
      audio_init_and_wait ();
      use_audio = TRUE;
      audio_set_play_and_wait (&sample, &task_control);
      audio_destroy ();
      task_control.part++;
    }

  complete_progress (err);

  return err;
}

static gint
cli_record (int argc, gchar *argv[], int *optind)
{
  const gchar *audio_file;

  if (*optind == argc)
    {
      error_print ("Source file missing");
      return EXIT_FAILURE;
    }
  else
    {
      audio_file = argv[*optind];
      (*optind)++;
    }

  controllable_set_active (&task_control.controllable, TRUE);
  task_control.callback = print_progress;
  current_path_progress = audio_file;

  task_control_reset (&task_control, 1);

  audio_init_and_wait ();
  use_audio = TRUE;
  audio_record_and_wait (RECORD_STEREO, &task_control);
  audio_destroy ();

  task_control.part++;
  complete_progress (0);

  sample_save_to_file (audio_file, &audio.sample, NULL,
		       SF_FORMAT_WAV | sample_get_internal_format ());

  return 0;
}

#if defined(__linux__)
static void
cli_end (int sig)
{
  controllable_set_active (&controllable, FALSE);
  controllable_set_active (&task_control.controllable, FALSE);
  if (use_audio)
    {
      audio_stop_playback ();
      audio_stop_recording ();
    }
}
#endif

static void
cli_print_help_cmd (const gchar *cmd, const gchar *args, const gchar *desc)
{
  fprintf (stderr, "  %s%s%s%s%s: %s\n", connected_to_tty ? "\033[1m" : "",
	   cmd, connected_to_tty ? "\033[0m" : "", args ? " " : "",
	   args ? args : "", desc);
}

static void
cli_print_help (const gchar *argv0)
{
  gchar *exec_name = g_path_get_basename (argv0);
  fprintf (stderr, "%s\n", PACKAGE_STRING);
  fprintf (stderr, "Usage: %s [ -v ] command\n", exec_name);
  fprintf (stderr, "\n");
  fprintf (stderr, "Device commands:\n");
  cli_print_help_cmd ("ld", NULL, "List devices");
  cli_print_help_cmd ("info", "device_number", "Show device info");
  cli_print_help_cmd ("df", "device_number",
		      "Show size and use of the available storages");
  cli_print_help_cmd ("send", "file device_number",
		      "Send MIDI data file to device");
  cli_print_help_cmd ("receive", "device_number file",
		      "Receive MIDI data file from device");
  cli_print_help_cmd ("upgrade", "firmware device_number",
		      "Upgrade the device");
  cli_print_help_cmd ("play", "file", "Play audio file");
  cli_print_help_cmd ("record", "file", "Record into file");
  fprintf (stderr, "\n");
  fprintf (stderr,
	   "Filesystem commands take the form connector:filesystem:operation parameters\n");
  fprintf (stderr, "Filesystem operations:\n");
  cli_print_help_cmd ("ls", "device_number:path_to_directory",
		      "List directory");
  cli_print_help_cmd ("mkdir", "device_number:path_to_directory",
		      "Create directory and its parent directories as needed");
  cli_print_help_cmd ("rmdir", "device_number:path_to_directory",
		      "Delete directory recursively");
  cli_print_help_cmd ("ul", "file device_number:path_to_file_or_directory",
		      "Upload file");
  cli_print_help_cmd ("dl",
		      "device_number:path_to_file_or_directory [ destination ]",
		      "Download file");
  cli_print_help_cmd ("rdl",
		      "device_number:path_to_file_or_directory [ destination ]",
		      "Download directory recursively");
  cli_print_help_cmd ("mv",
		      "device_number:path_to_file_or_directory device_number:path_to_file_or_directory",
		      "Move file");
  cli_print_help_cmd ("rm", "device_number:path_to_file", "Delete file");
  cli_print_help_cmd ("cl", "device_number:path_to_file", "Clear file");
  cli_print_help_cmd ("cp",
		      "device_number:path_to_file device_number:path_to_file",
		      "Copy file");
  cli_print_help_cmd ("sw",
		      "device_number:path_to_file device_number:path_to_file",
		      "Swap files");
  g_free (exec_name);
}

int
main (int argc, gchar *argv[])
{
  gint c;
  gint err;
  gchar *command;
  gint vflg = 0, errflg = 0;

  setup ();

  controllable_init (&controllable);
  controllable_init (&task_control.controllable);

#if defined(__linux__)
  struct sigaction action;

  action.sa_handler = cli_end;
  sigemptyset (&action.sa_mask);
  action.sa_flags = 0;
  sigaction (SIGTERM, &action, NULL);
  sigaction (SIGQUIT, &action, NULL);
  sigaction (SIGINT, &action, NULL);
  sigaction (SIGHUP, &action, NULL);
#endif

  while ((c = getopt (argc, argv, "v")) != -1)
    {
      switch (c)
	{
	case 'v':
	  vflg++;
	  break;
	case '?':
	  errflg++;
	}
    }

  if (optind == argc)
    {
      errflg = 1;
    }
  else
    {
      command = argv[optind];
      optind++;
    }

  if (vflg)
    {
      debug_level = vflg;
    }

  if (errflg > 0)
    {
      cli_print_help (argv[0]);
      exit (EXIT_FAILURE);
    }

  regconn_register ();
  regpref_register ();
  preferences_load ();
  preferences_set_boolean (PREF_KEY_MIX, FALSE);	//This might be required by devices using the audio link.

  if (!strcmp (command, "ld") || !strcmp (command, "list-devices"))
    {
      err = cli_ld ();
    }
  else if (!strcmp (command, "info") || !strcmp (command, "info-device"))
    {
      err = cli_info (argc, argv, &optind);
    }
  else if (!strcmp (command, "df") || !strcmp (command, "info-storage"))
    {
      err = cli_df (argc, argv, &optind);
    }
  else if (!strcmp (command, "send"))
    {
      err = cli_send (argc, argv, &optind);
    }
  else if (!strcmp (command, "receive"))
    {
      err = cli_receive (argc, argv, &optind);
    }
  else if (!strcmp (command, "upgrade"))
    {
      err = cli_upgrade_os (argc, argv, &optind);
    }
  else if (!strcmp (command, "play"))
    {
      err = cli_play (argc, argv, &optind);
    }
  else if (!strcmp (command, "record"))
    {
      err = cli_record (argc, argv, &optind);
    }
  else
    {
      err = command_set_parts (command, &connector, &fs, &op);
      if (err)
	{
	  goto end;
	}

      debug_print (1,
		   "Connector: \"%s\"; filesystem: \"%s\"; operation: \"%s\"",
		   connector, fs, op);

      if (!strcmp (op, "ls") || !strcmp (op, "list"))
	{
	  err = cli_list (argc, argv, &optind);
	}
      else if (!strcmp (op, "mkdir"))
	{
	  err = cli_command_path (argc, argv, &optind,
				  GET_FS_OPS_OFFSET (mkdir));
	}
      else if (!strcmp (op, "rm") || !strcmp (op, "rmdir"))
	{
	  err = cli_command_path (argc, argv, &optind,
				  GET_FS_OPS_OFFSET (delete));
	}
      else if (!strcmp (op, "download") || !strcmp (op, "dl"))
	{
	  err = cli_download (argc, argv, &optind, 0);
	}
      else if (!strcmp (op, "rdownload") || !strcmp (op, "rdl") ||
	       !strcmp (op, "backup"))
	{
	  err = cli_download (argc, argv, &optind, 1);
	}
      else if (!strcmp (op, "upload") || !strcmp (op, "ul"))
	{
	  err = cli_upload (argc, argv, &optind);
	}
      else if (!strcmp (op, "cl"))
	{
	  err = cli_command_path (argc, argv, &optind,
				  GET_FS_OPS_OFFSET (delete));
	}
      else if (!strcmp (op, "cp"))
	{
	  err = cli_command_src_dst (argc, argv, &optind,
				     GET_FS_OPS_OFFSET (copy));
	}
      else if (!strcmp (op, "sw"))
	{
	  err = cli_command_src_dst (argc, argv, &optind,
				     GET_FS_OPS_OFFSET (swap));
	}
      else if (!strcmp (op, "mv"))
	{
	  err = cli_command_mv_rename (argc, argv, &optind);
	}
      else
	{
	  error_print ("Command '%s' not recognized", command);
	  cli_print_help (argv[0]);
	  err = EXIT_FAILURE;
	}

      if (backend_check (&backend))
	{
	  backend_destroy (&backend);
	}

      g_free (connector);
      g_free (fs);
      g_free (op);
    }

end:
  if (err && err != EXIT_FAILURE && err != -ECANCELED)
    {
      error_print ("Error: %s", g_strerror (-err));
    }

  controllable_clear (&controllable);

  regconn_unregister ();
  regpref_unregister ();

  g_usleep (BE_REST_TIME_US * 2);
  return err ? EXIT_FAILURE : EXIT_SUCCESS;
}
