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

   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_ListBox.h"
#include "../../graphics/geometry/juce_RectangleList.h"
#include "../../graphics/imaging/juce_Image.h"


//==============================================================================
class ListViewport  : public Viewport
{
public:
    int firstIndex, firstWholeIndex, lastWholeIndex;
    bool hasUpdated;

    //==============================================================================
    ListViewport (ListBox* const owner_)
        : owner (owner_)
    {
        setWantsKeyboardFocus (false);

        Component* contentComp = new Component();
        setViewedComponent (contentComp);
        getViewedComponent()->addMouseListener (this, false);
        getViewedComponent()->setWantsKeyboardFocus (false);
    }

    ~ListViewport()
    {
        getViewedComponent()->removeMouseListener (this);
        getViewedComponent()->deleteAllChildren();
    }

    Component* getComponentForRowNumber (const int row) const
    {
        return getViewedComponent()->getChildComponent (row % jmax (1, getViewedComponent()->getNumChildComponents()));
    }

    Component* getComponentForRowNumberIfOnscreen (const int row) const
    {
        return (row >= firstIndex && row < firstIndex + getViewedComponent()->getNumChildComponents())
                 ? getComponentForRowNumber (row) : 0;
    }

    void visibleAreaChanged (int, int, int, int)
    {
        updateVisibleArea (true);
        owner->listWasScrolled();
    }

    void updateVisibleArea (const bool makeSureItUpdatesContent)
    {
        hasUpdated = false;

        int newX = getViewedComponent()->getX();
        int newY = getViewedComponent()->getY();
        int newW = jmax (owner->minimumRowWidth, getMaximumVisibleWidth());
        int newH = owner->totalItems * owner->getRowHeight();

        if (newY + newH < getMaximumVisibleHeight() && newH > getMaximumVisibleHeight())
            newY = getMaximumVisibleHeight() - newH;

        getViewedComponent()->setBounds (newX, newY, newW, newH);

        if (makeSureItUpdatesContent && ! hasUpdated)
            updateContents();
    }

    void updateContents()
    {
        hasUpdated = true;
        const int rowHeight = owner->getRowHeight();

        if (rowHeight > 0)
        {
            const int y = getViewPositionY();
            const int w = getViewedComponent()->getWidth();

            const int numNeeded = 2 + getMaximumVisibleHeight() / rowHeight;

            while (numNeeded > getViewedComponent()->getNumChildComponents())
            {
                Component* const newRowComp = owner->createRowComponent();
                jassert (newRowComp != 0); // the createRowComponent() method mustn't return a null pointer!

                getViewedComponent()->addAndMakeVisible (newRowComp);
            }

            jassert (numNeeded >= 0);

            while (numNeeded < getViewedComponent()->getNumChildComponents())
            {
                Component* const rowToRemove
                    = getViewedComponent()->getChildComponent (getViewedComponent()->getNumChildComponents() - 1);

                delete rowToRemove;
            }

            firstIndex = y / rowHeight;
            firstWholeIndex = (y + rowHeight - 1) / rowHeight;
            lastWholeIndex = (y + getMaximumVisibleHeight() - 1) / rowHeight;

            for (int i = 0; i < numNeeded; ++i)
            {
                const int row = i + firstIndex;
                Component* const rowComp = getComponentForRowNumber (row);

                owner->updateRowComponent (rowComp, row, owner->isRowSelected (row));
                rowComp->setBounds (0, row * rowHeight, w, rowHeight);
            }
        }

        if (owner->headerComponent != 0)
            owner->headerComponent->setBounds (owner->outlineThickness + getViewedComponent()->getX(),
                                               owner->outlineThickness,
                                               jmax (owner->getWidth(), getViewedComponent()->getWidth()),
                                               owner->headerComponent->getHeight());
    }

    void mouseUp (const MouseEvent&)
    {
        owner->backgroundClicked();
    }

    void paint (Graphics& g)
    {
        if (isOpaque())
            g.fillAll (owner->backgroundColour);
    }

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

private:
    ListBox* owner;

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


//==============================================================================
ListBox::ListBox (const String& name)
    : Component (name),
      headerComponent (0),
      totalItems (0),
      rowHeight (22),
      minimumRowWidth (0),
      scrollBarSize (18),
      outlineThickness (0),
      lastRowSelected (-1),
      mouseMoveSelects (false),
      multipleSelection (false),
      hasDoneInitialUpdate (false)
{
    addAndMakeVisible (viewport = new ListViewport (this));

    setWantsKeyboardFocus (true);
    setFocusOrder (12);

    setBackgroundColour (Colours::white);
}

ListBox::~ListBox()
{
    deleteAllChildren();
}

void ListBox::setMultipleSelectionEnabled (bool b)
{
    multipleSelection = b;
}

void ListBox::setMouseMoveSelectsRows (bool b)
{
    mouseMoveSelects = b;
    addMouseListener (this, true);
}

//==============================================================================
void ListBox::paint (Graphics& g)
{
    if (! hasDoneInitialUpdate)
        updateContent();

    g.fillAll (backgroundColour);

    if (outlineThickness > 0)
    {
        g.setColour (outlineColour);
        g.drawRect (0, 0, getWidth(), getHeight(), outlineThickness);
    }
}

void ListBox::resized()
{
    viewport->setBoundsInset (BorderSize (outlineThickness + ((headerComponent != 0) ? headerComponent->getHeight() : 0),
                                          outlineThickness,
                                          outlineThickness,
                                          outlineThickness));

    viewport->setSingleStepSizes (20, getRowHeight());

    viewport->updateVisibleArea (false);
}

//==============================================================================
void ListBox::updateContent()
{
    hasDoneInitialUpdate = true;
    totalItems = getNumRows();

    bool selectionChanged = false;

    if (selected [selected.size() - 1] >= totalItems)
    {
        selected.removeRange (totalItems, 0x7fffffff - totalItems);

        if (selected.size() == 0)
            lastRowSelected = totalItems - 1;
        else if (lastRowSelected >= totalItems)
            lastRowSelected = getSelectedRow (0);

        selectionChanged = true;
    }

    if (isVisible())
        viewport->updateVisibleArea (true);

    if (selectionChanged)
        selectedRowsChanged (lastRowSelected);
}

//==============================================================================
void ListBox::selectRow (const int row,
                         bool dontScroll,
                         bool deselectOthersFirst)
{
    selectRowInternal (row, dontScroll, deselectOthersFirst, false);
}

void ListBox::selectRowInternal (const int row,
                                 bool dontScroll,
                                 bool deselectOthersFirst,
                                 bool isMouseClick)
{
    if (! multipleSelection)
        deselectOthersFirst = true;

    if ((! isRowSelected (row))
         || (deselectOthersFirst && getNumSelectedRows() > 1))
    {
        if (row >= 0 && row < totalItems)
        {
            if (deselectOthersFirst)
                selected.clear();

            selected.addRange (row, 1);

            if (getHeight() == 0 || getWidth() == 0)
                dontScroll = true;

            viewport->hasUpdated = false;

            if (row < viewport->firstWholeIndex && ! dontScroll)
            {
                viewport->setViewPosition (viewport->getViewPositionX(),
                                           row * getRowHeight());
            }
            else if (row >= viewport->lastWholeIndex && ! dontScroll)
            {
                const int rowsOnScreen = viewport->lastWholeIndex - viewport->firstWholeIndex;

                if (row >= lastRowSelected + rowsOnScreen
                     && rowsOnScreen < totalItems - 1
                     && ! isMouseClick)
                {
                    viewport->setViewPosition (viewport->getViewPositionX(),
                                               jlimit (0, jmax (0, totalItems - rowsOnScreen), row)
                                                   * getRowHeight());
                }
                else
                {
                    viewport->setViewPosition (viewport->getViewPositionX(),
                                               jmax (0, (row  + 1) * getRowHeight() - viewport->getMaximumVisibleHeight()));
                }
            }

            if (! viewport->hasUpdated)
                viewport->updateContents();

            lastRowSelected = row;
            selectedRowsChanged (row);
        }
        else
        {
            if (deselectOthersFirst)
                deselectAllRows();
        }
    }
}

void ListBox::deselectRow (const int row)
{
    if (selected.contains (row))
    {
        selected.removeRange (row, 1);

        if (row == lastRowSelected)
            lastRowSelected = getSelectedRow (0);

        viewport->updateContents();
        selectedRowsChanged (lastRowSelected);
    }
}

void ListBox::setSelectedRows (const SparseSet<int>& setOfRowsToBeSelected)
{
    selected = setOfRowsToBeSelected;
    selected.removeRange (totalItems, 0x7fffffff - totalItems);

    if (! isRowSelected (lastRowSelected))
        lastRowSelected = -1;

    viewport->updateContents();
    selectedRowsChanged (lastRowSelected);
}

const SparseSet<int> ListBox::getSelectedRows() const
{
    return selected;
}

void ListBox::selectRangeOfRows (int firstRow, int lastRow)
{
    if (multipleSelection && (firstRow != lastRow))
    {
        const int numRows = totalItems - 1;
        firstRow = jlimit (0, jmax (0, numRows), firstRow);
        lastRow = jlimit (0, jmax (0, numRows), lastRow);

        selected.addRange (jmin (firstRow, lastRow),
                           abs (firstRow - lastRow) + 1);

        selected.removeRange (lastRow, 1);
    }

    selectRowInternal (lastRow, false, false, true);
}

void ListBox::flipRowSelection (const int row)
{
    if (isRowSelected (row))
        deselectRow (row);
    else
        selectRowInternal (row, false, false, true);
}

void ListBox::deselectAllRows()
{
    if (! selected.isEmpty())
    {
        selected.clear();
        lastRowSelected = -1;

        viewport->updateContents();
        selectedRowsChanged (lastRowSelected);
    }
}

void ListBox::selectRowsBasedOnModifierKeys (const int row,
                                             const ModifierKeys& mods)
{
    if (multipleSelection && mods.isCommandDown())
    {
        flipRowSelection (row);
    }
    else if (multipleSelection && mods.isShiftDown() && lastRowSelected >= 0)
    {
        selectRangeOfRows (lastRowSelected, row);
    }
    else
    {
        selectRowInternal (row, false, true, true);
    }
}

int ListBox::getNumSelectedRows() const
{
    return selected.size();
}

int ListBox::getSelectedRow (const int index) const
{
    return (index >= 0 && index < selected.size())
                ? selected [index] : -1;
}

bool ListBox::isRowSelected (const int row) const
{
    return selected.contains (row);
}

int ListBox::getLastRowSelected() const
{
    return (isRowSelected (lastRowSelected)) ? lastRowSelected : -1;
}

//==============================================================================
int ListBox::getRowContainingPosition (const int x, const int y)
{
    if (x >= 0 && x < getWidth())
    {
        const int row = (viewport->getViewPositionY() + y - viewport->getY()) / rowHeight;

        if (row >= 0 && row < totalItems)
            return row;
    }

    return -1;
}

Component* ListBox::getComponentForRowNumber (const int row) const
{
    return (row >= 0 && row < totalItems)
             ? viewport->getComponentForRowNumberIfOnscreen (row) : 0;
}

void ListBox::setVerticalPosition (double proportion)
{
    viewport->setViewPosition (viewport->getViewPositionX(),
                               jmax (0, roundDoubleToInt (proportion * (viewport->getViewedComponent()->getHeight() - viewport->getHeight()))));
}

int ListBox::getVisibleRowWidth() const
{
    return viewport->getViewWidth();
}

void ListBox::scrollToEnsureRowIsOnscreen (const int row)
{
    if (row < viewport->firstWholeIndex)
    {
        viewport->setViewPosition (viewport->getViewPositionX(),
                                   row * getRowHeight());
    }
    else if (row >= viewport->lastWholeIndex)
    {
        viewport->setViewPosition (viewport->getViewPositionX(),
                                   jmax (0, (row  + 1) * getRowHeight() - viewport->getMaximumVisibleHeight()));
    }
}

//==============================================================================
void ListBox::keyPressed (const KeyPress& key)
{
    const int numVisibleRows = viewport->getHeight() / getRowHeight();

    const bool multiple = multipleSelection
                            && (lastRowSelected >= 0)
                            && (key.getModifiers().isShiftDown()
                                 || key.getModifiers().isCtrlDown()
                                 || key.getModifiers().isCommandDown());

    if (key.isKeyCode (KeyPress::upKey))
    {
        if (multiple)
            selectRangeOfRows (lastRowSelected, lastRowSelected - 1);
        else
            selectRow (jmax (0, lastRowSelected - 1));
    }
    else if (key.isKeyCode (KeyPress::returnKey)
              && isRowSelected (lastRowSelected))
    {
        returnKeyPressed (lastRowSelected);
    }
    else if (key.isKeyCode (KeyPress::pageUpKey))
    {
        if (multiple)
            selectRangeOfRows (lastRowSelected, lastRowSelected - numVisibleRows);
        else
            selectRow (jmax (0, jmax (0, lastRowSelected) - numVisibleRows));
    }
    else if (key.isKeyCode (KeyPress::pageDownKey))
    {
        if (multiple)
            selectRangeOfRows (lastRowSelected, lastRowSelected + numVisibleRows);
        else
            selectRow (jmin (totalItems - 1, jmax (0, lastRowSelected) + numVisibleRows));
    }
    else if (key.isKeyCode (KeyPress::homeKey))
    {
        if (multiple && key.getModifiers().isShiftDown())
            selectRangeOfRows (lastRowSelected, 0);
        else
            selectRow (0);
    }
    else if (key.isKeyCode (KeyPress::endKey))
    {
        if (multiple && key.getModifiers().isShiftDown())
            selectRangeOfRows (lastRowSelected, totalItems - 1);
        else
            selectRow (totalItems - 1);
    }
    else if (key.isKeyCode (KeyPress::downKey))
    {
        if (multiple)
            selectRangeOfRows (lastRowSelected, lastRowSelected + 1);
        else
            selectRow (jmin (totalItems - 1, jmax (0, lastRowSelected) + 1));
    }
    else if ((key.isKeyCode (KeyPress::deleteKey) || key.isKeyCode (KeyPress::backspaceKey))
               && isRowSelected (lastRowSelected))
    {
        deleteKeyPressed (lastRowSelected);
    }
    else if (multiple && key == KeyPress (T('a'), ModifierKeys::commandModifier))
    {
        selectRangeOfRows (0, INT_MAX);
    }
    else
    {
        Component::keyPressed (key);
    }
}

void ListBox::keyStateChanged()
{
    if (! (KeyPress::isKeyCurrentlyDown (KeyPress::upKey)
           || KeyPress::isKeyCurrentlyDown (KeyPress::pageUpKey)
           || KeyPress::isKeyCurrentlyDown (KeyPress::downKey)
           || KeyPress::isKeyCurrentlyDown (KeyPress::pageDownKey)
           || KeyPress::isKeyCurrentlyDown (KeyPress::homeKey)
           || KeyPress::isKeyCurrentlyDown (KeyPress::endKey)
           || KeyPress::isKeyCurrentlyDown (KeyPress::returnKey)))
    {
        Component::keyStateChanged();
    }
}

void ListBox::mouseWheelMove (const MouseEvent& e, float increment)
{
    getVerticalScrollBar()->mouseWheelMove (e, increment);
}

void ListBox::mouseMove (const MouseEvent& e)
{
    if (mouseMoveSelects)
    {
        const MouseEvent e2 (e.getEventRelativeTo (this));

        selectRow (getRowContainingPosition (e2.x, e2.y), true);

        lastMouseX = e2.x;
        lastMouseY = e2.y;
    }
}

void ListBox::mouseExit (const MouseEvent& e)
{
    mouseMove (e);
}

//==============================================================================
void ListBox::setRowHeight (const int newHeight)
{
    rowHeight = jmax (1, newHeight);
    viewport->setSingleStepSizes (20, rowHeight);
    updateContent();
}

int ListBox::getNumRowsOnScreen() const throw()
{
    return viewport->getMaximumVisibleHeight() / rowHeight;
}

void ListBox::setMinimumContentWidth (const int newMinimumWidth)
{
    minimumRowWidth = newMinimumWidth;
    updateContent();
}

ScrollBar* ListBox::getVerticalScrollBar() const
{
    return viewport->getVerticalScrollBar();
}

void ListBox::setBackgroundColour (const Colour& newBackgroundColour)
{
    if (backgroundColour != newBackgroundColour)
    {
        repaint();
        backgroundColour = newBackgroundColour;

        setOpaque (newBackgroundColour.isOpaque());
        viewport->setOpaque (isOpaque());
    }
}

void ListBox::setOutlineColour (const Colour& outlineColour_,
                                const int outlineThickness_)
{
    outlineColour = outlineColour_;
    outlineThickness = outlineThickness_;
    resized();
}

void ListBox::setHeaderComponent (Component* const newHeaderComponent)
{
    if (headerComponent != newHeaderComponent)
    {
        if (headerComponent != 0)
            delete headerComponent;

        headerComponent = newHeaderComponent;

        addAndMakeVisible (newHeaderComponent);
        resized();
    }
}

Image* ListBox::createSnapshotOfSelectedRows()
{
    Image* snapshot = new Image (Image::ARGB, getWidth(), getHeight(), true);
    Graphics g (*snapshot);

    const int firstRow = getRowContainingPosition (0, 0);

    for (int i = getNumRowsOnScreen() + 2; --i >= 0;)
    {
        Component* rowComp = getComponentForRowNumber (firstRow + i);

        if (rowComp != 0 && isRowSelected (firstRow + i))
        {
            Image* const im = rowComp->createComponentSnapshot (Rectangle (0, 0, getWidth(), rowComp->getHeight()));

            g.drawImageAt (im,
                           rowComp->getScreenX() - getScreenX(),
                           rowComp->getScreenY() - getScreenY());

            delete im;
        }
    }

    return snapshot;
}


//==============================================================================
void ListBox::selectedRowsChanged (int)
{
}

void ListBox::returnKeyPressed (int)
{
}

void ListBox::deleteKeyPressed (int)
{
}

void ListBox::backgroundClicked()
{
}

void ListBox::listWasScrolled()
{
}


END_JUCE_NAMESPACE
