/* 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
#include <random>


/////////////////////// Qt includes
#include <QDebug>
#include <QColor>
#include <QColorDialog>


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


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

namespace MsXpS
{

namespace libXpertMassGui
{

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

\brief The ColorSelector class provides a pool of colors that are associated to
a boolean value that is a flag indicating if the colors are in use or not.

Facilities allow for recycling colors when they are not used anymore.
*/

/*!
\typealias MsXpS::libXpertMassGui::ColorUsagePair

Synonym for std::pair<QColor, bool>.

The boolean value is a flag that tells if the color is currently in use.
*/

/*!
\variable MsXpS::libXpertMassGui::ColorSelector::m_usageOfColors

\brief Pool of available colors associated to their usage status.

The pool of colors is in the form of a static vector of \l ColorUsagePair
instances.
*/

// Fill-in the colors that can be used. Set them to unused to start with.
using color_usage_pair = std::pair<QColor, bool>;

std::vector<color_usage_pair> ColorSelector::m_usageOfColors = {
  ColorUsagePair(QColor().fromRgb(0xff0000), false),
  ColorUsagePair(QColor().fromRgb(0x0000ff), false),
  ColorUsagePair(QColor().fromRgb(0x2a7622), false),
  ColorUsagePair(QColor().fromRgb(0xff00ff), false),
  ColorUsagePair(QColor().fromRgb(0xffa400), false),
  ColorUsagePair(QColor().fromRgb(0x00ffd7), false),
  ColorUsagePair(QColor().fromRgb(0x7a00ff), false),
  ColorUsagePair(QColor().fromRgb(0xff009c), false),
  ColorUsagePair(QColor().fromRgb(0x0072ff), false),
  ColorUsagePair(QColor().fromRgb(0xf08080), false),
  ColorUsagePair(QColor().fromRgb(0x4682b4), false),
  ColorUsagePair(QColor().fromRgb(0x556b2f), false),
  ColorUsagePair(QColor().fromRgb(0x7dbee7), false)};

/*!
\brief Constructs a ColorSelector instance.
*/
ColorSelector::ColorSelector()
{
  // List all the available colors.

  // for(auto &&pair : ColorSelector::m_usageOfColors)
  // qDebug() << "Mapped color:" << pair.first.rgb();
}

/*!
\brief Destructs this ColorSelector instance.
*/
ColorSelector::~ColorSelector()
{
}

/*!
\brief Add \a color to the vector of colors.

If \a color is not already in the member vector of colors, it is added to it
with a usage flag set to false.
*/
void
ColorSelector::addColor(const QColor &color)
{

  if(!color.isValid())
    {
      // qDebug() << "The color to add is invalid.";
      return;
    }

  auto result = std::find_if(m_usageOfColors.begin(),
                             m_usageOfColors.end(),
                             [color](const ColorUsagePair &item) {
                               return item.first == color;
                             });

  if(result == m_usageOfColors.end())
    {
      m_usageOfColors.push_back(ColorUsagePair(color, false));
    }
  else
    {
      // qDebug() << "The color to be added is already present in the pool."
      // "Doing nothing.";
    }
}

/*!
\brief Returns a color.

If there is a color in the pool that is not used, it is returned and its use
flag is set to true.

If there is not a single color in the pool that is available for use, a new
color needs to be prepared. Either \a make_random is true and a random color is
created, put in the pool with the use flag set to true and returned, or the
user is provided with the standard color-choosing dialog.

\sa chooseColor()
*/
QColor
ColorSelector::getColor(bool make_random)
{
  // qDebug();

  // We are asked to provide a color. Either we have one available, or we need
  // to provide the user with the opportunity to choose a new one or to
  // construct a new one randomly.

  // Search if there is still a color in the pool that is *not* used.
  bool is_used = false;
  auto result  = std::find_if(m_usageOfColors.begin(),
                             m_usageOfColors.end(),
                             [is_used](const ColorUsagePair &item) {
                               return item.second == is_used;
                             });

  QColor color;

  if(result != m_usageOfColors.end())
    {
      // qDebug() << "The color from the map is fine. Set it to used.";

      // Something was returned. By necessity it is not used (find_if above).
      color = result->first;

      // qDebug() << "Was it used?" << isUsedColor(color);

      // Now set it as a used color.
      result->second = true;

      // qDebug() << "The returned color is:" << color.rgb();

      // qDebug() << "And now is it used?" << isUsedColor(color);

      return color;
    }
  else
    {
      // No color was available (pair value false), we need to let the user
      // choose one with the color chooser or make one random color.

      if(make_random)
        {
          while(1)
            {
              color = getRandomColor();

              // We need to ensure that the random color is not used already.

              if(!isUsedColor(color))
                {

                  // qDebug()
                  //<< "The color randomly generated is fine. Set it to used.";

                  // At this point we will return a new color that we set as
                  // used in the pool map.

                  m_usageOfColors.push_back(ColorUsagePair(color, true));

                  return color;
                }
              else
                {

                  // Otherwise keep creating random colors.
                  // qDebug() << "The color randomly generated was used already
                  // !";
                }
            }
        }

      // At this point we know the caller wants to provide the user the
      // opportunity to select a color from the QColorDialog window.
      return chooseColor();
    }

  return QColor();
}

/*!
\brief Returns a color.

The user is provided with the color-choosing dialog. The choosed color is
checked for its presence in the pool of colors.

If the color is found in the pool of colors and \a allow_used is true, the
color is returned. Else, the user is provided a new opportunity to select an
unused color. In that case, the new color is added to the pool of colors and
the usage flag is set to true.
*/
QColor
ColorSelector::chooseColor(bool allow_used)
{
  // The user wants to actually choose a color directly from the Qt color
  // selector dialog.

  QColor color;

  while(1)
    {
      color = QColorDialog::getColor(
        Qt::white /* initial color of the returned color */,
        nullptr,
        "Please select a color for the plot");

      if(!color.isValid())
        {
          // The user cancels the operation.

          // qDebug() << "The color selection was cancelled by the user.";

          return color;
        }

      // Now test if the color that was selected exists already in the
      // pool of colors.

      if(!isUsedColor(color))
        {
          // The color was not used, so we will return it but first insert
          // it in the map a a used color.

          // qDebug()
          //<< "The color selected by the user is not used, so use it!";

          m_usageOfColors.push_back(ColorUsagePair(color, true));
          return color;
        }
      else
        {
          // The color has been choosen already. Only return if we can choose an
          // already chosen color.
          if(allow_used)
            return color;
        }

      // We'll continue forever :-). Until the user either select an
      // unused color or abandons the process.

      // qDebug() << "The color selected by the user is already used or "
      //"is invalid, please choose another one.";
    }

  return color;
}

/*!
\brief Returns a new color randomly created.

The color is generated from random H,S,V triplets.
*/
QColor
ColorSelector::getRandomColor()
{
  // Generate random H,S,V triplets.
  // stackoverflow.com/questions/5008804/generating-random-integer-from-a-range

  std::random_device rd; // only used once to initialise (seed) engine

  std::mt19937 rng(
    rd()); // random-number engine used (Mersenne-Twister in this case)

  std::uniform_int_distribution<int> uni_h(0, 359); // guaranteed unbiased

  auto random_h = uni_h(rng);

  std::uniform_int_distribution<int> uni_s(125, 255);

  auto random_s = uni_s(rng);

  std::uniform_int_distribution<int> uni_v(125, 255);

  auto random_v = uni_s(rng);

  // Now create the color.

  return QColor::fromHsv(random_h, random_s, random_v);
}

/*!
\brief Returns a color matching.

\a color is searched for in the pool of colors. If it is found, it is returned
and the usage flag is set to true (it might have been true already). If \a
color is not found in the pool, then it is added to the pool, its usage flag is
set to true and that same color is returned.
*/
QColor
ColorSelector::getColor(const QColor &color)
{
  // We are asked to provide a specific color. The only unacceptable situation
  // would be that the color is in the pool and that it is already used.

  auto result = std::find_if(m_usageOfColors.begin(),
                             m_usageOfColors.end(),
                             [color](const ColorUsagePair &item) {
                               return item.first == color;
                             });

  if(result == m_usageOfColors.end())
    {
      // The color is not in the pool, easy, just add it in the used state.

      m_usageOfColors.push_back(ColorUsagePair(color, true));

      return color;
    }
  else
    {
      // The color is in the pool, check if it is used or not.

      if(!result->second)
        {
          // The color is not used. Set it to the used state.

          result->second = true;

          return color;
        }
    }

  // The color is already used, return an invalid color so the caller knows
  // it.

  return QColor();
}

/*!
\brief Releases \a color.

\a color is searched for in the pool of colors. If it is not found, \a color is
added to the pool with its usage flag set to false. If \a color is found, its
usage flag is set to false (it might have been false already).
*/
void
ColorSelector::releaseColor(const QColor &color)
{

  if(!color.isValid())
    return;

  // Confirm that the color was effectively used.

  auto result = std::find_if(m_usageOfColors.begin(),
                             m_usageOfColors.end(),
                             [color](const ColorUsagePair &item) {
                               return item.first == color;
                             });

  if(result == m_usageOfColors.end())
    {
      // qDebug() << "The color to release does not appear to be in the pool."
      //<< "Setting it to the pool in unused status.";

      m_usageOfColors.push_back(ColorUsagePair(color, false));

      return;
    }

  if(result->second == false)
    {
      // qDebug() << "The color to release does not appear to be in use.";

      return;
    }

  // The color was found and is used, just set it to unused status.

  result->second = false;
}

/*!
\brief Returns true if \a color is in the pool of colors and its usage flag is
true, otherwise returns false.
*/
bool
ColorSelector::isUsedColor(const QColor &color)
{

  auto result = std::find_if(m_usageOfColors.begin(),
                             m_usageOfColors.end(),
                             [color](const ColorUsagePair &item) {
                               return item.first == color;
                             });

  if(result != m_usageOfColors.end())
    {
      return result->second;
    }

  return false;
}


} // namespace libXpertMassGui

} // namespace MsXpS
