//=========================================================
//  MusE
//  Linux Music Editor
//    $Id: dcanvas.cpp,v 1.16 2004/06/13 17:57:54 lunar_shuttle Exp $
//  (C) Copyright 1999 Werner Schweer (ws@seh.de)
//=========================================================

#include <qpainter.h>
#include <qapplication.h>
#include <qclipboard.h>
#include <qdragobject.h>

#include <stdio.h>
#include <values.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/mman.h>

#include "dcanvas.h"
#include "midieditor.h"
#include "drummap.h"
#include "event.h"
#include "mpevent.h"
#include "xml.h"
#include "globals.h"
#include "midiport.h"
#include "audio.h"
#include "velocity.h"

#define CARET   10
#define CARET2   5

//---------------------------------------------------------
//   DEvent
//---------------------------------------------------------

DEvent::DEvent(Event e, Part* p)
  : CItem(e, p)
      {
      int instr = e.pitch();
      int y  = instr * TH + TH/2;
      int tick = e.tick() + p->tick();
      setPos(QPoint(tick, y));
      setBBox(QRect(-CARET2, -CARET2, CARET, CARET));
      }

//---------------------------------------------------------
//   addItem
//---------------------------------------------------------

void DrumCanvas::addItem(Part* part, Event& event)
      {
      DEvent* ev = new DEvent(event, part);
      items.add(ev);
      }

//---------------------------------------------------------
//   DrumCanvas
//---------------------------------------------------------

DrumCanvas::DrumCanvas(MidiEditor* pr, QWidget* parent, int sx,
   int sy, const char* name)
   : EventCanvas(pr, parent, sx, sy, name)
      {
      setVirt(false);
      songChanged(SC_TRACK_INSERTED);
      }

//---------------------------------------------------------
//   moveItem
//---------------------------------------------------------

bool DrumCanvas::moveItem(CItem* item, const QPoint& pos, DragType dtype)
      {
      DEvent* nevent   = (DEvent*) item;
      MidiPart* part   = (MidiPart*)nevent->part();
      Event event      = nevent->event();
      int x            = pos.x();
      if (x < 0)
            x = 0;
      int ntick        = editor->rasterVal(x) - part->tick();
      if (ntick < 0)
            ntick = 0;
      int npitch       = y2pitch(pos.y());
      Event newEvent   = event.clone();

      newEvent.setPitch(npitch);
      newEvent.setTick(ntick);

      if (dtype == MOVE_COPY || dtype == MOVE_CLONE) {
            audio->msgAddEvent(newEvent, part, false);
            }
      else {
            audio->msgChangeEvent(event, newEvent, part, false);
            }
      return true;
      }

//---------------------------------------------------------
//   newItem
//---------------------------------------------------------

CItem* DrumCanvas::newItem(const QPoint& p, int state)
      {
      int instr = y2pitch(p.y());         //drumInmap[y2pitch(p.y())];
      int velo  = drumMap[instr].lv4;
      if (state == ShiftButton)
            velo = drumMap[instr].lv3;
      else if (state == ControlButton)
            velo = drumMap[instr].lv2;
      else if (state == (ControlButton | ShiftButton))
            velo = drumMap[instr].lv1;
      int tick = editor->rasterVal(p.x());
      tick    -= curPart->tick();
      Event e(Note);
      e.setTick(tick);
      e.setPitch(instr);
      e.setVelo(velo);
      e.setLenTick(drumMap[instr].len);
      return new DEvent(e, curPart);
      }

//---------------------------------------------------------
//   resizeItem
//---------------------------------------------------------

void DrumCanvas::resizeItem(CItem* item, bool)
      {
      DEvent* nevent = (DEvent*) item;
      Event ev = nevent->event();
      audio->msgDeleteEvent(ev, nevent->part());
      }

//---------------------------------------------------------
//   newItem
//---------------------------------------------------------

void DrumCanvas::newItem(CItem* item, bool noSnap)
      {
      DEvent* nevent = (DEvent*) item;
      Event event    = nevent->event();
      int x = item->x();
      if (!noSnap)
            x = editor->rasterVal(x);
      event.setTick(x - nevent->part()->tick());
      //int npitch = drumMap[y2pitch(item->y())].enote;
      int npitch = event.pitch();
      event.setPitch(npitch);

      //
      // check for existing event
      //    if found change command semantic from insert to delete
      //
      EventList* el = nevent->part()->events();
      iEvent lower  = el->lower_bound(event.tick());
      iEvent upper  = el->upper_bound(event.tick());

      for (iEvent i = lower; i != upper; ++i) {
            Event ev = i->second;
            if (ev.pitch() == npitch) {
                  audio->msgDeleteEvent(ev, nevent->part());
                  return;
                  }
            }

      audio->msgAddEvent(event, nevent->part());
      }

//---------------------------------------------------------
//   deleteItem
//---------------------------------------------------------

bool DrumCanvas::deleteItem(CItem* item)
      {
      Event ev = ((DEvent*)item)->event();
      audio->msgDeleteEvent(ev, ((DEvent*)item)->part());
      return false;
      }

//---------------------------------------------------------
//   drawItem
//---------------------------------------------------------

void DrumCanvas::drawItem(QPainter&p, const CItem*item, const QRect&)
      {
      p.setPen(black);
      DEvent* e   = (DEvent*) item;
      Event me    = e->event();

      int x = mapx(item->pos().x());
      int y = mapy(item->pos().y());

      QPointArray pa(4);
      pa.setPoint(0, x - CARET2, y);
      pa.setPoint(1, x,          y - CARET2);
      pa.setPoint(2, x + CARET2, y);
      pa.setPoint(3, x,          y + CARET2);

      int velo    = me.velo();
      DrumMap* dm = &drumMap[y2pitch(y)]; //Get the drum item

      QColor color;
      if (velo < dm->lv1)
            color.setRgb(240, 240, 255);
      else if (velo < dm->lv2)
            color.setRgb(200, 200, 255);
      else if (velo < dm->lv3)
            color.setRgb(170, 170, 255);
      else
            color.setRgb(0, 0, 255);

      if (e->part() != curPart)
            p.setBrush(lightGray);
      else if (item->isMoving()) {
            p.setBrush(gray);
            p.drawPolygon(pa);
            p.setBrush(black);
            QPoint p = map(item->mp());
            x = p.x();
            y = p.y();
            pa.setPoint(0, x-CARET2,  y + TH/2);
            pa.setPoint(1, x,         y + TH/2+CARET2);
            pa.setPoint(2, x+CARET2,  y + TH/2);
            pa.setPoint(3, x,         y + (TH-CARET)/2);
            }
      else if (item->isSelected())
            p.setBrush(black);
      else
            p.setBrush(color);
      p.drawPolygon(pa);
      }

//---------------------------------------------------------
//   drawCanvas
//---------------------------------------------------------

extern void drawTickRaster(QPainter& p, int, int, int, int, int);

void DrumCanvas::drawCanvas(QPainter& p, const QRect& rect)
      {
      int x = rect.x();
      int y = rect.y();
      int w = rect.width();
      int h = rect.height();

      //---------------------------------------------------
      //  horizontal lines
      //---------------------------------------------------

      int yy  = ((y-1) / TH) * TH + TH;
      for (; yy < y + h; yy += TH) {
            p.setPen(gray);
            p.drawLine(x, yy, x + w, yy);
            }

      //---------------------------------------------------
      // vertical lines
      //---------------------------------------------------

      drawTickRaster(p, x, y, w, h, editor->quant());
      }

//---------------------------------------------------------
//   y2pitch
//---------------------------------------------------------

int DrumCanvas::y2pitch(int y) const
      {
      int pitch = y/TH;
      if (pitch >= DRUM_MAPSIZE)
            pitch = DRUM_MAPSIZE-1;
      return pitch;
      }

//---------------------------------------------------------
//   pitch2y
//---------------------------------------------------------

int DrumCanvas::pitch2y(int pitch) const
      {
      return pitch * TH;
      }

//---------------------------------------------------------
//   cmd
//---------------------------------------------------------

void DrumCanvas::cmd(int cmd)
      {
      switch(cmd) {
            case CMD_CUT:
                  copy();
                  song->startUndo();
                  for (iCItem i = items.begin(); i != items.end(); ++i) {
                        if (!i->second->isSelected())
                              continue;
                        DEvent* e = (DEvent*)(i->second);
                        Event event = e->event();
                        audio->msgDeleteEvent(event, e->part(), false);
                        }
                  song->endUndo(SC_EVENT_REMOVED);
                  break;
            case CMD_COPY:
                  copy();
                  break;
            case CMD_PASTE:
                  paste();
                  break;
            case CMD_SELECT_ALL:     // select all
                  for (iCItem k = items.begin(); k != items.end(); ++k) {
                        if (!k->second->isSelected())
                              selectItem(k->second, true);
                        }
                  break;
            case CMD_SELECT_NONE:     // select none
                  deselectAll();
                  break;
            case CMD_SELECT_INVERT:     // invert selection
                  for (iCItem k = items.begin(); k != items.end(); ++k) {
                        selectItem(k->second, !k->second->isSelected());
                        }
                  break;
            case CMD_SELECT_ILOOP:     // select inside loop
                  for (iCItem k = items.begin(); k != items.end(); ++k) {
                        DEvent* nevent =(DEvent*)(k->second);
                        Part* part = nevent->part();
                        Event event = nevent->event();
                        unsigned tick  = event.tick() + part->tick();
                        if (tick < song->lpos() || tick >= song->rpos())
                              selectItem(k->second, false);
                        else
                              selectItem(k->second, true);
                        }
                  break;
            case CMD_SELECT_OLOOP:     // select outside loop
                  for (iCItem k = items.begin(); k != items.end(); ++k) {
                        DEvent* nevent = (DEvent*)(k->second);
                        Part* part     = nevent->part();
                        Event event    = nevent->event();
                        unsigned tick  = event.tick() + part->tick();
                        if (tick < song->lpos() || tick >= song->rpos())
                              selectItem(k->second, true);
                        else
                              selectItem(k->second, false);
                        }
                  break;
            case CMD_DEL:
                  if (selectionSize()) {
                        song->startUndo();
                        for (iCItem i = items.begin(); i != items.end(); ++i) {
                              if (!i->second->isSelected())
                                    continue;
                              Event ev = i->second->event();
                              audio->msgDeleteEvent(ev, i->second->part(), false);
                              }
                        song->endUndo(SC_EVENT_REMOVED);
                        }
                  return;

            case CMD_SAVE:
            case CMD_LOAD:
                  printf("DrumCanvas:: cmd not implemented %d\n", cmd);
                  break;

            case CMD_FIXED_LEN: //Set notes to the length specified in the drummap
                  if (!selectionSize())
                        break;
                  song->startUndo();
                  for (iCItem k = items.begin(); k != items.end(); ++k) {
                        if (k->second->isSelected()) {
                              DEvent* devent = (DEvent*)(k->second);
                              Event event    = devent->event();
                              Event newEvent = event.clone();
                              newEvent.setLenTick(drumMap[event.pitch()].len);
                              audio->msgChangeEvent(event, newEvent, devent->part() , false);
                              }
                        }
                  song->endUndo(SC_EVENT_MODIFIED);
                  break;
            case CMD_LEFT:
                  {
                  int frames = pos[0] - editor->rasterStep(pos[0]);
                  if (frames < 0)
                        frames = 0;
                  Pos p(frames,true);
                  song->setPos(0, p, true, true, true);
                  }
                  break;
            case CMD_RIGHT:
                  {
                  Pos p(pos[0] + editor->rasterStep(pos[0]), true);
                  song->setPos(0, p, true, true, true);
                  }
                  break;
            case CMD_MODIFY_VELOCITY:
                  {
                  Velocity w(this);
                  w.setRange(0); //TODO: Make this work! Probably put _to & _toInit in ecanvas instead
                  if (!w.exec())
                        break;
                  int range  = w.range();        // all, selected, looped, sel+loop
                  int rate   = w.rateVal();
                  int offset = w.offsetVal();

                  song->startUndo();
                  for (iCItem k = items.begin(); k != items.end(); ++k) {
                        DEvent* devent = (DEvent*)(k->second);
                        Event event    = devent->event();
                        if (event.type() != Note)
                              continue;
                        unsigned tick      = event.tick();
                        bool selected = k->second->isSelected();
                        bool inLoop   = (tick >= song->lpos()) && (tick < song->rpos());

                        if ((range == 0)
                           || (range == 1 && selected)
                           || (range == 2 && inLoop)
                           || (range == 3 && selected && inLoop)) {
                              int velo = event.velo();

                              //velo = rate ? (velo * 100) / rate : 64;
                              velo = (velo * rate) / 100;
                              velo += offset;

                              if (velo <= 0)
                                    velo = 1;
                              if (velo > 127)
                                    velo = 127;
                              if (event.velo() != velo) {
                                    Event newEvent = event.clone();
                                    newEvent.setVelo(velo);
                                    audio->msgChangeEvent(event, newEvent, devent->part(), false);
                                    }
                              }
                        }
                  song->endUndo(SC_EVENT_MODIFIED);
                  }
                  break;
            }
      redraw();
      }

//---------------------------------------------------------
//   getTextDrag
//---------------------------------------------------------

QTextDrag* DrumCanvas::getTextDrag(QWidget* parent)
      {
      //---------------------------------------------------
      //   generate event list from selected events
      //---------------------------------------------------

      EventList el;
      unsigned startTick = MAXINT;
      for (iCItem i = items.begin(); i != items.end(); ++i) {
            if (!i->second->isSelected())
                  continue;
            DEvent* ne = (DEvent*)(i->second);
            Event    e = ne->event();
            if (startTick == MAXINT)
                  startTick = e.tick();
            el.add(e);
            }

      //---------------------------------------------------
      //    write events as XML into tmp file
      //---------------------------------------------------

      FILE* tmp = tmpfile();
      if (tmp == 0) {
            fprintf(stderr, "EventCanvas::copy() fopen failed: %s\n",
               strerror(errno));
            return 0;
            }
      Xml xml(tmp);

      int level = 0;
      for (ciEvent e = el.begin(); e != el.end(); ++e)
            e->second.write(level, xml, -startTick);

      //---------------------------------------------------
      //    read tmp file into QTextDrag Object
      //---------------------------------------------------

      fflush(tmp);
      struct stat f_stat;
      if (fstat(fileno(tmp), &f_stat) == -1) {
            fprintf(stderr, "EventCanvas::copy() fstat failes:<%s>\n",
               strerror(errno));
            fclose(tmp);
            return 0;
            }
      int n = f_stat.st_size;
      char* fbuf  = (char*)mmap(0, n+1, PROT_READ|PROT_WRITE,
         MAP_PRIVATE, fileno(tmp), 0);
      fbuf[n] = 0;
      QTextDrag* drag = new QTextDrag(QString(fbuf), parent);
      drag->setSubtype("eventlist");
      munmap(fbuf, n);
      fclose(tmp);
      return drag;
      }

//---------------------------------------------------------
//   copy
//    cut copy paste
//---------------------------------------------------------

void DrumCanvas::copy()
      {
      QTextDrag* drag = getTextDrag(0);
      if (drag)
            QApplication::clipboard()->setData(drag, QClipboard::Clipboard);
      }

//---------------------------------------------------------
//   paste
//---------------------------------------------------------

int DrumCanvas::pasteAt(const QString& pt, int pos)
      {
      const char* p = pt.latin1();
      Xml xml(p);

      song->startUndo();
      for (;;) {
            Xml::Token token = xml.parse();
            QString tag = xml.s1();
            switch (token) {
                  case Xml::Error:
                  case Xml::End:
                        song->endUndo(SC_EVENT_INSERTED);
                        return pos;
                  case Xml::TagStart:
                        if (tag == "event") {
                              Event e(Note);
                              e.read(xml);
                              e.setTick(e.tick() + pos);
                              audio->msgAddEvent(e, curPart, false);
                              }
                        else
                              xml.unknown("EventCanvas::paste");
                        break;
                  case Xml::TagEnd:
                  default:
                        break;
                  }
            }
      }

//---------------------------------------------------------
//   paste
//    paste events
//---------------------------------------------------------

void DrumCanvas::paste()
      {
      QCString subtype("eventlist");
      QMimeSource* ms = QApplication::clipboard()->data();
      QString pt;
      if (!QTextDrag::decode(ms, pt, subtype)) {
            printf("cannot paste: bad data type\n");
            return;
            }
      pasteAt(pt, song->cpos());
      }

//---------------------------------------------------------
//   startDrag
//---------------------------------------------------------

void DrumCanvas::startDrag(CItem* /* item*/, bool copymode)
      {
      QTextDrag* drag = getTextDrag(this);
      if (drag) {
//            QApplication::clipboard()->setData(drag, QClipboard::Clipboard);

            if (copymode)
                  drag->dragCopy();
            else
                  drag->dragMove();
            }
      }

//---------------------------------------------------------
//   dragEnterEvent
//---------------------------------------------------------

void DrumCanvas::dragEnterEvent(QDragEnterEvent* event)
      {
      event->accept(QTextDrag::canDecode(event));
      }

//---------------------------------------------------------
//   dragMoveEvent
//---------------------------------------------------------

void DrumCanvas::dragMoveEvent(QDragMoveEvent*)
      {
//      printf("drag move %x\n", this);
      }

//---------------------------------------------------------
//   dragLeaveEvent
//---------------------------------------------------------

void DrumCanvas::dragLeaveEvent(QDragLeaveEvent*)
      {
//      printf("drag leave\n");
      }

//---------------------------------------------------------
//   dropEvent
//---------------------------------------------------------

void DrumCanvas::viewDropEvent(QDropEvent* event)
      {
      QString text;
      if (event->source() == this) {
            printf("local DROP\n");
            return;
            }
      if (QTextDrag::decode(event, text)) {
//            printf("drop <%s>\n", text.ascii());
            int x = editor->rasterVal(event->pos().x());
            if (x < 0)
                  x = 0;
            pasteAt(text, x);
            }
      }

//---------------------------------------------------------
//   keyPressed
//---------------------------------------------------------

void DrumCanvas::keyPressed(int index, bool)
      {
      int port = drumMap[index].port;
      int channel = drumMap[index].channel;
      int pitch = drumMap[index].anote;

      // play note:
      MidiPlayEvent e(0, port, channel, 0x90, pitch, 127);
      audio->msgPlayMidiEvent(&e);
      }

//---------------------------------------------------------
//   keyReleased
//---------------------------------------------------------

void DrumCanvas::keyReleased(int index, bool)
      {
      int port = drumMap[index].port;
      int channel = drumMap[index].channel;
      int pitch = drumMap[index].anote;

      // release note:
      MidiPlayEvent e(0, port, channel, 0x90, pitch, 0);
      audio->msgPlayMidiEvent(&e);
      }

//---------------------------------------------------------
//   mapChanged
//---------------------------------------------------------

void DrumCanvas::mapChanged(int spitch, int dpitch)
      {
      MidiTrack* curTrack = track();
      //TODO: Circumvent undo behaviour, since this isn't really a true change of the events,
      // but merely a change in pitch because the pitch relates to the order of the dlist.
      // Right now the sequencer spits out internalError: undoOp without startUndo() if start/stopundo is there, which is misleading
      // If start/stopundo is there, undo misbehaves since it doesn't undo but messes things up
      // Other solution: implement a specific undo-event for this (SC_DRUMMAP_MODIFIED or something) which undoes movement of
      // dlist-items (ml)
      PartList* parts=curTrack->parts();
      song->startUndo(); //Hmmm... me no like
      for (iPart part = parts->begin(); part != parts->end(); ++part) {
            EventList* events = part->second->events(); //curTrack->events();
            for (iEvent i = events->begin(); i != events->end(); ++i) {
                  Event event = i->second;
                  int pitch = event.pitch();
                  if (pitch == spitch) {
                        Event newEvent = event.clone();
                        newEvent.setPitch(dpitch);
                        audio->msgChangeEvent(event, newEvent, part->second, false);
                        }
                  else if (pitch == dpitch) {
                        Event newEvent = event.clone();
                        newEvent.setPitch(spitch);
                        audio->msgChangeEvent(event, newEvent, part->second, false);
                        }
                  }
            }
      song->endUndo(SC_EVENT_MODIFIED); //Hmmm... me no like
      song->update(SC_DRUMMAP);
      }

//---------------------------------------------------------
//   resizeEvent
//---------------------------------------------------------

void DrumCanvas::resizeEvent(QResizeEvent* ev)
      {
      if (ev->size().width() != ev->oldSize().width())
            emit newWidth(ev->size().width());
      EventCanvas::resizeEvent(ev);
      }


//---------------------------------------------------------
//   modifySelected
//---------------------------------------------------------

void DrumCanvas::modifySelected(NoteInfo::ValType type, int delta)
      {
      audio->msgIdle(true);
      song->startUndo();
      for (iCItem i = items.begin(); i != items.end(); ++i) {
            if (!(i->second->isSelected()))
                  continue;
            DEvent* e   = (DEvent*)(i->second);
            Event event = e->event();
            if (event.type() != Note)
                  continue;

            MidiPart* part = (MidiPart*)(e->part());
            Event newEvent = event.clone();

            switch (type) {
                  case NoteInfo::VAL_TIME:
                        break;
                  case NoteInfo::VAL_LEN:
                        /*
                        {
                        int len = event.lenTick() + delta;
                        if (len < 1)
                              len = 1;
                        newEvent.setLenTick(len);
                        }
                        */
                        printf("DrumCanvas::modifySelected - NoteInfo::VAL_LEN not implemented\n");
                        break;
                  case NoteInfo::VAL_VELON:
                        /*
                        {
                        int velo = event->velo() + delta;
                        if (velo > 127)
                              velo = 127;
                        else if (velo < 0)
                              velo = 0;
                        newEvent.setVelo(velo);
                        }
                        */
                        printf("DrumCanvas::modifySelected - NoteInfo::VAL_VELON not implemented\n");
                        break;
                  case NoteInfo::VAL_VELOFF:
                        /*
                        {
                        int velo = event.veloOff() + delta;
                        if (velo > 127)
                              velo = 127;
                        else if (velo < 0)
                              velo = 0;
                        newEvent.setVeloOff(velo);
                        }
                        */
                        printf("DrumCanvas::modifySelected - NoteInfo::VAL_VELOFF not implemented\n");
                        break;
                  case NoteInfo::VAL_PITCH:
                        {
                        int pitch = event.pitch() - delta; // Reversing order since the drumlist is displayed in increasing order
                        if (pitch > 127)
                              pitch = 127;
                        else if (pitch < 0)
                              pitch = 0;
                        newEvent.setPitch(pitch);
                        }
                        break;
                  }
            song->changeEvent(event, newEvent, part);
            song->undoOp(UndoOp::ModifyEvent, newEvent, event, part);
            }
      song->endUndo(SC_EVENT_MODIFIED);
      audio->msgIdle(false);
      }
