/* 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
{


ActionId::ActionId()
{
}

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)
{
}

ActionId::ActionId(const ActionId &other)
  : context(other.context),
    category(other.category),
    unit(other.unit),
    label(other.label)
{
}

void
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;
}

void
ActionId::initialize(const QString &action_id_text)
{
  // This ensures that "%2F" are replaced with '/'.
  *this = fromString(action_id_text);
}

void
ActionId::initialize(const ActionId &other)
{
  context  = other.context;
  category = other.category;
  unit     = other.unit;
  label    = other.label;
}

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

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

ActionId
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]);
}

bool
ActionId::isValid() const
{
  return !(context.isEmpty() && category.isEmpty() && unit.isEmpty() &&
           label.isEmpty());
}

void
ActionId::operator=(const ActionId &other)
{
  this->initialize(other);
}

bool
ActionId::operator<(const ActionId &other) const
{
  return std::tie(context, category, unit, label) <
         std::tie(other.context, other.category, other.unit, other.label);
}

bool
ActionId::operator==(const ActionId &other) const
{
  return context == other.context && category == other.category &&
         unit == other.unit && label == other.label;
}

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

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

The QAction-based operations are associated to an \l ActionId that identifies
the action and relates it to a QKeySequence keyboard key combination. This class
is used to let the user define the shortcuts to actions.
*/

/*!
\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 according to \a action_id, \a text (the label) and \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 this program.

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 text is the description of the created QAction.
\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;
}

const QMap<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);
    }
}

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 member \c m_actions map's items are iterated into and for each map item,
the shortcut is loaded from the settings and set to the corresponding action.
*/
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 Checks if \a key_sequence is in conflic with any item in the member
map.

Iterates in all the items of the member \l m_actions map and checks
if any of the corresponding action has a shortcut identical to \a
key_sequence. Whenever a map item relates to \a exclude_action_id, that item
is skipped.
*/
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;
}

QList<std::pair<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;
}

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;
}

int
ActionManager::actionCount() const
{
  return m_actions.size();
}

} // namespace libXpertMassGui

} // namespace MsXpS
