//=========================================================================
//  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/session/aeva/plugin/EditFreeformAttributesView.h"
#include "smtk/session/aeva/operators/EditFreeformAttributes.h"
#include "smtk/session/aeva/plugin/ui_EditFreeformAttributesView.h"

#include "smtk/attribute/Attribute.h"
#include "smtk/attribute/ComponentItem.h"
#include "smtk/attribute/DoubleItem.h"
#include "smtk/attribute/GroupItem.h"
#include "smtk/attribute/IntItem.h"
#include "smtk/attribute/StringItem.h"
#include "smtk/extension/qt/qtAttribute.h"
#include "smtk/extension/qt/qtAttributeItemInfo.h"
#include "smtk/extension/qt/qtUIManager.h"
#include "smtk/operation/Manager.h"
#include "smtk/resource/properties/CoordinateFrame.h"
#include "smtk/resource/Component.h"
#include "smtk/view/Configuration.h"
#include "smtk/view/Selection.h"
#include "smtk/view/SelectionObserver.h"

#include "pqActiveObjects.h"
#include "pqApplicationCore.h"
#include "pqPresetDialog.h"
#include "pqRenderView.h"
#include "pqServer.h"
#include "pqSettings.h"
#include "smtk/extension/paraview/widgets/pqSMTKCoordinateFrameItemWidget.h"

#include <QCheckBox>
#include <QColorDialog>
#include <QHBoxLayout>
#include <QLabel>
#include <QMessageBox>
#include <QPainter>
#include <QPixmap>
#include <QPointer>
#include <QPushButton>
#include <QScrollArea>
#include <QSpacerItem>
#include <QTableWidget>
#include <QVBoxLayout>

// On Windows MSVC 2015+, something is included that defines
// a macro named ERROR to be 0. This causes smtkErrorMacro()
// to expand into garbage (because smtk::io::Logger::ERROR
// gets expanded to smtk::io::Logger::0).
#ifdef ERROR
#undef ERROR
#endif

using namespace smtk::extension;
using smtk::attribute::ComponentItem;
using smtk::attribute::DoubleItem;
using smtk::resource::properties::CoordinateFrame;

namespace
{
constexpr auto coordinateFrameTextValue = "<coordinate frame>";
} // anonymous

class EditFreeformAttributesView::Internals : public Ui::EditFreeformAttributesView
{
public:
  Internals(const std::shared_ptr<smtk::operation::Operation>& op)
    : m_setUp(false)
    , m_currentOp(std::dynamic_pointer_cast<smtk::session::aeva::EditFreeformAttributes>(op))
  {
    m_attributeValueTable = nullptr;
    if (m_currentOp)
    {
      auto coordinateFrameGroup = m_currentOp->parameters()->findGroup("Coordinate Frame");
      m_widgetCoordinateFrameGroupItem = std::make_shared<smtk::attribute::GroupItem>(*coordinateFrameGroup);
      m_widgetCoordinateFrameGroupItem->setNumberOfGroups(1);
    }
    else
    {
      smtkErrorMacro(smtk::io::Logger::instance(), "Could not create an edit operation.");
    }
  }

  ~Internals() = default;

  void selectionModified(const std::string& /*source*/,
    const std::shared_ptr<smtk::view::Selection>& selection)
  {
    m_objects.clear();
    selection->currentSelectionByValue(m_objects, "selected", false);
    this->updateProperties();
  }

  void updateProperties()
  {
    struct Key
    {
      std::string name;
      std::string type;

      bool operator<(const Key& other) const
      {
        return this->name < other.name || (this->name == other.name && this->type < other.type);
      }
    };
    using Entry = struct
    {
      QVariant value;
      std::size_t count;
    };
    if (!m_setUp || !m_attributeValueTable)
    {
      return;
    }
    if (m_objects.size() == 1)
    {
      m_editorLabel->setText(QString("Editing freeform attributes for %1")
                               .arg(QString::fromStdString(m_objects.begin()->get()->name())));
    }
    else
    {
      m_editorLabel->setText(
        QString("Editing freeform attributes for %1 objects").arg(m_objects.size()));
    }
    m_attributeValueTable->clearContents();
    std::map<Key, Entry> props;
    std::size_t expected = 0;
    for (const auto& object : m_objects)
    {
      if (object)
      {
        ++expected;
        const auto& objectProps = object->properties();
        {
          const auto& intProps = objectProps.get<long>();
          for (const auto& propName : intProps.keys())
          {
            Key key{ propName, "integer" };
            auto it = props.find(key);
            if (it == props.end())
            {
              props[key] = Entry{ QVariant::fromValue(intProps.at(propName)), 1 };
            }
            else
            {
              ++props[key].count;
              QVariant val = QVariant::fromValue(intProps.at(propName));
              if (val != it->second.value)
              {
                props[key].value = QString("<multiple values>");
              }
            }
          }
        }
        {
          const auto& fpProps = objectProps.get<double>();
          for (const auto& propName : fpProps.keys())
          {
            Key key{ propName, "floating-point" };
            auto it = props.find(key);
            if (it == props.end())
            {
              props[key] = Entry{ QVariant::fromValue(fpProps.at(propName)), 1 };
            }
            else
            {
              ++props[key].count;
              QVariant val = QVariant::fromValue(fpProps.at(propName));
              if (val != it->second.value)
              {
                props[key].value = QString("<multiple values>");
              }
            }
          }
        }
        {
          const auto& fpProps = objectProps.get<CoordinateFrame>();
          for (const auto& propName : fpProps.keys())
          {
            Key key{ propName, "coordinate-frame" };
            auto it = props.find(key);
            if (it == props.end())
            {
              props[key] = Entry{ QString(coordinateFrameTextValue), 1 };
              const CoordinateFrame& frame = fpProps.at(propName);
            }
            else
            {
              ++props[key].count;
              props[key].value = QString(coordinateFrameTextValue);
            }
          }
        }
        {
          const auto& strProps = objectProps.get<std::string>();
          for (const auto& propName : strProps.keys())
          {
            Key key{ propName, "string" };
            auto it = props.find(key);
            if (it == props.end())
            {
              props[key] = Entry{ QString::fromStdString(strProps.at(propName)), 1 };
            }
            else
            {
              ++props[key].count;
              QVariant val = QString::fromStdString(strProps.at(propName));
              if (val != it->second.value)
              {
                props[key].value = QString("<multiple values>");
              }
            }
          }
        }
      }
    }
    m_attributeValueTable->setRowCount(props.size());
    int row = 0;
    for (auto& prop : props)
    {
      m_attributeValueTable->setItem(
        row, 0, new QTableWidgetItem(QString::fromStdString(prop.first.name)));
      m_attributeValueTable->setItem(
        row, 1, new QTableWidgetItem(QString::fromStdString(prop.first.type)));
      if (prop.second.count != expected)
      {
        prop.second.value = QString("<partial>");
      }
      m_attributeValueTable->setItem(row, 2, new QTableWidgetItem(prop.second.value.toString()));
      ++row;
    }
  }

  void copySelectedRowToEditor()
  {
    const auto& indices = m_attributeValueTable->selectionModel()->selectedIndexes();
    if (indices.size() != 3) // NB: 3 == number of columns per row.
    {
      return;
    }
    QModelIndex index = indices.front();
    if (!index.isValid())
    {
      return;
    }
    // Only a single row was selected. Copy data into editor
    m_attributeNameEdit->setText(index.data().toString());
    QString typeString = index.siblingAtColumn(1).data().toString();
    if (typeString == "string")
    {
      m_attributeTypeCombo->setCurrentIndex(0);
    }
    else if (typeString == "floating-point")
    {
      m_attributeTypeCombo->setCurrentIndex(1);
    }
    else if (typeString == "integer")
    {
      m_attributeTypeCombo->setCurrentIndex(2);
    }
    else if (typeString == "coordinate-frame")
    {
      // Copy first object's CoordinateFrame to the editor widget.
      // It is assumed that if m_objects has multiple objects, they all
      // have the same value of CoordinateFrame with the specific selected name.
      // Either way, the Apply operation will make it consistent.
      if (!m_objects.empty())
      {
        const auto& object = *m_objects.begin();
        if (object)
        {
          const std::string propName = index.data().toString().toStdString();
          const auto& objectProps = object->properties();
          if (objectProps.contains<CoordinateFrame>(propName))
          {
            const CoordinateFrame& f = objectProps.at<CoordinateFrame>(propName);
            updateWidgetCoordinateFrame(f);
          }
        }
      }
      // This triggers an event call to showHideCoordinateFrameWidget(), so we
      // want to make sure m_widgetCoordinateFrame is updated before that happens.
      m_attributeTypeCombo->setCurrentIndex(3);
    }

    m_attributeValuesEdit->setText(index.siblingAtColumn(2).data().toString());
    m_attributeValuesEdit->setDisabled(typeString == "coordinate-frame");
  }

  void editAttribute(const std::string& attributeName, bool erase = false)
  {
    // Check that inputs are valid
    QString attributeValues = m_attributeValuesEdit->text();
    QVariant val = attributeValues;
    if (attributeName.empty())
    {
      smtkErrorMacro(smtk::io::Logger::instance(), "You must specify an attribute name.");
      return;
    }
    auto op = m_currentOp;
    if (!op)
    {
      smtkErrorMacro(smtk::io::Logger::instance(), "Could not create an edit operation.");
      return;
    }
    auto assoc = op->parameters()->associations();
    assoc->setValues(m_objects.begin(), m_objects.end(), 0);
    op->parameters()->findString("name")->setValue(attributeName);
    auto stringItem = op->parameters()->findString("string value");
    auto doubleItem = op->parameters()->findDouble("float value");
    auto intItem = op->parameters()->findInt("integer value");
    auto groupItem = op->parameters()->findGroup("Coordinate Frame");

    if (erase)
    {
      stringItem->setNumberOfValues(0);
      doubleItem->setNumberOfValues(0);
      intItem->setNumberOfValues(0);
      groupItem->setNumberOfGroups(0);
    }
    else
    {
      int attributeType = m_attributeTypeCombo->currentIndex(); // 0 = string, 1 = fp, 2 = int, 3 = coordinate frame
      switch (attributeType)
      {
        default:
        case 0:
          stringItem->setNumberOfValues(1);
          stringItem->setValue(val.toString().toStdString());
          doubleItem->setNumberOfValues(0);
          intItem->setNumberOfValues(0);
          groupItem->setNumberOfGroups(0);
          break;
        case 1:
          stringItem->setNumberOfValues(0);
          doubleItem->setNumberOfValues(1);
          doubleItem->setValue(val.toDouble());
          intItem->setNumberOfValues(0);
          groupItem->setNumberOfGroups(0);
          break;
        case 2:
          stringItem->setNumberOfValues(0);
          doubleItem->setNumberOfValues(0);
          intItem->setNumberOfValues(1);
          intItem->setValue(val.toInt());
          groupItem->setNumberOfGroups(0);
          break;
        case 3:
          stringItem->setNumberOfValues(0);
          doubleItem->setNumberOfValues(0);
          intItem->setNumberOfValues(0);

          // TODO: assign a user-selected parent here
          if (!m_objects.empty())
          {
            assignParent(*m_objects.begin());
          }

          *groupItem = *m_widgetCoordinateFrameGroupItem;
          break;
      }
    }
    auto result = op->operate();
    (void)result; // TODO: Verify
    this->updateProperties();
  }

  bool updateWidgetCoordinateFrame(const CoordinateFrame& frame)
  {
    m_widgetCoordinateFrameGroupItem->setNumberOfGroups(1);
    auto originItem = m_widgetCoordinateFrameGroupItem->findAs<DoubleItem>("Origin");
    auto xAxisItem = m_widgetCoordinateFrameGroupItem->findAs<DoubleItem>("XAxis");
    auto yAxisItem = m_widgetCoordinateFrameGroupItem->findAs<DoubleItem>("YAxis");
    auto zAxisItem = m_widgetCoordinateFrameGroupItem->findAs<DoubleItem>("ZAxis");
    auto compItem = m_widgetCoordinateFrameGroupItem->findAs<ComponentItem>("Parent");
    if(originItem && xAxisItem && yAxisItem && zAxisItem && compItem)
    {
      originItem->setValues(frame.origin.begin(), frame.origin.end());
      xAxisItem->setValues(frame.xAxis.begin(), frame.xAxis.end());
      yAxisItem->setValues(frame.yAxis.begin(), frame.yAxis.end());
      zAxisItem->setValues(frame.zAxis.begin(), frame.zAxis.end());
      if (frame.parent.lock() != nullptr)
      {
        compItem->setValue(frame.parent.lock());
      }
      return true;
    }
    return false;
  }

  /// Assign a parent to the CoordinateFrame
  bool assignParent(smtk::resource::PersistentObjectPtr parent)
  {
    auto compItem = m_widgetCoordinateFrameGroupItem->findAs<ComponentItem>("Parent");
    auto comp = std::dynamic_pointer_cast<smtk::resource::Component>(parent);
    if (compItem && comp)
    {
      compItem->setNumberOfValues(1);
      compItem->setValue(comp);
      return true;
    }
    return false;
  }

  bool m_setUp;
  QPointer<QHBoxLayout> m_editorLayout;
  std::shared_ptr<smtk::session::aeva::EditFreeformAttributes> m_currentOp;
  smtk::view::SelectionObservers::Key m_selectionObserver;
  std::set<std::shared_ptr<smtk::resource::PersistentObject> > m_objects;
  std::shared_ptr<pqSMTKCoordinateFrameItemWidget::qtItem> m_frameWidget;
  smtk::attribute::GroupItemPtr m_widgetCoordinateFrameGroupItem = nullptr;
};

EditFreeformAttributesView::EditFreeformAttributesView(const smtk::view::Information& info)
  : qtBaseAttributeView(info)
  , m_p(new Internals(info.get<smtk::operation::Operation::Ptr>()))
{
  if (auto* uiManager = info.get<qtUIManager*>())
  {
    const auto& managers = uiManager->managers();
    const auto& selection = managers.get<smtk::view::Selection::Ptr>();
    m_p->m_selectionObserver = selection->observers().insert(
      [this](const std::string& src, std::shared_ptr<smtk::view::Selection> const& sel) {
        m_p->selectionModified(src, sel);
      },
      std::numeric_limits<smtk::view::SelectionObservers::Priority>::lowest(),
      /* initialize immediately */ true,
      "update freeform attribute editor");
  }
}

EditFreeformAttributesView::~EditFreeformAttributesView()
{
  delete m_p;
}

bool EditFreeformAttributesView::displayItem(smtk::attribute::ItemPtr item) const
{
  if (item && item->name() == "colors")
  {
    return false;
  }
  return this->qtBaseAttributeView::displayItem(item);
}

bool EditFreeformAttributesView::validateInformation(const smtk::view::Information& info)
{
  return qtOperationView::validateInformation(info);
}

qtBaseView* EditFreeformAttributesView::createViewWidget(const smtk::view::Information& info)
{
  EditFreeformAttributesView* view;
  if (!EditFreeformAttributesView::validateInformation(info))
  {
    return nullptr;
  }
  view = new EditFreeformAttributesView(info);
  view->buildUI();
  return view;
}

void EditFreeformAttributesView::createWidget()
{
  smtk::view::ConfigurationPtr view = this->configuration();
  if (!view)
  {
    return;
  }

  auto* parentlayout = static_cast<QVBoxLayout*>(this->parentWidget()->layout());

  // Delete any pre-existing widget
  if (this->Widget)
  {
    if (parentlayout)
    {
      parentlayout->removeWidget(this->Widget);
    }
    delete this->Widget;
  }

  // Create a new frame and lay it out
  this->Widget = new QFrame(this->parentWidget());
  this->Widget->setObjectName("EditFreeformAttributesView");
  auto* layout = new QVBoxLayout(this->Widget);
  layout->setMargin(0);
  this->Widget->setLayout(layout);
  this->Widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);

  m_p->m_editorLayout = new QHBoxLayout;
  this->updateUI();

  auto* wtmp = new QWidget;
  m_p->setupUi(wtmp);
  m_p->m_setUp = true;
  layout->addWidget(wtmp);

  QObject::connect(m_p->m_attributeValueTable,
    SIGNAL(itemSelectionChanged()),
    this,
    SLOT(tableSelectionChanged()));
  QObject::connect(m_p->m_addButton, SIGNAL(released()), this, SLOT(addOrReplaceAttribute()));
  QObject::connect(m_p->m_removeButton, SIGNAL(released()), this, SLOT(removeSelectedAttribute()));

  // Check if the CoordinateFrameWidget needs to be shown, if the current attribute type changes.
  QObject::connect(m_p->m_attributeTypeCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(showHideCoordinateFrameWidget(int)));

  // Show help when the info button is clicked.
  // QObject::connect(m_p->InfoBtn, SIGNAL(released()), this, SLOT(onInfo()));
}

void EditFreeformAttributesView::onShowCategory()
{
  this->updateUI();
}

void EditFreeformAttributesView::updateUI()
{
  smtk::view::ConfigurationPtr view = this->configuration();
  if (!view || !this->Widget)
  {
    return;
  }

  int i = view->details().findChild("AttributeTypes");
  if (i < 0)
  {
    return;
  }
  smtk::view::Configuration::Component& comp = view->details().child(i);
  std::string defName;
  for (std::size_t ci = 0; ci < comp.numberOfChildren(); ++ci)
  {
    smtk::view::Configuration::Component& attComp = comp.child(ci);
    //std::cout << "  component " << attComp.name() << "\n";
    if (attComp.name() != "Att")
    {
      continue;
    }
    std::string optype;
    if (attComp.attribute("Type", optype) && !optype.empty())
    {
      //std::cout << "    component type " << optype << "\n";
      if (optype == "edit freeform attributes")
      {
        //defName = optype;
        defName = "smtk::session::aeva::EditFreeformAttributes";
        break;
      }
    }
  }
  if (defName.empty())
  {
    return;
  }

  // expecting only 1 instance of the op?
  smtk::attribute::AttributePtr att = m_p->m_currentOp->parameters();
}

void EditFreeformAttributesView::requestOperation(const smtk::operation::OperationPtr& op)
{
  if (!op || !op->parameters())
  {
    return;
  }
  op->operate();
}

void EditFreeformAttributesView::tableSelectionChanged()
{
  m_p->copySelectedRowToEditor();
  // Check if the CoordinateFrameWidget needs to be shown, if the table selection changes.
  if (this->m_p && this->m_p->m_attributeTypeCombo)
  {
    showHideCoordinateFrameWidget(this->m_p->m_attributeTypeCombo->currentIndex());
  }
}

void EditFreeformAttributesView::addOrReplaceAttribute()
{
  std::string attributeName = m_p->m_attributeNameEdit->text().toStdString();
  m_p->editAttribute(attributeName, false);
}

void EditFreeformAttributesView::removeSelectedAttribute()
{
  const auto& indices = m_p->m_attributeValueTable->selectionModel()->selectedIndexes();
  for (const auto& index : indices)
  {
    if (index.column() == 0)
    {
      std::string attributeName = index.data().toString().toStdString();
      m_p->editAttribute(attributeName, true);
    }
  }
}

void EditFreeformAttributesView::showHideCoordinateFrameWidget(int index)
{
  if (index == 3)
  {

    qtAttributeItemInfo info(m_p->m_widgetCoordinateFrameGroupItem, this->m_viewInfo.configuration()->details(), this->Widget, this);
    auto tmpw = pqSMTKCoordinateFrameItemWidget::createCoordinateFrameItemWidget(info);
    m_p->m_frameWidget = std::shared_ptr<pqSMTKCoordinateFrameItemWidget::qtItem>(tmpw);
    m_p->m_attributeValuesEdit->setEnabled(false);
    m_p->m_attributeValuesEdit->setText(coordinateFrameTextValue);
  }
  else
  {
    m_p->m_frameWidget = nullptr;
    m_p->m_attributeValuesEdit->setEnabled(true);
  }
}

void EditFreeformAttributesView::valueChanged(smtk::attribute::ItemPtr valItem)
{
  (void)valItem;
  std::cout << "Item " << valItem->name() << " type " << valItem->type()
            << " changed; running op.\n";
  this->requestOperation(m_p->m_currentOp);
}

void EditFreeformAttributesView::requestModelEntityAssociation()
{
  this->updateUI();
}

void EditFreeformAttributesView::setInfoToBeDisplayed()
{
  m_infoDialog->displayInfo(this->configuration());
}
