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

   This file is part of the JUCE library - "Jules' Utility Class Extensions"
   Copyright 2004-6 by Raw Material Software ltd.

  ------------------------------------------------------------------------------

   JUCE can be redistributed and/or modified under the terms of the
   GNU General Public License, as published by the Free Software Foundation;
   either version 2 of the License, or (at your option) any later version.

   JUCE 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.

   You should have received a copy of the GNU General Public License
   along with JUCE; if not, visit www.gnu.org/licenses or write to the
   Free Software Foundation, Inc., 59 Temple Place, Suite 330, 
   Boston, MA 02111-1307 USA

  ------------------------------------------------------------------------------

   If you'd like to release a closed-source product which uses JUCE, commercial
   licenses are also available: visit www.rawmaterialsoftware.com/juce for
   more information.

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

#include "../../../../juce_core/basics/juce_StandardHeader.h"

BEGIN_JUCE_NAMESPACE

#include "juce_ComboBox.h"
#include "juce_SimpleListBox.h"
#include "../lookandfeel/juce_LookAndFeel.h"
#include "../../../events/juce_Timer.h"
#include "../../../../juce_core/threads/juce_Process.h"
#include "../../../../juce_core/text/juce_LocalisedStrings.h"

static const int initialiseId = 0x0123fbfb;
static const int gapAroundList = 3;


//==============================================================================
class ComboBoxPopupComponent  : public Component,
                                private SimpleListBoxModel,
                                private Timer
{
public:
    ComboBoxPopupComponent (ComboBox& owner_)
        : owner (owner_),
          needToScroll (true),
          dismissNow (false)
    {
        addAndMakeVisible (list = new SimpleListBox (String::empty, this));

        LookAndFeel& lf = owner_.getLookAndFeel();

        Font itemFont;
        lf.getComboBoxFonts (itemFont, font, owner_);

        list->setBackgroundColour (Colours::transparentBlack);
        list->setRowHeight (3 + roundFloatToInt (font.getHeight()));

        const Rectangle screen (owner_.getParentMonitorArea());

        list->setSize (owner_.getWidth(),
                       jmin (list->getNumRows() * list->getRowHeight(),
                             screen.getHeight() / 2));

        list->setMouseMoveSelectsRows (true);

        int maxWidth = 0;
        for (int i = jmin (200, owner_.getNumItems()); --i >= 0;)
            maxWidth = jmax (maxWidth, font.getStringWidth (owner_.getItemText (i)));

        maxWidth += roundFloatToInt (4.0f * font.getHeight());
        maxWidth = jmax (list->getWidth(), maxWidth);

        setSize (jmin (screen.getWidth() - 50, maxWidth + gapAroundList * 2),
                 gapAroundList * 2 + jmax (list->getRowHeight(), list->getHeight()));

        const bool above = (owner_.getScreenY() + owner_.getHeight() + getHeight() > screen.getBottom() - 20);

        setTopLeftPosition (jlimit (screen.getX(),
                                    screen.getRight() - getWidth(),
                                    owner_.getScreenX() - 2),
                            above ? jmax (screen.getY(), owner_.getScreenY() - getHeight())
                                  : owner_.getScreenY() + owner_.getHeight());

        if (above && (getBottom() > owner_.getScreenY()))
            setSize (getWidth(), owner_.getScreenY() - getY());

        setVisible (true);
        setAlwaysOnTop (true);
        setWantsKeyboardFocus (true);
        setOpaque (true);

        addToDesktop (ComponentPeer::windowHasDropShadow
                       | ComponentPeer::windowIsTemporary);

        postCommandMessage (initialiseId);
        startTimer (50);
    }

    ~ComboBoxPopupComponent()
    {
        exitModalState (0);
        setVisible (false);

        if (owner.isValidComponent() && owner.activePopup == this)
            owner.activePopup = 0;

        delete list;
    }

    //==============================================================================
    int getNumRows()
    {
        return owner.items.size();
    }

    void paintListBoxItem (int row,
                           Graphics& g,
                           int width,
                           int height,
                           bool rowIsSelected)
    {
        const ComboBox::ItemInfo* const item = owner.items [row];

        if (item != 0)
        {
            owner.getLookAndFeel()
                .drawComboBoxItem (g, width, height,
                                   rowIsSelected && item->isEnabled,
                                   item->isEnabled,
                                   item->id != 0 && owner.getSelectedId() == item->id,
                                   item->isHeading,
                                   item->isSeparator(),
                                   item->name,
                                   owner);
        }
    }

    void listBoxItemClicked (int row, const MouseEvent&)
    {
        returnKeyPressed (row);
    }

    void returnKeyPressed (int row)
    {
        const ComboBox::ItemInfo* const item = owner.items [row];

        if (item != 0 && item->isRealItem() && item->isEnabled)
        {
            dismiss();
            owner.setSelectedId (item->id);
        }
    }

    //==============================================================================
    void paint (Graphics& g)
    {
        owner.getLookAndFeel()
            .drawComboBoxPopupBackground (g, getWidth(), getHeight());
    }

    void paintOverChildren (Graphics& g)
    {
        if (getNumRows() == 0)
        {
            g.setFont (font);
            g.setColour (owner.getLookAndFeel().comboBoxPopupText.withAlpha (0.4f));

            g.drawText (owner.noChoicesMessage,
                        0, 0, getWidth(), getHeight(),
                        Justification::centred, false);
        }
    }

    void resized()
    {
        list->setBoundsInset (BorderSize (gapAroundList, 1, gapAroundList, 1));

        if (list->getHeight() > 0)
            scrollIfNeeded();
    }

    //==============================================================================
    void inputAttemptWhenModal()
    {
        dismiss();
    }

    void keyPressed (const KeyPress& key)
    {
        if (key.getKeyCode() == KeyPress::escapeKey)
            dismiss();
    }

    void mouseWheelMove (const MouseEvent& e, float inc)
    {
        list->mouseWheelMove (e, inc);
    }

    void mouseExit (const MouseEvent&)
    {
        list->deselectAllRows();
    }

    void handleMouseDrag (const MouseEvent& e)
    {
        const MouseEvent e2 (e.getEventRelativeTo (list));

        const int rowUnderMouse = list->getRowContainingPosition (e2.x, e2.y);

        if (rowUnderMouse >= 0
             && list->contains (e2.x, e2.y))
        {
            list->selectRow (rowUnderMouse);
        }
        else
        {
            list->deselectAllRows();
        }
    }

    void handleMouseUp (const MouseEvent& e)
    {
        const MouseEvent e2 (e.getEventRelativeTo (list));

        dismiss();

        if (contains (e2.x, e2.y))
        {
            const int rowUnderMouse = list->getRowContainingPosition (e2.x, e2.y);

            if (rowUnderMouse >= 0)
            {
                returnKeyPressed (rowUnderMouse);

                // (this component might now have been deleted)
                return;
            }
        }
    }

    //==============================================================================
    juce_UseDebuggingNewOperator

private:
    ComboBox& owner;
    SimpleListBox* list;
    Font font;
    bool needToScroll, dismissNow;

    void timerCallback()
    {
        Component* const currentFocus = getCurrentlyFocusedComponent();

        if (dismissNow
            || (! Process::isForegroundProcess())
            || (currentFocus  == 0)
            || (currentFocus != this
                 && (! isParentOf (currentFocus))
                 && currentFocus != &owner
                 && (! owner.isParentOf (currentFocus))))
        {
            delete this;
        }
    }

    void dismiss()
    {
        dismissNow = true;
        startTimer (1);
    }

    void handleCommandMessage (int commandId)
    {
        if (commandId == initialiseId)
        {
            toFront (true);
            scrollIfNeeded();
        }
        else
        {
            Component::handleCommandMessage (commandId);
        }
    }

    void broughtToFront()
    {
        // (overloaded to stop the base class bringing a modal component to the front)
    }

    void scrollIfNeeded()
    {
        if (needToScroll && owner.getNumItems() > 0 && owner.getSelectedItemIndex() > 1)
        {
            list->setVerticalPosition (owner.getSelectedItemIndex()
                                       / (double) (owner.getNumItems() - 1));
            needToScroll = false;
        }
    }

    ComboBoxPopupComponent (const ComboBoxPopupComponent&);
    const ComboBoxPopupComponent& operator= (const ComboBoxPopupComponent&);
};


//==============================================================================
ComboBox::ComboBox (const String& name)
    : Component (name),
      items (4),
      currentIndex (-1),
      isButtonDown (false),
      separatorPending (false),
      label (0),
      activePopup (0),
      popupDeletionWatcher (0)
{
    noChoicesMessage = TRANS("(no choices)");

    addAndMakeVisible (label = new Label (String::empty, String::empty));
    label->addChangeListener (this);
    label->addMouseListener (this, false);
    label->setDragRepeatInterval (50);

    setEditableText (false);
    setRepaintsOnMouseActivity (true);

    lookAndFeelChanged();
}

ComboBox::~ComboBox()
{
    deletePopup();
    deleteAllChildren();
}

//==============================================================================
void ComboBox::setEditableText (const bool isEditable)
{
    label->setEditable (isEditable, isEditable, false);

    setWantsKeyboardFocus (! isEditable);
    resized();
}

void ComboBox::setJustificationType (const Justification& justification)
{
    label->setJustificationType (justification);
}

void ComboBox::parentHierarchyChanged()
{
    deletePopup();
}

//==============================================================================
void ComboBox::addItem (const String& newItemText,
                        const int newItemId)
{
    // you can't add empty strings to the list..
    jassert (newItemText.isNotEmpty());

    // IDs must be non-zero, as zero is used to indicate a lack of selecion.
    jassert (newItemId != 0);

    // you shouldn't use duplicate item IDs!
    jassert (getItemForId (newItemId) == 0);

    if (newItemText.isNotEmpty() && newItemId != 0)
    {
        if (separatorPending)
        {
            separatorPending = false;

            ItemInfo* const item = new ItemInfo();
            item->id = 0;
            item->isEnabled = false;
            item->isHeading = false;
            items.add (item);
        }

        ItemInfo* const item = new ItemInfo();
        item->name = newItemText;
        item->id = newItemId;
        item->isEnabled = true;
        item->isHeading = false;
        items.add (item);
    }
}

void ComboBox::addSeparator()
{
    separatorPending = (items.size() > 0);
}

void ComboBox::addSectionHeading (const String& headingName)
{
    // you can't add empty strings to the list..
    jassert (headingName.isNotEmpty());

    if (headingName.isNotEmpty())
    {
        if (separatorPending)
        {
            separatorPending = false;

            ItemInfo* const item = new ItemInfo();
            item->id = 0;
            item->isEnabled = false;
            item->isHeading = false;
            items.add (item);
        }

        ItemInfo* const item = new ItemInfo();
        item->name = headingName;
        item->id = 0;
        item->isEnabled = true;
        item->isHeading = true;
        items.add (item);
    }
}

void ComboBox::setItemEnabled (const int itemId,
                               const bool isEnabled)
{
    ItemInfo* const item = getItemForId (itemId);

    if (item != 0)
        item->isEnabled = isEnabled;
}

void ComboBox::changeItemText (const int itemId,
                               const String& newText)
{
    ItemInfo* const item = getItemForId (itemId);

    jassert (item != 0);

    if (item != 0)
        item->name = newText;
}

void ComboBox::clear()
{
    items.clear();
    separatorPending = false;

    if (! label->isEditable())
        setSelectedItemIndex (-1);
}

//==============================================================================
ComboBox::ItemInfo* ComboBox::getItemForId (const int id) const
{
    jassert (id != 0);

    if (id != 0)
    {
        for (int i = items.size(); --i >= 0;)
            if (items.getUnchecked(i)->id == id)
                return items.getUnchecked(i);
    }

    return 0;
}

ComboBox::ItemInfo* ComboBox::getItemForIndex (const int index) const
{
    int n = 0;

    for (int i = 0; i < items.size(); ++i)
    {
        ItemInfo* const item = items.getUnchecked(i);

        if (item->isRealItem())
        {
            if (n++ == index)
                return item;
        }
    }

    return 0;
}

int ComboBox::getNumItems() const
{
    int n = 0;

    for (int i = items.size(); --i >= 0;)
    {
        ItemInfo* const item = items.getUnchecked(i);

        if (item->isRealItem())
            ++n;
    }

    return n;
}

const String ComboBox::getItemText (const int index) const
{
    ItemInfo* const item = getItemForIndex (index);

    if (item != 0)
        return item->name;

    return String::empty;
}

int ComboBox::getItemId (const int index) const
{
    ItemInfo* const item = getItemForIndex (index);

    return (item != 0) ? item->id : 0;
}


//==============================================================================
bool ComboBox::ItemInfo::isSeparator() const
{
    return name.isEmpty();
}

bool ComboBox::ItemInfo::isRealItem() const
{
    return ! (isHeading || name.isEmpty());
}

//==============================================================================
int ComboBox::getSelectedItemIndex() const
{
    return (currentIndex >= 0 && getText() == getItemText (currentIndex))
                ? currentIndex
                : -1;
}

void ComboBox::setSelectedItemIndex (const int index,
                                     const bool dontSendChangeMessage)
{
    if (getSelectedItemIndex() != index
         || activePopup != 0)
    {
        deletePopup();

        if (index >= 0 && index < getNumItems())
            currentIndex = index;
        else
            currentIndex = -1;

        label->setText (getItemText (currentIndex), false);

        if (! dontSendChangeMessage)
            sendActionMessage (getName());
    }
}

void ComboBox::setSelectedId (const int newItemId,
                              const bool dontSendChangeMessage)
{
    for (int i = getNumItems(); --i >= 0;)
    {
        if (getItemId(i) == newItemId)
        {
            setSelectedItemIndex (i, dontSendChangeMessage);
            break;
        }
    }
}

int ComboBox::getSelectedId() const
{
    const ItemInfo* const item = getItemForIndex (currentIndex);

    return (item != 0 && getText() == item->name)
                ? item->id
                : 0;
}

const String ComboBox::getText() const
{
    return label->getText();
}

void ComboBox::setText (const String& newText,
                        const bool dontSendChangeMessage)
{
    for (int i = items.size(); --i >= 0;)
    {
        ItemInfo* const item = items.getUnchecked(i);

        if (item->isRealItem()
             && item->name == newText)
        {
            setSelectedId (item->id, dontSendChangeMessage);
            return;
        }
    }

    currentIndex = -1;

    if (label->getText() != newText)
    {
        label->setText (newText, false);

        if (! dontSendChangeMessage)
            sendActionMessage (getName());
    }

    repaint();
}

void ComboBox::setTextWhenNothingSelected (const String& newMessage)
{
    textWhenNothingSelected = newMessage;
    repaint();
}

void ComboBox::setTextWhenNoChoicesAvailable (const String& newMessage)
{
    noChoicesMessage = newMessage;
}

//==============================================================================
void ComboBox::paint (Graphics& g)
{
    getLookAndFeel().drawComboBox (g,
                                   getWidth(),
                                   getHeight(),
                                   isButtonDown,
                                   label->getRight(),
                                   0,
                                   getWidth() - label->getRight(),
                                   getHeight(),
                                   *this);

    if (textWhenNothingSelected.isNotEmpty()
        && label->getText().isEmpty()
        && ! label->isBeingEdited())
    {
        g.setColour (getLookAndFeel().comboBoxText.withMultipliedAlpha (0.5f));
        g.setFont (label->getFont());
        g.drawFittedText (textWhenNothingSelected,
                          label->getX() + 2, label->getY() + 1,
                          label->getWidth() - 4, label->getHeight() - 2,
                          label->getJustificationType(),
                          jmax (1, (int) (label->getHeight() / label->getFont().getHeight())));
    }
}

void ComboBox::resized()
{
    if (getHeight() > 0 && getWidth() > 0)
    {
        Font itemFont, popupFont;
        getLookAndFeel().getComboBoxFonts (itemFont, popupFont, *this);

        label->setBounds (1, 1,
                          getWidth() + 3 - getHeight(),
                          getHeight() - 2);

        label->setFont (itemFont);
    }
}

void ComboBox::enablementChanged()
{
    repaint();
}

void ComboBox::lookAndFeelChanged()
{
    label->setColours (getLookAndFeel().comboBoxText,
                       Colours::transparentBlack,
                       getLookAndFeel().comboBoxText,
                       Colours::transparentBlack,
                       getLookAndFeel().textEditorHighlight,
                       Colours::transparentBlack);
}

//==============================================================================
void ComboBox::keyPressed (const KeyPress& key)
{
    if (key.isKeyCode (KeyPress::upKey)
        || key.isKeyCode (KeyPress::leftKey))
    {
        setSelectedItemIndex (jmax (0, currentIndex - 1));
    }
    else if (key.isKeyCode (KeyPress::downKey)
              || key.isKeyCode (KeyPress::rightKey))
    {
        setSelectedItemIndex (jmin (currentIndex + 1, getNumItems() - 1));
    }
    else
    {
        Component::keyPressed (key);
    }
}

void ComboBox::keyStateChanged()
{
    // only forward key events that aren't used by this component
    if (! (KeyPress::isKeyCurrentlyDown (KeyPress::upKey)
          || KeyPress::isKeyCurrentlyDown (KeyPress::leftKey)
          || KeyPress::isKeyCurrentlyDown (KeyPress::downKey)
          || KeyPress::isKeyCurrentlyDown (KeyPress::rightKey)))
    {
        Component::keyStateChanged();
    }
}

//==============================================================================
void ComboBox::focusGained (FocusChangeType)
{
    repaint();
}

void ComboBox::focusLost (FocusChangeType)
{
    repaint();
}

//==============================================================================
void ComboBox::changeListenerCallback (void*)
{
    sendActionMessage (getName());
}

//==============================================================================
void ComboBox::createPopup()
{
    jassert (activePopup == 0);

    activePopup = new ComboBoxPopupComponent (*this);
    popupDeletionWatcher = new ComponentDeletionWatcher (activePopup);
    activePopup->enterModalState();
}

void ComboBox::deletePopup()
{
    if (activePopup != 0 && ! popupDeletionWatcher->hasBeenDeleted())
        delete activePopup;

    activePopup = 0;
    deleteAndZero (popupDeletionWatcher);
}


//==============================================================================
void ComboBox::mouseDown (const MouseEvent& e)
{
    deletePopup();

    isButtonDown = isEnabled();

    if (isButtonDown
         && (e.component == this || ! label->isEditable()))
    {
        createPopup();
    }
}

void ComboBox::mouseDrag (const MouseEvent& e)
{
    if (popupDeletionWatcher != 0 && popupDeletionWatcher->hasBeenDeleted())
        deletePopup();

    if (isButtonDown
         && activePopup == 0
         && ! e.mouseWasClicked())
    {
        createPopup();
    }

    if (activePopup != 0)
        activePopup->handleMouseDrag (e);
}

void ComboBox::mouseUp (const MouseEvent& e2)
{
    if (popupDeletionWatcher != 0 && popupDeletionWatcher->hasBeenDeleted())
        deletePopup();

    repaint();
    const MouseEvent e (e2.getEventRelativeTo (this));

    // need to watch out for repeated mouse-ups arriving in succession because this is a mouselistener -
    // so only the fist mouse-up afer a mouse-down will be responded to..
    if (isButtonDown
         && (e.x >= 0 && e.x < getWidth() && e.y >= 0 && e.y < getHeight())
         && (e2.component == this || ! label->isEditable())
         && ! (activePopup != 0
                && activePopup->contains (e.getScreenX() - activePopup->getScreenX(),
                                          e.getScreenY() - activePopup->getScreenY())))
    {
        isButtonDown = false; // must set this before going modal..

        Component* const lastFocusedComponent = Component::getCurrentlyFocusedComponent();

        if (activePopup == 0)
            createPopup();

        activePopup->runModalLoop();

        if (lastFocusedComponent->isValidComponent())
            lastFocusedComponent->grabKeyboardFocus();
    }
    else
    {
        isButtonDown = false;

        if (activePopup != 0)
            activePopup->handleMouseUp (e);
    }
}

END_JUCE_NAMESPACE
