/*
  ==============================================================================

   This file is part of the JUCE library.
   Copyright (c) 2017 - ROLI Ltd.

   JUCE is an open source library subject to commercial or open-source
   licensing.

   By using JUCE, you agree to the terms of both the JUCE 5 End-User License
   Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
   27th April 2017).

   End User License Agreement: www.juce.com/juce-5-licence
   Privacy Policy: www.juce.com/juce-5-privacy-policy

   Or: You may also use this code under the terms of the GPL v3 (see
   www.gnu.org/licenses).

   JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
   EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
   DISCLAIMED.

  ==============================================================================
*/


#pragma once


class NoteComponent : public Component
{
public:
    NoteComponent (const MPENote& n, Colour colourToUse)
        : note (n), colour (colourToUse)
    {
    }

    //==============================================================================
    void update (const MPENote& newNote, Point<float> newCentre)
    {
        note = newNote;
        centre = newCentre;

        setBounds (getSquareAroundCentre (jmax (getNoteOnRadius(), getNoteOffRadius(), getPressureRadius()))
                     .getUnion (getTextRectangle())
                     .getSmallestIntegerContainer()
                     .expanded (3));

        repaint();
    }

    //==============================================================================
    void paint (Graphics& g) override
    {
        if (note.keyState == MPENote::keyDown || note.keyState == MPENote::keyDownAndSustained)
            drawPressedNoteCircle (g, colour);
        else if (note.keyState == MPENote::sustained)
            drawSustainedNoteCircle (g, colour);
        else
            return;

        drawNoteLabel (g, colour);
    }

    //==============================================================================
    MPENote note;
    Colour colour;
    Point<float> centre;

private:
    //==============================================================================
    void drawPressedNoteCircle (Graphics& g, Colour zoneColour)
    {
        g.setColour (zoneColour.withAlpha (0.3f));
        g.fillEllipse (translateToLocalBounds (getSquareAroundCentre (getNoteOnRadius())));
        g.setColour (zoneColour);
        g.drawEllipse (translateToLocalBounds (getSquareAroundCentre (getPressureRadius())), 2.0f);
    }

    //==============================================================================
    void drawSustainedNoteCircle (Graphics& g, Colour zoneColour)
    {
        g.setColour (zoneColour);
        Path circle, dashedCircle;
        circle.addEllipse (translateToLocalBounds (getSquareAroundCentre (getNoteOffRadius())));
        const float dashLengths[] = { 3.0f, 3.0f };
        PathStrokeType (2.0, PathStrokeType::mitered).createDashedStroke (dashedCircle, circle, dashLengths, 2);
        g.fillPath (dashedCircle);
    }

    //==============================================================================
    void drawNoteLabel (Graphics& g, Colour zoneColour)
    {
        Rectangle<int> textBounds = translateToLocalBounds (getTextRectangle()).getSmallestIntegerContainer();
        g.drawText ("+", textBounds, Justification::centred);
        g.drawText (MidiMessage::getMidiNoteName (note.initialNote, true, true, 3), textBounds, Justification::centredBottom);
        g.setFont (Font (22.0f, Font::bold));
        g.drawText (String (note.midiChannel), textBounds, Justification::centredTop);
    }

    //==============================================================================
    Rectangle<float> getSquareAroundCentre (float radius) const noexcept
    {
        return Rectangle<float> (radius * 2.0f, radius * 2.0f).withCentre (centre);
    }

    Rectangle<float> translateToLocalBounds (Rectangle<float> r) const noexcept
    {
        return r - getPosition().toFloat();
    }

    Rectangle<float> getTextRectangle() const noexcept
    {
        return Rectangle<float> (30.0f, 50.0f).withCentre (centre);
    }

    float getNoteOnRadius() const noexcept     { return note.noteOnVelocity.asUnsignedFloat() * maxNoteRadius; }
    float getNoteOffRadius() const noexcept    { return note.noteOffVelocity.asUnsignedFloat() * maxNoteRadius; }
    float getPressureRadius() const noexcept   { return note.pressure.asUnsignedFloat() * maxNoteRadius; }

    const float maxNoteRadius = 100.0f;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NoteComponent)
};

//==============================================================================
class Visualiser : public Component,
                   public MPEInstrument::Listener,
                   private AsyncUpdater
{
public:
    //==============================================================================
    Visualiser (const ZoneColourPicker& zoneColourPicker)
        : colourPicker (zoneColourPicker)
    {}

    //==============================================================================
    void paint (Graphics& g) override
    {
        g.fillAll (Colours::black);

        float noteDistance = float (getWidth()) / 128;
        for (int i = 0; i < 128; ++i)
        {
            float x = noteDistance * i;
            int noteHeight = int (MidiMessage::isMidiNoteBlack (i) ? 0.7 * getHeight() : getHeight());
            g.setColour (MidiMessage::isMidiNoteBlack (i) ? Colours::white : Colours::grey);
            g.drawLine (x, 0.0f, x, (float) noteHeight);

            if (i > 0 && i % 12 == 0)
            {
                g.setColour (Colours::grey);
                int octaveNumber = (i / 12) - 2;
                g.drawText ("C" + String (octaveNumber), (int) x - 15, getHeight() - 30, 30, 30, Justification::centredBottom);
            }
        }
    }

    //==============================================================================
    void noteAdded (MPENote newNote) override
    {
        const ScopedLock sl (lock);
        activeNotes.add (newNote);
        triggerAsyncUpdate();
    }

    void notePressureChanged (MPENote note) override { noteChanged (note); }
    void notePitchbendChanged (MPENote note) override { noteChanged (note); }
    void noteTimbreChanged (MPENote note) override { noteChanged (note); }
    void noteKeyStateChanged (MPENote note) override { noteChanged (note); }

    void noteChanged (MPENote changedNote)
    {
        const ScopedLock sl (lock);

        for (auto& note : activeNotes)
            if (note.noteID == changedNote.noteID)
                note = changedNote;

        triggerAsyncUpdate();
    }

    void noteReleased (MPENote finishedNote) override
    {
        const ScopedLock sl (lock);

        for (int i = activeNotes.size(); --i >= 0;)
            if (activeNotes.getReference(i).noteID == finishedNote.noteID)
                activeNotes.remove (i);

        triggerAsyncUpdate();
    }


private:
    //==============================================================================
    MPENote* findActiveNote (int noteID) const noexcept
    {
        for (auto& note : activeNotes)
            if (note.noteID == noteID)
                return &note;

        return nullptr;
    }

    NoteComponent* findNoteComponent (int noteID) const noexcept
    {
        for (auto& noteComp : noteComponents)
            if (noteComp->note.noteID == noteID)
                return noteComp;

        return nullptr;
    }

    //==============================================================================
    void handleAsyncUpdate() override
    {
        const ScopedLock sl (lock);

        for (int i = noteComponents.size(); --i >= 0;)
            if (findActiveNote (noteComponents.getUnchecked(i)->note.noteID) == nullptr)
                noteComponents.remove (i);

        for (auto& note : activeNotes)
            if (findNoteComponent (note.noteID) == nullptr)
                addAndMakeVisible (noteComponents.add (new NoteComponent (note, colourPicker.getColourForMidiChannel(note.midiChannel))));

        for (auto& noteComp : noteComponents)
            if (auto* noteInfo = findActiveNote (noteComp->note.noteID))
                noteComp->update (*noteInfo, getCentrePositionForNote (*noteInfo));
    }

    //==============================================================================
    Point<float> getCentrePositionForNote (MPENote note) const
    {
        float n = float (note.initialNote) + float (note.totalPitchbendInSemitones);
        float x = getWidth() * n / 128;
        float y = getHeight() * (1 - note.timbre.asUnsignedFloat());

        return Point<float> (x, y);
    }

    //==============================================================================
    OwnedArray<NoteComponent> noteComponents;
    CriticalSection lock;
    Array<MPENote> activeNotes;
    const ZoneColourPicker& colourPicker;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Visualiser)
};
