/*
 * Carla Native Plugins
 * Copyright (C) 2013-2014 Filipe Coelho <falktx@falktx.com>
 *
 * 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.
 */

#ifndef MIDI_BASE_HPP_INCLUDED
#define MIDI_BASE_HPP_INCLUDED

#include "CarlaMIDI.h"
#include "CarlaMutex.hpp"
#include "LinkedList.hpp"

#include "CarlaMathUtils.hpp"

#define MAX_EVENT_DATA_SIZE          4
#define MIN_PREALLOCATED_EVENT_COUNT 100
#define MAX_PREALLOCATED_EVENT_COUNT 1000

struct RawMidiEvent {
    uint64_t time;
    uint8_t  size;
    uint8_t  data[MAX_EVENT_DATA_SIZE];

    RawMidiEvent()
        : time(0),
          size(0)
    {
        carla_fill<uint8_t>(data, 0, MAX_EVENT_DATA_SIZE);
    }
};

class AbstractMidiPlayer
{
public:
    virtual ~AbstractMidiPlayer() {}
    virtual void writeMidiEvent(const uint64_t timePosFrame, const RawMidiEvent* const event) = 0;
};

class MidiPattern
{
public:
    MidiPattern(AbstractMidiPlayer* const player)
        : kPlayer(player),
          fMutex(),
          fData(),
          leakDetector_MidiPattern()
          //fStartTime(0),
          //fDuration(0)
    {
        CARLA_ASSERT(kPlayer != nullptr);
    }

    ~MidiPattern()
    {
        fData.clear();
    }

    void addControl(const uint64_t time, const uint8_t channel, const uint8_t control, const uint8_t value)
    {
        RawMidiEvent* ctrlEvent(new RawMidiEvent());
        ctrlEvent->time    = time;
        ctrlEvent->size    = 3;
        ctrlEvent->data[0] = uint8_t(MIDI_STATUS_CONTROL_CHANGE | (channel & 0x0F));
        ctrlEvent->data[1] = control;
        ctrlEvent->data[2] = value;

        append(ctrlEvent);
    }

    void addChannelPressure(const uint64_t time, const uint8_t channel, const uint8_t pressure)
    {
        RawMidiEvent* pressureEvent(new RawMidiEvent());
        pressureEvent->time    = time;
        pressureEvent->size    = 2;
        pressureEvent->data[0] = uint8_t(MIDI_STATUS_CHANNEL_PRESSURE | (channel & 0x0F));
        pressureEvent->data[1] = pressure;

        append(pressureEvent);
    }

    void addNote(const uint64_t time, const uint8_t channel, const uint8_t pitch, const uint8_t velocity, const uint32_t duration)
    {
        addNoteOn(time, channel, pitch, velocity);
        addNoteOff(time+duration, channel, pitch, velocity);
    }

    void addNoteOn(const uint64_t time, const uint8_t channel, const uint8_t pitch, const uint8_t velocity)
    {
        RawMidiEvent* noteOnEvent(new RawMidiEvent());
        noteOnEvent->time    = time;
        noteOnEvent->size    = 3;
        noteOnEvent->data[0] = uint8_t(MIDI_STATUS_NOTE_ON | (channel & 0x0F));
        noteOnEvent->data[1] = pitch;
        noteOnEvent->data[2] = velocity;

        append(noteOnEvent);
    }

    void addNoteOff(const uint64_t time, const uint8_t channel, const uint8_t pitch, const uint8_t velocity = 0)
    {
        RawMidiEvent* noteOffEvent(new RawMidiEvent());
        noteOffEvent->time    = time;
        noteOffEvent->size    = 3;
        noteOffEvent->data[0] = uint8_t(MIDI_STATUS_NOTE_OFF | (channel & 0x0F));
        noteOffEvent->data[1] = pitch;
        noteOffEvent->data[2] = velocity;

        append(noteOffEvent);
    }

    void addNoteAftertouch(const uint64_t time, const uint8_t channel, const uint8_t pitch, const uint8_t pressure)
    {
        RawMidiEvent* noteAfterEvent(new RawMidiEvent());
        noteAfterEvent->time    = time;
        noteAfterEvent->size    = 3;
        noteAfterEvent->data[0] = uint8_t(MIDI_STATUS_POLYPHONIC_AFTERTOUCH | (channel & 0x0F));
        noteAfterEvent->data[1] = pitch;
        noteAfterEvent->data[2] = pressure;

        append(noteAfterEvent);
    }

    void addProgram(const uint64_t time, const uint8_t channel, const uint8_t bank, const uint8_t program)
    {
        RawMidiEvent* bankEvent(new RawMidiEvent());
        bankEvent->time    = time;
        bankEvent->size    = 3;
        bankEvent->data[0] = uint8_t(MIDI_STATUS_CONTROL_CHANGE | (channel & 0x0F));
        bankEvent->data[1] = MIDI_CONTROL_BANK_SELECT;
        bankEvent->data[2] = bank;

        RawMidiEvent* programEvent(new RawMidiEvent());
        programEvent->time    = time;
        programEvent->size    = 2;
        programEvent->data[0] = uint8_t(MIDI_STATUS_PROGRAM_CHANGE | (channel & 0x0F));
        programEvent->data[1] = program;

        append(bankEvent);
        append(programEvent);
    }

    void addPitchbend(const uint64_t time, const uint8_t channel, const uint8_t lsb, const uint8_t msb)
    {
        RawMidiEvent* pressureEvent(new RawMidiEvent());
        pressureEvent->time    = time;
        pressureEvent->size    = 3;
        pressureEvent->data[0] = uint8_t(MIDI_STATUS_PITCH_WHEEL_CONTROL | (channel & 0x0F));
        pressureEvent->data[1] = lsb;
        pressureEvent->data[2] = msb;

        append(pressureEvent);
    }

    void addRaw(const uint64_t time, const uint8_t* data, const uint8_t size)
    {
        RawMidiEvent* rawEvent(new RawMidiEvent());
        rawEvent->time = time;
        rawEvent->size = size;

        carla_copy<uint8_t>(rawEvent->data, data, size);

        append(rawEvent);
    }

    void play(uint64_t timePosFrame, uint32_t frames)
    {
        if (! fMutex.tryLock())
            return;

        for (LinkedList<const RawMidiEvent*>::Itenerator it = fData.begin(); it.valid(); it.next())
        {
            const RawMidiEvent* const rawMidiEvent(it.getValue());

            if (timePosFrame > rawMidiEvent->time)
                continue;
            if (timePosFrame + frames <= rawMidiEvent->time)
                continue;

            kPlayer->writeMidiEvent(timePosFrame, rawMidiEvent);
        }

        fMutex.unlock();
    }

    void clear()
    {
        const CarlaMutexLocker sl(fMutex);
        fData.clear();
    }

private:
    AbstractMidiPlayer* const kPlayer;

    //uint32_t fStartTime; // unused
    //uint32_t fDuration;  // unused

    CarlaMutex fMutex;
    LinkedList<const RawMidiEvent*> fData;

    void append(const RawMidiEvent* const event)
    {
        if (fData.isEmpty())
        {
            const CarlaMutexLocker sl(fMutex);
            fData.append(event);
            return;
        }

        for (LinkedList<const RawMidiEvent*>::Itenerator it = fData.begin(); it.valid(); it.next())
        {
            const RawMidiEvent* const oldEvent(it.getValue());

            if (event->time >= oldEvent->time)
                continue;

            const CarlaMutexLocker sl(fMutex);
            fData.insertAt(event, it);
            return;
        }

        const CarlaMutexLocker sl(fMutex);
        fData.append(event);
    }

    CARLA_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MidiPattern)
};

#endif // MIDI_BASE_HPP_INCLUDED
