//=========================================================================
//  Copyright (c) Kitware, Inc.
//  All rights reserved.
//  See LICENSE.txt for details.
//
//  This software is distributed WITHOUT ANY WARRANTY; without even
//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
//  PURPOSE.  See the above copyright notice for more information.
//=========================================================================
#include "smtk/view/AvailableOperations.h"

#include "smtk/view/Selection.h"

#include "smtk/workflow/OperationFilterSort.h"

#include "smtk/operation/MetadataContainer.h"
#include "smtk/operation/Operation.h"

#include "smtk/attribute/ReferenceItemDefinition.h"

using namespace smtk::view;

AvailableOperations::AvailableOperations()
  : m_selectionMask(1)
  , m_selectionExact(true)
  , m_useSelection(true)
  , m_workflowFilter(nullptr)
  , m_workflowFilterObserverId(-1)
{
// For debugging:
#ifndef NDEBUG
  this->observe(
    [](AvailableOperations::Ptr self) {
      std::cout << "Available ops changed:\n";
      auto& mdByIdx = self->operationManager()->metadata().get<smtk::operation::IndexTag>();
      auto& avail = self->availableOperations();
      for (auto idx : avail)
      {
        auto mit = mdByIdx.find(idx);
        if (mit != mdByIdx.end())
        {
          std::cout << "  " << mit->typeName() << "\n";
        }
      }
    },
    false // do not immediately invoke.
    );
#endif
}

AvailableOperations::~AvailableOperations()
{
  if (m_operationManager)
  {
    m_operationManager->observers().erase(m_operationManagerObserverId);
  }
  if (m_selection)
  {
    m_selection->unobserve(m_selectionObserverId);
  }
}

void AvailableOperations::setSelection(SelectionPtr seln)
{
  if (seln == m_selection)
  {
    return;
  }
  if (m_selection)
  {
    m_selection->unobserve(m_selectionObserverId);
  }
  m_selection = seln;
  if (m_selection)
  {
    m_selectionObserverId = m_selection->observe(
      [this](const std::string& src, smtk::view::Selection::Ptr seln) {
        this->selectionModified(src, seln);
      },
      true // immediately notify
      );
  }
}

void AvailableOperations::setUseSelection(bool useSeln)
{
  if (m_useSelection == useSeln)
  {
    return;
  }

  m_useSelection = useSeln;

  // We only need to recompute when we have a selection.
  // Otherwise, m_useSelection has no effect.
  if (m_selection)
  {
    this->computeFromSelection();
  }
}

void AvailableOperations::setOperationManager(smtk::operation::ManagerPtr mgr)
{
  if (mgr == m_operationManager)
  {
    return;
  }

  if (m_operationManager)
  {
    m_operationManager->metadataObservers().erase(m_operationManagerObserverId);
  }
  m_operationManager = mgr;
  if (m_operationManager)
  {
    m_operationManagerObserverId = m_operationManager->metadataObservers().insert(
      [this](const smtk::operation::Metadata& operMeta, bool adding) {
        this->operationMetadataChanged(operMeta, adding);
      });
  }
  else
  {
    m_operationManagerObserverId = -1;
  }
}

void AvailableOperations::setWorkflowFilter(OperationFilterSort wf)
{
  if (wf == m_workflowFilter)
  {
    return;
  }

  if (m_workflowFilter)
  {
    m_workflowFilter->unobserve(m_workflowFilterObserverId);
  }
  m_workflowFilter = wf;
  if (m_workflowFilter)
  {
    m_workflowFilterObserverId =
      m_workflowFilter->observe([this]() { this->workflowFilterModified(); });
  }
  else
  {
    m_workflowFilterObserverId = -1;
  }
}

int AvailableOperations::observe(Observer fn, bool immediatelyInvoke)
{
  int handle;
  if (!fn)
  {
    handle = -1;
    return handle;
  }

  if (m_observers.empty())
  {
    handle = 0;
  }
  else
  {
    handle = m_observers.rbegin()->first + 1;
  }
  m_observers[handle] = fn;
  if (immediatelyInvoke)
  {
    fn(shared_from_this());
  }
  return handle;
}

bool AvailableOperations::unobserve(int observerId)
{
  auto it = m_observers.find(observerId);
  if (it != m_observers.end())
  {
    m_observers.erase(it);
    return true;
  }
  return false;
}

void AvailableOperations::operationMetadataChanged(
  const smtk::operation::Metadata& operMeta, bool adding)
{
  (void)operMeta;
  (void)adding;
  // std::cout << "Operation \"" << operMeta.typeName() << "\" idx " << operMeta.index() << " add " << (adding ? "Y" : "N") <<  "\n";
  this->computeFromSelection();
}

void AvailableOperations::selectionModified(const std::string& src, SelectionPtr seln)
{
  (void)src;  // We are never the source, so we don't need to terminate early to avoid recursion.
  (void)seln; // We already have the selection, so ignore this, too.
  this->computeFromSelection();
}

void AvailableOperations::workflowFilterModified()
{
  this->computeFromWorkingSet();
}

const AvailableOperations::Data* AvailableOperations::operationData(const Index& opIdx) const
{
  if (!m_workflowFilter)
  {
    return nullptr;
  }

  const auto& filterList = m_workflowFilter->filterList();
  auto it = filterList.find(opIdx);
  if (it == filterList.end())
  {
    return nullptr;
  }
  return &it->second;
}

void AvailableOperations::workingSet(smtk::operation::ManagerPtr operationsIn,
  smtk::view::SelectionPtr selectionIn, int selectionMaskIn, bool exactSelectionIn,
  OperationIndexSet& workingSetOut)
{
  workingSetOut.clear();
  if (selectionIn)
  {
    std::map<smtk::operation::Operation::Index, int> counts;
    const auto& selnMap = selectionIn->currentSelection();
    // Narrow the selection map down to the actual selected
    // set based on how the application wants us to use the selection:
    std::set<smtk::resource::PersistentObjectPtr> actual;
    for (auto entry : selnMap)
    {
      if ((exactSelectionIn && ((entry.second & selectionMaskIn) == selectionMaskIn)) ||
        (!exactSelectionIn && (entry.second & selectionMaskIn)))
      {
        actual.insert(entry.first);
      }
    }

    for (auto& md : operationsIn->metadata())
    {
      auto primaryAssociation = md.primaryAssociation();
      std::size_t numRequired =
        primaryAssociation ? primaryAssociation->numberOfRequiredValues() : 0;
      std::size_t maxAllowed = primaryAssociation ? primaryAssociation->maxNumberOfValues() : 0;
      std::size_t numSel = static_cast<std::size_t>(actual.size());
      bool extensible = primaryAssociation ? primaryAssociation->isExtensible() : false;
      bool optional = primaryAssociation ? primaryAssociation->isOptional() : false;

      // Maybe we can terminate early:
      if (numSel == 0 && (!primaryAssociation || optional || numRequired == 0))
      {
        // The actual selection is empty and the operation does not want
        // or need associations, mark it as available.
        workingSetOut.insert(md.index());
        continue;
      }
      else if (numSel == 0)
      {
        // The actual selection is empty but we have an association
        // rule that requires entries, so the operation is unavailable.
        continue;
      }
      else if (!primaryAssociation)
      {
        // The actual selection is non-empty and we don't want
        // want entries... the operation is unavailable.
        continue;
      }

      // Now we know there's an association and we have a non-empty
      // selection so there's a chance the operation should be available.
      if (maxAllowed > 0 && numSel > maxAllowed)
      {
        // Too many items selected.
        continue;
      }
      else if (numSel < numRequired)
      {
        // Too few items selected.
        continue;
      }
      else if (numSel > numRequired && !extensible)
      {
        continue;
      }

      // All the easy checks are done; see if the number and type
      // of items in the actual selection exactly match the requirements.
      bool match = true;
      for (auto item : actual)
      {
        if (!primaryAssociation->isValueValid(item))
        {
          match = false;
          break; // Nope, not a match.
        }
      }
      if (match)
      {
        if ((!extensible && numSel == numRequired) ||
          (extensible && numSel >= numRequired && (maxAllowed == 0 || numSel <= maxAllowed)))
        {
          workingSetOut.insert(md.index());
        }
      }
    }
  }
  else
  {
    for (auto& md : operationsIn->metadata())
    {
      workingSetOut.insert(md.index());
    }
  }
}

void AvailableOperations::availableOperations(const OperationIndexSet& workingSetIn,
  smtk::workflow::OperationFilterSortPtr filterIn, OperationIndexArray& out)
{
  if (filterIn)
  {
    filterIn->apply(workingSetIn, out);
  }
  else
  {
    out.clear();
    out.insert(out.end(), workingSetIn.begin(), workingSetIn.end());
  }
}

void AvailableOperations::computeFromSelection()
{
  if (!m_operationManager || m_operationManager->metadata().empty())
  {
    return;
  }
  OperationIndexSet workingSet;
  AvailableOperations::workingSet(m_operationManager, m_useSelection ? m_selection : nullptr,
    m_selectionMask, m_selectionExact, workingSet);
  if (m_workingSet == workingSet)
  {
    // The selection changed, but the set of available operators didn't.
    return;
  }
  // Now update the output array based on the working set:
  m_workingSet = workingSet;
  this->computeFromWorkingSet();
}

void AvailableOperations::computeFromWorkingSet()
{
  availableOperations(m_workingSet, m_workflowFilter, m_available);
  this->triggerObservers();
}

void AvailableOperations::triggerObservers()
{
  if (m_observers.empty())
  {
    return;
  }

  auto self = shared_from_this();
  for (auto entry : m_observers)
  {
    entry.second(self);
  }
}
