// Copyright(c) 2016 YamaArashi
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

#include <cstdio>
#include <cassert>
#include <string>
#include <vector>
#include <algorithm>
#include <memory>
#include "midi.h"
#include "main.h"
#include "error.h"
#include "agb.h"
#include "tables.h"

enum class MidiEventCategory
{
    Control,
    SysEx,
    Meta,
    Invalid,
};

MidiFormat g_midiFormat;
std::int_fast32_t g_midiTrackCount;
std::int16_t g_midiTimeDiv;

int g_midiChan;
std::int32_t g_initialWait;

static long s_trackDataStart;
static std::vector<Event> s_seqEvents;
static std::vector<Event> s_trackEvents;
static std::int32_t s_absoluteTime;
static int s_blockCount = 0;
static int s_minNote;
static int s_maxNote;

void Seek(long offset)
{
    if (std::fseek(g_inputFile, offset, SEEK_SET) != 0)
        RaiseError("failed to seek to %l", offset);
}

void Skip(long offset)
{
    if (std::fseek(g_inputFile, offset, SEEK_CUR) != 0)
        RaiseError("failed to skip %l bytes", offset);
}

std::string ReadSignature()
{
    char signature[4];

    if (std::fread(signature, 4, 1, g_inputFile) != 1)
        RaiseError("failed to read signature");

    return std::string(signature, 4);
}

std::uint32_t ReadInt8()
{
    int c = std::fgetc(g_inputFile);

    if (c < 0)
        RaiseError("unexpected EOF");

    return c;
}

std::uint32_t ReadInt16()
{
    std::uint32_t val = 0;
    val |= ReadInt8() << 8;
    val |= ReadInt8();
    return val;
}

std::uint32_t ReadInt24()
{
    std::uint32_t val = 0;
    val |= ReadInt8() << 16;
    val |= ReadInt8() << 8;
    val |= ReadInt8();
    return val;
}

std::uint32_t ReadInt32()
{
    std::uint32_t val = 0;
    val |= ReadInt8() << 24;
    val |= ReadInt8() << 16;
    val |= ReadInt8() << 8;
    val |= ReadInt8();
    return val;
}

std::uint32_t ReadVLQ()
{
    std::uint32_t val = 0;
    std::uint32_t c;

    do
    {
        c = ReadInt8();
        val <<= 7;
        val |= (c & 0x7F);
    } while (c & 0x80);

    return val;
}

void ReadMidiFileHeader()
{
    Seek(0);

    if (ReadSignature() != "MThd")
        RaiseError("MIDI file header signature didn't match \"MThd\"");

    std::uint32_t headerLength = ReadInt32();

    if (headerLength != 6)
        RaiseError("MIDI file header length isn't 6");

    std::uint16_t midiFormat = ReadInt16();

    if (midiFormat >= 2)
        RaiseError("unsupported MIDI format (%u)", midiFormat);

    g_midiFormat = (MidiFormat)midiFormat;
    g_midiTrackCount = ReadInt16();
    g_midiTimeDiv = ReadInt16();

    if (g_midiTimeDiv < 0)
        RaiseError("unsupported MIDI time division (%d)", g_midiTimeDiv);
}

long ReadMidiTrackHeader(long offset)
{
    Seek(offset);

    if (ReadSignature() != "MTrk")
        RaiseError("MIDI track header signature didn't match \"MTrk\"");

    long size = ReadInt32();

    s_trackDataStart = std::ftell(g_inputFile);

    return size + 8;
}

void StartTrack()
{
    Seek(s_trackDataStart);
    s_absoluteTime = 0;
}

void SkipEventData()
{
    Skip(ReadVLQ());
}

void DetermineEventCategory(MidiEventCategory& category, int& typeChan, int& size)
{
    typeChan = ReadInt8();

    if (typeChan == 0xFF)
    {
        category = MidiEventCategory::Meta;
        size = 0;
    }
    else if (typeChan >= 0xF0)
    {
        category = MidiEventCategory::SysEx;
        size = 0;
    }
    else if (typeChan >= 0x80)
    {
        category = MidiEventCategory::Control;

        switch (typeChan >> 4)
        {
        case 0xC:
        case 0xD:
            size = 1;
            break;
        default:
            size = 2;
            break;
        }
    }
    else
    {
        category = MidiEventCategory::Invalid;
    }
}

void MakeBlockEvent(Event& event, EventType type)
{
    event.type = type;
    event.param1 = s_blockCount++;
    event.param2 = 0;
}

std::string ReadEventText()
{
    char buffer[2];
    std::uint32_t length = ReadVLQ();

    if (length <= 2)
    {
        if (fread(buffer, length, 1, g_inputFile) != 1)
            RaiseError("failed to read event text");
    }
    else
    {
        Skip(length);
        length = 0;
    }

    return std::string(buffer, length);
}

bool ReadSeqEvent(Event& event)
{
    s_absoluteTime += ReadVLQ();
    event.time = s_absoluteTime;

    MidiEventCategory category;
    int typeChan;
    int size;

    DetermineEventCategory(category, typeChan, size);

    if (category == MidiEventCategory::Control)
    {
        Skip(size);
        return false;
    }

    if (category == MidiEventCategory::SysEx)
    {
        SkipEventData();
        return false;
    }

    if (category == MidiEventCategory::Invalid)
        RaiseError("invalid event");

    // meta event
    int metaEventType = ReadInt8();

    if (metaEventType >= 1 && metaEventType <= 7)
    {
        // text event
        std::string text = ReadEventText();

        if (text == "[")
            MakeBlockEvent(event, EventType::LoopBegin);
        else if (text == "][")
            MakeBlockEvent(event, EventType::LoopEndBegin);
        else if (text == "]")
            MakeBlockEvent(event, EventType::LoopEnd);
        else if (text == ":")
            MakeBlockEvent(event, EventType::Label);
        else
            return false;
    }
    else
    {
        switch (metaEventType)
        {
        case 0x2F: // end of track
            SkipEventData();
            event.type = EventType::EndOfTrack;
            event.param1 = 0;
            event.param2 = 0;
            break;
        case 0x51: // tempo
            if (ReadVLQ() != 3)
                RaiseError("invalid tempo size");

            event.type = EventType::Tempo;
            event.param1 = 0;
            event.param2 = ReadInt24();
            break;
        case 0x58: // time signature
        {
            if (ReadVLQ() != 4)
                RaiseError("invalid time signature size");

            int numerator = ReadInt8();
            int denominatorExponent = ReadInt8();

            if (denominatorExponent >= 16)
                RaiseError("invalid time signature denominator");

            Skip(2); // ignore other values

            int clockTicks = 96 * numerator * g_clocksPerBeat;
            int denominator = 1 << denominatorExponent;
            int timeSig = clockTicks / denominator;

            if (timeSig <= 0 || timeSig >= 0x10000)
                RaiseError("invalid time signature");

            event.type = EventType::TimeSignature;
            event.param1 = 0;
            event.param2 = timeSig;
            break;
        }
        default:
            SkipEventData();
            return false;
        }
    }

    return true;
}

void ReadSeqEvents()
{
    StartTrack();

    for (;;)
    {
        Event event = {};

        if (ReadSeqEvent(event))
        {
            s_seqEvents.push_back(event);

            if (event.type == EventType::EndOfTrack)
                return;
        }
    }
}

bool CheckNoteEnd(Event& event)
{
    event.param2 += ReadVLQ();

    MidiEventCategory category;
    int typeChan;
    int size;

    DetermineEventCategory(category, typeChan, size);

    if (category == MidiEventCategory::Control)
    {
        int chan = typeChan & 0xF;

        if (chan != g_midiChan)
        {
            Skip(size);
            return false;
        }

        switch (typeChan & 0xF0)
        {
        case 0x80: // note off
        {
            int note = ReadInt8();
            ReadInt8(); // ignore velocity
            if (note == event.note)
                return true;
            break;
        }
        case 0x90: // note on
        {
            int note = ReadInt8();
            int velocity = ReadInt8();
            if (velocity == 0 && note == event.note)
                return true;
            break;
        }
        default:
            Skip(size);
            break;
        }

        return false;
    }

    if (category == MidiEventCategory::SysEx)
    {
        SkipEventData();
        return false;
    }

    if (category == MidiEventCategory::Meta)
    {
        int metaEventType = ReadInt8();
        SkipEventData();

        if (metaEventType == 0x2F)
            RaiseError("note doesn't end");

        return false;
    }

    RaiseError("invalid event");
}

void FindNoteEnd(Event& event)
{
    long startPos = ftell(g_inputFile);

    event.param2 = 0;

    while (!CheckNoteEnd(event))
        ;

    Seek(startPos);
}

bool ReadTrackEvent(Event& event)
{
    s_absoluteTime += ReadVLQ();
    event.time = s_absoluteTime;

    MidiEventCategory category;
    int typeChan;
    int size;

    DetermineEventCategory(category, typeChan, size);

    if (category == MidiEventCategory::Control)
    {
        int chan = typeChan & 0xF;

        if (chan != g_midiChan)
        {
            Skip(size);
            return false;
        }

        switch (typeChan & 0xF0)
        {
        case 0x90: // note on
        {
            int note = ReadInt8();
            int velocity = ReadInt8();

            if (velocity != 0)
            {
                event.type = EventType::Note;
                event.note = note;
                event.param1 = velocity;
                FindNoteEnd(event);
                if (event.param2 > 0)
                {
                    if (note < s_minNote)
                        s_minNote = note;
                    if (note > s_maxNote)
                        s_maxNote = note;
                }
            }
            break;
        }
        case 0xB0: // controller event
            event.type = EventType::Controller;
            event.param1 = ReadInt8(); // controller index
            event.param2 = ReadInt8(); // value
            break;
        case 0xC0: // instrument change
            event.type = EventType::InstrumentChange;
            event.param1 = ReadInt8(); // instrument
            event.param2 = 0;
            break;
        case 0xE0: // pitch bend
            event.type = EventType::PitchBend;
            event.param1 = ReadInt8();
            event.param2 = ReadInt8();
            break;
        default:
            Skip(size);
            return false;
        }

        return true;
    }

    if (category == MidiEventCategory::SysEx)
    {
        SkipEventData();
        return false;
    }

    if (category == MidiEventCategory::Meta)
    {
        int metaEventType = ReadInt8();
        SkipEventData();

        if (metaEventType == 0x2F)
        {
            event.type = EventType::EndOfTrack;
            event.param1 = 0;
            event.param2 = 0;
            return true;
        }

        return false;
    }

    RaiseError("invalid event");
}

void ReadTrackEvents()
{
    StartTrack();

    s_trackEvents.clear();

    s_minNote = 0xFF;
    s_maxNote = 0;

    for (;;)
    {
        Event event = {};

        if (ReadTrackEvent(event))
        {
            s_trackEvents.push_back(event);

            if (event.type == EventType::EndOfTrack)
                return;
        }
    }
}

bool EventCompare(const Event& event1, const Event& event2)
{
    if (event1.time < event2.time)
        return true;

    if (event1.time > event2.time)
        return false;

    unsigned event1Type = (unsigned)event1.type;
    unsigned event2Type = (unsigned)event2.type;

    if (event1.type == EventType::Note)
        event1Type += event1.note;

    if (event2.type == EventType::Note)
        event2Type += event2.note;

    if (event1Type < event2Type)
        return true;

    if (event1Type > event2Type)
        return false;

    if (event1.type == EventType::EndOfTie)
    {
        if (event1.note < event2.note)
            return true;

        if (event1.note > event2.note)
            return false;
    }

    return false;
}

std::unique_ptr<std::vector<Event>> MergeEvents()
{
    std::unique_ptr<std::vector<Event>> events(new std::vector<Event>());

    unsigned trackEventPos = 0;
    unsigned seqEventPos = 0;

    while (s_trackEvents[trackEventPos].type != EventType::EndOfTrack
        && s_seqEvents[seqEventPos].type != EventType::EndOfTrack)
    {
        if (EventCompare(s_trackEvents[trackEventPos], s_seqEvents[seqEventPos]))
            events->push_back(s_trackEvents[trackEventPos++]);
        else
            events->push_back(s_seqEvents[seqEventPos++]);
    }

    while (s_trackEvents[trackEventPos].type != EventType::EndOfTrack)
        events->push_back(s_trackEvents[trackEventPos++]);

    while (s_seqEvents[seqEventPos].type != EventType::EndOfTrack)
        events->push_back(s_seqEvents[seqEventPos++]);

    // Push the EndOfTrack event with the larger time.
    if (EventCompare(s_trackEvents[trackEventPos], s_seqEvents[seqEventPos]))
        events->push_back(s_seqEvents[seqEventPos]);
    else
        events->push_back(s_trackEvents[trackEventPos]);

    return events;
}

void ConvertTimes(std::vector<Event>& events)
{
    for (Event& event : events)
    {
        event.time = (24 * g_clocksPerBeat * event.time) / g_midiTimeDiv;

        if (event.type == EventType::Note)
        {
            event.param1 = g_noteVelocityLUT[event.param1];

            std::uint32_t duration = (24 * g_clocksPerBeat * event.param2) / g_midiTimeDiv;

            if (duration == 0)
                duration = 1;

            if (!g_exactGateTime && duration < 96)
                duration = g_noteDurationLUT[duration];

            event.param2 = duration;
        }
    }
}

std::unique_ptr<std::vector<Event>> InsertTimingEvents(std::vector<Event>& inEvents)
{
    std::unique_ptr<std::vector<Event>> outEvents(new std::vector<Event>());

    Event timingEvent = {};
    timingEvent.time = 0;
    timingEvent.type = EventType::TimeSignature;
    timingEvent.param2 = 96 * g_clocksPerBeat;

    for (const Event& event : inEvents)
    {
        while (EventCompare(timingEvent, event))
        {
            outEvents->push_back(timingEvent);
            timingEvent.time += timingEvent.param2;
        }

        if (event.type == EventType::TimeSignature)
        {
            if (g_agbTrack == 1 && event.param2 != timingEvent.param2)
            {
                Event originalTimingEvent = event;
                originalTimingEvent.type = EventType::OriginalTimeSignature;
                outEvents->push_back(originalTimingEvent);
            }
            timingEvent.param2 = event.param2;
            timingEvent.time = event.time + timingEvent.param2;
        }

        outEvents->push_back(event);
    }

    return outEvents;
}

std::unique_ptr<std::vector<Event>> SplitTime(std::vector<Event>& inEvents)
{
    std::unique_ptr<std::vector<Event>> outEvents(new std::vector<Event>());

    std::int32_t time = 0;

    for (const Event& event : inEvents)
    {
        std::int32_t diff = event.time - time;

        if (diff > 96)
        {
            int wholeNoteCount = (diff - 1) / 96;
            diff -= 96 * wholeNoteCount;

            for (int i = 0; i < wholeNoteCount; i++)
            {
                time += 96;
                Event timeSplitEvent = {};
                timeSplitEvent.time = time;
                timeSplitEvent.type = EventType::TimeSplit;
                outEvents->push_back(timeSplitEvent);
            }
        }

        std::int32_t lutValue = g_noteDurationLUT[diff];

        if (lutValue != diff)
        {
            Event timeSplitEvent = {};
            timeSplitEvent.time = time + lutValue;
            timeSplitEvent.type = EventType::TimeSplit;
            outEvents->push_back(timeSplitEvent);
        }

        time = event.time;

        outEvents->push_back(event);
    }

    return outEvents;
}

std::unique_ptr<std::vector<Event>> CreateTies(std::vector<Event>& inEvents)
{
    std::unique_ptr<std::vector<Event>> outEvents(new std::vector<Event>());

    for (const Event& event : inEvents)
    {
        if (event.type == EventType::Note && event.param2 > 96)
        {
            Event tieEvent = event;
            tieEvent.param2 = -1;
            outEvents->push_back(tieEvent);

            Event eotEvent = {};
            eotEvent.time = event.time + event.param2;
            eotEvent.type = EventType::EndOfTie;
            eotEvent.note = event.note;
            outEvents->push_back(eotEvent);
        }
        else
        {
            outEvents->push_back(event);
        }
    }

    return outEvents;
}

void CalculateWaits(std::vector<Event>& events)
{
    g_initialWait = events[0].time;
    int wholeNoteCount = 0;

    for (unsigned i = 0; i < events.size() && events[i].type != EventType::EndOfTrack; i++)
    {
        events[i].time = events[i + 1].time - events[i].time;

        if (events[i].type == EventType::TimeSignature)
        {
            events[i].type = EventType::WholeNoteMark;
            events[i].param2 = wholeNoteCount++;
        }
    }
}

int CalculateCompressionScore(std::vector<Event>& events, int index)
{
    int score = 0;
    std::uint8_t lastParam1 = events[index].param1;
    std::uint8_t lastVelocity = 0x80u;
    EventType lastType = events[index].type;
    std::int32_t lastDuration = 0x80000000;
    std::uint8_t lastNote = 0x80u;

    if (events[index].time > 0)
        score++;

    for (int i = index + 1; !IsPatternBoundary(events[i].type); i++)
    {
        if (events[i].type == EventType::Note)
        {
            int val = 0;

            if (events[i].note != lastNote)
            {
                val++;
                lastNote = events[i].note;
            }

            if (events[i].param1 != lastVelocity)
            {
                val++;
                lastVelocity = events[i].param1;
            }

            std::int32_t duration = events[i].param2;

            if (g_noteDurationLUT[duration] != lastDuration)
            {
                val++;
                lastDuration = g_noteDurationLUT[duration];
            }

            if (duration != lastDuration)
                val++;

            if (val == 0)
                val = 1;

            score += val;
        }
        else
        {
            lastDuration = 0x80000000;

            if (events[i].type == lastType)
            {
                if ((lastType != EventType::Controller && (int)lastType != 0x25 && lastType != EventType::EndOfTie) || events[i].param1 == lastParam1)
                {
                    score++;
                }
                else
                {
                    score += 2;
                }
            }
            else
            {
                score += 2;
            }
        }

        lastParam1 = events[i].param1;
        lastType = events[i].type;

        if (events[i].time)
            ++score;
    }

    return score;
}

bool IsCompressionMatch(std::vector<Event>& events, int index1, int index2)
{
    index1++;
    index2++;

    do
    {
        if (events[index1] != events[index2])
            return false;

        index1++;
        index2++;
    } while (!IsPatternBoundary(events[index1].type));

    return IsPatternBoundary(events[index2].type);
}

void CompressWholeNote(std::vector<Event>& events, int index)
{
    for (int j = index + 1; events[j].type != EventType::EndOfTrack; j++)
    {
        while (events[j].type != EventType::WholeNoteMark)
        {
            j++;

            if (events[j].type == EventType::EndOfTrack)
                return;
        }

        if (IsCompressionMatch(events, index, j))
        {
            events[j].type = EventType::Pattern;
            events[j].param2 = events[index].param2 & 0x7FFFFFFF;
            events[index].param2 |= 0x80000000;
        }
    }
}

void Compress(std::vector<Event>& events)
{
    for (int i = 0; events[i].type != EventType::EndOfTrack; i++)
    {
        while (events[i].type != EventType::WholeNoteMark)
        {
            i++;

            if (events[i].type == EventType::EndOfTrack)
                return;
        }

        if (CalculateCompressionScore(events, i) >= 6)
        {
            CompressWholeNote(events, i);
        }
    }
}

void ReadMidiTracks()
{
    long trackHeaderStart = 14;

    ReadMidiTrackHeader(trackHeaderStart);
    ReadSeqEvents();

    g_agbTrack = 1;

    for (int midiTrack = 0; midiTrack < g_midiTrackCount; midiTrack++)
    {
        trackHeaderStart += ReadMidiTrackHeader(trackHeaderStart);

        for (g_midiChan = 0; g_midiChan < 16; g_midiChan++)
        {
            ReadTrackEvents();

            if (s_minNote != 0xFF)
            {
#ifdef DEBUG
                printf("Track%d = Midi-Ch.%d\n", g_agbTrack, g_midiChan + 1);
#endif

                std::unique_ptr<std::vector<Event>> events(MergeEvents());

                // We don't need TEMPO in anything but track 1.
                if (g_agbTrack == 1)
                {
                    auto it = std::remove_if(s_seqEvents.begin(), s_seqEvents.end(), [](const Event& event) { return event.type == EventType::Tempo; });
                    s_seqEvents.erase(it, s_seqEvents.end());
                }

                ConvertTimes(*events);
                events = InsertTimingEvents(*events);
                events = CreateTies(*events);
                std::stable_sort(events->begin(), events->end(), EventCompare);
                events = SplitTime(*events);
                CalculateWaits(*events);

                if (g_compressionEnabled)
                    Compress(*events);

                PrintAgbTrack(*events);

                g_agbTrack++;
            }
        }
    }
}