module Sound.JACK.MIDI (
    RawEvent,
    rawEvent,
    rawEventTime,
    rawEventBuffer,
    toRawEventFunction,

    Port, withPort,

    withProcess,

    Buffer,
    getBuffer,
    clearBuffer,

    readRawEvents,
    writeRawEvent,
    readRawEventsFromPort,
    writeRawEventsToPort,

    writeEvent,
    readEventsFromPort,
    writeEventsToPort,

    main,
    mainRaw,
    ) where

import qualified Sound.JACK.Private as Priv
import qualified Sound.JACK as Jack
import Sound.JACK.Private (Client, liftErrno, alloca, )
import Sound.JACK (Direction, Input, Output, )

import qualified Sound.JACK.Exception as JackExc
import qualified Sound.JACK.FFI.MIDI as JackMIDI
import qualified Sound.JACK.FFI as JackFFI
import Sound.JACK.FFI (NFrames, nframesIndices, )
import Sound.JACK.FFI.MIDI
          (RawEvent, EventBuffer, Buffer(Buffer), toRawEventFunction, )

import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL

import qualified Data.EventList.Absolute.TimeBody as EventList

import qualified Sound.MIDI.Message as Msg
import qualified Sound.MIDI.Parser.Report as Report

import qualified Control.Monad.Exception.Synchronous as Sync
import qualified Control.Monad.Trans.Class as Trans

import Foreign.Storable (Storable, peek, )
import Foreign.C.Error (Errno, )

import System.Environment (getProgName)
import Control.Monad (forM, )

import Data.Maybe (mapMaybe, )


type Port = Priv.Port EventBuffer

withPort ::
    (Direction dir,
     JackExc.ThrowsPortRegister e,
     JackExc.ThrowsErrno e) =>
       Client -- ^ Jack client
    -> String -- ^ name of the input port
    -> (Port dir -> Sync.ExceptionalT e IO a)
    -> Sync.ExceptionalT e IO a
withPort = Jack.withPort


{- |
Smart constructor for a raw MIDI event.
-}
rawEvent ::
       NFrames      -- ^ Sample index at which event is valid (relative to cycle start)
    -> B.ByteString -- ^ Raw MIDI data
    -> RawEvent
rawEvent = JackMIDI.RawEvent

rawEventTime :: RawEvent -> NFrames
rawEventTime = JackMIDI.time

rawEventBuffer :: RawEvent -> B.ByteString
rawEventBuffer = JackMIDI.buffer


-- | Creates an input and an output, and transforms all raw input events into raw output
--   events using the given function
mainRaw :: (NFrames -> RawEvent -> IO RawEvent) -- ^ transforms raw input to output events
            -> IO ()
mainRaw fun = do
    name <- getProgName
    Jack.handleExceptions $
        Jack.withClientDefault name $ \client ->
        Jack.withPort client "input" $ \input ->
        Jack.withPort client "output" $ \output ->
        withProcess client input fun output $
            Jack.withActivation client $ Trans.lift $ do
                putStrLn $ "started " ++ name ++ "..."
                Jack.waitForBreak

-- | Creates an input and an output, and transforms all input events into output
--   events using the given function
main :: (NFrames -> (NFrames, Msg.T) -> IO (NFrames, Msg.T)) -- ^ transforms input to output events
            -> IO ()
main fun = mainRaw (toRawEventFunction fun)


-- | sets the process loop of the JACK Client
withProcess ::
    (JackExc.ThrowsErrno e) =>
       Client   -- ^ the JACK Client, whose process loop will be set
    -> Port Input    -- ^ where to get events from
    -> (NFrames -> RawEvent -> IO RawEvent)
                -- ^ transforms input to output events
    -> Port Output   -- ^ where to put events
    -> Sync.ExceptionalT e IO a
    -> Sync.ExceptionalT e IO a
                -- ^ exception causing JACK to remove that client from the process() graph.
withProcess client input fun output =
    Jack.withProcess client $ wrapFun client input fun output

-- | reads all available MIDI Events on the given PortBuffer
readRawEvents ::
    (JackExc.ThrowsErrno e) =>
       Buffer Input  -- ^ the PortBuffer to read from
    -> Sync.ExceptionalT e IO [RawEvent]
readRawEvents buffer_ = do
    nEvents <- Trans.lift $ JackMIDI.get_event_count buffer_
    forM (nframesIndices nEvents) $ \n ->
        alloca $ \eventPtr -> do
            liftErrno $ JackMIDI.event_get eventPtr buffer_ n
            Trans.lift $ peek eventPtr
        -- putStrLn $ "Got MIDI Event number " ++ (show n) ++ " with result " ++ (show result) ++ " at JackMIDI.time " ++ (show $ JackMIDI.time event) ++ " size: " ++ (show $ size event) ++ " JackMIDI.buffer: " ++ (show $ JackMIDI.buffer event)


getBuffer ::
    Direction dir =>
    Port dir -> NFrames -> IO (Buffer dir)
getBuffer (Priv.Port port) n =
    fmap Buffer $ JackFFI.port_get_buffer port n

clearBuffer :: Buffer Output -> IO ()
clearBuffer portBuffer =
    JackMIDI.clear_buffer portBuffer

-- | writes a MIDI event to the PortBuffer of a MIDI output or throws eNOBUFS if JackMIDI.buffer is full
writeRawEvent ::
    (JackExc.ThrowsErrno e) =>
       Buffer Output         -- ^ the PortBuffer of the MIDI output to write to
    -> RawEvent              -- ^ the RawEvent to write
    -> Sync.ExceptionalT e IO ()
writeRawEvent portBuffer event =
    liftErrno $
        JackMIDI.withByteStringPtr (JackMIDI.buffer event) $ \ptr len ->
            JackMIDI.event_write portBuffer (JackMIDI.time event)
                ptr (fromIntegral len)
    -- putStrLn $ "Writing MIDI Event: JackMIDI.buffer: " ++ (show $ JackMIDI.buffer event)

readRawEventsFromPort ::
    (JackExc.ThrowsErrno e) =>
    Port Input -> NFrames ->
    Sync.ExceptionalT e IO [RawEvent]
readRawEventsFromPort port nframes =
    readRawEvents =<< (Trans.lift $ getBuffer port nframes)

{- |
Clears an output buffer and writes a sequence of events to it.
That is, you must only call this function once per callback.
-}
writeRawEventsToPort ::
    (JackExc.ThrowsErrno e) =>
    Port Output -> NFrames ->
    [RawEvent] ->
    Sync.ExceptionalT e IO ()
writeRawEventsToPort port nframes es = do
    buffer <- Trans.lift $ getBuffer port nframes
    Trans.lift $ clearBuffer buffer
    mapM_ (writeRawEvent buffer) es


parseEvent :: JackMIDI.RawEvent -> Maybe (NFrames, Msg.T)
parseEvent ev =
    case Msg.maybeFromByteString $ BL.fromChunks [rawEventBuffer ev] of
        Report.Cons _warnings result ->
            case result of
                Right dat -> Just (rawEventTime ev, dat)
                _ -> Nothing

{- |
Reads midi events from an input buffer
and converts them to a high-level representation.
Messages are simply ignored if they cannot be parsed.
-}
readEventsFromPort ::
    (JackExc.ThrowsErrno e) =>
    Port Input -> NFrames ->
    Sync.ExceptionalT e IO (EventList.T NFrames Msg.T)
readEventsFromPort port nframes =
    fmap (EventList.fromPairList . mapMaybe parseEvent) $
    readRawEventsFromPort port nframes


writeEvent ::
    (JackExc.ThrowsErrno e) =>
    Buffer Output -> NFrames -> Msg.T ->
    Sync.ExceptionalT e IO ()
writeEvent buffer pos =
    writeRawEvent buffer .
    JackMIDI.RawEvent pos .
    B.concat . BL.toChunks . Msg.toByteString

{- |
Clears an output buffer and writes a sequence of events to it.
That is, you must only call this function once per callback.
-}
writeEventsToPort ::
    (JackExc.ThrowsErrno e) =>
    Port Output -> NFrames ->
    EventList.T NFrames Msg.T ->
    Sync.ExceptionalT e IO ()
writeEventsToPort port nframes =
    writeRawEventsToPort port nframes .
    map (uncurry JackMIDI.RawEvent) .
    EventList.toPairList .
    fmap (B.concat . BL.toChunks . Msg.toByteString)


wrapFun ::
    Client ->
    Port Input -> (NFrames -> RawEvent -> IO RawEvent) ->
    Port Output ->
    NFrames -> Sync.ExceptionalT Errno IO ()
wrapFun client input fun output nframes = do
    inEvents <- readRawEventsFromPort input nframes
    -- when (not (null inEvents)) $ putStrLn $ "wrapFun: Got " ++ (show $ length inEvents) ++ " input events"
    lastCycle <- Trans.lift $ Jack.lastFrameTime client
    outEvents <- mapM (Trans.lift . fun lastCycle) inEvents
    -- when (not (null outEvents)) $ putStrLn $ "wrapFun: Got " ++ (show $ length outEvents) ++ " output events"
    writeRawEventsToPort output nframes outEvents