/*
 *   phatty.c
 *   Copyright (C) 2022 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 "phatty.h"
#include "common.h"
#include "scala.h"

#define PHATTY_ALPHABET " ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz!#$%&()*?@"
#define PHATTY_DEFAULT_CHAR '?'

#define PHATTY_MAX_PRESETS (100 + 1)
#define PHATTY_PROGRAM_SIZE 193

#define PHATTY_PRESET_TYPE_OFFSET 3	// 0x04 is panel; 0x05 is preset
#define PHATTY_PRESET_ID_OFFSET 5

#define MOOG_NAME_LEN 13

#define PHATTY_PRESETS_DIR "/presets"
#define PHATTY_PANEL_ID 0x100
#define PHATTY_MAX_SCALES 32

static const gchar *PHATTY_SCALE_EXTS[] = { "scl", NULL };

static const guint8 FAMILY_ID[] = { 0x0, 0x5 };
static const guint8 MODEL_ID[] = { 0x0, 0x1 };

static const guint8 PHATTY_REQUEST_PANEL[] =
  { 0xf0, 4, 5, 6, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xf7 };
static const guint8 PHATTY_REQUEST_PRESET[] =
  { 0xf0, 4, 5, 6, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xf7 };

struct phatty_iter_data
{
  guint next;
  struct backend *backend;
};

enum phatty_fs
{
  FS_PHATTY_PRESET,
  FS_PHATTY_SCALE
};

static gchar
phatty_decode_char (guint8 *data, gint position)
{
  gint index;
  gint k = (3 * (position / 2)) + 23;
  if (position % 2 == 0)
    {
      index = ((data[k] & 0x1) << 6) | (data[k + 1] & 0x3f);
    }
  else
    {
      index = ((data[k + 2] & 0x3) << 4) | ((data[k + 3] & 0x3c) >> 2);
    }
  if (index >= strlen (PHATTY_ALPHABET))
    {
      return PHATTY_DEFAULT_CHAR;
    }
  else
    {
      return PHATTY_ALPHABET[index];
    }
}

static void
phatty_encode_char (guint8 *data, gchar c, gint position)
{
  gchar *s = PHATTY_ALPHABET;
  gint k, index = 0;
  while (*s != 0 && *s != c)
    {
      s++;
      index++;
    }
  if (!*s)
    {
      index = 0;
    }
  // Code adapted from https://gitlab.com/jp-ma/phatty-editor/blob/master/libphatty/phatty-fmt.x
  k = (3 * (position / 2)) + 23;
  if (position % 2 == 0)
    {
      data[k] &= ~0x1;
      data[k] |= (index >> 6) & 0x01;
      data[k + 1] &= ~0x3f;
      data[k + 1] |= index & 0x3f;
    }
  else
    {
      data[k + 2] &= ~0x3;
      data[k + 2] |= (index >> 4) & 0x7;
      data[k + 3] &= ~0x3c;
      data[k + 3] |= (index & 0xf) << 2;
    }
}

void
phatty_set_preset_name (guint8 *preset, const gchar *preset_name)
{
  gint i;
  gchar *sanitized = common_get_sanitized_name (preset_name, PHATTY_ALPHABET,
						PHATTY_DEFAULT_CHAR);
  const gchar *c = sanitized;
  for (i = 0; i < strlen (preset_name); i++, c++)
    {
      phatty_encode_char (preset, *c, i);
    }
  for (; i < MOOG_NAME_LEN; i++, c++)
    {
      phatty_encode_char (preset, ' ', i);
    }
  g_free (sanitized);
}

void
phatty_get_preset_name (guint8 *preset, gchar *preset_name)
{
  gchar *c = preset_name;
  for (gint i = 0; i < MOOG_NAME_LEN; i++, c++)
    {
      *c = phatty_decode_char (preset, i);
    }
  *c = 0;
  c--;
  for (gint i = MOOG_NAME_LEN; i > 0; i--, c--)
    {
      if (*c == ' ')
	{
	  *c = 0;
	}
      else
	{
	  break;
	}
    }
}

static gchar *
phatty_get_download_path (struct backend *backend,
			  const struct fs_operations *ops,
			  const gchar *dst_dir, const gchar *src_path,
			  struct idata *preset)
{

  guint id = 0;
  if (common_slot_get_id_from_path (src_path, &id))
    {
      return NULL;
    }
  guint digits = id == PHATTY_PANEL_ID ? 0 : 2;
  return common_slot_get_download_path (backend, ops, dst_dir, src_path,
					preset, digits);
}

static GByteArray *
phatty_get_panel_dump_msg ()
{
  GByteArray *tx_msg = g_byte_array_sized_new (sizeof (PHATTY_REQUEST_PANEL));
  g_byte_array_append (tx_msg, PHATTY_REQUEST_PANEL,
		       sizeof (PHATTY_REQUEST_PANEL));
  return tx_msg;
}

static GByteArray *
phatty_get_preset_dump_msg (guint8 id)
{
  GByteArray *tx_msg =
    g_byte_array_sized_new (sizeof (PHATTY_REQUEST_PRESET));
  g_byte_array_append (tx_msg, PHATTY_REQUEST_PRESET,
		       sizeof (PHATTY_REQUEST_PRESET));
  tx_msg->data[PHATTY_PRESET_ID_OFFSET] = id;
  return tx_msg;
}

static gint
phatty_next_preset_dentry (struct item_iterator *iter)
{
  gchar preset_name[MOOG_NAME_LEN + 1];
  GByteArray *tx_msg, *rx_msg;
  struct phatty_iter_data *data = iter->data;

  if (data->next >= PHATTY_MAX_PRESETS)
    {
      return -ENOENT;
    }

  if (data->next + 1 == PHATTY_MAX_PRESETS)
    {
      item_set_name (&iter->item, "%s", COMMON_PANEL_NAME);
      iter->item.id = PHATTY_PANEL_ID;
      iter->item.slot[0] = 0;
      iter->item.type = ITEM_TYPE_FILE;
      iter->item.size = PHATTY_PROGRAM_SIZE;
      (data->next)++;
    }
  else
    {
      tx_msg = phatty_get_preset_dump_msg (data->next);
      rx_msg = backend_tx_and_rx_sysex (data->backend, tx_msg, -1);
      if (!rx_msg)
	{
	  return -EIO;
	}
      phatty_get_preset_name (rx_msg->data, preset_name);
      item_set_name (&iter->item, "%s", preset_name);
      iter->item.id = data->next;
      common_slot_set_slot_padded (&iter->item, 2);
      iter->item.type = ITEM_TYPE_FILE;
      iter->item.size = PHATTY_PROGRAM_SIZE;
      (data->next)++;

      free_msg (rx_msg);
    }

  return 0;
}

static gint
phatty_read_dir (struct backend *backend, struct item_iterator *iter,
		 const gchar *dir, const gchar **extensions)
{
  if (!strcmp (dir, "/"))
    {
      struct phatty_iter_data *data =
	g_malloc (sizeof (struct phatty_iter_data));
      data->next = 0;
      data->backend = backend;
      item_iterator_init (iter, dir, data, phatty_next_preset_dentry, g_free);
      return 0;
    }
  else
    {
      return -ENOTDIR;
    }
}

static gint
phatty_download (struct backend *backend, const gchar *path,
		 struct idata *preset, struct task_control *control)
{
  guint id;
  gint err = 0;
  gboolean panel;
  GByteArray *tx_msg, *rx_msg;
  gchar name[MOOG_NAME_LEN + 1];

  err = common_slot_get_id_from_path (path, &id);
  if (err)
    {
      return err;
    }

  if (id == PHATTY_PANEL_ID)
    {
      tx_msg = phatty_get_panel_dump_msg ();
      panel = TRUE;
    }
  else
    {
      if (id >= PHATTY_MAX_PRESETS)
	{
	  return -EINVAL;
	}
      tx_msg = phatty_get_preset_dump_msg (id);
      panel = FALSE;
    }

  err = common_data_tx_and_rx (backend, tx_msg, &rx_msg, control);
  if (err)
    {
      return err;
    }
  if (rx_msg->len != PHATTY_PROGRAM_SIZE)
    {
      err = -EINVAL;
      goto cleanup;
    }

  if (!panel)
    {
      phatty_get_preset_name (rx_msg->data, name);
    }
  idata_init (preset, rx_msg, strdup (panel ? COMMON_PANEL_NAME : name), NULL,
	      NULL);
  return 0;

cleanup:
  free_msg (rx_msg);
  return err;
}

static gint
phatty_upload (struct backend *backend, const gchar *path,
	       struct idata *preset, struct task_control *control)
{
  gint err;
  guint id;

  if (preset->content->len != PHATTY_PROGRAM_SIZE)
    {
      return -EINVAL;
    }

  err = common_slot_get_id_from_path (path, &id);
  if (err)
    {
      return err;
    }

  if (id == PHATTY_PANEL_ID)
    {
      preset->content->data[PHATTY_PRESET_TYPE_OFFSET] = 0x04;
      preset->content->data[PHATTY_PRESET_ID_OFFSET] = 0x01;
    }
  else
    {
      if (id >= PHATTY_MAX_PRESETS)
	{
	  return -EINVAL;
	}

      preset->content->data[PHATTY_PRESET_TYPE_OFFSET] = 0x05;
      preset->content->data[PHATTY_PRESET_ID_OFFSET] = id;
    }

  return common_data_tx (backend, preset->content, control);
}

static gint
phatty_rename (struct backend *backend, const gchar *src, const gchar *dst)
{
  guint id;
  gint err;
  struct task_control control;
  struct sysex_transfer transfer;
  struct idata preset;

  debug_print (1, "Renaming preset...");
  err = common_slot_get_id_from_path (src, &id);
  if (err)
    {
      return err;
    }

  controllable_init (&control.controllable);
  control.callback = NULL;

  err = phatty_download (backend, src, &preset, &control);
  if (err)
    {
      goto end;
    }

  phatty_set_preset_name (preset.content->data, dst);
  sysex_transfer_init_tx (&transfer, idata_steal (&preset));
  err = backend_tx_sysex (backend, &transfer, NULL);
  sysex_transfer_clear (&transfer);

end:
  controllable_clear (&control.controllable);
  return err;
}

static const struct fs_operations FS_PHATTY_PRESET_OPERATIONS = {
  .id = FS_PHATTY_PRESET,
  .options = FS_OPTION_SINGLE_OP | FS_OPTION_SLOT_STORAGE |
    FS_OPTION_SHOW_SIZE_COLUMN | FS_OPTION_SHOW_SLOT_COLUMN |
    FS_OPTION_ALLOW_SEARCH,
  .name = "preset",
  .gui_name = "Presets",
  .gui_icon = FS_ICON_PRESET,
  .file_icon = FS_ICON_PRESET,
  .max_name_len = MOOG_NAME_LEN,
  .readdir = phatty_read_dir,
  .rename = phatty_rename,
  .download = phatty_download,
  .upload = phatty_upload,
  .load = common_file_load,
  .save = common_file_save,
  .get_exts = common_sysex_get_extensions,
  .get_upload_path = common_slot_get_upload_path,
  .get_download_path = phatty_get_download_path,
  .select_item = common_midi_program_change
};

static gint
phatty_scale_read_dir (struct backend *backend, struct item_iterator *iter,
		       const gchar *dir, const gchar **extensions)
{
  struct common_simple_read_dir_data *data;

  if (strcmp (dir, "/"))
    {
      return -ENOTDIR;
    }

  data = g_malloc (sizeof (struct common_simple_read_dir_data));
  data->next = 0;
  data->last = PHATTY_MAX_SCALES - 1;

  item_iterator_init (iter, dir, data, common_simple_next_dentry, g_free);

  return 0;
}

static gint
phatty_scale_upload (struct backend *backend, const gchar *path,
		     struct idata *scale, struct task_control *control)
{
  guint id;
  GByteArray *input = scale->content;

  if (common_slot_get_id_from_path (path, &id))
    {
      return -EINVAL;
    }

  input->data[5] = 0;		//bank
  input->data[6] = id;		//scale

  return common_data_tx (backend, input, control);
}

static const gchar **
phatty_scale_get_extensions (struct backend *backend,
			     const struct fs_operations *ops)
{
  return PHATTY_SCALE_EXTS;
}

static const struct fs_operations FS_PHATTY_SCALE_OPERATIONS = {
  .id = FS_PHATTY_SCALE,
  .options = FS_OPTION_SINGLE_OP | FS_OPTION_SLOT_STORAGE |
    FS_OPTION_SHOW_SLOT_COLUMN,
  .name = "scale",
  .gui_name = "Scales",
  .gui_icon = FS_ICON_KEYS,
  .file_icon = FS_ICON_KEYS,
  .readdir = phatty_scale_read_dir,
  .upload = phatty_scale_upload,
  .load = common_load_2_byte_octave_tuning_msg,
  .get_exts = phatty_scale_get_extensions,
  .get_upload_path = common_slot_get_upload_path
};

static gint
phatty_handshake (struct backend *backend)
{
  if (memcmp (backend->midi_info.company, MOOG_ID, sizeof (MOOG_ID)) ||
      memcmp (backend->midi_info.family, FAMILY_ID, sizeof (FAMILY_ID)) ||
      memcmp (backend->midi_info.model, MODEL_ID, sizeof (MODEL_ID)))
    {
      return -ENODEV;
    }

  gslist_fill (&backend->fs_ops, &FS_PHATTY_PRESET_OPERATIONS,
	       &FS_PHATTY_SCALE_OPERATIONS, NULL);
  snprintf (backend->name, LABEL_MAX, "Moog Little Phatty");

  return 0;
}

const struct connector CONNECTOR_PHATTY = {
  .name = "phatty",
  .handshake = phatty_handshake,
  .type = CONNECTOR_TYPE_MIDI,
  .regex = ".*Phatty.*"
};
