#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# A piano roll viewer/editor
# Copyright (C) 2012-2020 Filipe Coelho <falktx@falktx.com>
# Copyright (C) 2014-2015 Perry Nguyen
#
# 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 any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# For a full copy of the GNU General Public License see the doc/GPL.txt file.

# ------------------------------------------------------------------------------------------------------------
# Imports (Global)

from PyQt5.QtCore import Qt, QRectF, QPointF, pyqtSignal
from PyQt5.QtGui import QColor, QFont, QPen, QPainter, QTransform
from PyQt5.QtWidgets import QGraphicsItem, QGraphicsLineItem, QGraphicsOpacityEffect, QGraphicsRectItem, QGraphicsSimpleTextItem
from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView
from PyQt5.QtWidgets import QWidget, QLabel, QComboBox, QHBoxLayout, QVBoxLayout, QStyle

# ------------------------------------------------------------------------------------------------------------
# Imports (Custom)

from carla_shared import *

# ------------------------------------------------------------------------------------------------------------
# MIDI definitions, copied from CarlaMIDI.h

MAX_MIDI_CHANNELS = 16
MAX_MIDI_NOTE     = 128
MAX_MIDI_VALUE    = 128
MAX_MIDI_CONTROL  = 120 # 0x77

MIDI_STATUS_BIT  = 0xF0
MIDI_CHANNEL_BIT = 0x0F

# MIDI Messages List
MIDI_STATUS_NOTE_OFF              = 0x80 # note (0-127), velocity (0-127)
MIDI_STATUS_NOTE_ON               = 0x90 # note (0-127), velocity (0-127)
MIDI_STATUS_POLYPHONIC_AFTERTOUCH = 0xA0 # note (0-127), pressure (0-127)
MIDI_STATUS_CONTROL_CHANGE        = 0xB0 # see 'Control Change Messages List'
MIDI_STATUS_PROGRAM_CHANGE        = 0xC0 # program (0-127), none
MIDI_STATUS_CHANNEL_PRESSURE      = 0xD0 # pressure (0-127), none
MIDI_STATUS_PITCH_WHEEL_CONTROL   = 0xE0 # LSB (0-127), MSB (0-127)

# MIDI Message type
def MIDI_IS_CHANNEL_MESSAGE(status): return status >= MIDI_STATUS_NOTE_OFF and status <  MIDI_STATUS_BIT
def MIDI_IS_SYSTEM_MESSAGE(status):  return status >= MIDI_STATUS_BIT      and status <= 0xFF
def MIDI_IS_OSC_MESSAGE(status):     return status == '/'                   or status == '#'

# MIDI Channel message type
def MIDI_IS_STATUS_NOTE_OFF(status):              return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_NOTE_OFF
def MIDI_IS_STATUS_NOTE_ON(status):               return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_NOTE_ON
def MIDI_IS_STATUS_POLYPHONIC_AFTERTOUCH(status): return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_POLYPHONIC_AFTERTOUCH
def MIDI_IS_STATUS_CONTROL_CHANGE(status):        return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_CONTROL_CHANGE
def MIDI_IS_STATUS_PROGRAM_CHANGE(status):        return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_PROGRAM_CHANGE
def MIDI_IS_STATUS_CHANNEL_PRESSURE(status):      return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_CHANNEL_PRESSURE
def MIDI_IS_STATUS_PITCH_WHEEL_CONTROL(status):   return MIDI_IS_CHANNEL_MESSAGE(status) and (status & MIDI_STATUS_BIT) == MIDI_STATUS_PITCH_WHEEL_CONTROL

# MIDI Utils
def MIDI_GET_STATUS_FROM_DATA(data):  return data[0] & MIDI_STATUS_BIT  if MIDI_IS_CHANNEL_MESSAGE(data[0]) else data[0]
def MIDI_GET_CHANNEL_FROM_DATA(data): return data[0] & MIDI_CHANNEL_BIT if MIDI_IS_CHANNEL_MESSAGE(data[0]) else 0

# ------------------------------------------------------------------------------------------------------------
# Graphics Items

class NoteExpander(QGraphicsRectItem):
    def __init__(self, length, height, parent):
        QGraphicsRectItem.__init__(self, 0, 0, length, height, parent)
        self.parent = parent
        self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
        self.setAcceptHoverEvents(True)

        clearpen = QPen(QColor(0,0,0,0))
        self.setPen(clearpen)

        self.orig_brush = QColor(0, 0, 0, 0)
        self.hover_brush = QColor(200, 200, 200)
        self.stretch = False

    def mousePressEvent(self, event):
        QGraphicsRectItem.mousePressEvent(self, event)
        self.stretch = True

    def hoverEnterEvent(self, event):
        QGraphicsRectItem.hoverEnterEvent(self, event)
        if self.parent.isSelected():
            self.parent.setBrush(self.parent.select_brush)
        else:
            self.parent.setBrush(self.parent.orig_brush)
        self.setBrush(self.hover_brush)

    def hoverLeaveEvent(self, event):
        QGraphicsRectItem.hoverLeaveEvent(self, event)
        if self.parent.isSelected():
            self.parent.setBrush(self.parent.select_brush)
        elif self.parent.hovering:
            self.parent.setBrush(self.parent.hover_brush)
        else:
            self.parent.setBrush(self.parent.orig_brush)
        self.setBrush(self.orig_brush)

class NoteItem(QGraphicsRectItem):
    '''a note on the pianoroll sequencer'''
    def __init__(self, height, length, note_info):
        QGraphicsRectItem.__init__(self, 0, 0, length, height)

        self.setFlag(QGraphicsItem.ItemIsMovable)
        self.setFlag(QGraphicsItem.ItemIsSelectable)
        self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
        self.setAcceptHoverEvents(True)

        clearpen = QPen(QColor(0,0,0,0))
        self.setPen(clearpen)
        self.orig_brush = QColor(note_info[3], 0, 0)
        self.hover_brush = QColor(note_info[3] + 100, 200, 100)
        self.select_brush = QColor(note_info[3] + 100, 100, 100)
        self.setBrush(self.orig_brush)

        self.note = note_info
        self.length = length
        self.piano = self.scene

        self.pressed = False
        self.hovering = False
        self.moving_diff = (0,0)
        self.expand_diff = 0

        l = 5
        self.front = NoteExpander(l, height, self)
        self.back = NoteExpander(l, height, self)
        self.back.setPos(length - l, 0)

    def paint(self, painter, option, widget=None):
        paint_option = option
        paint_option.state &= ~QStyle.State_Selected
        QGraphicsRectItem.paint(self, painter, paint_option, widget)

    def setSelected(self, boolean):
        QGraphicsRectItem.setSelected(self, boolean)
        if boolean: self.setBrush(self.select_brush)
        else: self.setBrush(self.orig_brush)

    def hoverEnterEvent(self, event):
        self.hovering = True
        QGraphicsRectItem.hoverEnterEvent(self, event)
        if not self.isSelected():
            self.setBrush(self.hover_brush)

    def hoverLeaveEvent(self, event):
        self.hovering = False
        QGraphicsRectItem.hoverLeaveEvent(self, event)
        if not self.isSelected():
            self.setBrush(self.orig_brush)
        elif self.isSelected():
            self.setBrush(self.select_brush)

    def mousePressEvent(self, event):
        QGraphicsRectItem.mousePressEvent(self, event)
        self.setSelected(True)
        self.pressed = True

    def mouseMoveEvent(self, event):
        pass

    def moveEvent(self, event):
        offset = event.scenePos() - event.lastScenePos()

        if self.back.stretch:
            self.expand(self.back, offset)
        else:
            self.move_pos = self.scenePos() + offset \
                    + QPointF(self.moving_diff[0],self.moving_diff[1])
            pos = self.piano().enforce_bounds(self.move_pos)
            pos_x, pos_y = pos.x(), pos.y()
            pos_sx, pos_sy = self.piano().snap(pos_x, pos_y)
            self.moving_diff = (pos_x-pos_sx, pos_y-pos_sy)
            if self.front.stretch:
                right = self.rect().right() - offset.x() + self.expand_diff
                if (self.scenePos().x() == self.piano().piano_width and offset.x() < 0) \
                        or right < 10:
                    self.expand_diff = 0
                    return
                self.expand(self.front, offset)
                self.setPos(pos_sx, self.scenePos().y())
            else:
                self.setPos(pos_sx, pos_sy)

    def expand(self, rectItem, offset):
        rect = self.rect()
        right = rect.right() + self.expand_diff
        if rectItem == self.back:
            right += offset.x()
            if right > self.piano().grid_width:
                right = self.piano().grid_width
            elif right < 10:
                right = 10
            new_x = self.piano().snap(right)
        else:
            right -= offset.x()
            new_x = self.piano().snap(right+2.75)
        if self.piano().snap_value: new_x -= 2.75 # where does this number come from?!
        self.expand_diff = right - new_x
        self.back.setPos(new_x - 5, 0)
        rect.setRight(new_x)
        self.setRect(rect)

    def updateNoteInfo(self, pos_x, pos_y):
        self.note[0] = self.piano().get_note_num_from_y(pos_y)
        self.note[1] = self.piano().get_note_start_from_x(pos_x)
        self.note[2] = self.piano().get_note_length_from_x(
                self.rect().right() - self.rect().left())
        #print("note: {}".format(self.note))

    def mouseReleaseEvent(self, event):
        QGraphicsRectItem.mouseReleaseEvent(self, event)
        self.pressed = False
        if event.button() == Qt.LeftButton:
            self.moving_diff = (0,0)
            self.expand_diff = 0
            self.back.stretch = False
            self.front.stretch = False
            (pos_x, pos_y,) = self.piano().snap(self.pos().x(), self.pos().y())
            self.setPos(pos_x, pos_y)
            self.updateNoteInfo(pos_x, pos_y)

    def updateVelocity(self, event):
        offset = event.scenePos().x() - event.lastScenePos().x()
        self.note[3] += int(offset/5)
        if self.note[3] > 127:
            self.note[3] = 127
        elif self.note[3] < 0:
            self.note[3] = 0
        print("new vel: {}".format(self.note[3]))
        self.orig_brush = QColor(self.note[3], 0, 0)
        self.select_brush = QColor(self.note[3] + 100, 100, 100)
        self.setBrush(self.orig_brush)


class PianoKeyItem(QGraphicsRectItem):
    def __init__(self, width, height, note, parent):
        QGraphicsRectItem.__init__(self, 0, 0, width, height, parent)
        self.setPen(QPen(QColor(0,0,0,80)))
        self.width = width
        self.height = height
        self.note = note
        self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
        self.setAcceptHoverEvents(True)
        self.hover_brush = QColor(200, 0, 0)
        self.click_brush = QColor(255, 100, 100)
        self.pressed = False

    def hoverEnterEvent(self, event):
        QGraphicsRectItem.hoverEnterEvent(self, event)
        self.orig_brush = self.brush()
        self.setBrush(self.hover_brush)

    def hoverLeaveEvent(self, event):
        if self.pressed:
            self.pressed = False
            self.setBrush(self.hover_brush)
        QGraphicsRectItem.hoverLeaveEvent(self, event)
        self.setBrush(self.orig_brush)

    #def mousePressEvent(self, event):
    #    self.pressed = True
    #    self.setBrush(self.click_brush)

    def mouseMoveEvent(self, event):
        """this may eventually do something"""
        pass

    def mouseReleaseEvent(self, event):
        self.pressed = False
        QGraphicsRectItem.mouseReleaseEvent(self, event)
        self.setBrush(self.hover_brush)

class PianoRoll(QGraphicsScene):
    '''the piano roll'''

    noteclicked = pyqtSignal(int,bool)
    midievent = pyqtSignal(list)
    measureupdate = pyqtSignal(int)
    modeupdate = pyqtSignal(str)

    def __init__(self, time_sig = '4/4', num_measures = 4, quantize_val = '1/8'):
        QGraphicsScene.__init__(self)
        self.setBackgroundBrush(QColor(50, 50, 50))
        self.mousePos = QPointF()

        self.notes = []
        self.removed_notes = []
        self.selected_notes = []
        self.piano_keys = []

        self.marquee_select = False
        self.insert_mode = False
        self.velocity_mode = False
        self.place_ghost = False
        self.ghost_note = None
        self.default_ghost_vel = 100
        self.ghost_vel = self.default_ghost_vel

        ## dimensions
        self.padding = 2

        ## piano dimensions
        self.note_height = 10
        self.start_octave = -2
        self.end_octave = 8
        self.notes_in_octave = 12
        self.total_notes = (self.end_octave - self.start_octave) * self.notes_in_octave + 1
        self.piano_height = self.note_height * self.total_notes
        self.octave_height = self.notes_in_octave * self.note_height

        self.piano_width = 34

        ## height
        self.header_height = 20
        self.total_height = self.piano_height - self.note_height + self.header_height
        #not sure why note_height is subtracted

        ## width
        self.full_note_width = 250 # i.e. a 4/4 note
        self.snap_value = None
        self.quantize_val = quantize_val

        ### dummy vars that will be changed
        self.time_sig = (0,0)
        self.measure_width = 0
        self.num_measures = 0
        self.max_note_length = 0
        self.grid_width = 0
        self.value_width = 0
        self.grid_div = 0
        self.piano = None
        self.header = None
        self.play_head = None
        self.last_piano_note = None

        self.setGridDiv()
        self.default_length = 1. / self.grid_div


    # -------------------------------------------------------------------------
    # Callbacks

    def movePlayHead(self, transportInfo):
        ticksPerBeat = transportInfo['ticksPerBeat']
        max_ticks = ticksPerBeat * self.time_sig[0] * self.num_measures
        cur_tick = ticksPerBeat * self.time_sig[0] * transportInfo['bar'] + ticksPerBeat * transportInfo['beat'] + transportInfo['tick']
        frac = (cur_tick % max_ticks) / max_ticks
        self.play_head.setPos(QPointF(frac * self.grid_width, 0))

    def setTimeSig(self, time_sig):
        self.time_sig = time_sig
        self.measure_width = self.full_note_width * self.time_sig[0]/self.time_sig[1]
        self.max_note_length = self.num_measures * self.time_sig[0]/self.time_sig[1]
        self.grid_width = self.measure_width * self.num_measures
        self.setGridDiv()

    def setMeasures(self, measures):
        #try:
        self.num_measures = float(measures)
        self.max_note_length = self.num_measures * self.time_sig[0]/self.time_sig[1]
        self.grid_width = self.measure_width * self.num_measures
        self.refreshScene()
        #except:
            #pass

    def setDefaultLength(self, length):
        v = list(map(float, length.split('/')))
        if len(v) < 3:
            self.default_length = \
                    v[0] if len(v)==1 else \
                    v[0] / v[1]
            pos = self.enforce_bounds(self.mousePos)
            if self.insert_mode:
                self.makeGhostNote(pos.x(), pos.y())

    def setGridDiv(self, div=None):
        if not div: div = self.quantize_val
        try:
            val = list(map(int, div.split('/')))
            if len(val) < 3:
                self.quantize_val = div
                self.grid_div = val[0] if len(val)==1 else val[1]
                self.value_width = self.full_note_width / float(self.grid_div) if self.grid_div else None
                self.setQuantize(div)

                self.refreshScene()
        except ValueError:
            pass

    def setQuantize(self, value):
        val = list(map(float, value.split('/')))
        if len(val) == 1:
            self.quantize(val[0])
            self.quantize_val = value
        elif len(val) == 2:
            self.quantize(val[0] / val[1])
            self.quantize_val = value

    # -------------------------------------------------------------------------
    # Event Callbacks

    def keyPressEvent(self, event):
        QGraphicsScene.keyPressEvent(self, event)
        if event.key() == Qt.Key_F:
            if not self.insert_mode:
                self.velocity_mode = False
                self.insert_mode = True
                self.makeGhostNote(self.mousePos.x(), self.mousePos.y())
                self.modeupdate.emit('insert_mode')
            elif self.insert_mode:
                self.insert_mode = False
                if self.place_ghost: self.place_ghost = False
                self.removeItem(self.ghost_note)
                self.ghost_note = None
                self.modeupdate.emit('')
        elif event.key() == Qt.Key_D:
            if self.velocity_mode:
                self.velocity_mode = False
                self.modeupdate.emit('')
            else:
                if self.insert_mode:
                    self.removeItem(self.ghost_note)
                self.ghost_note = None
                self.insert_mode = False
                self.place_ghost = False
                self.velocity_mode = True
                self.modeupdate.emit('velocity_mode')
        elif event.key() == Qt.Key_A:
            if all((note.isSelected() for note in self.notes)):
                for note in self.notes:
                    note.setSelected(False)
                self.selected_notes = []
            else:
                for note in self.notes:
                    note.setSelected(True)
                self.selected_notes = self.notes[:]
        elif event.key() in (Qt.Key_Delete, Qt.Key_Backspace):
            self.notes = [note for note in self.notes if note not in self.selected_notes]
            for note in self.selected_notes:
                self.removeItem(note)
                self.midievent.emit(["midievent-remove", note.note[0], note.note[1], note.note[2], note.note[3]])
                del note
            self.selected_notes = []

    def mousePressEvent(self, event):
        QGraphicsScene.mousePressEvent(self, event)

        if self.piano.contains(event.scenePos()):
            item = self.itemAt(event.scenePos(), QTransform())
            if isinstance(item, PianoKeyItem) and item.note is not None:
                self.last_piano_note = item.note
                self.noteclicked.emit(self.last_piano_note, True)
                return

        if not (any(key.pressed for key in self.piano_keys)
                or any(note.pressed for note in self.notes)):
            for note in self.selected_notes:
                note.setSelected(False)
            self.selected_notes = []

            if event.button() == Qt.LeftButton:
                if self.insert_mode:
                    self.place_ghost = True
                else:
                    self.marquee_select = True
                    self.marquee_rect = QRectF(event.scenePos().x(), event.scenePos().y(), 1, 1)
                    self.marquee = QGraphicsRectItem(self.marquee_rect)
                    self.marquee.setBrush(QColor(255, 255, 255, 100))
                    self.addItem(self.marquee)

        else:
            for s_note in self.notes:
                if s_note.pressed and s_note in self.selected_notes:
                    break
                elif s_note.pressed and s_note not in self.selected_notes:
                    for note in self.selected_notes:
                        note.setSelected(False)
                    self.selected_notes = [s_note]
                    break
            for note in self.selected_notes:
                if not self.velocity_mode:
                    note.mousePressEvent(event)

    def mouseMoveEvent(self, event):
        QGraphicsScene.mouseMoveEvent(self, event)

        if self.last_piano_note is not None:
            return

        self.mousePos = event.scenePos()
        if not (any((key.pressed for key in self.piano_keys))):
            m_pos = event.scenePos()
            if self.insert_mode and self.place_ghost: #placing a note
                m_width = self.ghost_rect.x() + self.ghost_rect_orig_width
                if m_pos.x() > m_width:
                    m_new_x = self.snap(m_pos.x())
                    self.ghost_rect.setRight(m_new_x)
                    self.ghost_note.setRect(self.ghost_rect)
                #self.adjust_note_vel(event)
            else:
                m_pos = self.enforce_bounds(m_pos)

                if self.insert_mode: #ghostnote follows mouse around
                    (m_new_x, m_new_y) = self.snap(m_pos.x(), m_pos.y())
                    self.ghost_rect.moveTo(m_new_x, m_new_y)
                    try:
                        self.ghost_note.setRect(self.ghost_rect)
                    except RuntimeError:
                        self.ghost_note = None
                        self.makeGhostNote(m_new_x, m_new_y)

                elif self.marquee_select:
                    marquee_orig_pos = event.buttonDownScenePos(Qt.LeftButton)
                    if marquee_orig_pos.x() < m_pos.x() and marquee_orig_pos.y() < m_pos.y():
                        self.marquee_rect.setBottomRight(m_pos)
                    elif marquee_orig_pos.x() < m_pos.x() and marquee_orig_pos.y() > m_pos.y():
                        self.marquee_rect.setTopRight(m_pos)
                    elif marquee_orig_pos.x() > m_pos.x() and marquee_orig_pos.y() < m_pos.y():
                        self.marquee_rect.setBottomLeft(m_pos)
                    elif marquee_orig_pos.x() > m_pos.x() and marquee_orig_pos.y() > m_pos.y():
                        self.marquee_rect.setTopLeft(m_pos)
                    self.marquee.setRect(self.marquee_rect)
                    self.selected_notes = []
                    for item in self.collidingItems(self.marquee):
                        if item in self.notes:
                            self.selected_notes.append(item)

                    for note in self.notes:
                        if note in self.selected_notes: note.setSelected(True)
                        else: note.setSelected(False)

                elif self.velocity_mode:
                    if Qt.LeftButton == event.buttons():
                        for note in self.selected_notes:
                            note.updateVelocity(event)

                elif not self.marquee_select: #move selected
                    if Qt.LeftButton == event.buttons():
                        x = y = False
                        if any(note.back.stretch for note in self.selected_notes):
                            x = True
                        elif any(note.front.stretch for note in self.selected_notes):
                            y = True
                        for note in self.selected_notes:
                            note.back.stretch = x
                            note.front.stretch = y
                            note.moveEvent(event)

    def mouseReleaseEvent(self, event):
        QGraphicsScene.mouseReleaseEvent(self, event)

        if self.last_piano_note is not None:
            self.noteclicked.emit(self.last_piano_note, False)
            self.last_piano_note = None
            return

        if not (any((key.pressed for key in self.piano_keys)) or any((note.pressed for note in self.notes))):
            if event.button() == Qt.LeftButton:
                if self.place_ghost and self.insert_mode:
                    self.place_ghost = False
                    note_start = self.get_note_start_from_x(self.ghost_rect.x())
                    note_num = self.get_note_num_from_y(self.ghost_rect.y())
                    note_length = self.get_note_length_from_x(self.ghost_rect.width())
                    self.drawNote(note_num, note_start, note_length, self.ghost_vel)
                    self.midievent.emit(["midievent-add", note_num, note_start, note_length, self.ghost_vel])
                    self.makeGhostNote(self.mousePos.x(), self.mousePos.y())
                elif self.marquee_select:
                    self.marquee_select = False
                    self.removeItem(self.marquee)

        elif not self.marquee_select:
            for note in self.selected_notes:
                old_info = note.note[:]
                note.mouseReleaseEvent(event)
                if self.velocity_mode:
                    note.setSelected(True)
                if not old_info == note.note:
                    self.midievent.emit(["midievent-remove", old_info[0], old_info[1], old_info[2], old_info[3]])
                    self.midievent.emit(["midievent-add", note.note[0], note.note[1], note.note[2], note.note[3]])

    # -------------------------------------------------------------------------
    # Internal Functions

    def drawHeader(self):
        self.header = QGraphicsRectItem(0, 0, self.grid_width, self.header_height)
        #self.header.setZValue(1.0)
        self.header.setPos(self.piano_width, 0)
        self.addItem(self.header)

    def drawPiano(self):
        piano_keys_width = self.piano_width - self.padding
        labels = ('B','Bb','A','Ab','G','Gb','F','E','Eb','D','Db','C')
        black_notes = (2,4,6,9,11)
        piano_label = QFont()
        piano_label.setPointSize(6)
        self.piano = QGraphicsRectItem(0, 0, piano_keys_width, self.piano_height)
        self.piano.setPos(0, self.header_height)
        self.addItem(self.piano)

        key = PianoKeyItem(piano_keys_width, self.note_height, 78, self.piano)
        label = QGraphicsSimpleTextItem('C9', key)
        label.setPos(18, 1)
        label.setFont(piano_label)
        key.setBrush(QColor(255, 255, 255))
        for i in range(self.end_octave - self.start_octave, 0, -1):
            for j in range(self.notes_in_octave, 0, -1):
                note = (self.end_octave - i + 3) * 12 - j
                if j in black_notes:
                    key = PianoKeyItem(piano_keys_width/1.4, self.note_height, note, self.piano)
                    key.setBrush(QColor(0, 0, 0))
                    key.setZValue(1.0)
                    key.setPos(0, self.note_height * j + self.octave_height * (i - 1))
                elif (j - 1) and (j + 1) in black_notes:
                    key = PianoKeyItem(piano_keys_width, self.note_height * 2, note, self.piano)
                    key.setBrush(QColor(255, 255, 255))
                    key.setPos(0, self.note_height * j + self.octave_height * (i - 1) - self.note_height/2.)
                elif (j - 1) in black_notes:
                    key = PianoKeyItem(piano_keys_width, self.note_height * 3./2, note, self.piano)
                    key.setBrush(QColor(255, 255, 255))
                    key.setPos(0, self.note_height * j + self.octave_height * (i - 1) - self.note_height/2.)
                elif (j + 1) in black_notes:
                    key = PianoKeyItem(piano_keys_width, self.note_height * 3./2, note, self.piano)
                    key.setBrush(QColor(255, 255, 255))
                    key.setPos(0, self.note_height * j + self.octave_height * (i - 1))
                if j == 12:
                    label = QGraphicsSimpleTextItem('{}{}'.format(labels[j - 1],
                                                                  self.end_octave - i + 1),
                                                                  key)
                    label.setPos(18, 6)
                    label.setFont(piano_label)
                self.piano_keys.append(key)

    def drawGrid(self):
        black_notes = [2,4,6,9,11]
        scale_bar = QGraphicsRectItem(0, 0, self.grid_width, self.note_height, self.piano)
        scale_bar.setPos(self.piano_width, 0)
        scale_bar.setBrush(QColor(100,100,100))
        clearpen = QPen(QColor(0,0,0,0))
        for i in range(self.end_octave - self.start_octave, self.start_octave - self.start_octave, -1):
            for j in range(self.notes_in_octave, 0, -1):
                scale_bar = QGraphicsRectItem(0, 0, self.grid_width, self.note_height, self.piano)
                scale_bar.setPos(self.piano_width, self.note_height * j + self.octave_height * (i - 1))
                scale_bar.setPen(clearpen)
                if j not in black_notes:
                    scale_bar.setBrush(QColor(120,120,120))
                else:
                    scale_bar.setBrush(QColor(100,100,100))

        measure_pen = QPen(QColor(0, 0, 0, 120), 3)
        half_measure_pen = QPen(QColor(0, 0, 0, 40), 2)
        line_pen = QPen(QColor(0, 0, 0, 40))
        for i in range(0, int(self.num_measures) + 1):
            measure = QGraphicsLineItem(0, 0, 0, self.piano_height + self.header_height - measure_pen.width(), self.header)
            measure.setPos(self.measure_width * i, 0.5 * measure_pen.width())
            measure.setPen(measure_pen)
            if i < self.num_measures:
                number = QGraphicsSimpleTextItem('%d' % (i + 1), self.header)
                number.setPos(self.measure_width * i + 5, 2)
                number.setBrush(Qt.white)
                for j in self.frange(0, self.time_sig[0]*self.grid_div/self.time_sig[1], 1.):
                    line = QGraphicsLineItem(0, 0, 0, self.piano_height, self.header)
                    line.setZValue(1.0)
                    line.setPos(self.measure_width * i + self.value_width * j, self.header_height)
                    if j == self.time_sig[0]*self.grid_div/self.time_sig[1] / 2.0:
                        line.setPen(half_measure_pen)
                    else:
                        line.setPen(line_pen)

    def drawPlayHead(self):
        self.play_head = QGraphicsLineItem(self.piano_width, self.header_height, self.piano_width, self.total_height)
        self.play_head.setPen(QPen(QColor(255,255,255,50), 2))
        self.play_head.setZValue(1.)
        self.addItem(self.play_head)

    def refreshScene(self):
        list(map(self.removeItem, self.notes))
        self.selected_notes = []
        self.piano_keys = []
        self.clear()
        self.drawPiano()
        self.drawHeader()
        self.drawGrid()
        self.drawPlayHead()

        for note in self.notes[:]:
            if note.note[1] >= (self.num_measures * self.time_sig[0]):
                self.notes.remove(note)
                self.removed_notes.append(note)
                #self.midievent.emit(["midievent-remove", note.note[0], note.note[1], note.note[2], note.note[3]])
            elif note.note[2] > self.max_note_length:
                new_note = note.note[:]
                new_note[2] = self.max_note_length
                self.notes.remove(note)
                self.drawNote(new_note[0], new_note[1], self.max_note_length, new_note[3], False)
                self.midievent.emit(["midievent-remove", note.note[0], note.note[1], note.note[2], note.note[3]])
                self.midievent.emit(["midievent-add", new_note[0], new_note[1], new_note[2], new_note[3]])

        for note in self.removed_notes[:]:
            if note.note[1] < (self.num_measures * self.time_sig[0]):
                self.removed_notes.remove(note)
                self.notes.append(note)

        list(map(self.addItem, self.notes))
        if self.views():
            self.views()[0].setSceneRect(self.itemsBoundingRect())

    def clearNotes(self):
        self.clear()
        self.notes = []
        self.removed_notes = []
        self.selected_notes = []
        self.drawPiano()
        self.drawHeader()
        self.drawGrid()

    def makeGhostNote(self, pos_x, pos_y):
        """creates the ghostnote that is placed on the scene before the real one is."""
        if self.ghost_note:
            self.removeItem(self.ghost_note)
        length = self.full_note_width * self.default_length
        (start, note) = self.snap(pos_x, pos_y)
        self.ghost_vel = self.default_ghost_vel
        self.ghost_rect = QRectF(start, note, length, self.note_height)
        self.ghost_rect_orig_width = self.ghost_rect.width()
        self.ghost_note = QGraphicsRectItem(self.ghost_rect)
        self.ghost_note.setBrush(QColor(230, 221, 45, 100))
        self.addItem(self.ghost_note)

    def drawNote(self, note_num, note_start=None, note_length=None, note_velocity=None, add=True):
        """
        note_num: midi number, 0 - 127
        note_start: 0 - (num_measures * time_sig[0]) so this is in beats
        note_length: 0 - (num_measures  * time_sig[0]/time_sig[1]) this is in measures
        note_velocity: 0 - 127
        """

        info = [note_num, note_start, note_length, note_velocity]

        if not note_start % (self.num_measures * self.time_sig[0]) == note_start:
            #self.midievent.emit(["midievent-remove", note_num, note_start, note_length, note_velocity])
            while not note_start % (self.num_measures * self.time_sig[0]) == note_start:
                self.setMeasures(self.num_measures+1)
            self.measureupdate.emit(self.num_measures)
            self.refreshScene()

        x_start = self.get_note_x_start(note_start)
        if note_length > self.max_note_length:
            note_length = self.max_note_length + 0.25
        x_length = self.get_note_x_length(note_length)
        y_pos = self.get_note_y_pos(note_num)

        note = NoteItem(self.note_height, x_length, info)
        note.setPos(x_start, y_pos)

        self.notes.append(note)
        if add:
            self.addItem(note)

    # -------------------------------------------------------------------------
    # Helper Functions

    def frange(self, x, y, t):
        while x < y:
            yield x
            x += t

    def quantize(self, value):
        self.snap_value = float(self.full_note_width) * value if value else None

    def snap(self, pos_x, pos_y = None):
        if self.snap_value:
            pos_x = int(round((pos_x - self.piano_width) / self.snap_value)) \
                    * self.snap_value + self.piano_width
        if pos_y is not None:
            pos_y = int((pos_y - self.header_height) / self.note_height) \
                    * self.note_height + self.header_height
        return (pos_x, pos_y) if pos_y is not None else pos_x

    def adjust_note_vel(self, event):
        m_pos = event.scenePos()
        #bind velocity to vertical mouse movement
        self.ghost_vel += (event.lastScenePos().y() - m_pos.y())/10
        if self.ghost_vel < 0:
            self.ghost_vel = 0
        elif self.ghost_vel > 127:
            self.ghost_vel = 127

        m_width = self.ghost_rect.x() + self.ghost_rect_orig_width
        if m_pos.x() < m_width:
            m_pos.setX(m_width)
        m_new_x = self.snap(m_pos.x())
        self.ghost_rect.setRight(m_new_x)
        self.ghost_note.setRect(self.ghost_rect)


    def enforce_bounds(self, pos):
        if pos.x() < self.piano_width:
            pos.setX(self.piano_width)
        elif pos.x() > self.grid_width + self.piano_width:
            pos.setX(self.grid_width + self.piano_width)
        if pos.y() < self.header_height + self.padding:
            pos.setY(self.header_height + self.padding)
        return pos

    def get_note_start_from_x(self, note_x):
        return (note_x - self.piano_width) / (self.grid_width / self.num_measures / self.time_sig[0])


    def get_note_x_start(self, note_start):
        return self.piano_width + \
                (self.grid_width / self.num_measures / self.time_sig[0]) * note_start

    def get_note_x_length(self, note_length):
        return float(self.time_sig[1]) / self.time_sig[0] * note_length * self.grid_width / self.num_measures

    def get_note_length_from_x(self, note_x):
        return float(self.time_sig[0]) / self.time_sig[1] * self.num_measures / self.grid_width \
                * note_x


    def get_note_y_pos(self, note_num):
        return self.header_height + self.note_height * (self.total_notes - note_num - 1)

    def get_note_num_from_y(self, note_y_pos):
        return -(((note_y_pos - self.header_height) / self.note_height) - self.total_notes + 1)

class PianoRollView(QGraphicsView):
    def __init__(self, parent, time_sig = '4/4', num_measures = 4, quantize_val = '1/8'):
        QGraphicsView.__init__(self, parent)
        self.piano = PianoRoll(time_sig, num_measures, quantize_val)
        self.setScene(self.piano)
        #self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

        x = 0   * self.sceneRect().width() + self.sceneRect().left()
        y = 0.4 * self.sceneRect().height() + self.sceneRect().top()
        self.centerOn(x, y)

        self.setAlignment(Qt.AlignLeft)
        self.o_transform = self.transform()
        self.zoom_x = 1
        self.zoom_y = 1

    def setZoomX(self, scale_x):
        self.setTransform(self.o_transform)
        self.zoom_x = 1 + scale_x / float(99) * 2
        self.scale(self.zoom_x, self.zoom_y)

    def setZoomY(self, scale_y):
        self.setTransform(self.o_transform)
        self.zoom_y = 1 + scale_y / float(99)
        self.scale(self.zoom_x, self.zoom_y)

# ------------------------------------------------------------------------------------------------------------

class ModeIndicator(QWidget):
    def __init__(self, parent):
        QWidget.__init__(self, parent)
        #self.setGeometry(0, 0, 30, 20)
        self.setFixedSize(30,20)
        self.mode = None

    def paintEvent(self, event):
        event.accept()

        painter = QPainter(self)

        painter.setPen(QPen(QColor(0, 0, 0, 0)))
        if self.mode == 'velocity_mode':
            painter.setBrush(QColor(127, 0, 0))
        elif self.mode == 'insert_mode':
            painter.setBrush(QColor(0, 100, 127))
        else:
            painter.setBrush(QColor(0, 0, 0, 0))
        painter.drawRect(0, 0, 30, 20)

    def changeMode(self, new_mode):
        self.mode = new_mode
        self.update()

# ------------------------------------------------------------------------------------------------------------
