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

   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_KeyPressMappingSet.h"
#include "../../../../juce_core/basics/juce_Time.h"


//==============================================================================
KeyPressMappingSet::CommandInfo::CommandInfo()
{
}

KeyPressMappingSet::CommandInfo::CommandInfo (const KeyPressMappingSet::CommandInfo& other)
    : commandUID (other.commandUID),
      description (other.description),
      category (other.category),
      callbackFunction (other.callbackFunction),
      wantsKeyUpDownCallbacks (other.wantsKeyUpDownCallbacks),
      shouldBeVisibleInKeymapEditor (other.shouldBeVisibleInKeymapEditor),
      shouldCommandBeReadOnlyInEditor (other.shouldCommandBeReadOnlyInEditor)
{
    for (int i = 0; i < other.keyPresses.size(); ++i)
        keyPresses.add (new KeyPress (*other.keyPresses.getUnchecked(i)));
}

//==============================================================================
KeyPressMappingSet::KeyPressMappingSet()
{
}

KeyPressMappingSet::KeyPressMappingSet (const KeyPressMappingSet& other)
{
    for (int i = 0; i < other.commands.size(); ++i)
        commands.add (new CommandInfo (*other.commands.getUnchecked (i)));
}

KeyPressMappingSet::~KeyPressMappingSet()
{
}

void KeyPressMappingSet::addCommand (const int commandUID,
                                     const String& commandDescription,
                                     const String& categoryName,
                                     KeyPressCallbackFunction* callbackFunction,
                                     const bool wantsKeyUpDownCallbacks,
                                     const bool shouldBeVisibleInKeymapEditor,
                                     const bool shouldCommandBeReadOnlyInEditor)
{
    jassert (commandUID != 0);                  // can't have a zero UID
    jassert (commandDescription.isNotEmpty());  // commands should supply a proper description..

    if (commandUID != 0)
    {
        removeCommand (commandUID);

        CommandInfo* const c = new CommandInfo();

        c->commandUID = commandUID;
        c->description = commandDescription;
        c->category = categoryName;
        c->callbackFunction = callbackFunction;
        c->wantsKeyUpDownCallbacks = wantsKeyUpDownCallbacks;
        c->shouldBeVisibleInKeymapEditor = shouldBeVisibleInKeymapEditor;
        c->shouldCommandBeReadOnlyInEditor = shouldCommandBeReadOnlyInEditor;

        commands.add (c);
        sendChangeMessage (this);
    }
}

KeyPressMappingSet::CommandInfo* KeyPressMappingSet::findCommandForID (const int commandUID) const throw()
{
    for (int i = commands.size(); --i >= 0;)
        if (commands.getUnchecked(i)->commandUID == commandUID)
            return commands.getUnchecked(i);

    return 0;
}

void KeyPressMappingSet::removeCommand (const int commandUID)
{
    for (int i = commands.size(); --i >= 0;)
    {
        if (commands.getUnchecked(i)->commandUID == commandUID)
        {
            commands.remove (i);
            sendChangeMessage (this);
        }
    }
}

void KeyPressMappingSet::removeAllCommands()
{
    commands.clear();
    sendChangeMessage (this);
}

const String KeyPressMappingSet::getDescriptionOfCommand (const int commandUID) const throw()
{
    const CommandInfo* const ci = findCommandForID (commandUID);

    if (ci != 0)
        return ci->description;

    return String::empty;
}

bool KeyPressMappingSet::shouldCommandBeVisibleInEditor (const int commandUID) const
{
    const CommandInfo* const ci = findCommandForID (commandUID);

    return (ci != 0) && ci->shouldBeVisibleInKeymapEditor;
}

bool KeyPressMappingSet::shouldCommandBeReadOnlyInEditor (const int commandUID) const
{
    const CommandInfo* const ci = findCommandForID (commandUID);

    return (ci != 0) && ci->shouldCommandBeReadOnlyInEditor;
}

const StringArray KeyPressMappingSet::getCommandCategories() const throw()
{
    StringArray s;

    for (int i = 0; i < commands.size(); ++i)
        s.addIfNotAlreadyThere (commands.getUnchecked(i)->category, false);

    return s;
}

const Array <int> KeyPressMappingSet::getCommandsInCategory (const String& categoryName) const throw()
{
    Array <int> results (4);

    for (int i = 0; i < commands.size(); ++i)
        if (commands.getUnchecked(i)->category == categoryName)
            results.add (commands.getUnchecked(i)->commandUID);

    return results;
}

const Array <int> KeyPressMappingSet::getAllCommands() const throw()
{
    Array <int> results (4);

    for (int i = 0; i < commands.size(); ++i)
        results.add (commands.getUnchecked(i)->commandUID);

    return results;
}

int KeyPressMappingSet::getKeyPressesAssignedToCommand (const int commandUID,
                                                        OwnedArray<KeyPress>& keyPresses) const throw()
{
    int num = 0;

    for (int i = 0; i < commands.size(); ++i)
    {
        if (commands.getUnchecked(i)->commandUID == commandUID)
        {
            for (int j = 0; j < commands.getUnchecked(i)->keyPresses.size(); ++j)
            {
                keyPresses.add (new KeyPress (*(commands.getUnchecked(i)->keyPresses[j])));
                ++num;
            }

            break;
        }
    }

    return num;
}

void KeyPressMappingSet::addKeyPress (const int commandUID,
                                      const KeyPress& newKeyPress,
                                      int insertIndex)
{
    if (findCommandForKeyPress (newKeyPress) != commandUID)
    {
        removeKeyPress (newKeyPress);

        if (newKeyPress.isValid())
        {
            for (int i = commands.size(); --i >= 0;)
            {
                if (commands.getUnchecked(i)->commandUID == commandUID)
                {
                    commands.getUnchecked(i)->keyPresses.insert (insertIndex, new KeyPress (newKeyPress));

                    sendChangeMessage (this);
                    break;
                }
            }
        }
    }
}

void KeyPressMappingSet::clearAllKeyPresses()
{
    for (int i = commands.size(); --i >= 0;)
        commands.getUnchecked(i)->keyPresses.clear();

    sendChangeMessage (this);
}

void KeyPressMappingSet::clearAllKeyPresses (const int commandUID)
{
    CommandInfo* const ci = findCommandForID (commandUID);

    if (ci != 0)
    {
        ci->keyPresses.clear();
        sendChangeMessage (this);
    }
}

void KeyPressMappingSet::removeKeyPress (const KeyPress& keypress)
{
    if (keypress.isValid())
    {
        for (int i = commands.size(); --i >= 0;)
        {
            CommandInfo* const ci = commands.getUnchecked(i);

            for (int j = ci->keyPresses.size(); --j >= 0;)
            {
                if (keypress == *(ci->keyPresses[j]))
                {
                    ci->keyPresses.remove (j);
                    sendChangeMessage (this);
                }
            }
        }
    }
}

void KeyPressMappingSet::removeKeyPress (const int commandUID,
                                         const int keyPressIndex)
{
    for (int i = commands.size(); --i >= 0;)
    {
        if (commands.getUnchecked(i)->commandUID == commandUID)
        {
            commands.getUnchecked(i)->keyPresses.remove (keyPressIndex);
            sendChangeMessage (this);
            break;
        }
    }
}

//==============================================================================
const int magicNumber = 0x8ab4820f;

bool KeyPressMappingSet::invokeCommand (const int commandUID,
                                        const bool invokeLaterOnEventThread) const
{
    if (invokeLaterOnEventThread)
    {
        postMessage (new Message (magicNumber, commandUID, 0, 0), false);
        return true;
    }
    else
    {
        if (isSafeToInvokeCommand (commandUID))
        {
            const CommandInfo* const ci = findCommandForID (commandUID);

            if (ci != 0 && ci->callbackFunction != 0)
            {
                KeyPressCallbackFunctionInfo info;
                info.commandUID = commandUID;
                info.description = ci->description;
                info.componentWhereKeyWasPressed = 0;
                info.isKeyDown = true;
                info.millisecsSinceKeyPressed = 0;

                (*(ci->callbackFunction)) (info);

                if (ci->wantsKeyUpDownCallbacks)
                {
                    info.isKeyDown = false;
                    info.millisecsSinceKeyPressed = 5;

                    (*(ci->callbackFunction)) (info);
                }

                return true;
            }
        }

        return false;
    }
}

void KeyPressMappingSet::handleMessage (const Message& message)
{
    if (message.intParameter1 == magicNumber)
        invokeCommand (message.intParameter2, false);
}

bool KeyPressMappingSet::invokeCommandForKeyPress (const KeyPress& keyPress) const
{
    return invokeCommand (findCommandForKeyPress (keyPress));
}

int KeyPressMappingSet::findCommandForKeyPress (const KeyPress& keyPress) const
{
    const CommandInfo* const ci = findCommandForKeyPressInt (keyPress);

    if (ci != 0)
        return ci->commandUID;

    return 0;
}

bool KeyPressMappingSet::containsMapping (const int commandUID,
                                          const KeyPress& keyPress) const
{
    const CommandInfo* const ci = findCommandForKeyPressInt (keyPress);

    return ci != 0 && ci->commandUID == commandUID;
}

KeyPressMappingSet::CommandInfo* KeyPressMappingSet::findCommandForKeyPressInt (const KeyPress& keyPress) const throw()
{
    if (keyPress.isValid())
    {
        for (int i = commands.size(); --i >= 0;)
        {
            CommandInfo* const ci = commands.getUnchecked(i);

            for (int j = ci->keyPresses.size(); --j >= 0;)
                if (keyPress == *(ci->keyPresses[j]))
                    return ci;
        }
    }

    return 0;
}

//==============================================================================
bool KeyPressMappingSet::restoreFromMemento (const XmlElement& xmlVersion)
{
    if (xmlVersion.hasTagName (T("KEYMAPPINGS")))
    {
        const XmlElement* map = xmlVersion.getFirstChildElement();

        while (map != 0)
        {
            const int commandId = map->getStringAttribute (T("commandId")).getHexValue32();

            if (commandId != 0)
            {
                const KeyPress key (KeyPress::createFromDescription (map->getStringAttribute (T("key"))));

                if (map->hasTagName (T("MAPPING")))
                {
                    addKeyPress (commandId, key);
                }
                else if (map->hasTagName (T("UNMAPPING")))
                {
                    if (containsMapping (commandId, key))
                        removeKeyPress (key);
                }
            }

            map = map->getNextElement();
        }

        return true;
    }

    return false;
}

XmlElement* KeyPressMappingSet::createMemento (const KeyPressMappingSet* defaultSet) const
{
    XmlElement* const doc = new XmlElement (T("KEYMAPPINGS"));

    int i;
    for (i = 0; i < commands.size(); ++i)
    {
        const CommandInfo* const ci = commands.getUnchecked(i);

        for (int j = 0; j < ci->keyPresses.size(); ++j)
        {
            if (defaultSet == 0
                 || ! defaultSet->containsMapping (ci->commandUID, *ci->keyPresses[j]))
            {
                XmlElement* const map = new XmlElement (T("MAPPING"));

                map->setAttribute (T("commandId"), String::toHexString (ci->commandUID));
                map->setAttribute (T("description"), ci->description);
                map->setAttribute (T("key"), ci->keyPresses[j]->getTextDescription());

                doc->addChildElement (map);
            }
        }
    }

    if (defaultSet != 0)
    {
        for (i = 0; i < defaultSet->commands.size(); ++i)
        {
            const CommandInfo* const ci = defaultSet->commands.getUnchecked(i);

            for (int j = 0; j < ci->keyPresses.size(); ++j)
            {
                if (! containsMapping (ci->commandUID, *ci->keyPresses[j]))
                {
                    XmlElement* const map = new XmlElement (T("UNMAPPING"));

                    map->setAttribute (T("commandId"), String::toHexString (ci->commandUID));
                    map->setAttribute (T("description"), ci->description);
                    map->setAttribute (T("key"), ci->keyPresses[j]->getTextDescription());

                    doc->addChildElement (map);
                }
            }
        }
    }

    return doc;
}

//==============================================================================
void KeyPressMappingSet::keyPressed (const KeyPress& key,
                                     Component* originatingComponent)
{
    CommandInfo* const ci = findCommandForKeyPressInt (key);

    if (ci != 0
            && (! ci->wantsKeyUpDownCallbacks)
            && ci->callbackFunction != 0
            && isSafeToInvokeCommand (ci->commandUID))
    {
        KeyPressCallbackFunctionInfo info;
        info.commandUID = ci->commandUID;
        info.description = ci->description;
        info.componentWhereKeyWasPressed = originatingComponent;
        info.isKeyDown = true;
        info.millisecsSinceKeyPressed = 0;
        info.keyPress = key;

        (*(ci->callbackFunction)) (info);
    }
}

void KeyPressMappingSet::keyStateChanged (Component* originatingComponent)
{
    const uint32 now = Time::getMillisecondCounter();

    for (int i = commands.size(); --i >= 0;)
    {
        CommandInfo* const ci = commands.getUnchecked(i);

        if (ci->wantsKeyUpDownCallbacks
             && isSafeToInvokeCommand (ci->commandUID))
        {
            for (int j = ci->keyPresses.size(); --j >= 0;)
            {
                const KeyPress key (* ci->keyPresses[j]);
                const bool isDown = key.isCurrentlyDown();

                int keyPressEntryIndex = 0;
                bool wasDown = false;

                for (int k = keysDown.size(); --k >= 0;)
                {
                    if (key == keysDown.getUnchecked(k)->key)
                    {
                        keyPressEntryIndex = k;
                        wasDown = true;
                        break;
                    }
                }

                if (isDown != wasDown)
                {
                    KeyPressCallbackFunctionInfo info;
                    info.commandUID = ci->commandUID;
                    info.description = ci->description;
                    info.componentWhereKeyWasPressed = originatingComponent;
                    info.isKeyDown = isDown;
                    info.keyPress = key;

                    if (isDown)
                    {
                        info.millisecsSinceKeyPressed = 0;

                        KeyPressTime* const k = new KeyPressTime();
                        k->key = key;
                        k->timeWhenPressed = now;

                        keysDown.add (k);
                    }
                    else
                    {
                        const uint32 pressTime = keysDown.getUnchecked (keyPressEntryIndex)->timeWhenPressed;
                        info.millisecsSinceKeyPressed = (now > pressTime) ? now - pressTime : 0;

                        keysDown.remove (keyPressEntryIndex);
                    }

                    (*(ci->callbackFunction)) (info);
                }
            }
        }
    }
}

bool KeyPressMappingSet::isSafeToInvokeCommand (const int) const
{
    return true;
}

END_JUCE_NAMESPACE
