//=========================================================================
//  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 "qtCMBOptionsDialog.h"
#include "ui_qtCMBOptionsDialog.h"

#include "pqUndoStack.h"
#include "qtCMBOptionsContainer.h"
#include "qtCMBOptionsPage.h"

#include <QAbstractItemModel>
#include <QHeaderView>
#include <QList>
#include <QMap>
#include <QString>

class qtCMBOptionsDialogModelItem
{
public:
  qtCMBOptionsDialogModelItem();
  qtCMBOptionsDialogModelItem(const QString& name);
  ~qtCMBOptionsDialogModelItem();

  qtCMBOptionsDialogModelItem* Parent;
  QString Name;
  QList<qtCMBOptionsDialogModelItem*> Children;
};

class qtCMBOptionsDialogModel : public QAbstractItemModel
{
public:
  qtCMBOptionsDialogModel(QObject* parent = 0);
  ~qtCMBOptionsDialogModel() override;

  int rowCount(const QModelIndex& parent = QModelIndex()) const override;
  int columnCount(const QModelIndex& parent = QModelIndex()) const override;
  QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override;
  QModelIndex parent(const QModelIndex& child) const override;

  QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;

  QModelIndex getIndex(const QString& path) const;
  QString getPath(const QModelIndex& index) const;
  void addPath(const QString& path);
  bool removeIndex(const QModelIndex& index);

private:
  QModelIndex getIndex(qtCMBOptionsDialogModelItem* item) const;

private:
  qtCMBOptionsDialogModelItem* Root;
};

class qtCMBOptionsDialogForm : public Ui::qtCMBOptionsFrame
{
public:
  qtCMBOptionsDialogForm();
  ~qtCMBOptionsDialogForm();

  QMap<QString, qtCMBOptionsPage*> Pages;
  qtCMBOptionsDialogModel* Model;
  int ApplyUseCount;
  bool ApplyNeeded;
};

qtCMBOptionsDialogModelItem::qtCMBOptionsDialogModelItem()
  : Name()
  , Children()
{
  this->Parent = 0;
}

qtCMBOptionsDialogModelItem::qtCMBOptionsDialogModelItem(const QString& name)
  : Name(name)
  , Children()
{
  this->Parent = 0;
}

qtCMBOptionsDialogModelItem::~qtCMBOptionsDialogModelItem()
{
  QList<qtCMBOptionsDialogModelItem*>::Iterator iter = this->Children.begin();
  for (; iter != this->Children.end(); ++iter)
  {
    delete *iter;
  }
}

qtCMBOptionsDialogModel::qtCMBOptionsDialogModel(QObject* parentObject)
  : QAbstractItemModel(parentObject)
{
  this->Root = new qtCMBOptionsDialogModelItem();
}

qtCMBOptionsDialogModel::~qtCMBOptionsDialogModel()
{
  delete this->Root;
}

int qtCMBOptionsDialogModel::rowCount(const QModelIndex& parentIndex) const
{
  qtCMBOptionsDialogModelItem* item = this->Root;
  if (parentIndex.isValid())
  {
    item = reinterpret_cast<qtCMBOptionsDialogModelItem*>(parentIndex.internalPointer());
  }

  return item->Children.size();
}

int qtCMBOptionsDialogModel::columnCount(const QModelIndex&) const
{
  return 1;
}

QModelIndex qtCMBOptionsDialogModel::index(
  int row, int column, const QModelIndex& parentIndex) const
{
  qtCMBOptionsDialogModelItem* item = this->Root;
  if (parentIndex.isValid())
  {
    item = reinterpret_cast<qtCMBOptionsDialogModelItem*>(parentIndex.internalPointer());
  }

  if (column == 0 && row >= 0 && row < item->Children.size())
  {
    return this->createIndex(row, column, item->Children[row]);
  }

  return QModelIndex();
}

QModelIndex qtCMBOptionsDialogModel::parent(const QModelIndex& child) const
{
  if (child.isValid())
  {
    qtCMBOptionsDialogModelItem* item =
      reinterpret_cast<qtCMBOptionsDialogModelItem*>(child.internalPointer());
    return this->getIndex(item->Parent);
  }

  return QModelIndex();
}

QVariant qtCMBOptionsDialogModel::data(const QModelIndex& idx, int role) const
{
  if (idx.isValid())
  {
    qtCMBOptionsDialogModelItem* item =
      reinterpret_cast<qtCMBOptionsDialogModelItem*>(idx.internalPointer());
    if (role == Qt::DisplayRole || role == Qt::ToolTipRole)
    {
      return QVariant(item->Name);
    }
  }

  return QVariant();
}

QModelIndex qtCMBOptionsDialogModel::getIndex(const QString& path) const
{
  qtCMBOptionsDialogModelItem* item = this->Root;
  QStringList names = path.split(".");
  QStringList::Iterator iter = names.begin();
  for (; item && iter != names.end(); ++iter)
  {
    qtCMBOptionsDialogModelItem* child = 0;
    QList<qtCMBOptionsDialogModelItem*>::Iterator jter = item->Children.begin();
    for (; jter != item->Children.end(); ++jter)
    {
      if ((*jter)->Name == *iter)
      {
        child = *jter;
        break;
      }
    }

    item = child;
  }

  if (item && item != this->Root)
  {
    return this->getIndex(item);
  }

  return QModelIndex();
}

QString qtCMBOptionsDialogModel::getPath(const QModelIndex& idx) const
{
  if (idx.isValid())
  {
    QString path;
    qtCMBOptionsDialogModelItem* item =
      reinterpret_cast<qtCMBOptionsDialogModelItem*>(idx.internalPointer());
    if (item)
    {
      path = item->Name;
      item = item->Parent;
    }

    while (item && item != this->Root)
    {
      path.prepend(".").prepend(item->Name);
      item = item->Parent;
    }

    return path;
  }

  return QString();
}

void qtCMBOptionsDialogModel::addPath(const QString& path)
{
  qtCMBOptionsDialogModelItem* item = this->Root;
  QStringList names = path.split(".");
  QStringList::Iterator iter = names.begin();
  for (; iter != names.end(); ++iter)
  {
    qtCMBOptionsDialogModelItem* child = 0;
    QList<qtCMBOptionsDialogModelItem*>::Iterator jter = item->Children.begin();
    for (; jter != item->Children.end(); ++jter)
    {
      if ((*jter)->Name == *iter)
      {
        child = *jter;
        break;
      }
    }

    if (!child)
    {
      child = new qtCMBOptionsDialogModelItem(*iter);
      child->Parent = item;
      QModelIndex parentIndex = this->getIndex(item);
      int row = item->Children.size();
      this->beginInsertRows(parentIndex, row, row);
      item->Children.append(child);
      this->endInsertRows();
    }

    item = child;
  }
}

bool qtCMBOptionsDialogModel::removeIndex(const QModelIndex& idx)
{
  if (idx.isValid())
  {
    qtCMBOptionsDialogModelItem* item =
      reinterpret_cast<qtCMBOptionsDialogModelItem*>(idx.internalPointer());
    if (item->Children.size() == 0)
    {
      QModelIndex parentIndex = this->getIndex(item->Parent);
      this->beginRemoveRows(parentIndex, idx.row(), idx.row());
      item->Parent->Children.removeAt(idx.row());
      this->endRemoveRows();
      delete item;
      return true;
    }
  }

  return false;
}

QModelIndex qtCMBOptionsDialogModel::getIndex(qtCMBOptionsDialogModelItem* item) const
{
  if (item && item->Parent)
  {
    return this->createIndex(item->Parent->Children.indexOf(item), 0, item);
  }

  return QModelIndex();
}

qtCMBOptionsDialogForm::qtCMBOptionsDialogForm()
  : Ui::qtCMBOptionsFrame()
  , Pages()
{
  this->Model = new qtCMBOptionsDialogModel();
  this->ApplyUseCount = 0;
  this->ApplyNeeded = false;
}

qtCMBOptionsDialogForm::~qtCMBOptionsDialogForm()
{
  delete this->Model;
}

qtCMBOptionsDialog::qtCMBOptionsDialog(QWidget* widgetParent)
  : QDialog(widgetParent)
{
  this->Form = new qtCMBOptionsDialogForm();
  this->Form->setupUi(this);
  this->Form->PageNames->setModel(this->Form->Model);

  // Hide the tree widget header view.
  this->Form->PageNames->header()->hide();

  // Hide the apply and reset buttons until they are needed.
  this->Form->ApplyButton->setEnabled(false);
  this->Form->ResetButton->setEnabled(false);
  this->Form->ApplyButton->hide();
  this->Form->ResetButton->hide();

  this->connect(this->Form->PageNames->selectionModel(),
    SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)), this,
    SLOT(changeCurrentPage()));
  this->connect(this->Form->ApplyButton, SIGNAL(clicked()), this, SLOT(applyChanges()));
  this->connect(this->Form->ResetButton, SIGNAL(clicked()), this, SLOT(resetChanges()));
  this->connect(this->Form->CloseButton, SIGNAL(clicked()), this, SLOT(accept()));
  this->connect(this->Form->RestoreDefaults, SIGNAL(clicked()), this, SLOT(restoreDefaults()));
}

qtCMBOptionsDialog::~qtCMBOptionsDialog()
{
  delete this->Form;
}

void qtCMBOptionsDialog::accept()
{
  this->applyChanges();
  this->QDialog::accept();
}

bool qtCMBOptionsDialog::isApplyNeeded() const
{
  return this->Form->ApplyNeeded;
}

void qtCMBOptionsDialog::setApplyNeeded(bool applyNeeded)
{
  if (applyNeeded != this->Form->ApplyNeeded)
  {
    if (!applyNeeded)
    {
      this->Form->ApplyNeeded = false;
      this->Form->ApplyButton->setEnabled(false);
      this->Form->ResetButton->setEnabled(false);
    }
    else if (this->Form->ApplyUseCount > 0)
    {
      this->Form->ApplyNeeded = true;
      this->Form->ApplyButton->setEnabled(true);
      this->Form->ResetButton->setEnabled(true);
    }
  }
}

void qtCMBOptionsDialog::addOptions(const QString& path, qtCMBOptionsPage* options)
{
  if (!options)
  {
    return;
  }

  // See if the page is a container.
  qtCMBOptionsContainer* container = qobject_cast<qtCMBOptionsContainer*>(options);
  if (!container && path.isEmpty())
  {
    return;
  }

  // See if the page/container uses the apply button.
  if (options->isApplyUsed())
  {
    this->Form->ApplyUseCount++;
    if (this->Form->ApplyUseCount == 1)
    {
      this->Form->ApplyButton->show();
      this->Form->ResetButton->show();
      QObject::connect(this, SIGNAL(accepted()), this, SLOT(applyChanges()));
    }

    this->connect(options, SIGNAL(changesAvailable()), this, SLOT(enableButtons()));
  }

  // Add the widget to the stack.
  this->Form->Stack->addWidget(options);

  // Add the page(s) to the map and the model.
  if (container)
  {
    // If the path is not empty, use it as the page prefix.
    QString prefix;
    if (!path.isEmpty())
    {
      prefix = path;
      prefix.append(".");
    }

    container->setPagePrefix(prefix);

    // Get the list of pages from the container.
    QStringList pathList = container->getPageList();
    QStringList::Iterator iter = pathList.begin();
    for (; iter != pathList.end(); ++iter)
    {
      this->Form->Pages.insert(prefix + *iter, options);
      this->Form->Model->addPath(prefix + *iter);
    }
  }
  else
  {
    this->Form->Pages.insert(path, options);
    this->Form->Model->addPath(path);
  }

  this->Form->PageNames->expandAll();
}

void qtCMBOptionsDialog::addOptions(qtCMBOptionsContainer* options)
{
  this->addOptions(QString(), options);
}

void qtCMBOptionsDialog::removeOptions(qtCMBOptionsPage* options)
{
  if (!options)
  {
    return;
  }

  // Remove the widget from the stack.
  this->Form->Stack->removeWidget(options);

  // See if the options use the apply button.
  if (options->isApplyUsed())
  {
    this->Form->ApplyUseCount--;
    if (this->Form->ApplyUseCount == 0)
    {
      this->Form->ApplyNeeded = false;
      this->Form->ApplyButton->setEnabled(false);
      this->Form->ResetButton->setEnabled(false);
      this->Form->ApplyButton->hide();
      this->Form->ResetButton->hide();
      QObject::disconnect(this, SIGNAL(accepted()), this, SLOT(applyChanges()));
    }

    this->disconnect(options, 0, this, 0);
  }

  // Search the map for the paths to remove.
  QMap<QString, qtCMBOptionsPage*>::Iterator iter = this->Form->Pages.begin();
  while (iter != this->Form->Pages.end())
  {
    if (*iter == options)
    {
      QString path = iter.key();
      iter = this->Form->Pages.erase(iter);

      // Remove the item from the tree model as well.
      QModelIndex index = this->Form->Model->getIndex(path);
      QPersistentModelIndex parentIndex = index.parent();
      if (this->Form->Model->removeIndex(index))
      {
        // Remove any empty parent items.
        while (parentIndex.isValid() && this->Form->Model->rowCount(parentIndex) == 0)
        {
          index = parentIndex;
          parentIndex = index.parent();

          // Make sure the index path isn't in the map.
          path = this->Form->Model->getPath(index);
          if (this->Form->Pages.find(path) == this->Form->Pages.end())
          {
            if (!this->Form->Model->removeIndex(index))
            {
              break;
            }
          }
        }
      }
    }
    else
    {
      ++iter;
    }
  }
}

void qtCMBOptionsDialog::setCurrentPage(const QString& path)
{
  QModelIndex current = this->Form->Model->getIndex(path);
  this->Form->PageNames->setCurrentIndex(current);
}

void qtCMBOptionsDialog::applyChanges()
{
  if (this->Form->ApplyNeeded)
  {
    BEGIN_UNDO_SET("Changed View Settings");
    emit this->aboutToApplyChanges();
    QMap<QString, qtCMBOptionsPage*>::Iterator iter = this->Form->Pages.begin();
    for (; iter != this->Form->Pages.end(); ++iter)
    {
      (*iter)->applyChanges();
    }

    this->setApplyNeeded(false);
    emit this->appliedChanges();
    END_UNDO_SET();
  }
}

void qtCMBOptionsDialog::resetChanges()
{
  if (this->Form->ApplyNeeded)
  {
    QMap<QString, qtCMBOptionsPage*>::Iterator iter = this->Form->Pages.begin();
    for (; iter != this->Form->Pages.end(); ++iter)
    {
      (*iter)->resetChanges();
    }

    this->setApplyNeeded(false);
  }
}

void qtCMBOptionsDialog::restoreDefaults()
{
  QMap<QString, qtCMBOptionsPage*>::Iterator iter = this->Form->Pages.begin();
  for (; iter != this->Form->Pages.end(); ++iter)
  {
    (*iter)->restoreDefaults();
  }

  this->setApplyNeeded(true);
}

void qtCMBOptionsDialog::changeCurrentPage()
{
  // Get the current index from the view.
  QModelIndex current = this->Form->PageNames->currentIndex();

  // Look up the path for the current index.
  QString path = this->Form->Model->getPath(current);
  QMap<QString, qtCMBOptionsPage*>::Iterator iter = this->Form->Pages.find(path);
  if (iter == this->Form->Pages.end())
  {
    // If no page is found, show the blank page.
    this->Form->Stack->setCurrentWidget(this->Form->BlankPage);
  }
  else
  {
    this->Form->Stack->setCurrentWidget(*iter);
    qtCMBOptionsContainer* container = qobject_cast<qtCMBOptionsContainer*>(*iter);
    if (container)
    {
      // Get the path prefix from the container.
      QString prefix = container->getPagePrefix();
      if (!prefix.isEmpty())
      {
        // Remove the prefix from the path.
        path.remove(0, prefix.length());
      }

      // Set the page on the container object.
      container->setPage(path);
    }
  }
}

void qtCMBOptionsDialog::enableButtons()
{
  this->setApplyNeeded(true);
}
