/******************************************************
 *
 * triggerize - implementation file
 *
 * copyleft (c) IOhannes m zmölnig
 *
 *   2016:forum::für::umläute:2016
 *
 *   institute of electronic music and acoustics (iem)
 *
 ******************************************************
 *
 * license: GNU General Public License v.2 (or later)
 *
 ******************************************************/

#include "m_pd.h"

#include "g_canvas.h"
#include "m_imp.h"

#define MARK() post("%s:%d\t%s", __FILE__, __LINE__, __FUNCTION__)

/* ------------ utilities ---------- */
static t_gobj*o2g(t_object*obj) {
  return &(obj->te_g);
}
static t_object*g2o(t_gobj*gobj) {
  return pd_checkobject(&gobj->g_pd);
}
t_gobj*glist_getlast(t_glist*cnv) {
  t_gobj*result=NULL;
  for(result=cnv->gl_list; result->g_next;) result=result->g_next;
  return result;
}
static void dereconnect(t_glist*cnv, t_object*org, t_object*replace) {
  t_gobj*gobj;
  for(gobj=cnv->gl_list; gobj; gobj=gobj->g_next) {
    t_object*obj=g2o(gobj);
    int obj_nout=0;
    int nout;
    if(!obj)continue;
    obj_nout=obj_noutlets(obj);
    for(nout=0; nout<obj_nout; nout++) {
      t_outlet*out=0;
      t_outconnect*conn=obj_starttraverseoutlet(obj, &out, nout);
      while(conn) {
        int which;
        t_object*dest=0;
        t_inlet *in =0;
        conn=obj_nexttraverseoutlet(conn, &dest, &in, &which);
        if(dest!=org)
          continue;
        obj_disconnect(obj, nout, dest, which);
        obj_connect(obj, nout, replace, which);
      }
    }
  }
}
static t_object*triggerize_createobj(t_glist*x, t_binbuf*b) {
  t_pd *boundx = s__X.s_thing, *boundn = s__N.s_thing;
  s__X.s_thing = &x->gl_pd;
  s__N.s_thing = &pd_canvasmaker;

  binbuf_eval(b, 0, 0, 0);

  s__X.s_thing = boundx;
  s__N.s_thing = boundn;
  return g2o(glist_getlast(x));
}
static void stack_conn(t_object*new, int*newoutlet, t_object*org, int orgoutlet, t_outconnect*conn){
  t_object*dest=0;
  t_inlet *in =0;
  int which;

  conn=obj_nexttraverseoutlet(conn, &dest, &in, &which);
  if(conn)
    stack_conn(new, newoutlet, org, orgoutlet, conn);
  obj_disconnect(org, orgoutlet, dest, which);
  obj_connect(new, *newoutlet, dest, which);
  (*newoutlet)++;
}
static int has_fanout(t_object*obj) {
  int obj_nout=obj_noutlets(obj);
  int nout;
  /* check if we actually do have a fan out */
  for(nout=0; nout<obj_nout; nout++) {
    t_outlet*out=0;
    t_outconnect*conn=obj_starttraverseoutlet(obj, &out, nout);
    int count=0;
    if(obj_issignaloutlet(obj, nout))
      continue;
    while(conn) {
      int which;
      t_object*dest=0;
      t_inlet *in =0;
      if(count)return 1;
      conn=obj_nexttraverseoutlet(conn, &dest, &in, &which);
      count++;
    }
  }
  return 0;
}
static int only_triggers_selected(t_glist*cnv) {
  const t_symbol*s_trigger=gensym("trigger");
  t_gobj*gobj = NULL;

  for(gobj=cnv->gl_list; gobj; gobj=gobj->g_next) {
    t_object*obj=g2o(gobj);
    if(obj && glist_isselected(cnv, gobj) && (s_trigger != obj->te_g.g_pd->c_name)) {
      return 0;
    }
  }
  return 1;
}

/* ------------------------- triggerize ---------------------------- */
static int triggerize_fanout_inplace(t_glist*x, t_object*obj) {
  /* avoid fanouts in [t] objects by adding additional outlets */

  int posX=obj->te_xpix;
  int posY=obj->te_ypix;
  t_atom*argv=binbuf_getvec(obj->te_binbuf);
  int    argc=binbuf_getnatom(obj->te_binbuf);
  int obj_nout=obj_noutlets(obj);
  int nout, newout;
  t_binbuf*b=0;
  t_object*stub=0;

  /* check if we actually do have a fan out */
  if(!has_fanout(obj))return 0;

  /* create a new trigger object, that has outlets for the fans */
  b=binbuf_new();
  binbuf_addv(b, "ssii", gensym("#X"), gensym("obj"), posX, posY);
  binbuf_add(b, 1, argv);
  argc--; argv++;
  for(nout=0; nout<obj_nout; nout++) {
    t_outlet*out=0;
    t_outconnect*conn=obj_starttraverseoutlet(obj, &out, nout);
    while(conn) {
      int which;
      t_object*dest=0;
      t_inlet *in =0;
      conn=obj_nexttraverseoutlet(conn, &dest, &in, &which);
      binbuf_add(b, 1, argv);
    }
    argv++; argc--;
  }
  binbuf_addsemi(b);
  stub=triggerize_createobj(x, b);
  binbuf_free(b);

  /* connect */
  newout=0;
  dereconnect(x, obj, stub);
  for(nout=0; nout<obj_nout; nout++) {
    t_outlet*out=0;
    t_outconnect*conn=obj_starttraverseoutlet(obj, &out, nout);
    stack_conn(stub, &newout, obj, nout, conn);
  }

  /* free old object */
  glist_delete(x, o2g(obj));
  return 1;
}
static int triggerize_fanout(t_glist*x, t_object*obj) {
  const t_symbol*s_trigger=gensym("trigger");
  int obj_nout=obj_noutlets(obj);
  int nout;
  int posX=obj->te_xpix-10;
  int posY=obj->te_ypix+20;
  t_binbuf*b=binbuf_new();
  int didit=0;

  /* if the object is a [trigger], we just insert new outlets */
  if(s_trigger == obj->te_g.g_pd->c_name) {
    return triggerize_fanout_inplace(x, obj);
  }

  /* for other objects, we create a new [trigger a a] object and replace the fan-out with that */
  for(nout=0; nout<obj_nout; nout++) {
    t_outlet*out=0;
    t_outconnect*conn=obj_starttraverseoutlet(obj, &out, nout);
    int count=0;
    if(obj_issignaloutlet(obj, nout))
      continue;
    while(conn) {
      int which;
      t_object*dest=0;
      t_inlet *in =0;
      conn=obj_nexttraverseoutlet(conn, &dest, &in, &which);
      count++;
    }
    if(count>1) {
      /* fan out */
      int i;
      t_object*stub=0;
      binbuf_clear(b);
      binbuf_addv(b, "ssiis", gensym("#X"), gensym("obj"), posX, posY, gensym("t"));
      for(i=0; i<count; i++) {
        binbuf_addv(b, "s", gensym("a"));
      }
      binbuf_addsemi(b);
      stub=triggerize_createobj(x, b);
      conn=obj_starttraverseoutlet(obj, &out, nout);
      i=0;
      while(conn) {
        int which;
        t_object*dest=0;
        t_inlet *in =0;
        conn=obj_nexttraverseoutlet(conn, &dest, &in, &which);
        obj_disconnect(obj, nout, dest, which);
        obj_connect(stub, count-i-1, dest, which);
        i++;
      }
      obj_connect(obj, nout, stub, 0);
      glist_select(x, o2g(stub));
    }
    didit++;
  }
  binbuf_free(b);
  return didit;
}
static int triggerize_fanouts(t_glist*cnv) {
  t_gobj*gobj = NULL;
  int count=0;
  for(gobj=cnv->gl_list; gobj; gobj=gobj->g_next) {
    t_object*obj=g2o(gobj);
    if(obj && glist_isselected(cnv, gobj) && triggerize_fanout(cnv, obj))
      count++;
  }
  return count;
}

static int triggerize_line(t_glist*x) {
  /* triggerize a single selected line, by inserting a [t a] object
   * (or it's signal equivalen) */
  t_editor*ed=x->gl_editor;
  int src_obj, src_out, dst_obj ,dst_in;
  t_gobj *src = 0, *dst = 0;
  t_binbuf*b=0;
  int posx=100, posy=100;
  t_object*stub=0;

  if(!ed->e_selectedline)
    return 0;
  src_obj=ed->e_selectline_index1;
  src_out=ed->e_selectline_outno;
  dst_obj=ed->e_selectline_index2;
  dst_in =ed->e_selectline_inno;
  for (src = x->gl_list; src_obj; src = src->g_next, src_obj--)
    if (!src->g_next) goto bad;
  for (dst = x->gl_list; dst_obj; dst = dst->g_next, dst_obj--)
    if (!dst->g_next) goto bad;
  src_obj=ed->e_selectline_index1;
  dst_obj=ed->e_selectline_index2;

  if(1) {
    t_object*obj1=g2o(src);
    t_object*obj2=g2o(dst);
    if(obj1 && obj2) {
      posx=(obj1->te_xpix+obj2->te_xpix)>>1;
      posy=(obj1->te_ypix+obj2->te_ypix)>>1;
    }
  }

  b=binbuf_new();
  if(obj_issignaloutlet(g2o(src), src_out)) {
    binbuf_addv(b, "ssiiiisi;", gensym("#N"), gensym("canvas"), 200, 100, 190, 200, gensym("nop~"), 0);
    binbuf_addv(b, "ssiis;", gensym("#X"), gensym("obj"), 50, 70, gensym("inlet~"));
    binbuf_addv(b, "ssiis;", gensym("#X"), gensym("obj"), 50,140, gensym("outlet~"));
    binbuf_addv(b, "ssiiii;", gensym("#X"), gensym("connect"), 0,0,1,0);
    binbuf_addv(b, "ssiiss", gensym("#X"), gensym("restore"), posx, posy, gensym("pd"), gensym("nop~"));
  } else {
    binbuf_addv(b,"ssii", gensym("#X"), gensym("obj"), posx, posy);
    binbuf_addv(b,"ss", gensym("t"), gensym("a"));
  }
  binbuf_addsemi(b);
  stub=triggerize_createobj(x, b);
  binbuf_free(b);b=0;

  obj_disconnect(g2o(src), src_out, g2o(dst), dst_in);
  obj_connect(g2o(src), src_out, stub, 0);
  obj_connect(stub, 0, g2o(dst), dst_in);
  glist_select(x, o2g(stub));

  return 1;
 bad:
  return 0;
}
static int minimize_trigger(t_glist*cnv, t_object*obj) {
  /* remove all unused outlets from [trigger] */
  t_binbuf*b=binbuf_new();
  t_atom*argv=binbuf_getvec(obj->te_binbuf);
  t_object*stub=0;
  int obj_nout=obj_noutlets(obj);
  int nout;
  int count = 0;

  binbuf_addv(b, "ssii", gensym("#X"), gensym("obj"), obj->te_xpix, obj->te_ypix);
  binbuf_add(b, 1, argv);
  /* go through all the outlets, and add those that have connections */
  for(nout = 0; nout < obj_nout; nout++) {
    t_outlet*out=0;
    t_outconnect*conn = obj_starttraverseoutlet(obj, &out, nout);
    if(conn) {
      binbuf_add(b, 1, argv + 1 + nout);
    } else {
      count++;
    }
  }
  if(!count || count == obj_nout) {
    /* either no outlet to delete or all: skip this object */
    binbuf_free(b);
    return 0;
  }
  /* create the replacement object (which only has outlets that are going to be connected) */
  stub=triggerize_createobj(cnv, b);

  /* no go through the original object's outlets, and duplicate the connection
   * of each to the new object */
  for(nout=0, count=0; nout<obj_nout; nout++) {
    t_outlet*out=0;
    t_outconnect*conn=obj_starttraverseoutlet(obj, &out, nout);
    if(!conn)
      /* nothing connected here; skip it */
      continue;
    /* repeat all connections of this outlet (should only be one) */
    while(conn) {
      int which;
      t_object*dest=0;
      t_inlet *in =0;
      conn=obj_nexttraverseoutlet(conn, &dest, &in, &which);
      obj_disconnect(obj, nout, dest, which);
      obj_connect(stub, count, dest, which);
    }
    count++;
  }
  binbuf_free(b);
  dereconnect(cnv, obj, stub);
  glist_delete(cnv, o2g(obj));
  return 1;
}
static int expand_trigger(t_glist*cnv, t_object*obj) {
  /*
   * expand the trigger to the left, by inserting a first "a" outlet
   */
  t_binbuf*b=binbuf_new();
  int argc=binbuf_getnatom(obj->te_binbuf);
  t_atom*argv=binbuf_getvec(obj->te_binbuf);
  t_object*stub=0;
  int obj_nout=obj_noutlets(obj);
  int nout;

  binbuf_addv(b, "ssii", gensym("#X"), gensym("obj"), obj->te_xpix, obj->te_ypix);
  binbuf_add(b, 1, argv);
  binbuf_addv(b, "s", gensym("a"));
  binbuf_add(b, argc-1, argv+1);
  stub=triggerize_createobj(cnv, b);
  for(nout=0; nout<obj_nout; nout++) {
    t_outlet*out=0;
    t_outconnect*conn=obj_starttraverseoutlet(obj, &out, nout);
    while(conn) {
      int which;
      t_object*dest=0;
      t_inlet *in =0;
      conn=obj_nexttraverseoutlet(conn, &dest, &in, &which);
      obj_disconnect(obj, nout, dest, which);
      obj_connect(stub, nout+1, dest, which);
    }
  }
  binbuf_free(b);
  dereconnect(cnv, obj, stub);
  glist_delete(cnv,o2g(obj));
  return 1;
}
typedef int (*t_fun_withobject)(t_glist*, t_object*);
static int with_triggers(t_glist*cnv, t_fun_withobject fun) {
  const t_symbol*s_trigger=gensym("trigger");
  int count=0;
  t_gobj*gobj = NULL;
  for(gobj=cnv->gl_list; gobj; gobj=gobj->g_next) {
    t_object*obj=g2o(gobj);
    if(obj && glist_isselected(cnv, gobj)) {
      const t_symbol*c_name=obj->te_g.g_pd->c_name;
      if((s_trigger == c_name) && fun(cnv, obj))
        count++;
    }
  }
  return count;
}
static int triggerize_triggers(t_glist*cnv) {
  /* cleanup [trigger] objects */
  int count=0;

  /* nothing to do, if the selection is not exclusively [trigger] objects
   */
  if(!only_triggers_selected(cnv))
     return 0;

  /* TODO: here's the place to merge multiple connected triggers
   */

  /*
   * remove all unused outlets from (selected) triggers
   */
  count = with_triggers(cnv, minimize_trigger);
  if(count)
    return count;

  /*
   * expand each (selected) trigger to the left
   */
  count = with_triggers(cnv, expand_trigger);
  if(count)
    return count;

  /* nothing more to do */
  return 0;
}

static void canvas_do_triggerize(t_glist*cnv) {
  /*
   * selected msg-connection: insert [t a]
   * selected sig-connection: insert [pd nop~]
   * selected [trigger]s with fan-outs: remove them (by inserting new outlets of the correct type)
   * selected objects with fan-outs: remove fan-outs
   * selected [trigger]: remove unused outlets
   * selected [trigger]: else, add left-most "a" outlet
   */
  if(triggerize_line(cnv))return;
  if(triggerize_fanouts(cnv))return;
  if(triggerize_triggers(cnv))return;
}
static void canvas_triggerize(t_glist*cnv) {
  int dspstate;
  if(NULL == cnv)return;

  /* suspend system */
  dspstate = canvas_suspend_dsp();

  canvas_do_triggerize(cnv);

  /* restore state */
  canvas_redraw(cnv);
  glist_redraw(cnv);
  canvas_resume_dsp(dspstate);
}

void triggerize_setup(void)
{
  int major, minor, bugfix;
  sys_getversion(&major, &minor, &bugfix);
  if((major>0 || minor >= 49)) {
    error("triggerize built into Pd>=0.49, not enabling iem::punish::triggerize");
    return;
  }
  if(NULL==canvas_class) {
    verbose(10, "triggerize detected class_new() @ %p", class_new);
    return;
  }
  post("triggerize - insert [trigger] ad lib.");

  if(NULL==zgetfn(&canvas_class, gensym("triggerize")))
    class_addmethod(canvas_class, (t_method)canvas_triggerize, gensym("triggerize"), 0);
}
