#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net )

This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),

Laborejo2 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 3 of the License, or
(at your option) 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.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""

import logging; logger = logging.getLogger(__name__); logger.info("import")

#Standard Library

#Third party
from PyQt5 import QtCore, QtGui, QtWidgets

#Template

#Our own files
import engine.api as api

from .constantsAndConfigs import constantsAndConfigs
from .grid import GuiGrid
from .conductor import Conductor, ConductorTransparentBlock
from .musicstructures import GuiBlockHandle, GuiTrack
from .graphs import CCGraphTransparentBlock, CCPath
from .cursor import Cursor, Playhead, Selection


class GuiScore(QtWidgets.QGraphicsScene):
    def __init__(self, parentView):
        super().__init__()
        self.parentView = parentView
        self.setItemIndexMethod(QtWidgets.QGraphicsScene.NoIndex)


        self.tracks = {} #trackId:guiTrack, #if we don't save the instances here in Python space Qt will loose them and they will not be displayed without any error message.

        self.deleteOnIdleStack = [] # a stack that holds hidden items that need to be deleted. Hiding is much faster than deleting so we use that for the blocking function. Since we always recreate items and never re-use this is ok as a list. no need for a set.
        self._deleteOnIdleLoop = QtCore.QTimer()
        self._deleteOnIdleLoop.start(0) #0 means "if there is time"
        self._deleteOnIdleLoop.timeout.connect(self._deleteOnIdle) #processes deleteOnIdleStack

        self.duringTrackDragAndDrop = None #switched to a QGraphicsItem (e.g. GuiTrack) while a track is moved around by the mouse
        self.duringBlockDragAndDrop = None #switched to a QGraphicsItem (e.g. GuiTrack) while a block is moved around by the mouse

        self.conductor = Conductor(parentView = self.parentView)
        self.addItem(self.conductor)
        self.conductor.setPos(0, -1 * self.conductor.totalHeight)

        self.yStart = self.conductor.y() - self.conductor.totalHeight/2

        self.hiddenTrackCounter = QtWidgets.QGraphicsSimpleTextItem("") #filled in by self.redraw on callback tracksChanged (loading or toggling visibility of backend tracks)
        self.addItem(self.hiddenTrackCounter)

        self.backColor = QtGui.QColor()
        self.backColor.setNamedColor("#fdfdff")
        self.setBackgroundBrush(self.backColor)

        self.grid = GuiGrid(parentScene=self)
        self.addItem(self.grid)
        self.grid.setPos(0, -20 * constantsAndConfigs.stafflineGap) #this is more calculation than simply using self.yStart, and might require manual adjustment in the future, but at least it guarantees the grid matches the staffline positions
        self.grid.setZValue(-50)

        self.cachedSceneHeight = 0 #set in self.redraw. Used by updateTrack to set the sceneRect

        #All Cursors
        self.cursor = Cursor()
        self.addItem(self.cursor)
        self.cursor.setZValue(90)
        self.selection = Selection()
        self.selection.setZValue(80)
        self.addItem(self.selection)
        self.playhead = Playhead(self)
        self.addItem(self.playhead)
        self.playhead.setY(self.yStart)
        self.playhead.setZValue(100)

        #Callbacks
        api.callbacks.tracksChanged.append(self.redraw)
        api.callbacks.updateTrack.append(self.updateTrack)
        api.callbacks.updateBlockTrack.append(self.trackPaintBlockBackgroundColors)

        api.callbacks.updateGraphTrackCC.append(self.updateGraphTrackCC)
        api.callbacks.updateGraphBlockTrack.append(self.updateGraphBlockTrack)
        api.callbacks.graphCCTracksChanged.append(self.syncCCsToBackend)

    def updateMode(self, nameAsString):
        assert nameAsString in constantsAndConfigs.availableEditModes

        for track in self.tracks.values():
            track.updateMode(nameAsString)

        self.grid.updateMode(nameAsString)

    def maxTrackLength(self):
        if self.tracks:
            return max(tr.lengthInPixel for tr in self.tracks.values())
            #return max(max(tr.lengthInPixel for tr in self.tracks.values()), self.parentView.geometry().width())
            #return max(max(tr.lengthInPixel for tr in self.scene().tracks.values()), self.scene().parentView.geometry().width()) + self.scene().parentView.geometry().width()
        else:
            return 0 #self.parentView.geometry().width()

    def updateSceneRect(self):
        self.parentView.setSceneRect(QtCore.QRectF(-5, self.yStart, self.maxTrackLength() + 300, self.cachedSceneHeight)) #x,y,w,h

    def updateTrack(self, trackId, staticRepresentationList):
        """for callbacks"""
        if trackId in self.tracks:
            self.tracks[trackId].redraw(staticRepresentationList)
        #else:
            #hidden track. But this can still happen through the data editor
        self.parentView.updateMode()
        self.updateSceneRect()

    def trackPaintBlockBackgroundColors(self, trackId, staticBlocksRepresentation):
        if trackId in self.tracks:
            self.tracks[trackId].paintBlockBackgroundColors(staticBlocksRepresentation)
        #else:
            #hidden track.

    def updateGraphTrackCC(self, trackId, ccNumber, staticRepresentationList):
        """TrackId is a real notation track which has a dict of CCs"""
        if trackId in self.tracks:
            track = self.tracks[trackId]
            ccPath = track.ccPaths[ccNumber]
            ccPath.createGraphicItemsFromData(staticRepresentationList)

    def updateGraphBlockTrack(self, trackId, ccNumber, staticRepresentationList):
        """TrackId is a real notation track which has a dict of CCs"""
        if trackId in self.tracks:
            self.tracks[trackId].ccPaths[ccNumber].updateGraphBlockTrack(staticRepresentationList)

    def toggleNoteheadsRectangles(self):
        for track in self.tracks.values():
            track.toggleNoteheadsRectangles()

    def syncCCsToBackend(self, trackId, listOfCCsInThisTrack):
        """Delete ccGraphs that are gui-only,
        create gui versions of graphs that are backend-only.
        Don't touch the others.

        This is the entry point for new CCPaths. They get created
        only here.
        """
        #TODO: and moved from one CC value to another?
        guiCCs = set(self.tracks[trackId].ccPaths.keys())

        for backendCC in listOfCCsInThisTrack:
            if backendCC in guiCCs:
                guiCCs.remove(backendCC) #all right. no update needed.
            else: #new CC. not existent in the Gui yet. Create.
                #This is the place where we create new CCPaths
                new = CCPath(parentGuiTrack = self.tracks[trackId], parentDataTrackId = trackId)
                self.tracks[trackId].ccPaths[backendCC] = new #store in the GUI Track
                new.setParentItem(self.tracks[trackId])
                new.setPos(0,0)
                #new.setZValue(100)

        #all items left in the set are GUI-only CCs. which means the backend graphs were deleted. Delete them here as well.
        for cc in guiCCs:
            self.removeWhenIdle(self.tracks[trackId].ccPaths[cc])
            del self.tracks[trackId].ccPaths[cc]


    def redraw(self, listOfStaticTrackRepresentations):
        """The order of guiTracks depends on the backend index.
        This way it is a no-brainer, we don't need to maintain our own
        order, just sync with the backend when the callback comes in.

        Also handles meta data like track names.
        But not the actual track content, which is done
        through self.updateTrack which has its own api-callback

        called by callbacksDatabase.tracksChanged"""

        for track in self.tracks.values():
            track.hide()

        doubleTrackOffset = 0
        for trackExportObject in listOfStaticTrackRepresentations:
            if not trackExportObject["id"] in self.tracks:
                guiTrack = GuiTrack(self, trackExportObject)
                self.tracks[trackExportObject["id"]] = guiTrack
                self.addItem(guiTrack)
                guiTrack.secondStageInitNowThatWeHaveAScene()

            self.tracks[trackExportObject["id"]].staticExportItem = trackExportObject
            self.tracks[trackExportObject["id"]].setPos(0, constantsAndConfigs.trackHeight * trackExportObject["index"] + doubleTrackOffset)
            self.tracks[trackExportObject["id"]].setZValue(0) #direct comparison only possible with the grid, which is at -50
            self.tracks[trackExportObject["id"]].nameGraphic.setText(trackExportObject["name"])
            self.tracks[trackExportObject["id"]].show()

            if trackExportObject["double"]:
                doubleTrackOffset += constantsAndConfigs.trackHeight

        toDelete = []
        for trackId, track in self.tracks.items():
            if not track.isVisible():
                toDelete.append((trackId, track))

        for trackId_, track_ in toDelete:
            self.removeWhenIdle(track_)
            del self.tracks[trackId_]

        #Finally, under the last track, tell the user how many hidden tracks there are
        nrOfHiddenTracks = len(api.session.data.hiddenTracks)
        if nrOfHiddenTracks:
            self.hiddenTrackCounter.setText("… and {} hidden tracks".format(nrOfHiddenTracks))
        else: #remove previous status message
            self.hiddenTrackCounter.setText("")

        belowLastTrack = constantsAndConfigs.trackHeight * (trackExportObject["index"] + 1) + doubleTrackOffset
        self.hiddenTrackCounter.setPos(5, belowLastTrack)
        self.cachedSceneHeight = belowLastTrack + constantsAndConfigs.trackHeight

    def removeWhenIdle(self, item):
        """Call this function instead of removeItem. You are responsible to delete the item from any
        list or other container where it was stored in the meantime yourself.

        Other methods tried:
        -removeItem much slower
        -create second scene and do scene2.addItem to get it out here - same as removeItem, if not slower
        -delete a track and recreate a new one - same as all removeItems
        -just hide the track and create a new one. slightly slower as hiding just the items.
        -item.setFlag(QtWidgets.QGraphicsItem.ItemHasNoContents) slow, but not as slow as removeItem
        -just hide items, never delete them is fast but gets slower the more items are hidden in the track
        """
        item.hide()
        self.deleteOnIdleStack.append(item) #This will actually delete the item and remove it from the scene when there is idle time

    def _deleteOnIdle(self):
        """Hiding a QGraphicsItem is much faster than removing it from the scene. To keep the
        GUI responsive but also to get rid of the old data we hide in createGraphicItemsFromData
        and whenever there is time the actual removing and deleting will happen here.

        This function is connected to a timer(0) defined in self init.
        """
        if self.deleteOnIdleStack:
            deleteMe = self.deleteOnIdleStack.pop()
            self.removeItem(deleteMe) #This is the only line in the program that should call scene.removeItem
            del deleteMe


    def trackAt(self, qScenePosition):
        """trackAt always returns the full GuiTrack, even if in ccEdit mode."""

        if qScenePosition.y() < self.conductor.y() + self.conductor.totalHeight/4:
            return self.conductor

        for guiTrack in sorted(self.tracks.values(), key = lambda gT: gT.y()):
            if guiTrack.y() >= qScenePosition.y() - constantsAndConfigs.trackHeight/2:
                return guiTrack
        else:
            return None #no track here.

    def blockAt(self, qScenePosition):
        track = self.trackAt(qScenePosition)
        if track is self.conductor:
            return self.conductor.blockAt(qScenePosition.x())
        if self.parentView.mode() in ("block", "notation"):
            if track:
                return track.blockAt(qScenePosition.x())
        elif self.parentView.mode() == "cc":
            if track and constantsAndConfigs.ccViewValue in track.ccPaths:
                return track.ccPaths[constantsAndConfigs.ccViewValue].blockAt(qScenePosition.x())
        else:
            raise NotImplementedError
        return None


    def notTrueAnymore_wheelEvent(self, event):
        """
        This docstring was either wrong in the first place or something changed in my code.
        However, we do not need to eat wheelEvent here anymore. On the contrary: It will block
        items, like the tempoItem from receiving wheelEvents.
        The commented out if/else for item detection also has a chance to work, but we don't need
        to test for that at all. Standard Qt-behavoiour is fine.
        What follows is the old docstring, for legacy documentation reasons:

        We MUST handle the event somehow. Otherwise background grid items will block the views(!)
        wheel scrolling, even when disabled and setting accepting mouse events to none.
        This is a qt bug that won't be fixed because API stability over correctnes (according to the
        bugtracker.

        Contrary to other parts of the system event.ignore and accept actually mean something.
        ignore will tell the caller to use the event itself, e.g. scroll.

        This event gets the wheel BEFORE the main window (zoom)
        """
        #item = self.itemAt(event.scenePos(), self.parentView.transform())
        #if type(item) is items.Note:
        #super().wheelEvent(event) #send to child item
        #else:
        event.ignore() #so the view scrolls or we zoom


    def stretchXCoordinates(self, factor):
        """Reposition the items on the X axis.
        Call goes through all parents/children, starting from ScoreView._stretchXCoordinates.
        Docstring there."""
        #The big structures have a fixed position at (0,0) and move its child items, like notes, internally
        #Some items, like the cursor, move around, as a whole item, in the scene directly and need no stretchXCoordinates() themselves.
        self.grid.stretchXCoordinates(factor)
        self.conductor.stretchXCoordinates(factor)
        self.cursor.setX(self.cursor.pos().x() * factor)
        self.playhead.setX(self.playhead.pos().x() * factor)
        self.selection.stretchXCoordinates(factor)

        for track in self.tracks.values():
            track.stretchXCoordinates(factor)

        self.updateSceneRect()

    #Macro-Structure: Score / Track / Block Moving and Duplicating
    #Hold the ALT Key to unlock the moving mode.super().keyPressEvent(event)
    #No note-editing requires a mouse action, so the mouse is free for controlling other aspects.
    #Like zooming or moving blocks around.

    """
    def keyPressEvent(self, event):
        #Triggers only if there is no shortcut for an action.
        modifiers = QtWidgets.QApplication.keyboardModifiers()
        if modifiers == QtCore.Qt.AltModifier:
            Alt

        super().keyPressEvent(event)
    """

    def mousePressEvent(self, event):
        """Pressing the mouse button is the first action of drag
        and drop. We make the mouse cursor invisible so the user
        can see where the object is moving
        """
        if self.parentView.mode() == "block": #CC Block Moving is done in its own if clause below.
            if event.button() == QtCore.Qt.LeftButton:
                modifiers = QtWidgets.QApplication.keyboardModifiers()

                #Block Move
                if modifiers == QtCore.Qt.NoModifier:
                    block = self.blockAt(event.scenePos())
                    if block: #works for note blocks and conductor blocks
                        block.staticExportItem["guiPosStart"] = block.pos() #without shame we hijack the backend-dict.
                        self.duringBlockDragAndDrop = block
                        block.mousePressEventCustom(event)

                #Track Move
                elif (modifiers == QtCore.Qt.AltModifier or modifiers == QtCore.Qt.ShiftModifier ) and self.parentView.mode() == "block":
                    track = self.trackAt(event.scenePos())
                    if track and not track is self.conductor:
                        #self.parentView.setCursor(QtCore.Qt.BlankCursor)
                        self.cursor.hide()
                        track.staticExportItem["guiPosStart"] = track.pos()
                        self.duringTrackDragAndDrop = track

            elif event.button() == QtCore.Qt.RightButton:
                """Fake context menu event. The built-in one never got
                the correct detection.  Yes, this will hardcode the
                context menu to right mouse button, which is not really
                the only way to trigger a context menu. However, too
                much time has been spent to get this right already.
                Good enough is better than good."""
                block = self.blockAt(event.scenePos())
                if block: #works for note blocks and conductor blocks
                    block.contextMenuEventCustom(event)
                    event.accept() #eat it
                    return

        #CC is a special case because we need to handle points and block movement at the same time.
        #Hence the alt/shift key is mandatory here.  That was also the reason why everything was with the middle mouse button AND shortcuts once,
        #but that was too inconvenient overall. This system is not 100% straightforward but easy enough to remember. And more robust against accidents.
        elif self.parentView.mode() == "cc" and event.button() == QtCore.Qt.LeftButton:
            modifiers = QtWidgets.QApplication.keyboardModifiers()
            if (modifiers == QtCore.Qt.AltModifier or modifiers == QtCore.Qt.ShiftModifier):
                block = self.blockAt(event.scenePos())
                if block and type(block) is CCGraphTransparentBlock:
                    block.staticExportItem["guiPosStart"] = block.pos() #without shame we hijack the backend-dict.
                    self.duringBlockDragAndDrop = block
                    block.mousePressEventCustom(event)
                    event.accept() #eat it
                    return

        super().mousePressEvent(event)


    def mouseMoveEvent(self, event):
        """Catches certain mouse events for moving tracks and blocks.
        Otherwise the event is propagated to the real QGraphicsItem.
        Don't forget that an item needs to have the flag movable or selectable or else
        it will not get mouseRelease or mouseMove events. MousePress always works.
        """
        super().mouseMoveEvent(event)

        if self.duringTrackDragAndDrop:
            #X is locked for tracks.
            x = self.duringTrackDragAndDrop.staticExportItem["guiPosStart"].x()
            y = event.scenePos().y()
            self.duringTrackDragAndDrop.setPos(x, y)
        elif self.duringBlockDragAndDrop:
            #GuiBlockHandles are children of a Track. Their Y position will be offset by the track when we move them around with the mouse.
            #Which is the same the distance to the first track is on top. We need to substract that for a good look and feel.
            #The actual later dropping of the block is handled with the mouse cursor coordinates, the moving colored block is just eyecandy.
            x = event.scenePos().x()
            y = event.scenePos().y() - self.duringBlockDragAndDrop.parentGuiTrack.y()
            self.duringBlockDragAndDrop.setPos(x, y)
        #else:
        #    #this happens EVERY mouse move. Just idle mouse movement over the window.



    def mouseReleaseEvent(self, event):
        """Catches certain mouse events for moving tracks and blocks.
        Otherwise the event is propagated to the real QGraphicsItem.
        Don't forget that an item needs to have the flag movable or selectable or else
        it will not get mouseRelease or mouseMove events. MousePress always works."""

        #self.parentView.unsetCursor()  #While moving stuff the mouse-cursor is hidden. Reset.
        #self.cursor.show() #Our own position cursor #TODO: why was that in here? It shows the cursor after a mouseclick, even in CC mode (it should not)

        tempBlockDragAndDrop = self.duringBlockDragAndDrop
        tempTrackDragAndDrop = self.duringTrackDragAndDrop
        self.duringBlockDragAndDrop = None
        self.duringTrackDragAndDrop = None
        #Now we can exit the function safely at any time without the need to reset the dragAndDrop storage in different places

        if tempTrackDragAndDrop: #This is only for GuiTrack aka note tracks. CC tracks follow the gui track and the conductor cannot be moved.
            assert not tempBlockDragAndDrop
            assert type(tempTrackDragAndDrop) is GuiTrack
            dragTrackPosition = tempTrackDragAndDrop.pos().y()
            trackPositions = sorted([guiTrack.pos().y() for id, guiTrack in self.tracks.items()])
            trackPositions.remove(dragTrackPosition)

            listOfTrackIds = api.session.data.asListOfTrackIds()
            listOfTrackIds.remove(tempTrackDragAndDrop.staticExportItem["id"])

            #Calculate the new track position and trigger a redraw
            canditateForNewTrackPosition = 0 #this takes care of a score with only one track and also if you move a track to the 0th position
            for y in trackPositions:
                if dragTrackPosition >= y:
                    canditateForNewTrackPosition = trackPositions.index(y) +1

            listOfTrackIds.insert(canditateForNewTrackPosition, tempTrackDragAndDrop.staticExportItem["id"])
            if len(listOfTrackIds) == 1:
                tempTrackDragAndDrop.setPos(tempTrackDragAndDrop.staticExportItem["guiPosStart"]) #no need to trigger an api call with undo history etc.
            else:
                api.rearrangeTracks(listOfTrackIds)

        elif tempBlockDragAndDrop: #CC blocks, note blocks and conductor blocks
            assert not tempTrackDragAndDrop
            tempBlockDragAndDrop.mouseReleaseEventCustom(event)
            targetTrack = self.trackAt(event.scenePos())  #this ALWAYS returns a Conductor, GuiTrack or None. Not a CC sub-track. for CC see below when we test the block type and change this variable.
            targetBlock = self.blockAt(event.scenePos())
            dragBlockId = tempBlockDragAndDrop.staticExportItem["id"]

            #First some basic checks:
            if not targetTrack: #Only drag and drop into tracks.
                return None
            if targetBlock is tempBlockDragAndDrop: #block got moved on itself
                return None


            #Now check what kind of block moving we are dealing with
            #If the drag and drop mixes different block/track types we exit
            blockType = type(tempBlockDragAndDrop)
            conductorBlock = noteBlock = ccBlock = False

            if blockType is GuiBlockHandle:
                if type(targetTrack) is GuiTrack and self.parentView.mode() in ("block", "notation"):
                    noteBlock = True
                else: #Drag and Drop between different track types.
                    return None

            elif blockType is ConductorTransparentBlock:
                if targetTrack is self.conductor:
                    conductorBlock = True
                else: #Drag and Drop between different track types.
                    return None

            elif blockType is CCGraphTransparentBlock:
                if (not type(targetTrack) is GuiTrack) or (not self.parentView.mode() == "cc"):
                    return None

                ccBlock = True
                if constantsAndConfigs.ccViewValue in targetTrack.ccPaths: #this is only the backend database of CCs in tracks but it should be in sync.
                    targetTrackId = targetTrack.staticExportItem["id"] #we need this later. save it before changing the targetBlock variable
                    targetTrack = targetTrack.ccPaths[constantsAndConfigs.ccViewValue]
                else:
                    return None #TODO: Create a new CC sub-track with the moved block as first block. Mainly a backend call.  Needs a call at the end of this function as well.

            else:
                raise TypeError("Block must be a conductor, note or CC type but is {}".format(blockType))

            #We now have a track and the track type matches the block type.
            #Find the position to insert.
            if targetBlock is None: #behind the last block
                positionToInsert = len(targetTrack.transparentBlockHandles) #essentially we want append() but insert() is compatible with all types of operation here. len is based 1, so len results in "after the last one" position.
            else:
                assert type(targetBlock) is blockType
                positionToInsert = targetTrack.transparentBlockHandles.index(targetBlock)
            assert positionToInsert >= 0

            #Create the new order by putting the old block into a new slot and removing it from its old position
            newBlockOrder = [guiBlock.staticExportItem["id"] for guiBlock in targetTrack.transparentBlockHandles]
            if targetTrack == tempBlockDragAndDrop.parent:
                newBlockOrder.remove(dragBlockId) #block will be at another position and removed from its old one.
            newBlockOrder.insert(positionToInsert, dragBlockId)

            #Finally call the appropriate backend function which will trigger a GuiUpdate.
            if targetTrack is tempBlockDragAndDrop.parent: #Same track or within the conductor track
                if conductorBlock:
                    assert targetTrack is self.conductor
                    api.rearrangeTempoBlocks(newBlockOrder)
                elif noteBlock:
                    api.rearrangeBlocks(targetTrack.staticExportItem["id"], newBlockOrder)
                elif ccBlock:
                    api.rearrangeCCBlocks(targetTrackId, constantsAndConfigs.ccViewValue, newBlockOrder)
                #else is already catched by a Else-TypeError above
            else: #Different Track,
                if conductorBlock or targetTrack is self.conductor:
                    raise RuntimeError("How did this slip through? Checking for cross-track incompatibility was already done above")
                elif noteBlock:
                    api.moveBlockToOtherTrack(dragBlockId, targetTrack.staticExportItem["id"], newBlockOrder)
                elif ccBlock: #TODO: Also different CC in the same GuiTrack
                    api.moveCCBlockToOtherTrack(dragBlockId, targetTrack.parentDataTrackId, newBlockOrder)

        elif self.parentView.mode() == "notation" and (not (tempBlockDragAndDrop or tempTrackDragAndDrop)) and event.button() == 1: #a positional mouse left click in a note-track, but only it not drag-drop release.
            track = self.trackAt(event.scenePos())
            if track and not track is self.conductor:
                modifiers = QtWidgets.QApplication.keyboardModifiers()
                trackId = track.staticExportItem["id"]
                if modifiers == QtCore.Qt.ShiftModifier:
                    api.selectToTickindex(trackId, event.scenePos().x() * constantsAndConfigs.ticksToPixelRatio)
                else:
                    api.toTickindex(trackId, event.scenePos().x() * constantsAndConfigs.ticksToPixelRatio)

        super().mouseReleaseEvent(event)
