text: /*
  * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
  * its licensors.
  *
  * For complete copyright and license terms please see the LICENSE at the root of this
  * distribution (the "License"). All use of this software is governed by the License,
  * or, if provided, by the license below or the license accompanying this file. Do not
  * remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  *
  */
  
  #include <Editor/PropertyWidgets/MotionSetMotionIdHandler.h>
  #include <EMotionFX/Tools/EMotionStudio/EMStudioSDK/Source/MotionSetSelectionWindow.h>
  #include <Editor/AnimGraphEditorBus.h>
  #include <QHBoxLayout>
  #include <QMessageBox>
  #include <QLocale>
  #include <EMotionFX/Tools/EMotionStudio/EMStudioSDK/Source/EMStudioManager.h>
  
  
  namespace EMotionFX
  {
      float MotionSelectionIdWidgetController::s_displayedRoundingError = 0.0f;
  
      AZ_CLASS_ALLOCATOR_IMPL(MotionSetMotionIdPicker, EditorAllocator, 0)
      AZ_CLASS_ALLOCATOR_IMPL(MotionIdRandomSelectionWeightsHandler, EditorAllocator, 0)
      AZ_CLASS_ALLOCATOR_IMPL(MotionSetMultiMotionIdHandler, EditorAllocator, 0)
      AZ_CLASS_ALLOCATOR_IMPL(MotionSelectionIdWidgetController, EditorAllocator, 0)
  
      const float MotionSetMotionIdPicker::s_defaultWeight = 1.0f;
  
      void MotionSelectionIdWidgetController::ResetDisplayedRoundingError()
      {
          s_displayedRoundingError = 0.0f;
      }
  
      MotionSelectionIdWidgetController::MotionSelectionIdWidgetController(QGridLayout* layout,
          int graphicLayoutRowIndex, 
          const IRandomMotionSelectionDataContainer* dataContainer,
          bool displayMotionSelectionWeight)
          : m_dataContainer(dataContainer),
          m_displayMotionSelectionWeight(displayMotionSelectionWeight)
      {
          int column = 0;
          // Motion name
          m_labelMotion = new QLabel();
          m_labelMotion->setObjectName("m_labelMotion");
          m_labelMotion->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
          layout->addWidget(m_labelMotion, graphicLayoutRowIndex, column);
          column++;
          // Motion position x
          QHBoxLayout* layoutX = new QHBoxLayout();
          layoutX->setAlignment(Qt::AlignRight);
          layoutX->setSpacing(2);
          layoutX->setMargin(2);
          m_randomWeightSpinbox = new MysticQt::DoubleSpinBox();
          m_randomWeightSpinbox->setSingleStep(0.1);
          m_randomWeightSpinbox->setDecimals(1);
  
          m_randomWeightSpinbox->setRange(0, FLT_MAX);
  
          layoutX->addWidget(m_randomWeightSpinbox);
  
          layout->addLayout(layoutX, graphicLayoutRowIndex, column);
          column++;
          m_normalizedProbabilityText = new QLineEdit();
  
          // The read only text for the normalized probabilities does not need the space for the SpinBox buttons
          m_normalizedProbabilityText->setMaximumWidth(m_randomWeightSpinbox->maximumWidth() * 0.5);
          m_normalizedProbabilityText->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed);
          m_normalizedProbabilityText->setEnabled(false);
  
          layout->addWidget(m_normalizedProbabilityText, graphicLayoutRowIndex, column);
          column++;
  
          const int iconSize = 20;
  
          // Remove motion
          m_removeButton = new QPushButton();
          m_removeButton->setToolTip("Remove motion");
          m_removeButton->setMinimumSize(iconSize, iconSize);
          m_removeButton->setMaximumSize(iconSize, iconSize);
          m_removeButton->setIcon(QIcon(":/EMotionFX/Trash.png"));
          layout->addWidget(m_removeButton, graphicLayoutRowIndex, column);
  
          if (!m_displayMotionSelectionWeight)
          {
              m_randomWeightSpinbox->setVisible(false);
              m_normalizedProbabilityText->setVisible(false);
          }
      }
  
      void MotionSelectionIdWidgetController::Hide()
      {
          m_labelMotion->hide();
          if (m_displayMotionSelectionWeight)
          {
              m_randomWeightSpinbox->hide();
              m_normalizedProbabilityText->hide();
          }
          m_removeButton->hide();
      }
  
      void MotionSelectionIdWidgetController::Show()
      {
          m_labelMotion->show();
          if (m_displayMotionSelectionWeight)
          {
              m_randomWeightSpinbox->show();
              m_normalizedProbabilityText->show();
          }
          m_removeButton->show();
      }
  
      void MotionSelectionIdWidgetController::UpdateId(size_t id)
      {
          m_id = id;
      }
  
      void MotionSelectionIdWidgetController::DestroyGuis()
      {
          m_labelMotion->deleteLater();
          m_removeButton->deleteLater();
          m_randomWeightSpinbox->deleteLater();
          m_normalizedProbabilityText->deleteLater();
      }
  
      size_t MotionSelectionIdWidgetController::GetId() const
      {
          return m_id;
      }
  
      void MotionSelectionIdWidgetController::Update()
      {
          const double weight = m_dataContainer->GetWeight(m_id);
          m_randomWeightSpinbox->setValue(weight);
          const double actualPercentage = 100.0 * weight / m_dataContainer->GetWeightSum();
          const double compensatedValue = actualPercentage - s_displayedRoundingError;
          const double roundedValue = qRound(compensatedValue);
          s_displayedRoundingError = roundedValue - actualPercentage;
          QString str = m_normalizedProbabilityText->locale().toString(roundedValue, 'f', 1);
          m_normalizedProbabilityText->setText(str);
          m_labelMotion->setText(m_dataContainer->GetMotionId(m_id).c_str());
      }
  
      MotionSetMotionIdPicker::MotionSetMotionIdPicker(QWidget* parent, bool displaySelectionWeights)
          : QWidget(parent)
          , m_displaySelectionWeights(displaySelectionWeights)
      {
          QVBoxLayout* vLayout = new QVBoxLayout();
          vLayout->setMargin(0);
          setLayout(vLayout);
      }
  
      MotionSetMotionIdPicker::~MotionSetMotionIdPicker()
      {
          // Destroying the dynamically allocated controller of each row
          m_motionWidgetControllers.clear();
      }
  
      void MotionSetMotionIdPicker::SetMotionIds(const AZStd::vector<AZStd::string>& motionIds)
      {
          HandleSelectedMotionsUpdate(motionIds);
          InitializeWidgets();
          UpdateGui();
      }
  
      void MotionSetMotionIdPicker::SetMotions(const AZStd::vector<AZStd::pair<AZStd::string, float>>& motions)
      {
          // Display the weights (not the cumulative non normalized probability that is passed from the serialized data)
          float cumulativeWeight = 0.0f;
          m_motions.clear();
          m_motions.reserve(motions.size());
          for (unsigned int i = 0; i < motions.size(); ++i)
          {
              m_motions.emplace_back(motions[i].first, motions[i].second - cumulativeWeight);
              cumulativeWeight = motions[i].second;
          }
          m_weightsSum = cumulativeWeight;
          InitializeWidgets();
          UpdateGui();
      }
  
      const AZStd::vector<AZStd::pair<AZStd::string, float>>& MotionSetMotionIdPicker::GetMotions() const
      {
          return m_motions;
      }
  
      AZStd::vector<AZStd::string> MotionSetMotionIdPicker::GetMotionIds() const
      {
          AZStd::vector<AZStd::string> motionIds;
          motionIds.reserve(m_motions.size());
          for (const auto& motionPair : m_motions)
          {
              motionIds.emplace_back(motionPair.first);
          }
          return motionIds;
      }
  
      /// This method updates the motion random selection weights
      /// setting default weights for those motions which were not in the data
      /// and deletes the motions that have not been selected if they were in the data
      /// exsisting motions will keep their current weight as set by the user with the GUI
      void MotionSetMotionIdPicker::HandleSelectedMotionsUpdate(const AZStd::vector<AZStd::string>& motionIds)
      {
          AZStd::unordered_map<AZStd::string, float> tmpRandomWeightsTable;
  
          for (size_t i = 0; i < m_motions.size(); ++i)
          {
              tmpRandomWeightsTable.emplace(m_motions[i]);
          }
          m_weightsSum = 0;
          m_motions.clear();
          m_motions.reserve(motionIds.size());
  
          AZStd::unordered_map<AZStd::string, float>::iterator tmpWeightTableIterator;
          for (const AZStd::string& motionId : motionIds)
          {
              float weight = s_defaultWeight;
              tmpWeightTableIterator = tmpRandomWeightsTable.find(motionId);
              if (tmpWeightTableIterator != tmpRandomWeightsTable.end())
              {
                  weight = tmpWeightTableIterator->second;
              }
              m_weightsSum += weight;
              m_motions.emplace_back(motionId, weight);
          }
      }
  
      void MotionSetMotionIdPicker::OnPickClicked()
      {
          EMotionFX::MotionSet* motionSet = nullptr;
          AnimGraphEditorRequestBus::BroadcastResult(motionSet, &AnimGraphEditorRequests::GetSelectedMotionSet);
          if (!motionSet)
          {
              QMessageBox::warning(this, "No Motion Set", "Cannot open motion selection window. No valid motion set selected.");
              return;
          }
  
          // Create and show the motion picker window
          EMStudio::MotionSetSelectionWindow motionPickWindow(this);
          motionPickWindow.GetHierarchyWidget()->SetSelectionMode(false);
          motionPickWindow.Update(motionSet);
          motionPickWindow.setModal(true);
          AZStd::vector<AZStd::string> motionIds;
          motionIds.reserve(m_motions.size());
          for (const auto& motionIdRandomWeightPair : m_motions)
          { 
              motionIds.emplace_back(motionIdRandomWeightPair.first);
          }
          motionPickWindow.Select(motionIds, motionSet);
  
          if (motionPickWindow.exec() == QDialog::Rejected)   // we pressed cancel or the close cross
          {
              return;
          }
  
          HandleSelectedMotionsUpdate(motionPickWindow.GetHierarchyWidget()->GetSelectedMotionIds(motionSet));
  
          InitializeWidgets();
  
          UpdateGui();
  
          emit SelectionChanged();
      }
  
      void MotionSetMotionIdPicker::InitializeWidgets()
      {
          if (!m_containerWidget)
          {
              m_containerWidget = new QWidget();
              m_containerWidget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
              QVBoxLayout* widgetLayout = new QVBoxLayout();
              QHBoxLayout* topRowLayout = new QHBoxLayout();
  
              // Add helper label left of the add button.
              m_addMotionsLabel = new QLineEdit("");
              m_addMotionsLabel->setEnabled(false);
  
              topRowLayout->addWidget(m_addMotionsLabel);
  
              m_pickButton = new QPushButton(this);
              EMStudio::EMStudioManager::MakeTransparentButton(m_pickButton, "/Images/Icons/Plus.png", "Add motions to blend space");
              connect(m_pickButton, &QPushButton::clicked, this, &MotionSetMotionIdPicker::OnPickClicked);
              topRowLayout->addWidget(m_pickButton);
              m_pickButton->setToolTip(QString("Add motions"));
  
              widgetLayout->addLayout(topRowLayout);
  
              QWidget* motionsWidget = new QWidget(m_containerWidget);
  
              motionsWidget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
              QGridLayout* motionsLayout = new QGridLayout();
              motionsLayout->setMargin(0);
              QLabel* labelColumn0 = new QLabel("");
              motionsLayout->addWidget(labelColumn0, 0, 0);
              QLabel* labelColumn1 = new QLabel("Probability weight");
              motionsLayout->addWidget(labelColumn1, 0, 1);
              QLabel* labelColumn2 = new QLabel("Probability (100%)");
              motionsLayout->addWidget(labelColumn2, 0, 2);
              if (!m_displaySelectionWeights)
              {
                  labelColumn0->setVisible(false);
                  labelColumn1->setVisible(false);
                  labelColumn2->setVisible(false);
              }
              motionsWidget->setLayout(motionsLayout);
              widgetLayout->addWidget(motionsWidget);
  
              m_motionsLayout = motionsLayout;
  
              m_containerWidget->setLayout(widgetLayout);
              layout()->addWidget(m_containerWidget);
          }
          size_t layoutRowIndex = m_motionWidgetControllers.size();
          if (!m_displaySelectionWeights)
          {
              m_motionsLayout->setAlignment(Qt::AlignLeft);
          }
          else
          {
              // Making room for the grid header row
              layoutRowIndex++;
          }
          // Build more rows if needed
          if (m_motions.size() > m_motionWidgetControllers.size())
          {
              for (size_t widgetcontrollerCounter = m_motions.size() - m_motionWidgetControllers.size(); widgetcontrollerCounter > 0; --widgetcontrollerCounter)
              {
                  m_motionWidgetControllers.emplace_back(AZStd::make_unique<MotionSelectionIdWidgetController>(m_motionsLayout, static_cast<int>(layoutRowIndex++), this, m_displaySelectionWeights));
                  MotionSelectionIdWidgetController* motionWidget = m_motionWidgetControllers.back().get();
                  connect(motionWidget->m_randomWeightSpinbox, static_cast<void (MysticQt::DoubleSpinBox::*)(double)>(&MysticQt::DoubleSpinBox::valueChanged), this,
                      [this, motionWidget](double value)
                      {
                          OnRandomWeightChanged(motionWidget->GetId(), value);
                      }
                  );
                  connect(motionWidget->m_removeButton, &QPushButton::clicked, [this, motionWidget]()
                  {
                      OnRemoveMotion(motionWidget->GetId());
                  });
              }
          }
          // Bind the row to the data and hide those that are not needed
          size_t id = 0;
          for(auto widgetsControllerIterator = m_motionWidgetControllers.begin(); widgetsControllerIterator != m_motionWidgetControllers.end(); ++widgetsControllerIterator)
          {
              if (id < m_motions.size())
              {
                  (*widgetsControllerIterator)->UpdateId(id++);
                  (*widgetsControllerIterator)->Show();
              }
              else
              {
                  (*widgetsControllerIterator)->Hide();
              }
          }
      }
  
      void MotionSetMotionIdPicker::OnRandomWeightChanged(size_t id, double value)
      {
          m_weightsSum += value - m_motions[id].second;
          m_motions[id].second = value;
          UpdateGui();
          emit SelectionChanged();
      }
  
      float MotionSetMotionIdPicker::GetWeight(size_t id) const
      {
          return m_motions[id].second;
      }
  
      float MotionSetMotionIdPicker::GetWeightSum() const
      {
          return m_weightsSum;
      }
  
      const AZStd::string& MotionSetMotionIdPicker::GetMotionId(size_t id) const
      {
          return m_motions[id].first;
      }
  
      void MotionSetMotionIdPicker::UpdateGui()
      {
          MotionSelectionIdWidgetController::ResetDisplayedRoundingError();
          auto widgetsIterator = m_motionWidgetControllers.begin();
          size_t validGuisCount = 0;
          for(; validGuisCount < m_motions.size() && widgetsIterator != m_motionWidgetControllers.end(); ++widgetsIterator, ++validGuisCount)
          {
              (*widgetsIterator)->Update();
          }
  
          if (m_motions.size() > 0)
          {
              m_addMotionsLabel->setText(AZStd::string::format("%u motions selected", m_motions.size()).c_str());
          }
          else
          {
              m_addMotionsLabel->setText("Select motions");
          }
      }
  
      void MotionSetMotionIdPicker::OnRemoveMotion(size_t id)
      {
          m_weightsSum -= m_motions[id].second;
          m_motions.erase(m_motions.begin() + id);
          MotionSelectionIdWidgetController::ResetDisplayedRoundingError();
          InitializeWidgets();
  
          UpdateGui();
          emit SelectionChanged();
      }
  
      //---------------------------------------------------------------------------------------------------------------------------------------------------------
  
      AZ::u32 MotionIdRandomSelectionWeightsHandler::GetHandlerName() const
      {
          return AZ_CRC("MotionSetMotionIdsRandomSelectionWeights", 0xc882da3c);
      }
  
  
      QWidget* MotionIdRandomSelectionWeightsHandler::CreateGUI(QWidget* parent)
      {
          MotionSetMotionIdPicker* picker = aznew MotionSetMotionIdPicker(parent, true);
  
          connect(picker, &MotionSetMotionIdPicker::SelectionChanged, this, [picker]()
          {
              EBUS_EVENT(AzToolsFramework::PropertyEditorGUIMessages::Bus, RequestWrite, picker);
          });
  
          return picker;
      }
  
  
      void MotionIdRandomSelectionWeightsHandler::ConsumeAttribute(MotionSetMotionIdPicker* GUI, AZ::u32 attrib, AzToolsFramework::PropertyAttributeReader* attrValue, const char* debugName)
      {
          if (attrib == AZ::Edit::Attributes::ReadOnly)
          {
              bool value;
              if (attrValue->Read<bool>(value))
              {
                  GUI->setEnabled(!value);
              }
          }
      }
  
  
      void MotionIdRandomSelectionWeightsHandler::WriteGUIValuesIntoProperty(size_t index, MotionSetMotionIdPicker* GUI, property_t& instance, AzToolsFramework::InstanceDataNode* node)
      {
          // Please Note: the values stored in the serialized data that will be used to randomly select the motion to play
          // contain the cumulative non normalized probability
          // whilst the data in the GUIs contain the randomselection weights
          instance.clear();
          const auto& motions = GUI->GetMotions();
          instance.reserve(motions.size());
  
          // Store in the node's data the cumulative non normalized probability (not the weights)
          float cumulativeWeight = 0.0f;
          for (size_t i = 0; i < motions.size(); ++i)
          {
              cumulativeWeight += motions[i].second;
              instance.emplace_back(motions[i].first, cumulativeWeight);
          }
      }
  
  
      bool MotionIdRandomSelectionWeightsHandler::ReadValuesIntoGUI(size_t index, MotionSetMotionIdPicker* GUI, const property_t& instance, AzToolsFramework::InstanceDataNode* node)
      {
          QSignalBlocker signalBlocker(GUI);
          GUI->SetMotions(instance);
          return true;
      }
  
      //---------------------------------------------------------------------------------------------------------------------------------------------------------
  
      AZ::u32 MotionSetMultiMotionIdHandler::GetHandlerName() const
      {
          return AZ_CRC("MotionSetMotionIds", 0x8695c0fa);
      }
  
  
      QWidget* MotionSetMultiMotionIdHandler::CreateGUI(QWidget* parent)
      {
          MotionSetMotionIdPicker* picker = aznew MotionSetMotionIdPicker(parent, false);
  
          connect(picker, &MotionSetMotionIdPicker::SelectionChanged, this, [picker]()
          {
              EBUS_EVENT(AzToolsFramework::PropertyEditorGUIMessages::Bus, RequestWrite, picker);
          });
  
          return picker;
      }
  
  
      void MotionSetMultiMotionIdHandler::ConsumeAttribute(MotionSetMotionIdPicker* GUI, AZ::u32 attrib, AzToolsFramework::PropertyAttributeReader* attrValue, const char* debugName)
      {
          if (attrib == AZ::Edit::Attributes::ReadOnly)
          {
              bool value;
              if (attrValue->Read<bool>(value))
              {
                  GUI->setEnabled(!value);
              }
          }
      }
  
  
      void MotionSetMultiMotionIdHandler::WriteGUIValuesIntoProperty(size_t index, MotionSetMotionIdPicker* GUI, property_t& instance, AzToolsFramework::InstanceDataNode* node)
      {
          instance = GUI->GetMotionIds();
      }
  
  
      bool MotionSetMultiMotionIdHandler::ReadValuesIntoGUI(size_t index, MotionSetMotionIdPicker* GUI, const property_t& instance, AzToolsFramework::InstanceDataNode* node)
      {
          QSignalBlocker signalBlocker(GUI);
          GUI->SetMotionIds(instance);
          return true;
      }
  } // namespace EMotionFX
  
  #include <Source/Editor/PropertyWidgets/MotionSetMotionIdHandler.moc>