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

   This file is part of the JUCE library.
   Copyright (c) 2022 - Raw Material Software Limited

   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 7 End-User License
   Agreement and JUCE Privacy Policy.

   End User License Agreement: www.juce.com/juce-7-licence
   Privacy Policy: www.juce.com/juce-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.

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

namespace juce
{

struct MPEKeyboardComponent::MPENoteComponent : public Component
{
    MPENoteComponent (MPEKeyboardComponent& o, uint16 sID, uint8 initial, float noteOnVel, float press)
       : owner (o),
         radiusScale (owner.getKeyWidth() / 1.5f),
         noteOnVelocity (noteOnVel),
         pressure (press),
         sourceID (sID),
         initialNote (initial)
    {
    }

    float getStrikeRadius() const      { return 5.0f + getNoteOnVelocity() * radiusScale * 2.0f; }
    float getPressureRadius() const    { return 5.0f + getPressure() * radiusScale * 2.0f; }

    float getNoteOnVelocity() const    { return noteOnVelocity; }
    float getPressure() const          { return pressure; }

    Point<float> getCentrePos() const  { return getBounds().toFloat().getCentre(); }

    void paint (Graphics& g) override
    {
        auto strikeSize = getStrikeRadius() * 2.0f;
        auto pressSize = getPressureRadius() * 2.0f;
        auto bounds = getLocalBounds().toFloat();

        g.setColour (owner.findColour (noteCircleFillColourId));
        g.fillEllipse (bounds.withSizeKeepingCentre (strikeSize, strikeSize));

        g.setColour (owner.findColour (noteCircleOutlineColourId));
        g.drawEllipse (bounds.withSizeKeepingCentre (pressSize, pressSize), 1.0f);
    }

    //==========================================================================
    MPEKeyboardComponent& owner;

    float radiusScale = 0.0f, noteOnVelocity = 0.0f, pressure = 0.5f;
    uint16 sourceID = 0;
    uint8 initialNote = 0;
    bool isLatched = true;
};

//==============================================================================
MPEKeyboardComponent::MPEKeyboardComponent (MPEInstrument& instr, Orientation orientationToUse)
    : KeyboardComponentBase (orientationToUse),
      instrument (instr)
{
    updateZoneLayout();
    colourChanged();
    setKeyWidth (25.0f);

    instrument.addListener (this);
}

MPEKeyboardComponent::~MPEKeyboardComponent()
{
    instrument.removeListener (this);
}

//==============================================================================
void MPEKeyboardComponent::drawKeyboardBackground (Graphics& g, Rectangle<float> area)
{
    g.setColour (findColour (whiteNoteColourId));
    g.fillRect (area);
}

void MPEKeyboardComponent::drawWhiteKey (int midiNoteNumber, Graphics& g, Rectangle<float> area)
{
    if (midiNoteNumber % 12 == 0)
    {
        auto fontHeight = jmin (12.0f, getKeyWidth() * 0.9f);
        auto text = MidiMessage::getMidiNoteName (midiNoteNumber, true, true, getOctaveForMiddleC());

        g.setColour (findColour (textLabelColourId));
        g.setFont (Font (fontHeight).withHorizontalScale (0.8f));

        switch (getOrientation())
        {
            case horizontalKeyboard:
                g.drawText (text, area.withTrimmedLeft (1.0f).withTrimmedBottom (2.0f),
                            Justification::centredBottom, false);
                break;
            case verticalKeyboardFacingLeft:
                g.drawText (text, area.reduced (2.0f), Justification::centredLeft, false);
                break;
            case verticalKeyboardFacingRight:
                g.drawText (text, area.reduced (2.0f), Justification::centredRight, false);
                break;
            default:
                break;
        }
    }
}

void MPEKeyboardComponent::drawBlackKey (int /*midiNoteNumber*/, Graphics& g, Rectangle<float> area)
{
    g.setColour (findColour (whiteNoteColourId));
    g.fillRect (area);

    g.setColour (findColour (blackNoteColourId));

    if (isHorizontal())
    {
        g.fillRoundedRectangle (area.toFloat().reduced ((area.getWidth() / 2.0f) - (getBlackNoteWidth() / 12.0f),
                                                        area.getHeight() / 4.0f), 1.0f);
    }
    else
    {
        g.fillRoundedRectangle (area.toFloat().reduced (area.getWidth() / 4.0f,
                                                        (area.getHeight() / 2.0f) - (getBlackNoteWidth() / 12.0f)), 1.0f);
    }
}

void MPEKeyboardComponent::colourChanged()
{
    setOpaque (findColour (whiteNoteColourId).isOpaque());
    repaint();
}

//==========================================================================
MPEValue MPEKeyboardComponent::mousePositionToPitchbend (int initialNote, Point<float> mousePos)
{
    auto constrainedMousePos = [&]
    {
        auto horizontal = isHorizontal();

        auto posToCheck = jlimit (0.0f,
                                  horizontal ? (float) getWidth() - 1.0f : (float) getHeight(),
                                  horizontal ? mousePos.x : mousePos.y);

        auto bottomKeyRange = getRectangleForKey (jmax (getRangeStart(), initialNote - perNotePitchbendRange));
        auto topKeyRange    = getRectangleForKey (jmin (getRangeEnd(),   initialNote + perNotePitchbendRange));

        auto lowerLimit = horizontal ? bottomKeyRange.getCentreX()
                                     : getOrientation() == Orientation::verticalKeyboardFacingRight ? topKeyRange.getCentreY()
                                                                                                    : bottomKeyRange.getCentreY();

        auto upperLimit = horizontal ? topKeyRange.getCentreX()
                                     : getOrientation() == Orientation::verticalKeyboardFacingRight ? bottomKeyRange.getCentreY()
                                                                                                    : topKeyRange.getCentreY();

        posToCheck = jlimit (lowerLimit, upperLimit, posToCheck);

        return horizontal ? Point<float> (posToCheck, 0.0f)
                          : Point<float> (0.0f, posToCheck);
    }();

    auto note = getNoteAndVelocityAtPosition (constrainedMousePos, true).note;

    if (note == -1)
    {
        jassertfalse;
        return {};
    }

    auto fractionalSemitoneBend = [&]
    {
        auto noteRect = getRectangleForKey (note);

        switch (getOrientation())
        {
            case horizontalKeyboard:          return (constrainedMousePos.x - noteRect.getCentreX()) / noteRect.getWidth();
            case verticalKeyboardFacingRight: return (noteRect.getCentreY() - constrainedMousePos.y) / noteRect.getHeight();
            case verticalKeyboardFacingLeft:  return (constrainedMousePos.y - noteRect.getCentreY()) / noteRect.getHeight();
        }

        jassertfalse;
        return 0.0f;
    }();

    auto totalNumSemitones = ((float) note + fractionalSemitoneBend) - (float) initialNote;

    return MPEValue::fromUnsignedFloat (jmap (totalNumSemitones, (float) -perNotePitchbendRange, (float) perNotePitchbendRange, 0.0f, 1.0f));
}

MPEValue MPEKeyboardComponent::mousePositionToTimbre (Point<float> mousePos)
{
    auto delta = [mousePos, this]
    {
        switch (getOrientation())
        {
            case horizontalKeyboard:          return mousePos.y;
            case verticalKeyboardFacingLeft:  return (float) getWidth() - mousePos.x;
            case verticalKeyboardFacingRight: return mousePos.x;
        }

        jassertfalse;
        return 0.0f;
    }();

    return MPEValue::fromUnsignedFloat (jlimit (0.0f, 1.0f, 1.0f - (delta / getWhiteNoteLength())));
}

void MPEKeyboardComponent::mouseDown (const MouseEvent& e)
{
    auto newNote = getNoteAndVelocityAtPosition (e.position).note;

    if (newNote >= 0)
    {
        auto channel = channelAssigner->findMidiChannelForNewNote (newNote);

        instrument.noteOn (channel, newNote, MPEValue::fromUnsignedFloat (velocity));
        sourceIDMap[e.source.getIndex()] = instrument.getNote (instrument.getNumPlayingNotes() - 1).noteID;

        instrument.pitchbend (channel, MPEValue::centreValue());
        instrument.timbre    (channel, mousePositionToTimbre (e.position));
        instrument.pressure  (channel, MPEValue::fromUnsignedFloat (e.isPressureValid()
                                                                    && useMouseSourcePressureForStrike ? e.pressure
                                                                                                       : pressure));
    }
}

void MPEKeyboardComponent::mouseDrag (const MouseEvent& e)
{
    auto noteID = sourceIDMap[e.source.getIndex()];
    auto note = instrument.getNoteWithID (noteID);

    if (! note.isValid())
        return;

    auto noteComponent = std::find_if (noteComponents.begin(),
                                       noteComponents.end(),
                                       [noteID] (auto& comp) { return comp->sourceID == noteID; });

    if (noteComponent == noteComponents.end())
        return;

    if ((*noteComponent)->isLatched && std::abs (isHorizontal() ? e.getDistanceFromDragStartX()
                                                                : e.getDistanceFromDragStartY()) > roundToInt (getKeyWidth() / 4.0f))
    {
        (*noteComponent)->isLatched = false;
    }

    auto channel = channelAssigner->findMidiChannelForExistingNote (note.initialNote);

    if (! (*noteComponent)->isLatched)
        instrument.pitchbend (channel, mousePositionToPitchbend (note.initialNote, e.position));

    instrument.timbre (channel, mousePositionToTimbre (e.position));
    instrument.pressure (channel, MPEValue::fromUnsignedFloat (e.isPressureValid()
                                                               && useMouseSourcePressureForStrike ? e.pressure
                                                                                                  : pressure));
}

void MPEKeyboardComponent::mouseUp (const MouseEvent& e)
{
    auto note = instrument.getNoteWithID (sourceIDMap[e.source.getIndex()]);

    if (! note.isValid())
        return;

    instrument.noteOff (channelAssigner->findMidiChannelForExistingNote (note.initialNote),
                        note.initialNote, MPEValue::fromUnsignedFloat (lift));
    channelAssigner->noteOff (note.initialNote);
    sourceIDMap.erase (e.source.getIndex());
}

void MPEKeyboardComponent::focusLost (FocusChangeType)
{
    for (auto& comp : noteComponents)
    {
        auto note = instrument.getNoteWithID (comp->sourceID);

        if (note.isValid())
            instrument.noteOff (channelAssigner->findMidiChannelForExistingNote (note.initialNote),
                                note.initialNote, MPEValue::fromUnsignedFloat (lift));
    }
}

//==============================================================================
void MPEKeyboardComponent::updateZoneLayout()
{
    {
        const ScopedLock noteLock (activeNotesLock);
        activeNotes.clear();
    }

    noteComponents.clear();

    if (instrument.isLegacyModeEnabled())
    {
        channelAssigner = std::make_unique<MPEChannelAssigner> (instrument.getLegacyModeChannelRange());
        perNotePitchbendRange = instrument.getLegacyModePitchbendRange();
    }
    else
    {
        auto layout = instrument.getZoneLayout();

        if (layout.isActive())
        {
            auto zone = layout.getLowerZone().isActive() ? layout.getLowerZone()
                                                         : layout.getUpperZone();

            channelAssigner = std::make_unique<MPEChannelAssigner> (zone);
            perNotePitchbendRange = zone.perNotePitchbendRange;
        }
        else
        {
            channelAssigner.reset();
        }
    }
}

void MPEKeyboardComponent::addNewNote (MPENote note)
{
    noteComponents.push_back (std::make_unique<MPENoteComponent> (*this, note.noteID, note.initialNote,
                                                                  note.noteOnVelocity.asUnsignedFloat(),
                                                                  note.pressure.asUnsignedFloat()));
    auto& comp = noteComponents.back();

    addAndMakeVisible (*comp);
    comp->toBack();
}

void MPEKeyboardComponent::handleNoteOns (std::set<MPENote>& notesToUpdate)
{
    for (auto& note : notesToUpdate)
    {
        if (! std::any_of (noteComponents.begin(),
                           noteComponents.end(),
                           [note] (auto& comp) { return comp->sourceID == note.noteID; }))
        {
            addNewNote (note);
        }
    }
}

void MPEKeyboardComponent::handleNoteOffs (std::set<MPENote>& notesToUpdate)
{
    auto removePredicate = [&notesToUpdate] (std::unique_ptr<MPENoteComponent>& comp)
    {
        return std::none_of (notesToUpdate.begin(),
                             notesToUpdate.end(),
                             [&comp] (auto& note) { return comp->sourceID == note.noteID; });
    };

    noteComponents.erase (std::remove_if (std::begin (noteComponents),
                                          std::end (noteComponents),
                                          removePredicate),
                          std::end (noteComponents));

    if (noteComponents.empty())
        stopTimer();
}

void MPEKeyboardComponent::updateNoteComponentBounds (const MPENote& note, MPENoteComponent& noteComponent)
{
    auto xPos = [&]
    {
        const auto currentNote = note.initialNote + (float) note.totalPitchbendInSemitones;
        const auto noteBend = currentNote - std::floor (currentNote);

        const auto noteBounds = getRectangleForKey ((int) currentNote);
        const auto nextNoteBounds = getRectangleForKey ((int) currentNote + 1);

        const auto horizontal = isHorizontal();

        const auto distance = noteBend * (horizontal ? nextNoteBounds.getCentreX() - noteBounds.getCentreX()
                                                     : nextNoteBounds.getCentreY() - noteBounds.getCentreY());

        return (horizontal ? noteBounds.getCentreX() : noteBounds.getCentreY()) + distance;
    }();

    auto yPos = [&]
    {
        const auto currentOrientation = getOrientation();

        const auto timbrePosition = (currentOrientation == horizontalKeyboard
                                    || currentOrientation == verticalKeyboardFacingRight ? 1.0f - note.timbre.asUnsignedFloat()
                                                                                         : note.timbre.asUnsignedFloat());

        return timbrePosition * getWhiteNoteLength();
    }();

    const auto centrePos = (isHorizontal() ? Point<float> (xPos, yPos)
                                           : Point<float> (yPos, xPos));

    const auto radius = jmax (noteComponent.getStrikeRadius(), noteComponent.getPressureRadius());

    noteComponent.setBounds (Rectangle<float> (radius * 2.0f, radius * 2.0f)
                               .withCentre (centrePos)
                               .getSmallestIntegerContainer());
}

static bool operator< (const MPENote& n1, const MPENote& n2) noexcept  { return n1.noteID < n2.noteID; }

void MPEKeyboardComponent::updateNoteComponents()
{
    std::set<MPENote> notesToUpdate;

    {
        ScopedLock noteLock (activeNotesLock);

        for (const auto& note : activeNotes)
            if (note.second)
                notesToUpdate.insert (note.first);
    };

    handleNoteOns  (notesToUpdate);
    handleNoteOffs (notesToUpdate);

    for (auto& comp : noteComponents)
    {
        auto noteForComponent = std::find_if (notesToUpdate.begin(),
                                              notesToUpdate.end(),
                                              [&comp] (auto& note) { return note.noteID == comp->sourceID; });

        if (noteForComponent != notesToUpdate.end())
        {
            comp->pressure = noteForComponent->pressure.asUnsignedFloat();
            updateNoteComponentBounds (*noteForComponent, *comp);

            comp->repaint();
        }
    }
}

void MPEKeyboardComponent::timerCallback()
{
    updateNoteComponents();
}

//==============================================================================
void MPEKeyboardComponent::noteAdded (MPENote newNote)
{
    {
        const ScopedLock noteLock (activeNotesLock);
        activeNotes.push_back ({ newNote, true });
    }

    startTimerHz (30);
}

void MPEKeyboardComponent::updateNoteData (MPENote& changedNote)
{
    const ScopedLock noteLock (activeNotesLock);

    for (auto& note : activeNotes)
    {
        if (note.first.noteID == changedNote.noteID)
        {
            note.first = changedNote;
            note.second = true;
            return;
        }
    }
}

void MPEKeyboardComponent::notePressureChanged (MPENote changedNote)
{
    updateNoteData (changedNote);
}

void MPEKeyboardComponent::notePitchbendChanged (MPENote changedNote)
{
    updateNoteData (changedNote);
}

void MPEKeyboardComponent::noteTimbreChanged (MPENote changedNote)
{
    updateNoteData (changedNote);
}

void MPEKeyboardComponent::noteReleased (MPENote finishedNote)
{
    const ScopedLock noteLock (activeNotesLock);

    activeNotes.erase (std::remove_if (std::begin (activeNotes),
                                       std::end (activeNotes),
                                       [finishedNote] (auto& note) { return note.first.noteID == finishedNote.noteID; }),
                       std::end (activeNotes));
}

void MPEKeyboardComponent::zoneLayoutChanged()
{
    MessageManager::callAsync ([this] { updateZoneLayout(); });
}

} // namespace juce
