#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <ctype.h>
// oslib
#include "os.h"
#include "wimp.h"
#include "hourglass.h"
#include "drawfile.h"
#include "plugin.h"

#include "midiutils.h"
#include "main.h"
#include "midi.h"


static char *voices, *map;
static int defaultvoice, playing, quit, nomime;

static int start_playback(byte *buffer, int size, int *duration);
static int load_midi_file(char *midifile, os_dynamic_area_no *area, byte **buffer, int *size);
static int init_wimp(int isplugin);
static int poll_standalone(void);
static int poll_plugin(void);


#define MAXOBJECTS   16

typedef struct OBJECT {
  os_dynamic_area_no area;
  byte *ptr;
  int size, status, helper;
  int duration, position, updatetime;
  os_box bbox;

  wimp_w window, parent_window;
  plugin_p plugin_handle;
  plugin_b browser_handle;
  wimp_t browser_task;
} OBJECT;

static OBJECT objects[MAXOBJECTS];
static wimp_w plugin_window;
#define MAXICONDATA           1000
static char icondata[MAXICONDATA];

#define PLUGINSTATUS_UNUSED             0
#define PLUGINSTATUS_OPENING            1
#define PLUGINSTATUS_PLAYING            2
#define PLUGINSTATUS_PAUSED             3

static drawfile_diagram *active_drawfile, *inactive_drawfile;
static int active_drawfile_size, inactive_drawfile_size;


static int plugin_open(wimp_message *block);
static int plugin_close(wimp_message *block);
static int plugin_reshape(wimp_message *block);
static int plugin_stream_as_file(wimp_message *block);
static int plugin_stream_new(wimp_message *block);
static int plugin_send_closed(int filei, int ref, int quitting);
static int find_instance(int a, int b, int what);
static void close_instance(int i);
static void set_current_instance(int inst);

#define NOT_PLAYING           -1


void render_drawfile(wimp_draw *redraw, drawfile_diagram *drawfile, int size) {

  os_trfm trfm;
  int scaling, scalex, scaley, x, y, wx, wy, dx, dy;
  wimp_window_state state;

  state.w = redraw->w;
  xwimp_get_window_state(&state);

  x = state.visible.x0 - state.xscroll;
  y = state.visible.y1 - state.yscroll;
  // calculate size of window
  wx = state.visible.x1 - state.visible.x0;
  wy = state.visible.y1 - state.visible.y0;
  // calculate size of drawfile
  dx = (drawfile->bbox.x1 - drawfile->bbox.x0)/256;
  dy = (drawfile->bbox.y1 - drawfile->bbox.y0)/256;
  scalex = (wx<<16)/dx;
  scaley = (wy<<16)/dy;
  if (scalex > scaley)
    scaling = scaley;
  else
    scaling = scalex;
  // don't enlarge too much
  if (scaling > 1.5*(1<<16))   scaling = 1.5*(1<<16);
  // center, by adding half of the difference between the
  // window size and the drawfile size
  x += (wx-dx*scaling/(1<<16))/2;
  y += (wy-dy*scaling/(1<<16))/2;

  trfm.entries[0][0] = scaling;
  trfm.entries[0][1] = 0;
  trfm.entries[1][0] = 0;
  trfm.entries[1][1] = scaling;
  trfm.entries[2][0] = x*256;
  trfm.entries[2][1] = y*256;
  xdrawfile_render(0, drawfile, size, &trfm, 0, 0);
}


drawfile_diagram *load_drawfile(char *filename, int *size) {

  FILE *fh;
  drawfile_diagram *p;

  fh = fopen(filename, "rb");
  if (!fh)   return NULL;
  fseek(fh, 0, SEEK_END);
  *size = (int)ftell(fh);
  fseek(fh, 0, SEEK_SET);
  p = malloc(*size);
  if (!p) {
    fclose(fh);
    return NULL;
  }
  fread(p, 1, *size, fh);
  fclose(fh);

  return p;
}


int main(int argc, char *argv[]) {
// *MIDIPlay [switches] [<midifile>]
//  switches [-map <filename>]
//           [-voices <filename>]
//           [-default <internal>]
//           [-plugin]
//           [-nomime]
  char *midifile;
  int arg, i, isplugin;

  // parse arguments
  if (argc < 2)  exit(-1);

  midifile = map = voices = NULL;
  defaultvoice = 0;
  playing = NOT_PLAYING;
  isplugin = 0;
  nomime = 0;
  quit = 0;

  for (arg = 1; arg < argc; arg++) {
    if (strcmp(argv[arg], "-plugin") == 0) {
      isplugin = 1;
    } else if (strcmp(argv[arg], "-nomime") == 0) {
      nomime = 1;
    } else if (strcmp(argv[arg], "-map") == 0) {
      arg++;
      map = argv[arg];
    } else if (strcmp(argv[arg], "-voices") == 0) {
      arg++;
      voices = argv[arg];
    } else if (strcmp(argv[arg], "-default") == 0) {
      arg++;
      defaultvoice = atoi(argv[arg]);
    } else if (arg == argc-1)
      midifile = argv[arg];
  }

  for (i = 0; i < MAXOBJECTS; i++)   objects[i].status = PLUGINSTATUS_UNUSED;

  if (init_wimp(isplugin))  exit(-1);

  if (!isplugin) {
    // started as standalone player
    os_dynamic_area_no area;
    byte *buffer;
    int size;

    if (!midifile)  exit(-1);
    if (!load_midi_file(midifile, &area, &buffer, &size))
      if (!start_playback(buffer, size, NULL)) {
        playing = 1;
        while (!poll_standalone())  ;
      }
    if (playing != NOT_PLAYING)  midi_stop();
    xosdynamicarea_delete(area);

  } else {
    // started as plugin
    active_drawfile = load_drawfile("<MIDIPlayer$Dir>.Active", &active_drawfile_size);
    inactive_drawfile = load_drawfile("<MIDIPlayer$Dir>.InActive", &inactive_drawfile_size);
    while ((!quit) && (!poll_plugin()))   ;

    if (playing != NOT_PLAYING) {
      midi_stop();
      playing = NOT_PLAYING;
    }

    // tell browser that we're closing the instances
    for (i = 0; i < MAXOBJECTS; i++)
      if (objects[i].status != PLUGINSTATUS_UNUSED) {
        plugin_send_closed(i, i, 1);
        close_instance(i);
      }
  }

  xwimp_close_down(0);
}



int init_wimp(int isplugin) {

  int messages[] = { message_PLUG_IN_OPEN,
                     message_PLUG_IN_CLOSE,
                     message_PLUG_IN_RESHAPE,
                     message_PLUG_IN_STREAM_NEW,
                     message_PLUG_IN_STREAM_AS_FILE,
                     0 };

  if (xwimp_initialise(350, "MIDI Player", (struct wimp_message_list *)&messages, NULL, NULL))
     return 1;

  if (isplugin) {
    // load window
    struct wimp_window *window;
    char temp[2000];

    if (xwimp_open_template("<MIDIPlayer$Dir>.Templates"))  return 1;
    window = (struct wimp_window *)temp;
    if (xwimp_load_template(window, icondata, icondata+MAXICONDATA, (byte *)-1, "PlugIn", 0,
                            NULL, NULL, NULL)) {
      xwimp_close_template();
      return 1;
    }
    if (xwimp_create_window(window, &plugin_window)) {
      xwimp_close_template();
      return 1;
    }
    xwimp_close_template();
  }

  return 0;
}



int poll_standalone() {

  int position;
  wimp_block block;
  wimp_event_no reason;

  xwimp_poll(0, &block, NULL, &reason);

  switch (reason) {
  case wimp_NULL_REASON_CODE:
    if (playing == NOT_PLAYING)                             return 0;
    if (midi_poll(&position, NULL, NULL) != MIDI_OK) {
      playing = NOT_PLAYING;
      return 1;
    }
    break;

  case wimp_USER_MESSAGE:
  case wimp_USER_MESSAGE_RECORDED:
  case wimp_USER_MESSAGE_ACKNOWLEDGE:
    switch (block.message.action) {
    case message_QUIT:
      return 1;
      break;
    }
    break;
  }

  return 0;
}


int poll_plugin() {

  wimp_block block;
  wimp_event_no reason;

  if (playing == NOT_PLAYING)
    xwimp_poll(wimp_MASK_NULL, &block, NULL, &reason);
  else
    xwimp_poll(0, &block, NULL, &reason);

  switch (reason) {
  case wimp_NULL_REASON_CODE:
    if (playing != NOT_PLAYING) {
      if (midi_poll(&objects[playing].position, NULL, NULL) != MIDI_OK)  return 1;
      objects[playing].updatetime = clock();
    }
    break;

  case wimp_REDRAW_WINDOW_REQUEST:
    {
      bool more;
      int inst;

      inst = find_instance((int)block.redraw.w, 0, 2);
      xwimp_redraw_window(&block.redraw, &more);
      while (more) {
        if ((inst == playing) && (playing != NOT_PLAYING))
          render_drawfile(&block.redraw, active_drawfile, active_drawfile_size);
        else
          render_drawfile(&block.redraw, inactive_drawfile, inactive_drawfile_size);
        xwimp_get_rectangle(&block.redraw, &more);
      }
    }
    break;

  case wimp_MOUSE_CLICK:
    {
      int inst;
      inst = find_instance((int)block.pointer.w, 0, 2);
      if (inst >= 0) {
        // it was one of our windows!
        OBJECT *obj;
        wimp_i icn;

        icn = block.pointer.i;
        obj = objects+inst;
        if (inst != playing) {
          // if playing, stop playback
          if (playing != NOT_PLAYING)  midi_stop();
          playing = NOT_PLAYING;
          // attempt to play selected instance
          if (!start_playback(obj->ptr, obj->size, &obj->duration))
            set_current_instance(inst);
        }
      }
    }
    break;

  case wimp_USER_MESSAGE:
  case wimp_USER_MESSAGE_RECORDED:
  case wimp_USER_MESSAGE_ACKNOWLEDGE:
    switch (block.message.action) {
    case message_QUIT:
      return 1;
      break;

    case message_PLUG_IN_OPEN:
      plugin_open((wimp_message *)&block);
      break;

    case message_PLUG_IN_CLOSE:
      plugin_close((wimp_message *)&block);
      break;

    case message_PLUG_IN_RESHAPE:
      plugin_reshape((wimp_message *)&block);
      break;

    case message_PLUG_IN_STREAM_AS_FILE:
      plugin_stream_as_file((wimp_message *)&block);
      break;

    case message_PLUG_IN_STREAM_NEW:
      plugin_stream_new((wimp_message *)&block);
      break;
    }
    break;
  }

  return 0;
}



int load_midi_file(char *midifile, os_dynamic_area_no *area, byte **buffer, int *size) {

  FILE *fh;

  // read MIDI file
  fh = fopen(midifile, "rb");
  if (!fh)  return 1;
  fseek(fh, 0, SEEK_END);
  *size = (int)ftell(fh);
  fseek(fh, 0, SEEK_SET);

  if (xosdynamicarea_create(osdynamicarea_ALLOCATE_AREA, *size, (byte *)-1,
                            os_AREA_NO_USER_DRAG, *size, NULL, NULL, "MIDI Buffer",
                            area, buffer, NULL)) {
    fclose(fh);
    exit(-1);
  }
  fread(*buffer, 1, *size, fh);
  fclose(fh);
  return 0;
}



int start_playback(byte *buffer, int size, int *duration) {
// start playback of in-memory midi-file
  int dur;

  xhourglass_on();
  midi_init(map, voices, defaultvoice);
  if (midi_start(buffer, size, &dur) == MIDI_OK) {
    if (duration)   *duration = dur;
    xhourglass_off();
    return 0;
  }
  xhourglass_off();
  return 1;
}


char *read_string(char *p, int len) {

  char *out;

  out = malloc(len+1);
  if (!out)  return NULL;
  memcpy(out, p, len);
  out[len] = '\0';
  return out;
}


int read_parameter_file(char *filename, int *width, int *height, os_colour *bgcol, char *title, char *mimetype) {

  FILE *fh;
  char *buffer, *name, *mime, *data;
  int size, ii, *p;

  // read parameter file
  fh = fopen(filename, "rb");
  if (!fh)  return 1;
  fseek(fh, 0, SEEK_END);
  size = (int)ftell(fh);
  fseek(fh, 0, SEEK_SET);
  buffer = malloc(size+4);
  if (!buffer) {
    fclose(fh);
    return 1;
  }
  fread(buffer, 1, size, fh);
  fclose(fh);

  ii = 0;
  p = (int *)buffer;
  name = mime = data = NULL;
  while ((p[ii/4]) && (ii < size)) {
    int rtype, rlen, i, datalen;

    i = ii/4;

    // record header
    rtype = p[i+0];
    rlen = p[i+1];
    i += 2;

    // record name
    name = read_string((char *)&p[i+1], p[i+0]);
    i += 1 + (p[i+0]+3)/4;

    // data
    if (p[i]) {
      data = (char *)&p[i+1];
      datalen = p[i];
      i += 1 + (p[i+0]+3)/4;
    } else
      i++;

    // mime
    if (p[i]) {
      mime = read_string((char *)&p[i+1], p[i+0]);
      i += 1 + (p[i+0]+3)/4;
    } else
      i++;

    // parse data
    for (i = 0; i < strlen(name); i++)    name[i] = tolower(name[i]);
    if ((strcmp(name, "bgcolour") == 0) ||
        (strcmp(name, "bgcolor") == 0)) {
    } else if (strcmp(name, "width") == 0) {
    } else if (strcmp(name, "height") == 0) {
    } else if ((strcmp(name, "data") == 0) || (strcmp(name, "src") == 0)) {
      if (mime)  strcpy(mimetype, mime);
    }

    if (name)  free(name);
    if (mime)  free(mime);
    name = mime = data = NULL;

    ii = (ii+8+rlen+3) & ~3;
  }

  free(buffer);
  return 0;
}


void set_current_instance(int inst) {

  OBJECT *obj;
  int i;

  obj = objects+inst;
  // mark instance as playing
  obj->status = PLUGINSTATUS_PLAYING;
  // mark all other instances as not playing
  for (i = 0; i < MAXOBJECTS; i++)
    if ((i != inst) && (objects[i].status == PLUGINSTATUS_PLAYING)) {
      objects[i].status = PLUGINSTATUS_PAUSED;
      xwimp_force_redraw(objects[i].window, 0, 0, 4000, 4000);
    }

  playing = inst;
  xwimp_force_redraw(obj->window, 0, 0, 4000, 4000);
}


void close_instance(int inst) {

  OBJECT *obj;
  int n, i;

  obj = objects+inst;
  // delete window, release memory etc.
  xwimp_delete_window(obj->window);
  if (obj->area)  xosdynamicarea_delete(obj->area);
  obj->status = PLUGINSTATUS_UNUSED;

  // stop playback
  if (inst == playing) {
    midi_stop();
    playing = NOT_PLAYING;
  }

  // count no. of instances that are still loaded
  n = 0;
  for (i = 0; i < MAXOBJECTS; i++)
    if (objects[i].status != PLUGINSTATUS_UNUSED)  n++;
  if (n == 0)  quit = 1;
}


int find_instance(int a, int b, int what) {

  OBJECT *obj;
  int i;

  for (i = 0; i < MAXOBJECTS; i++) {
    obj = &objects[i];
    if (obj->status != PLUGINSTATUS_UNUSED) {
      switch (what) {
      case 0:
        if ( ((int)obj->plugin_handle == a) &&
             ((int)obj->browser_handle == b) )    return i;
        break;
      case 1:
        if ((int)obj->browser_handle == a)        return i;
        break;
      case 2:
        if ((int)obj->window == a)                return i;
        break;
      }
    }
  }

  return -1;
}


int plugin_open(wimp_message *block) {
// parse parameterfile, tell browser how we want the data, create/open window
  char *parameterfile;
  plugin_message_open *openmsg;
  plugin_message_opening *openingmsg;
  wimp_message msg;
  wimp_window_state state;
  wimp_window_info *info;
  wimp_window *window;
  wimp_open open;
  char temp[1000], mimetype[256], title[256];
  int newfile, x, y, width, height;
  os_colour bgcol;
  OBJECT *obj;

  newfile = 0;
  while ((objects[newfile].status != PLUGINSTATUS_UNUSED) && (newfile < MAXOBJECTS))
    newfile++;

  if (newfile == MAXOBJECTS)  return 1;
  obj = objects+newfile;

  openmsg = (plugin_message_open *)block->data.reserved;
  if (openmsg->filename.offset > 256)
    parameterfile = openmsg->filename.pointer;
  else
    parameterfile = (char *)openmsg + openmsg->filename.offset;

  mimetype[0] = '\0';
  if (read_parameter_file(parameterfile, &width, &height, &bgcol, title, mimetype))
    return 1;
  if (!nomime)
    if (strcmp(mimetype, "audio/midi"))      return 1;

  obj->area = NULL;
  obj->helper = 0;
  if (openmsg->flags & plugin_OPEN_AS_HELPER)  obj->helper = 1;
  obj->status = PLUGINSTATUS_OPENING;
  obj->browser_task = block->sender;
  obj->browser_handle = openmsg->browser;
  obj->plugin_handle = (plugin_p)clock();  // pseudo-unique
  obj->parent_window = openmsg->parent_window;
  obj->bbox.x0 = openmsg->bbox.x0;
  obj->bbox.y0 = openmsg->bbox.y0;
  obj->bbox.x1 = openmsg->bbox.x1;
  obj->bbox.y1 = openmsg->bbox.y1;

  openingmsg = (plugin_message_opening *)&msg.data.reserved;
  msg.size = 32;
  msg.your_ref = block->my_ref;
  msg.action = message_PLUG_IN_OPENING;
  openingmsg->flags = plugin_OPENING_WANTS_DATA_FETCHING +
                      plugin_OPENING_STILL_BUSY;
  if (obj->helper)  openingmsg->flags |= plugin_OPENING_HAS_HELPER_WINDOW;
  openingmsg->plugin = obj->plugin_handle;
  openingmsg->browser = obj->browser_handle;
  xwimp_send_message(wimp_USER_MESSAGE, &msg, obj->browser_task);

  state.w = obj->parent_window;
  xwimp_get_window_state(&state);
  x = state.visible.x0 - state.xscroll;
  y = state.visible.y1 - state.yscroll;

  info = (wimp_window_info *)temp;
  window = (wimp_window *)(temp+4);
  info->w = plugin_window;
  xwimp_get_window_info(info);
  if (!obj->helper)  window->flags &= ~wimp_WINDOW_TITLE_ICON;
  xwimp_create_window(window, &obj->window);
  open.w = obj->window;
  open.visible.x0 = x+obj->bbox.x0;
  open.visible.y0 = y+obj->bbox.y0;
  open.visible.x1 = x+obj->bbox.x1;
  open.visible.y1 = y+obj->bbox.y1;
  open.xscroll = open.yscroll = 0;
  open.next = wimp_TOP;
  xwimp_open_window_nested(&open, obj->parent_window, 0);

  return 0;
}


int plugin_send_closed(int filei, int ref, int quitting) {

  wimp_message msg;
  plugin_message_closed *closedmsg;
  OBJECT *obj;

  obj = objects+filei;
  closedmsg = (plugin_message_closed *)&msg.data.reserved;
  msg.size = 32;
  msg.your_ref = ref;
  msg.action = message_PLUG_IN_CLOSED;
  closedmsg->flags = 0;
  if (quitting)  closedmsg->flags = 1;
  closedmsg->plugin = obj->plugin_handle;
  closedmsg->browser = obj->browser_handle;
  xwimp_send_message(wimp_USER_MESSAGE, &msg, obj->browser_task);

  return 0;
}


int plugin_close(wimp_message *msg) {
// close instance upon request from browser
  plugin_message_close *closemsg;
  int inst;
  OBJECT *obj;

  closemsg = (plugin_message_close *)&msg->data.reserved;
  inst = find_instance((int)closemsg->plugin, (int)closemsg->browser, 0);
  if (inst == -1)   return 1;
  obj = objects+inst;
  plugin_send_closed(inst, msg->my_ref, 0);

  close_instance(inst);

  return 0;
}


int plugin_reshape(wimp_message *msg) {
// change window size/position on request from browser
  plugin_message_reshape *reshapemsg;
  int inst, x, y;
  wimp_window_state parent_state, plugin_state;
  wimp_open open;
  OBJECT *obj;

  reshapemsg = (plugin_message_reshape *)&msg->data.reserved;
  inst = find_instance((int)reshapemsg->plugin, (int)reshapemsg->browser, 0);
  if (inst == -1)   return 1;
  obj = objects+inst;

  obj->parent_window = reshapemsg->parent_window;
  obj->bbox.x0 = reshapemsg->bbox.x0;
  obj->bbox.y0 = reshapemsg->bbox.y0;
  obj->bbox.x1 = reshapemsg->bbox.x1;
  obj->bbox.y1 = reshapemsg->bbox.y1;

  parent_state.w = obj->parent_window;
  xwimp_get_window_state(&parent_state);
  x = parent_state.visible.x0 - parent_state.xscroll;
  y = parent_state.visible.y1 - parent_state.yscroll;
  plugin_state.w = obj->window;
  xwimp_get_window_state(&plugin_state);

  open.w = obj->window;
  open.visible.x0 = x + obj->bbox.x0;
  open.visible.y0 = y + obj->bbox.y0;
  open.visible.x1 = x + obj->bbox.x1;
  open.visible.y1 = y + obj->bbox.y1;
  open.xscroll = open.yscroll = 0;
  open.next = plugin_state.next;
  xwimp_open_window_nested(&open, obj->parent_window, 0);
  return 0;
}


int plugin_stream_new(wimp_message *msg) {
// browser requests new stream to be opened _to_ us
  plugin_message_stream_new *streamnewmsg;
  int inst;
  OBJECT *obj;

  streamnewmsg = (plugin_message_stream_new *)&msg->data.reserved;
  inst = find_instance((int)streamnewmsg->browser, 0, 1);
  if (inst == -1)   return 1;
  obj = objects+inst;

  streamnewmsg->plugin = (plugin_p)clock();       // pseudo-unique
  streamnewmsg->stream = (plugin_s)streamnewmsg->plugin;
  streamnewmsg->flags = 3;
  msg->your_ref = msg->my_ref;

  xwimp_send_message(wimp_USER_MESSAGE, msg, obj->browser_task);
  return 0;
}


int plugin_stream_as_file(wimp_message *msg) {
// browser has fetched the data, now we load it and use it

  plugin_message_stream_as_file *asfilemsg;
  int inst, size, ok;
  OBJECT *obj;
  char *filename;
  byte *buffer;
  os_dynamic_area_no area;

  asfilemsg = (plugin_message_stream_as_file *)&msg->data.reserved;
  inst = find_instance((int)asfilemsg->plugin, (int)asfilemsg->browser, 0);
  if (inst == -1)   return 1;
  obj = objects+inst;

  if (asfilemsg->filename.offset > 256)
    filename = asfilemsg->filename.pointer;
  else
    filename = (char *)asfilemsg + asfilemsg->filename.offset;

  // stop any other file that may be playing
  if (playing != NOT_PLAYING) {
    midi_stop();
    playing = NOT_PLAYING;
  }
  ok = 0;
  if (!load_midi_file(filename, &area, &buffer, &size)) {
    obj->area = area;
    obj->ptr = buffer;
    obj->size = size;
    if (!start_playback(buffer, size, &obj->duration))   ok = 1;
  }

  if (ok) {
    // playback started OK, so mark new instance as
    // playing and mark all other as not playing
    set_current_instance(inst);
  } else {
    // abort ?
  }

  return 0;
}
