/*
  ZynAddSubFX - a software synthesizer

  EnvelopeFreeEdit.cpp - Envelope Edit View
  Copyright (C) 2016 Mark McCurry

  This program 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 2
  of the License, or (at your option) any later version.
*/
#include "EnvelopeFreeEdit.h"
#include "../Misc/Util.h"
#include <FL/Fl.H>
#include <FL/fl_draw.H>
#include <cstdlib>
#include <cassert>
#include <rtosc/rtosc.h>

using namespace zyncarla;

EnvelopeFreeEdit::EnvelopeFreeEdit(int x,int y, int w, int h, const char *label)
:Fl_Box(x,y,w,h,label), Fl_Osc_Widget(this)
{
    pair=NULL;
    currentpoint=-1;
    cpx=0;
    lastpoint=-1;
}

void EnvelopeFreeEdit::init(void)
{
    oscRegister("Penvpoints");
    oscRegister("Penvdt");
    oscRegister("Penvval");
    oscRegister("Penvsustain");

    //register for non-bulk types
    for(int i=0; i<MAX_ENVELOPE_POINTS; ++i) {
        osc->createLink(loc+string("Penvdt") + to_s(i), this);
        osc->createLink(loc+string("Penvval") + to_s(i), this);
    }
}

void EnvelopeFreeEdit::OSC_raw(const char *msg)
{
    const char *args = rtosc_argument_string(msg);
    if(strstr(msg,"Penvpoints") && !strcmp(args, "i")) {
        Penvpoints = rtosc_argument(msg, 0).i;
    } else if(strstr(msg,"Penvdt") && !strcmp(args, "b")) {
        rtosc_blob_t b = rtosc_argument(msg, 0).b;
        assert(b.len == MAX_ENVELOPE_POINTS);
        memcpy(Penvdt, b.data, MAX_ENVELOPE_POINTS);
    } else if(strstr(msg,"Penvval") && !strcmp(args, "b")) {
        rtosc_blob_t b = rtosc_argument(msg, 0).b;
        assert(b.len == MAX_ENVELOPE_POINTS);
        memcpy(Penvval, b.data, MAX_ENVELOPE_POINTS);
    } else if(strstr(msg, "Penvval") && !strcmp(args, "c")) {
        const char *str = strstr(msg, "Penvval");
        int id = atoi(str+7);
        assert(0 <= id && id < MAX_ENVELOPE_POINTS);
        Penvval[id] = rtosc_argument(msg, 0).i;
    } else if(strstr(msg, "Penvdt") && !strcmp(args, "c")) {
        const char *str = strstr(msg, "Penvdt");
        int id = atoi(str+6);
        assert(0 <= id && id < MAX_ENVELOPE_POINTS);
        Penvdt[id] = rtosc_argument(msg, 0).i;
    } else if(strstr(msg,"Penvsustain") && !strcmp(args, "i")) {
        Penvsustain = rtosc_argument(msg, 0).i;
    }
    redraw();
    do_callback();
}

void EnvelopeFreeEdit::setpair(Fl_Box *pair_)
{
    pair=pair_;
}

int EnvelopeFreeEdit::getpointx(int n) const
{
    const int lx=w()-10;
    int npoints=Penvpoints;

    float  sum=0;
    for(int i=1; i<npoints; ++i)
        sum+=getdt(i)+1;

    float sumbefore=0;//the sum of all points before the computed point
    for(int i=1; i<=n; ++i)
        sumbefore+=getdt(i)+1;

    return (int) (sumbefore/(float) sum*lx);
}

int EnvelopeFreeEdit::getpointy(int n) const
{
    const int ly=h()-10;

    return (1.0-Penvval[n]/127.0)*ly;
}

static inline int distance_fn(int dx, int dy) {
    return dx*dx+dy*dy;
}

int EnvelopeFreeEdit::getnearest(int x,int y) const
{
    x-=5;y-=5;

    int nearestpoint=0;
    int nearest_distance_sq=distance_fn(x-getpointx(0), y-getpointy(0));
    for(int i=1; i<Penvpoints; ++i){
        int distance_sq=distance_fn(x-getpointx(i), y-getpointy(i));
        if (distance_sq<nearest_distance_sq) {
            nearestpoint=i;
            nearest_distance_sq=distance_sq;
        }
    }

    return nearestpoint;
}

static float dt(char val)
{
    return (powf(2.0f, val / 127.0f * 12.0f) - 1.0f) * 10.0f; //miliseconds
}

float EnvelopeFreeEdit::getdt(int i) const
{
    return dt(Penvdt[i]);
}

static bool ctrldown, altdown;

void EnvelopeFreeEdit::draw(void)
{
    int ox=x(),oy=y(),lx=w(),ly=h();
    //if (env->Pfreemode==0)
    //    env->converttofree();
    const int npoints=Penvpoints;

    if (active_r()) fl_color(FL_BLACK);
    else fl_color(90,90,90);
    if (!active_r()) currentpoint=-1;

    fl_rectf(ox,oy,lx,ly);

    //Margins
    ox+=5;oy+=5;lx-=10;ly-=10;

    //draw the lines
    fl_color(FL_GRAY);

    const int midline = oy+ly*(1-64.0/127);
    fl_line_style(FL_SOLID);
    fl_line(ox+2,midline,ox+lx-2,midline);

    //draws the evelope points and lines
    Fl_Color alb=FL_WHITE;
    if (!active_r()) alb=fl_rgb_color(180,180,180);
    fl_color(alb);
    int oldxx=0,xx=0,oldyy=0,yy=getpointy(0);
    fl_rectf(ox-3,oy+yy-3,6,6);
    for (int i=1; i<npoints; ++i){
        oldxx=xx;oldyy=yy;
        xx=getpointx(i);yy=getpointy(i);
        if (i==currentpoint || (ctrldown && i==lastpoint))
            fl_color(FL_RED);
        else
            fl_color(alb);
        fl_line(ox+oldxx,oy+oldyy,ox+xx,oy+yy);
        fl_rectf(ox+xx-3,oy+yy-3,6,6);
    }

    //draw the last moved point point (if exists)
    if (lastpoint>=0){
        fl_color(FL_CYAN);
        fl_rectf(ox+getpointx(lastpoint)-5,oy+getpointy(lastpoint)-5,10,10);
    }

    //draw the sustain position
    if(Penvsustain>0){
        fl_color(FL_YELLOW);
        xx=getpointx(Penvsustain);
        fl_line(ox+xx,oy+0,ox+xx,oy+ly);
    }

    //Show the envelope duration and the current line duration
    fl_font(FL_HELVETICA|FL_BOLD,10);
    float time=0.0;
    if (currentpoint<=0 && (!ctrldown||lastpoint <= 0)){
        fl_color(alb);
        for(int i=1; i<npoints; ++i)
            time+=getdt(i);
    } else {
        fl_color(FL_RED);
        time=getdt(lastpoint);
    }
    char tmpstr[20];
    if (!altdown || ctrldown) {
        if (time<1000.0)
            snprintf((char *)&tmpstr,20,"%.1fms",time);
        else
            snprintf((char *)&tmpstr,20,"%.2fs",time/1000.0);
        fl_draw(tmpstr,ox+lx-20,oy+ly-10,20,10,FL_ALIGN_RIGHT,NULL,0);
    }
    if (!altdown || !ctrldown) {
        if (lastpoint>=0){
            snprintf((char *)&tmpstr,20,"%d", Penvval[lastpoint]);
            fl_draw(tmpstr,ox+lx-20,oy+ly-23,20,10,FL_ALIGN_RIGHT,NULL,0);
        }
    }
}

int EnvelopeFreeEdit::handle(int event)
{
    const int x_=Fl::event_x()-x();
    const int y_=Fl::event_y()-y();
    static Fl_Widget *old_focus;
    int key, old_mod_state;

    switch(event) {
      case FL_ENTER:
          old_focus=Fl::focus();
          Fl::focus(this);
          // Otherwise the underlying window seems to regrab focus,
          // and I can't see the KEYDOWN action.
          return 1;
      case FL_LEAVE:
          Fl::focus(old_focus);
          break;
      case FL_KEYDOWN:
      case FL_KEYUP:
          key = Fl::event_key();
          if (key==FL_Alt_L || key==FL_Alt_R) {
              altdown = (event==FL_KEYDOWN);
              redraw();
              if (pair!=NULL) pair->redraw();
          }
          if (key==FL_Control_L || key==FL_Control_R){
              ctrldown = (event==FL_KEYDOWN);
              redraw();
              if (pair!=NULL) pair->redraw();
          }
          break;
      case FL_PUSH:
            currentpoint=getnearest(x_,y_);
            cpx=x_;
            cpy=y_;
            cpdt=Penvdt[currentpoint];
            cpval=Penvval[currentpoint];
            lastpoint=currentpoint;
            redraw();
            if (pair)
                pair->redraw();
            return 1;
      case FL_RELEASE:
            currentpoint=-1;
            redraw();
            if (pair)
                pair->redraw();
            return 1;
      case FL_MOUSEWHEEL:
          if (Fl::event_buttons())
              return 1;
          if (lastpoint>=0) {
              int delta = Fl::event_dy() * (Fl::event_shift() ? 4 : 1);
              if (!ctrldown) {
                  int ny = Penvval[lastpoint] - delta;
                  ny = ny < 0 ? 0 : ny > 127 ? 127 : ny;
                  Penvval[lastpoint] = ny;
                  oscWrite(to_s("Penvval")+to_s(lastpoint), "c", ny);
                  oscWrite("Penvval","");
              } else if (lastpoint > 0) {
                  int newdt = Penvdt[lastpoint] - delta;
                  newdt = newdt < 0 ? 0 : newdt > 127 ? 127 : newdt;
                  Penvdt[lastpoint] = newdt;
                  oscWrite(to_s("Penvdt")+to_s(lastpoint),  "c", newdt);
                  oscWrite("Penvdt","");
              }
              redraw();
              if (pair!=NULL) pair->redraw();
              return 1;
          }
      case FL_DRAG:
          if (currentpoint>=0){
              old_mod_state = mod_state;
              mod_state = ctrldown << 1 | altdown;
              if (old_mod_state != mod_state) {
                  cpx=x_;
                  cpy=y_;
                  cpdt=Penvdt[currentpoint];
                  cpval=Penvval[currentpoint];
                  old_mod_state = mod_state;
              }

              if (!altdown || !ctrldown) {
                  const int dy=(int)((cpy-y_)/3.0);
                  const int newval=limit(cpval+dy, 0, 127);

                  Penvval[currentpoint]=newval;
                  oscWrite(to_s("Penvval")+to_s(currentpoint), "c", newval);
                  oscWrite("Penvval","");
              }

              if (!altdown || ctrldown) {
                  const int dx=(int)((x_-cpx)*0.1);
                  const int newdt=limit(cpdt+dx,0,127);

                  if(currentpoint!=0)
                      Penvdt[currentpoint]=newdt;
                  else
                      Penvdt[currentpoint]=0;
                  oscWrite(to_s("Penvdt")+to_s(currentpoint),  "c", newdt);
                  oscWrite("Penvdt","");
              }

              redraw();

              if(pair)
                  pair->redraw();
              return 1;
          }
    }
      // Needed to propagate undo/redo keys.
    return 0;
}

void EnvelopeFreeEdit::update(void)
{
    oscWrite("Penvpoints");
    oscWrite("Penvdt");
    oscWrite("Penvval");
    oscWrite("Penvsustain");
}

void EnvelopeFreeEdit::rebase(std::string new_base)
{
    osc->renameLink(loc+"Penvpoints", new_base+"Penvpoints", this);
    osc->renameLink(loc+"Penvdt", new_base+"Penvdt", this);
    osc->renameLink(loc+"Penvval",    new_base+"Penvval", this);
    osc->renameLink(loc+"Penvsustain", new_base+"Penvsustain", this);
    for(int i=0; i<MAX_ENVELOPE_POINTS; ++i) {
        string dt  = string("Penvdt")  + to_s(i);
        string val = string("Penvval") + to_s(i);
        osc->renameLink(loc+dt, new_base+dt, this);
        osc->renameLink(loc+val, new_base+val, this);
    }
    loc = new_base;
    update();
}
