/*
  !MidiPlay   A MIDI synthesiser and file player.

  command.c - Command input and output

  created  19/01/10
  1.0      14/06/14
  modified for Risc OS version 2022
*/

#include "main.h"
#include "kbd.h"
#include "player.h"
#include "filters.h"
#include "midisyn.h"
#include "editor.h"
#include "kbd_dsplay.h"
#include "wimp.h"
#include "lib.h"
#include "ro_main.h"

// error messages, these must agree with the enumerated list err_msg_e in main.h
static const char * const err_msg[] =
{
  "out of range",
  "invalid parameters",
  "unrecognised command",
  "invalid volume",
  "invalid sector",
  "invalid size",
  "invalid address",
  "invalid data",
  "invalid gain",
  "invalid delay",
  "invalid rate",
  "invalid routing",
  "invalid value",
  "instrument not defined",
  "no sequence data",
  "no header",
  "invalid header length",
  "unsupported format",
  "invalid number of tracks",
  "file too large",
  "problem reading file",
  "incomplete song",
  "problem opening file",
  "midi initialisation error",
  "midi termination error",
  "no file loaded",
  "no extension",
  "no match",
  "no kit",
  "no bank",
  "no instrument",
  "too many instruments",
  "too many banks",
  "too many waves",
  "unknown heading",
  "not editable",
  "invalid crc",
  "too many files",
  "cannot allocate memory",
  "program has not been initialised",
  "invalid device",
  "no connected device",
  "too many patches",
  "unsupported",
  "Instrument must have a name",
  "Invalid Instrument number",
  "Instrument deleted",
  "Instruments use this waveform, it cannot be deleted",
  "Waveform number out of range",
  "Waveform data not provided"
};

const char offon[2][4] = {"off","on"};

void display_error(int err, char *info);
extern const char sw_version[]; // version.c
int cmd_sleep(char *msg, int len); // ro_audio.c



/*
 * cmd_asm
 * -------
 * Displays equate values required by the assembler synth code
 * (for code development use)
 */
/*
static int cmd_asm(char *msg, int len)
{
  if(len == 2)
    if(msg[1] == 'e')
      myprintf("SEED %d\nGEN %d\nGEN_SIZE %d\n",
               (int)&syn.seed - (int)&syn,
               (int)&syn.gen - (int)&syn,
               sizeof(gen_t));

  return NO_ERROR_;
}
*/

/*
 * cmd_rate
 * --------
 * Sets the sample rate
 */
#define MIN_RATE 20000
#define MAX_RATE 100000
static int cmd_rate(char *msg, int len)
{
  int rate;

  if(msg[0] == '?') // display help
    myprintf(" Sets the sample rate.\n"
           "  ,<rate>  Set the sample rate, %d to %d\n", MIN_RATE, MAX_RATE);

  else if(len == 0) // display status
    myprintf("Sample Rate = %d\n", syn.sample_rate);

  else if(msg[0] == '!') // report for controller
    myprintf("RATE,%d\n", syn.sample_rate);

  else if(sscanf(msg, ",%d", &rate) != 1)
    return -PARAMETER_ERROR;

  else if((rate < MIN_RATE) || (rate > MAX_RATE))
    return -OUT_OF_RANGE;

  else
  {
    calculate_tables(rate);
    ro.audio->init();
    init_filters();
    restart_player();
  }

  return NO_ERROR_;
}


/*
 * cmd_audio
 * ---------
 */
int cmd_audio(char *msg, int len)
{
  if(len == 0) // display status
  {
    myprintf("Audio status:\n"
           "  %s%s\n",
           (syn.switches & (1<<MONO_AUDIO)) ? "mono" : "stereo",
           (syn.switches & (1<<L_R_SWAP)) ? ", L/R swapped" : "");
  }

  else if(msg[0] == '?') // display help
    myprintf(" Sets and displays the Audio Controls.\n"
           "  ,M,<value>  set mono switch     0=stereo, 1=mono\n"
           "  ,S,<value>  set swap L/R switch 0=normal, 1=swapped\n");

  else if(msg[0] == '!') // report for controller
  {
    myprintf("AUDIO,M,%d\nAUDIO,S,%d\n",
         (syn.switches >> MONO_AUDIO) & 1,
         (syn.switches >> L_R_SWAP) & 1);
  }

  else
  {
    int n = 0;

    if(len > 3)
      n = atoi(msg+3);

    if((n < 0) || (n > 1))
      return -OUT_OF_RANGE;

    switch(msg[1])
    {
      case 'M': case 'm': // mono
        syn.switches = (syn.switches & ~(1<<MONO_AUDIO))   | ((n & 1)<<MONO_AUDIO);
        midiPlayer_display(ITEM_MONO, &n);
        break;

      case 'S': case 's': // L/R swap
        syn.switches = (syn.switches & ~(1<<L_R_SWAP))   | ((n & 1)<<L_R_SWAP);
        midiPlayer_display(ITEM_SWAP, &n);
        break;

      default:
        return -PARAMETER_ERROR;
    }
  }

  return NO_ERROR_;
}


/*
 * display_patch
 * -------------
 */
static void display_patch(int n)
{
  chan_t *c = &syn.chan[n];

  myprintf("Channel %d, Program %d, Bank %d:%d",
         (n & (NUM_MIDI_CHANS-1)) + 1, c->patch.prg, c->patch.hi, c->patch.lo);

  if((n & (NUM_MIDI_CHANS-1)) == 9)
    myprintf(", percussion");
  else
  {
    int b = syn.idat.banks[c->patch.lo];
    if(b)
    {
      int i = syn.idat.bank[b-1].ins[c->patch.prg];
      if(i)
        myprintf(", %s", syn.idat.instrument[i-1].name);
    }
  }
  myprintf("\n");
}


/*
 * cmd_map
 * -------
 * Displays a channel patch map
 */
static int cmd_map(char *msg, int len)
{
  int i;

  if(len == 0)
    for(i=0; i<NUM_MIDI_CHANS; i++)
      display_patch(i);

  else if(msg[0] == '?') // display help
    myprintf(" Displays the channel patch for all MIDI channels\n");

  return NO_ERROR_;
}


/*
 * cmd_volume
 * ----------
 * Sets the master volume
 */
#define MIN_VOL 0
#define MAX_VOL 1023
static int cmd_volume(char *msg, int len)
{
  int vol;

  if(msg[0] == '?') // display help
    myprintf(" Sets the master volume.\n"
           "  ,<vol>  Set the volume, %d to %d\n", MIN_VOL, MAX_VOL);

  else if(len == 0) // display status
    myprintf("Volume = %d\n", syn.master_volume);

  else if(msg[0] == '!') // report for controller
    myprintf("VOLUME,%d\n", syn.master_volume);

  else if(sscanf(msg, ",%d", &vol) != 1)
    return -PARAMETER_ERROR;

  else
  {
    if(vol < MIN_VOL)
      vol = MIN_VOL;
    else if(vol > MAX_VOL)
      vol = MAX_VOL;
    syn.master_volume = vol;
    midiPlayer_display(ITEM_VOLUME, &vol);
  }

  return NO_ERROR_;
}


/*
 * cmd_balance
 * -----------
 * Sets the left right channel balance
 */
#define RANGE 20
extern const unsigned short int pantab[MIDI_DATA_RANGE];
static int cmd_balance(char *msg, int len)
{
  int bal;

  if(len == 0) // display status
    myprintf("Balance = %d\n", syn.balance);

  else if(msg[0] == '?') // display help
    myprintf(" Sets the left right channel balance.\n"
           "  ,<bal>  Set the balance, -%d to +%d\n", RANGE, RANGE);

  else if(msg[0] == '!') // report for controller
    myprintf("BALANCE,%d\n", syn.balance);

  else if(sscanf(msg, ",%d", &bal) != 1)
    return -PARAMETER_ERROR;

  else
  {
    if(bal < -RANGE)
      bal = -RANGE;
    else if(bal > RANGE)
      bal = RANGE;
    syn.balance = bal;
    syn.lpan = pantab[((RANGE + bal) * 127) / (2*RANGE)];
    syn.rpan = pantab[((RANGE - bal) * 127) / (2*RANGE)];
    midiPlayer_display(ITEM_BALANCE, &bal);
  }

  return NO_ERROR_;
}


/*
 * cmd_debug
 * ---------
 */
static int cmd_debug(char *msg, int len)
{
  int data;

  if(msg[0] == '?') // display help
    myprintf(" Sets, and reports the debug flags\n"
           "  ,<flags>  set flags to a combination of the following\n"
           "     hex flag values.\n"
           "      1 = MIDI messages\n"
           "      2 = Generator stack status\n"
           "      4 = MIDI calculations\n"
           "      8 = Portamento control\n"
           "     10 = Received commands\n"
           "     20 = Keyboard activity\n"
           "     40 = Log to file in addition to console\n"
           " Power up default is all flags off.\n");

  else if(len == 0) // display status
    myprintf("Debug flags = %08X\n", mpg.debug);

  else if(msg[0] == '!') // report for controller
    myprintf("DEBUG,%X\n", mpg.debug);

  else if(sscanf(msg, ",%X", &data) != 1)
    return -PARAMETER_ERROR;

  else
    mpg.debug = data;

  return NO_ERROR_;
}


/*
 * cmd_version
 * -----------
 */
static int cmd_version(char *msg, int len)
{
  if(len != 0)
  {
    if(msg[0] == '?') // display help
    {
      myprintf(" Displays the software version and build details.\n");
      return NO_ERROR_;
    }
    else if(msg[0] == '!') // controller report only requires s/w version
    {
      myprintf("VERSION,%s\n", sw_version);
      return NO_ERROR_;
    }
  }

  // display software version and build configuration
  myprintf("%s\n", sw_version);

  if(len != 0)
    myprintf("Build configuration:\n"
             " %d note polyphony.\n"
             " A5 frequency %dHz\n",
             NUM_GENS,
             FREQ_A5);

  return NO_ERROR_;
}


/*
 * Command table
 * -------------
 * List of commands and their associated handlers.
 * Commands can be abbreviated. The first string that matches
 * starting from the list top will be executed .
 */
typedef struct cmds_s
{
  char *name;
  int (* fn)(char *msg, int len);
} cmds_t;

static const cmds_t cmds[] =
{
//  command      function              file
//  --------     ------------          ---------
//  {"ASM",        cmd_asm        },  // command.c
  {"AUDIO",      cmd_audio      },  // command.c
  {"BALANCE",    cmd_balance    },  // command.c
  {"BANK",       cmd_bank,      },  // editor.c
  {"BUFFER",     cmd_buffer,    },  // ssound.c
  {"CHANNEL",    cmd_channel,   },  // editor.c
  {"CHORUS",     cmd_chorus     },  // filters.c
  {"CONVERT",    cmd_convert    },  // midi_player.c
  {"DEBUG",      cmd_debug      },  // command.c
  {"ECHO",       cmd_echo       },  // filters.c
  {"FILTERS",    cmd_filters    },  // filters.c
  {"FLANGER",    cmd_flanger    },  // filters.c
  {"GLIDE",      cmd_glide      },  // midi.c
  {"INSTRUMENT", cmd_instrument },  // editor.c
  {"INTERFACE",  cmd_interface  },  // ro_audio.c
  {"KEYBOARD",   cmd_keyboard,  },  // kbd_dsplay.c
  {"LOAD",       cmd_load       },  // midi_player.c
  {"MAP",        cmd_map        },  // command.c
  {"NOTE",       cmd_note       },  // kbd.c
  {"PITCH",      cmd_pitch      },  // midi_player.c
  {"PLAYER",     cmd_play       },  // midi_player.c
  {"POLY",       cmd_poly       },  // kbd.c
  {"PROGRAM",    cmd_program    },  // editor.c
  {"RATE",       cmd_rate       },  // command.c
  {"RESET",      cmd_reset      },  // kbd.c
  {"RESTORE",    cmd_restore    },  // store.c
  {"REVERB",     cmd_reverb     },  // filters.c
  {"SAVE",       cmd_save       },  // midi_player.c
  {"SLEEP",      cmd_sleep      },  // ro_audio.c
  {"STATUS",     cmd_status     },  // store.c
  {"STORE",      cmd_store      },  // store.c
  {"TEMPO",      cmd_tempo      },  // midi_player.c
  {"TONE",       cmd_tone       },  // filters.c
  {"USER",       cmd_user       },  // kbd.c
  {"VELOCITY",   cmd_velocity   },  // editor.c
  {"VERSION",    cmd_version    },  // command.c
  {"VOLUME",     cmd_volume     }   // command.c
};

#define NUM_CMDS (sizeof(cmds)/sizeof(cmds_t))


/*
 * cmd_help
 * --------
 * Lists the commands and a short description of each
 */
static int cmd_help(char *msg, int len)
{
  myprintf("\n"
         "Command List.\n"
         "Commands can be abbreviated. The first string that\n"
         "matches starting from the list top will be executed.\n\n"
         "?           List of commands.\n"
         "<command>?  Help for the command.\n"
         "??          Help for all commands.\n"
         "<command>   Status for the command, if applicable.\n"
         "?*          Status for all commands.\n"
         "<command>!  Report for the command, if applicable.\n"
         "?!          Report for all commands.\n\n");

  int i;
  for(i = 0; i < NUM_CMDS; i++)
  {
    myprintf("%s\n", cmds[i].name); // display command name
    // ignore any returned errors
    if(msg[1] == '?')
      cmds[i].fn("?", 1);  // command help
    else if(msg[1] == '*')
      cmds[i].fn("", 0);   // command status
    else if(msg[1] == '!')
      cmds[i].fn("!", 1);  // command report
    else
      continue;
    myprintf("\n");
  }
  return NO_ERROR_;
}


/*
 * cmd_select
 * ----------
 * Checks msg against a list of valid commands and executes the associated
 * function if one is found. Returns any error.
 */
#define ABBR_LEN  1  // minimum length for abbreviated matches
#define SEPARATOR(x) (((x)==',')||((x)=='?')||((x)=='!')) // field delimiters
static int cmd_select(char *msg, int len)
{
  if(msg[0] == '#') // ignore comment line
    return NO_ERROR_;

  if(msg[0] == '@') // print comment line
    return myprintf("%s\n", msg+1);

  if(msg[0] == '?') // print help
    return cmd_help(msg, len);

  int i, j;
  for(i = 0; i < NUM_CMDS; i++)
    for(j = 0; j >= 0;)
    {
      char c1 = cmds[i].name[j];
      char c2 = msg[j] & ~0x20; // make alpha characters upper case

      if((c1 != 0) && (c1 == c2)) // matching character
        j++; // continue checking this command
      else if(((c2 <= ' ')||SEPARATOR(c2)) && // separator
              ((c1 == 0) || (j >= ABBR_LEN))) // exact match, abbreviated match
        return cmds[i].fn(msg+j, len-j);
      else
        j = -1; // not this command
    }

  return -UNRECOGNISED;
}


/*
 * midiPlayer_cmd_in
 * -----------------
 * command stream input.
 * Builds up a command message to send to the interpreter. Commands
 * are terminated by any control character (0x00..0x1f)
 * Displays any errors that are returned.
 */
void midiPlayer_cmd_in(int c)
{
  #define BUF_LEN 1024 // maximum command length
  static char buff[BUF_LEN]; // command input buffer
  static int ix; // command input buffer index

  if(mpg.debug & (1<<DEBUG_CMDS)) // echo character
  {
    if(c >= ' ')
      myprintf("%c",c);
    else
      myprintf("\n");
  }

  if(c >= ' ') // not a control character
  {
    if(ix < BUF_LEN-1)
      buff[ix++] = c; // add character to message
  }
  else if(ix > 0) // control character, process message
  {
    buff[ix] = 0; // add terminator
    display_error(cmd_select(buff, ix), buff); // search for command function
    ix = 0;
  }
  // provide a line feed when return with no command is entered from the console
  else if(c == '\r')
    myprintf("\n");
}


// 32 bit hex printer. Converts unsigned int to max. 8 character ascii.
static void htoa(unsigned int n)
{
  unsigned int x;

  if((x = n >> 4) != 0) // divide number by base and check for zero
  {
    n &= 15; // create remainder
    htoa(x); // call again if quotient not zero
  }
  midiPlayer_cmd_in(n > 9 ? n + 'A' - 10 : n + '0'); // store remainder as character
}

// 32 bit decimal printer. Converts unsigned int to max. 10 character ascii.
static void uitoa(unsigned int n)
{
  unsigned int x;

  if((x = n / 10) != 0) // divide number by base and check for zero
  {
    n -= x * 10; // create remainder
    uitoa(x); // call again if quotient not zero
  }
  midiPlayer_cmd_in(n + '0'); // store remainder as character
}

/*
 * midiPlayer_cmds
 * ---------------
 * This is to make it easier for the calling program to create commands.
 * It is a reduced version of printf. Supported formatting specifiers
 * are %d %u %p %x %X %c %s
 */
void midiPlayer_cmds(const char *cmds, ...)
{
  char *p;
  int i;
  va_list a;
  va_start(a, cmds);

  for (;;)  // until full format string read
  {
    while ((i = *cmds++) != '%')  // until '%' or '\0'
    {
      midiPlayer_cmd_in(i); // copy format string character (including the '\0')
      if (!i)
      {
        va_end(a);
        return;
      }
    }

    switch (i = *cmds++)
    {
      case 'c': // single character
        i = va_arg(a, int);
      default: // unsupported format specifier
        midiPlayer_cmd_in(i);
        break;

      case 's': // string
        p = va_arg(a, char *);
        while ((i = *p++) != 0)
          midiPlayer_cmd_in(i);
        break;

      case 'd': // signed decimal number
        if ((i = va_arg(a, int)) < 0)
        {
          i = - i;
          midiPlayer_cmd_in('-');
        }
        uitoa(i);
        break;

      case 'u': // unsigned decimal number
        uitoa(va_arg(a, int));
        break;

      case 'p': case 'x': case 'X': // pointer or hex number
        htoa(va_arg(a, int));
        break;
    }
  }
}


/*
 * myprintf
 * --------
 * Redirects to the stdout provider and
 * optionally prints to log file or choices file.
 */
int myprintf(const char *format, ...)
{
  va_list arg;
  int n = 0;

  if(!mpg.log && (mpg.debug & (1<<DEBUG_LOG)))
  { // open log file
    if((mpg.log = fopen(APP_DIR".log", "w")) == NULL)
      myprintf("Cannot open log file\n");
  }
  else if(mpg.log && !(mpg.debug & (1<<DEBUG_LOG)))
  { // close log file
    fclose(mpg.log);
    mpg.log = 0;
  }
  if(mpg.log)
  { // print to log file
    va_start(arg, format);
    n = vfprintf(mpg.log, format, arg);
    va_end(arg);
  }

  va_start(arg, format);
  if(mpg.choices)
    n = vfprintf(mpg.choices, format, arg); // print to choices file
  else // redirect to stdout provider, but not when writing Choices
    n = midiPlayer_vprintf(format, arg);
  va_end(arg);

  return n;
}


/*
 * display_error
 * -------------
 * displays the given error number as text with
 * associated information if provided.
 */
void display_error(int err, char *info)
{
  if(err < NO_ERROR_)
  {
    if(err <= -NUM_ERRORS)
      myprintf("Unknown error code %d\n", err);
    else if(info)
      myprintf("Error, %s (%s)\n", err_msg[-err-1], info);
    else
      myprintf("Error, %s\n", err_msg[-err-1]);
  }
}

