/* BEGIN software license
 *
 * MsXpertSuite - mass spectrometry software suite
 * -----------------------------------------------
 * Copyright 2009--2026 by Filippo Rusconi
 *
 * http://www.msxpertsuite.org
 *
 * This file is part of the MsXpertSuite project.
 *
 * The MsXpertSuite project is the successor of the massXpert project. This
 * project now includes various independent modules:
 *
 * - massXpert, model polymer chemistries and simulate mass spectrometric data;
 * - mineXpert, a powerful TIC chromatogram/mass spectrum viewer/miner;
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
 *
 * END software license
 */


/////////////////////// StdLib includes


/////////////////////// Qt includes
#include <QDebug>
#include <QAction>
#include <QKeySequence>
#include <QSettings>


/////////////////////// pappsomspp includes


/////////////////////// libXpertMassCore includes
#include "MsXpS/libXpertMassCore/Utils.hpp"

/////////////////////// Local includes
#include "MsXpS/libXpertMassGui/ActionManager.hpp"

namespace MsXpS
{

namespace libXpertMassGui
{


///////////////////////// ActionManager::ActionId ///////////////////////////
///////////////////////// ActionManager::ActionId ///////////////////////////
///////////////////////// ActionManager::ActionId ///////////////////////////

/*!
\struct MsXpS::libXpertMassGui::ActionManager::ActionId
\inmodule libXpertMassGui
\ingroup XpertMassGuiUtilities
\inheaderfile MsXpS/libXpertMassGui/ActionManager.hpp

\brief The ActionId struct encapsulates the details about a GUI action.

The ActionId that identifies the action is related to QAction instances
in the ActionManager's m_actions member datum.
*/

/*!
\variable \
MsXpS::libXpertMassGui::ActionManager::ActionId::context

This token describes the situation in which the action might be triggered. For
example, context might be "ProgramWindow" or "DecimalPlacesOptionsDlg".
*/

/*!
\variable \
MsXpS::libXpertMassGui::ActionManager::ActionId::category

This token describes the category of the action, like "File" or "Edit".
*/

/*!
\variable \
MsXpS::libXpertMassGui::ActionManager::ActionId::unit

This token describes the action itself, like "Open" or "Copy".
*/

/*!
\variable \
MsXpS::libXpertMassGui::ActionManager::ActionId::label

This string describes the action in full for the user, like "Open mass
spectrometry file full in memory".
*/

/*!
\brief Constructs an emtpy ActionId instance.
*/
ActionManager::ActionId::ActionId()
{
}

/*!
\brief Constructs a fully qualified ActionId instance.

\list

\li \a the_context: the situation in which the action might be triggered
\li \a the_category: the category of the action, like "File" or "Edit"
\li \a the_unit: the action itself, like "Open" or "Copy"
\li \a the_label: full description for the user, like "Open mass
spectrometry file full in memory"

\endlist
*/
ActionManager::ActionId::ActionId(const QString &the_context,
                                  const QString &the_category,
                                  const QString &the_unit,
                                  const QString &the_label)
  : context(the_context),
    category(the_category),
    unit(the_unit),
    label(the_label)
{
}

/*!
\brief Constructs an ActionId instance using \a other as a template.
*/
ActionManager::ActionId::ActionId(const ActionId &other)
  : context(other.context),
    category(other.category),
    unit(other.unit),
    label(other.label)
{
}

/*!
\brief Initializes this ActionId instance.

\list

\li \a the_context: the situation in which the action might be triggered
\li \a the_category: the category of the action, like "File" or "Edit"
\li \a the_unit: the action itself, like "Open" or "Copy"
\li \a the_label: full description for the user, like "Open mass
spectrometry file full in memory"

\endlist
*/
void
ActionManager::ActionId::initialize(const QString &the_context,
                                    const QString &the_category,
                                    const QString &the_unit,
                                    const QString &the_label)
{
  context  = the_context;
  category = the_category;
  unit     = the_unit;
  label    = the_label;
}

/*!
\brief Initializes this ActionId using \a action_id_text as a template.
*/
void
ActionManager::ActionId::initialize(const QString &action_id_text)
{
  // This ensures that "%2F" are replaced with '/'.
  *this = fromString(action_id_text);
}

/*!
\brief Initializes this ActionId using \a other as a template.
*/
void
ActionManager::ActionId::initialize(const ActionId &other)
{
  context  = other.context;
  category = other.category;
  unit     = other.unit;
  label    = other.label;
}

/*!
\brief Crafts a string describing this ActionId instance and returns it.

The string is created as a '|'-concatenated string like this:

\code
context + "|" + category + "|" + unit + "|" + label
\endcode

*/
QString
ActionManager::ActionId::toString() const
{
  return context + "|" + category + "|" + unit + "|" + label;
}

/*!
\brief Crafts a QStringList describing this ActionId instance and returns it.

The string list contains all the members in the followng order:

\code
context, category, unit, label
\endcode

 */
QStringList
ActionManager::ActionId::toStringList() const
{
  return toString().split("|");
}

/*!
\brief Creates an ActionId instance by parsing \a text and returns it.

\a text should have the strings formatted like this:

\code
context + "|" + category + "|" + unit + "|" + label
\endcode
*/
ActionManager::ActionId
ActionManager::ActionId::fromString(const QString &text)
{
  QStringList string_list = text.split('|');
  // qDebug() << "The string_list has size:" << string_list.size();
  Q_ASSERT(string_list.size() == 4);

  return ActionId(
    string_list[0], string_list[1], string_list[2], string_list[3]);
}

/*!
\brief Returns true if all the members are non-empty, false otherwise.
*/
bool
ActionManager::ActionId::isValid() const
{
  return !(context.isEmpty() && category.isEmpty() && unit.isEmpty() &&
           label.isEmpty());
}

/*!
\brief Assigns \a other to this ActionId instance.
*/
void
ActionManager::ActionId::operator=(const ActionId &other)
{
  this->initialize(other);
}

/*!
\brief Returns true if any one of the members of the ActionId instances are
different.

The four members are compared pairwise in \a other and in this instance and
as soon as one pair differs, true is returned if this instance's member is
lexicographically smaller than the other's instance member. If the tested member
of the two instances are identical, the other members are tested in the same
manner. If, at the end the two instances were found identical, false is
returned because this instance is thus not less than other.
*/
bool
ActionManager::ActionId::operator<(const ActionId &other) const
{
  return std::tie(context, category, unit, label) <
         std::tie(other.context, other.category, other.unit, other.label);
}

/*!
\brief Returns true if all the members of this instance and \a other are
identical, false otherwise.
*/
bool
ActionManager::ActionId::operator==(const ActionId &other) const
{
  return context == other.context && category == other.category &&
         unit == other.unit && label == other.label;
}

///////////////////////// ActionManager ///////////////////////////
///////////////////////// ActionManager ///////////////////////////
///////////////////////// ActionManager ///////////////////////////

/*!
\class MsXpS::libXpertMassGui::ActionManager
\inmodule libXpertMassGui
\ingroup ActionManager
\inheaderfile MsXpS/libXpertMassGui/ActionManager.hpp

\brief The ActionManager class provides a management system for relating QAction
and QKeySequence.

The QAction-based operations are associated to \l ActionId instances that
identify the action and relate it to a QKeySequence keyboard key combination.
This class is used to let the user define the shortcuts to actions.

The member map is protected:

\code
QMap<ActionId, QAction *> m_actions
\endcode
*/

/*!
\brief Constructs an ActionManager using \a parent for parentship.
*/
ActionManager::ActionManager(QObject *parent): QObject{parent}
{
}

/*!
\brief Destructs the ActionManager.
*/
ActionManager::~ActionManager()
{
  qDeleteAll(m_actions);
  m_actions.clear();
}

/*!
\brief Installs an action described in \a action_id, that will be bound to \a
key_sequence.

Installing an action encompasses multiple steps. The general idea is that this
function is called in code that is setting up a QAction and that QAction might
benefit from a shortcut (a QKeySequence) as defined by the user and stored in
the settings of the program that uses this library.

The first step is thus to check if \a action_id is found in the member map of
<ActionId, QAction *> pairs. If found, then the corresponding QAction pointer is
returned (with no modification: if its shortcut is empty, that shortcut was
meant by the user to stay empty).

If no \a action_id is found in the member map, then a new QAction is allocated
and the \a key_sequence is set to it as shortcut.

\a action_id is mapped to the QAction that is returned.

Returns the QAction.
*/
QAction *
ActionManager::installAction(const ActionId &action_id,
                             const QKeySequence &key_sequence)
{
  // We get an ActionId, which comprises in particular the label, that is,
  // the text to be associated to the action ("Open mass spectrometry file", for
  // example). The action_id.label member does never contain the text form
  // of the shortcut. This is added during processing.

  // Installing an action may either:

  // Case 1. Allocate a new QAction:
  // No item is found in the member // m_actions container that matches
  // action_id. We thus need to allocate a // brand new QAction.

  // Case 2. Return a pointer to an already allocated QAction (a single QAction
  // might be used many times by adding it to different widgets):
  // The member m_actions container contains an item that matches action_id. All
  // we have to do is return the corresponding QAction.

  // qDebug().noquote() << "Installing action: " << action_id.toString()
  //                    << " with shortcut sequence: " << key_sequence;

  // The key_sequence argument contains the QKeySequence to be used as the
  // shortcut for the action. If that key_sequence is already used in any
  // QAction mapped in m_actions (of course excluding the action_id item, which
  // might exist already), then we do not use it in crafting the new QAction.

  QList<std::pair<ActionId, QAction *>> conflicting_items =
    conflictingItems(key_sequence, action_id);

  if(conflicting_items.size())
    {
      // At least one m_actions item having a QAction for which the shortcut
      // was found to be identical to key_sequence has been found.

      // We thus won't use the key_sequence for crafting the QAction's shortcut
      // if we need to allocate a new one.
      QString text;
      foreach(auto pair, conflicting_items)
        text += QString("%1\n").arg(pair.first.toString());

      // qDebug().noquote() << "The conflicting items for shortcut: "
      //                    << key_sequence << " are: " << text;
    }
  else
    {
      // qDebug() << "No conflicting items for key sequence"
      //          << key_sequence.toString();
    }

  // Check if there is a m_actions item that matches action_id.
  QAction *action_p = getAction(action_id);

  if(action_p != nullptr)
    {
#if 0
      // Do not do this: because that means that after having loaded the actions
      // from the QSettings, right at application time, when the program itself
      // installs actions with hard-coded shortcut sequences, it would overwrite
      // those configured by the user and stored in the QSettings !

      // There was a m_actions container item by the same action_id. Good.

      // If there was no conflict with key_sequence, that means that
      // there was not a single item in the container that had the
      // same shortcut as key_sequence (apart from an item identical to
      // action_id that might have preexisted).

      // In this case, we can reset the shortcut of the returned action_p
      // and set it to key_sequence. This update logic means that the last
      // installed action's shortcut is used and allows for changing it somehow.

      qDebug() << "There was a matching action:" << action_p->text()
               << action_p->shortcut();

      if(!conflicting_items.size())
        {
          QKeySequence returned_action_shortcut_key_sequence =
            action_p->shortcut();

          if(returned_action_shortcut_key_sequence != key_sequence)
            {
              // The best way to actually reset shortcut is to provide a list
              // with only the shortcut we want and call setShortcuts__s__.
              action_p->setShortcuts({key_sequence});

              QString label_with_shortcut = QString("%1 (%2)").arg(
                action_id.label, key_sequence.toString());

              action_p->setText(label_with_shortcut);
              action_p->setToolTip(label_with_shortcut);

              qDebug() << "Updated shortcut (and label) for the action from "
                       << returned_action_shortcut_key_sequence << "to"
                       << key_sequence << "new label: " << label_with_shortcut;
            }
        }
#endif

      // qDebug() << "Returning action found:" << action_p->text()
      //          << action_p->shortcut();

      return action_p;
    }

  // Since no QAction was found by action_id, we allocate a new one
  // and to it a shortcut corresponding to key_sequence.

  // We do not parent the action because Application::~Application will destroy
  // them. Also, set the label to the action, with the shortcut text associated
  // to it.
  QString label_with_shortcut =
    QString("%1 (%2)").arg(action_id.label, key_sequence.toString());
  action_p = new QAction(label_with_shortcut,
                         /*parent*/ nullptr);
  action_p->setShortcut(key_sequence);
  action_p->setToolTip(label_with_shortcut);

  // Now that we have the new QAction, set it to the container.
  m_actions[action_id] = action_p;

  // qDebug() << "Installed new action:" << action_id.toString() << "-->"
  //          << action_p->text() << "shortcut:" << action_p->shortcut();

  return action_p;
}

/*!
\brief Returns a const reference to the member map.

The map relates ActionId instances to their associated QAction instances (as
pointers).
*/
const QMap<ActionManager::ActionId, QAction *> &
ActionManager::getActionMap() const
{
  return m_actions;
}

/*!
\brief Returns the action that is mapped to \a action_id.
*/
QAction *
ActionManager::getAction(const ActionId &action_id) const
{
  // We cannot use the action_id object itself for the search
  // because we reuse the same ActionId object many times
  // simply by action_id.initialize(). We need to deep-check
  // the action_id contents.

  for(auto it = m_actions.begin(); it != m_actions.end(); ++it)
    {
      if(it.key() == action_id)
        return it.value();
    }

  return nullptr;
}

/*!
\brief Sets \a key_sequence as the shortcut for \a action_id.

A QAction must have been created already and must be found
as an item mapped to \a action_id.
*/
void
ActionManager::setShortcut(const ActionId &action_id,
                           const QKeySequence &key_sequence)
{
  QAction *action_p = getAction(action_id);

  if(action_p != nullptr)
    {
      // qDebug() << "For action:" << action_id.toString()
      //          << "setting shortcut:" << key_sequence;
      action_p->setShortcut(key_sequence);
    }
}

/*!
\brief Return the shortcut associated to \a action_id.

If no action can be found matching \a action_id, then an empty
QKeySequence instance is returned.
*/
QKeySequence
ActionManager::getShortcut(const ActionId &action_id)
{
  QAction *action_p = m_actions.value(action_id);

  if(action_p == nullptr)
    return QKeySequence();

  return action_p->shortcut();
}

/*!
\brief Saves the action data to the QSettings-based settings.

The action data are written to in the file referenced by static datum
\l libXpertMassCore::Utils::configSettingsFilePath in QSettings::IniFormat
format.

The member \c m_actions map's items are iterated into and for each map item,
the corresponding \l ActionId and QAction shortcut are written to the
settings.

*/
void
ActionManager::saveActionData() const
{
  // qDebug() << "The config settings file path:"
  //          << libXpertMassCore::Utils::configSettingsFilePath;

  QSettings settings(libXpertMassCore::Utils::configSettingsFilePath,
                     QSettings::IniFormat);

  settings.remove("ActionData");

  settings.beginGroup("ActionData");

  // qDebug() << "The map has " << m_actions.size() << "items, with keys:";

#if 0
  QList<ActionId> key_list = m_actions.keys();
  // qDebug() << "There are this count of keys in list:" << key_list.size();

  // foreach(auto action_id, key_list)
  for(int iter = 0; iter < key_list.size(); ++iter)
    {
      qDebug() << "At iter:" << iter
               << "one key:" << key_list.at(iter).toString();
    }

  QList<QAction *> value_list = m_actions.values();
  qDebug() << "There are this count of values in list:" << value_list.size();

  // foreach(auto action_p, value_list)
  for(int iter = 0; iter < value_list.size(); ++iter)
    {
      qDebug() << "At iter:" << iter
               << "one value:" << value_list.at(iter)->shortcut();
    }
#endif

  // Remember m_actions is:
  // QMap<ActionId, QAction *> m_actions;

  // The settings key is going to be created as a string in the following
  // format:
  // <context>|<category>|<unit>|<label>
  //
  // Any of these elements (label in particular) might contain '/' which
  // are special characters in QSettings lingo. We thus need to encode
  // the string above and store it as the encoded string.

  for(auto it = m_actions.begin(); it != m_actions.end(); ++it)
    {
      QString decoded_settings_key = it.key().toString();

      // qDebug() << "Iterating in key:" << decoded_settings_key
      //          << "with value:" << it.value()->shortcut().toString();

      QString encoded_settings_key = QString::fromLatin1(
        decoded_settings_key.toUtf8().toBase64(QByteArray::Base64UrlEncoding));

      settings.setValue(encoded_settings_key,
                        it.value()->shortcut().toString());
    }

  settings.endGroup();
}

/*!
\brief Loads the action data from the QSettings-based settings.

The action data are read from the file referenced by static datum
\l libXpertMassCore::Utils::configSettingsFilePath in QSettings::IniFormat
format.

The action data are loaded from the configuration settings file in the form of
a string list. The strings are parsed and the member map's
is filled by calling \l installAction().
*/
void
ActionManager::loadActionData()
{
  // qDebug() << "The config settings file path:"
  //          << libXpertMassCore::Utils::configSettingsFilePath;

  QSettings settings(libXpertMassCore::Utils::configSettingsFilePath,
                     QSettings::IniFormat);

  settings.beginGroup("ActionData");

  // We want to iterate in all the settings for this group.
  // Remember (see saveActionData()), the settings keys are encoded.

  ActionId action_id;
  QStringList settings_keys = settings.childKeys();

  for(QString &encoded_settings_key : settings_keys)
    {
      QString decoded_settings_key = QString::fromUtf8(QByteArray::fromBase64(
        encoded_settings_key.toLatin1(), QByteArray::Base64UrlEncoding));

      // qDebug() << "Initializing ActionId with settings key:"
      //          << decoded_settings_key;

      action_id.initialize(decoded_settings_key);

      // Of course, to retrieve the value, we need the key as it is in the
      // QSettings file, that is, in the encoded format.
      QVariant value_variant = settings.value(encoded_settings_key);
      QString value_string   = value_variant.toString();
      QKeySequence key_sequence(value_string);

      // qDebug().noquote() << "Now installing loaded key: "
      //                    << action_id.toString()
      //                    << "with shortcut: " << key_sequence;

      installAction(action_id, key_sequence);
    }

  settings.endGroup();

  // qDebug() << "At this point all the loaded action data:" << toString();
}

/*!
\brief Returns true if \a key_sequence is in conflict with any item in the
member map.

Iterates in all the items of the member map and checks
if any of the corresponding action has a shortcut identical to \a
key_sequence. Whenever a map item relates has a key identical to \a
exclude_action_id, that item is skipped while \a was_found_p is set to true (if
non-nullptr).

If one map item maps an action with the same QKeySequence as \a key_sequence,
true is returned immediately.

If \a key_sequence is empty, false is returned as it is considered to never
conflict.

If no map item maps an action  with the same QKeySequence as \a key_sequence,
false is returned.
*/
bool
ActionManager::isInConflict(const QKeySequence &key_sequence,
                            const ActionId &exclude_action_id,
                            bool *was_found_p) const
{
  // Attention, that took me some time to understand:
  // action_p->setShortcut(QKeySequence());
  // actually sets a non empty shortcut !!!
  // This is why we need to actually ensure that the text
  // representation is empty.
  // One way to remove the shortcuts from an action is:
  // action_p->setShortcuts({});
  // which is the same as
  // action_p->setShortcuts(QList<QKeySequence>())

  // An empty key_sequence never conflicts
  if(key_sequence.toString(QKeySequence::PortableText).isEmpty())
    return false;

  if(was_found_p != nullptr)
    *was_found_p = false;

  for(auto it = m_actions.begin(); it != m_actions.end(); ++it)
    {
      // We may provide the exclude_action_id to exclude it
      // from the tests because that is the action id
      // we test before installing it in an updated form.
      if(it.key() == exclude_action_id)
        {
          if(was_found_p != nullptr)
            *was_found_p = true;
          continue;
        }

      // At this point, if the action has shortcut == to key_sequence,
      // then there is a conflict.
      if(it.value()->shortcut() == key_sequence)
        return true;
    }
  return false;
}

/*!
\brief Returns a list of conflicting items.

A member map item is conflicting with \a key_sequence if it maps a QAction
with that same shortcut sequence.

The map is searched for items that map a QAction having \a key_sequence
as the QKeySequence shortcut. If any one of the map items map an ActionId
identical to \a exclude_action_id, that map item is skipped because
this is the way to ensure that a new item is updated in the map.

\sa isInConflict()
*/
QList<std::pair<ActionManager::ActionId, QAction *>>
ActionManager::conflictingItems(const QKeySequence &key_sequence,
                                const ActionId &exclude_action_id) const
{
  QList<std::pair<ActionId, QAction *>> conflicting_items;

  // Attention, that took me some time to understand:
  // action_p->setShortcut(QKeySequence());
  // actually sets a non empty shortcut !!!
  // This is why we need to actually ensure that the text
  // representation is empty.
  // One way to remove the shortcuts from an action is:
  // action_p->setShortcuts({});
  // which is the same as
  // action_p->setShortcuts(QList<QKeySequence>())

  bool is_key_sequence_empty =
    key_sequence.toString(QKeySequence::PortableText).isEmpty();
  // We never consider that an empty sequence is conflicting, as most
  // actions have no QKeySequence shortcut set for them.

  if(!is_key_sequence_empty)
    {
      // qDebug() << "Searching for conflicting items for key sequence:"
      //          << key_sequence.toString();

      for(auto it = m_actions.begin(); it != m_actions.end(); ++it)
        {
          // We may provide the exclude_action_id to exclude it
          // from the tests because that is the action id
          // we test before installing it in an updated form (or not).
          // Remember, we load all the actions from settings, which means
          // installing them. The, when the program is run a bunch of actions
          // are installed again that might be identical to those already
          // loaded.

          if(it.key() == exclude_action_id)
            {
              continue;
            }

          // At this point, if the action has shortcut == to key_sequence,
          // then there is a conflict.
          if(it.value()->shortcut() == key_sequence)
            {
              conflicting_items.append({it.key(), it.value()});

              // qDebug() << "Appended conflicting item:" << it.key().toString()
              //          << "for key sequence:" << key_sequence;
            }
        }
    }

  return conflicting_items;
}

/*!
\brief Crafts a string representing this instance and returns it.
*/
QString
ActionManager::toString()
{
  QString text;

  QMap<ActionId, QAction *>::iterator the_iterator = m_actions.begin();
  while(the_iterator != m_actions.end())
    {
      text += the_iterator.key().toString() + "-" +
              the_iterator.value()->shortcut().toString();

      text += "\n";

      ++the_iterator;
    }

  return text;
}

/*!
\brief Return the number of items in the member map that relates ActionId to
QACtion instances.
*/
int
ActionManager::actionCount() const
{
  return m_actions.size();
}

} // namespace libXpertMassGui

} // namespace MsXpS
