/***************************************************************************

    source::worx xtree
    Copyright © 2024-2025 c.holzheuer
    christoph.holzheuer@gmail.com

    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 2 of the License, or
    (at your option) any later version.

***************************************************************************/


#include <QMessageBox>
#include <QUndoStack>

#include <xqexception.h>
#include <xqviewmodel.h>
#include <xqselectionmodel.h>
#include <xqtreetable.h>
#include <xqcommand.h>
#include <xqitemdelegate.h>
#include <xqitemfactory.h>
#include <znode_factory.h>


// create global dummy item as
// fallback return value (klappt nicht)
//Q_GLOBAL_STATIC(XQItem,s_dummyItem)


//! hilfsfunkion, zeigt den string-content() für alle elemente der liste

void showItemList( const XQItemList& list)
{
  for(const auto& entry : list )
    qDebug() << " --- itemList: " << ((XQItem*)entry)->content();
  qDebug();
}


//! Konstruktur mit parent.

XQViewModel::XQViewModel( QObject* parent )
  : QStandardItemModel{ parent }, _itemFactory{ XQItemFactory::instance() }
{
  invisibleRootItem()->setData( "[rootItem]", Qt::DisplayRole );
  setItemPrototype( new XQItem );
}


//! gibt einen static-cast<QXItem*> auf 'invisibleRootItem()' zurück

const XQItem& XQViewModel::xqRootItem()
{
  // das ist ein hack, denn 'invisibleRootItem()' ist und bleibt ein
  // QStandardItem. Trick: keine eigenen members in XQItem, alles
  // dynamisch über den ItemData Mechanismus wie in QStandardItem

  return *static_cast<XQItem*>(invisibleRootItem());

}


//! hifsfunktion, die das item zu einen index zurückgibt

XQItem& XQViewModel::xqItemFromIndex(const QModelIndex& index) const
{
  if( index.isValid() )
  {
    QStandardItem* xqItem = QStandardItemModel::itemFromIndex(index);
    if( xqItem )
      return *static_cast<XQItem*>(xqItem);
  }
  return XQItem::fallBackDummyItem();
}

//! hilfsfunktiom, die das erste xqitem einer zeile zurückgibt.

XQItem& XQViewModel::xqFirstItem(int row) const
{
  return *static_cast<XQItem*>( QStandardItemModel::item(row) );
}


//! initialisiert dieses model über den namen. Es wird hier
//! nur die strukur erzeugt, keine inhalte.

void XQViewModel::initModel(const QString& modelName)
{
  /*
    model
      section
        header
        data
      section
        ...

   */
  // model rootnode finden -> <DocumentTreeModel>
  XQNodePtr modelSheet = _itemFactory.findModelSheet(  modelName ); // throws

  // #1: über alle sections
  for( auto& section : modelSheet->children() )
  {
    // #2: (optionalen?) header erzeugen
    const XQNodePtr header = section->find_child_by_tag_name( "Header");
    if( header )
    {      
      XQItemList list = _itemFactory.makeContentRow( header, nullptr );
      addSection(list,  section );
    }

  }
}


//! hilfsfunktion: fügt die liste unserem model hinzu und erzeugt eine 'section'.
//! die section kann erst gültig sein, wenn die items im model gelandet sind,
//! deswegen ist das hier zusammengefasst.

//! erzeugt dann eine section aus einer frisch erzeugten itemlist. der erste modelindex
//! der liste und der unterknoten 'Data' werden gespeichert.

void XQViewModel::addSection(const XQItemList& list, const XQNodePtr& sheetNode )
{
  // 1. die liste darf nicht leer sein
  Q_ASSERT(!list.isEmpty());
  // 2. sheetNode muss da sein
  Q_ASSERT(sheetNode);
  // 3. 'ContenType' muss vorhanden sein
  if( !sheetNode->has_attribute( c_ContentType) )
    throw XQException( "section list: Section node needs attribute 'ContentType'!");
  // 4. Data child muss auch da sein
  XQNodePtr dataNode = sheetNode->find_child_by_tag_name( c_Data );
  if( !dataNode )
    throw XQException( "section list: 'Data' child is missing!");

  // 5. das erzeugt dann auch valide indices
  appendRow(list);

  XQModelSection section(list[0]->index(), dataNode );
 _sections.addAtKey(sheetNode->attribute( c_ContentType), section);

  emit sectionCreated( &section );

}


//! SLOT, der aufgerufen wird, wenn eine edit-action getriggert wurde.

void XQViewModel::onActionTriggered(QAction* action)
{
  qDebug() << " --- onActionTriggered: count:" << XQNode::s_Count;

  // all selected indices
  QModelIndexList selectionList = treeTable()->selectionModel()->selectedRows();
  // extract command type
  XQCommand::CmdType cmdType = action->data().value<XQCommand::CmdType>();

  switch( cmdType )
  {
    // just handle undo ...
    case XQCommand::cmdUndo :
      return _undoStack->undo();

    // ... or do/redo
    case XQCommand::cmdRedo :
      return _undoStack->redo();

    // for copy & cut, we create a clone of the dataNodes in the clipboard
    case XQCommand::cmdCopy :
    case XQCommand::cmdCut :
      // don't 'copy' empty selections
      if( !selectionList.isEmpty() )
        _clipBoard.saveNodes( selectionList );
      // for copy, we are done, since copy cannot be undone
      if(  cmdType == XQCommand::cmdCopy )
        return;

    default:
      break;
  }

  // we create a command
  XQCommand* command = new XQCommand( cmdType, this );
  // store the row positions of the selected indices
  command->saveNodes( selectionList );
  command->setOriginIndex( treeTable()->currentIndex() );

  // execute command
  _undoStack->push( command );


}

/*
    switch (command.commandType())
    {
      case XQCommand::cmdToggleSection:
        return cmdToggleSection( command.originIndex() );

      case XQCommand::cmdCut:
        return cmdCut( command );

      case XQCommand::cmdPaste:
        return cmdPaste( command );

      case XQCommand::cmdNew:
        return cmdNew( command );

      case XQCommand::cmdDelete:
        return cmdDelete( command );

      case XQCommand::cmdMove:
        break;

      default:
        qDebug() << " --- onCommandRedo: default: not handled: " << command.toString();
    }
    */

//! führt die 'redo' action des gegebenen commnds aus.

void XQViewModel::onCommandRedo( XQCommand& command )
{
  static MemCallMap redoCalls
  {
    { XQCommand::cmdToggleSection, &XQViewModel::cmdToggleSection },
    { XQCommand::cmdCut,           &XQViewModel::cmdCut },
    { XQCommand::cmdPaste,         &XQViewModel::cmdPaste },
    { XQCommand::cmdNew,           &XQViewModel::cmdNew },
    { XQCommand::cmdDelete,        &XQViewModel::cmdDelete }
  };

  try
  {
    MemCall memCall = redoCalls[command.commandType()];
    if( memCall )
      (this->*memCall)( command );
    else
      qDebug() << " --- onCommandRedo: default: not handled: " << command.toString();
  }
  catch( XQException& exception )
  {
    qDebug() << exception.what();
    QMessageBox::critical( nullptr, "Failure", QString("Failure: %1").arg(exception.what()) );
  }
}

/*
  try
  {
    switch (command.commandType())
    {
    case XQCommand::cmdToggleSection:
      return cmdToggleSection( command.originIndex() );
      break;

    // undo Cut -> perform undoCut
    case XQCommand::cmdCut:
      return cmdCutUndo( command );

    // undo Paste -> perform Cut
    case XQCommand::cmdPaste:
      return cmdPasteUndo( command );

    // undo Move -> perform move back
    case XQCommand::cmdMove:
      // not yet implemented
      break;

    // undo New -> perform Delete
    case XQCommand::cmdNew:
      cmdNewUndo( command );
      break;

    // undo Delete -> perform New
    case XQCommand::cmdDelete:
      qDebug() << " --- onCommandUndo: delete: " << command.toString();
      return cmdDeleteUndo( command );

    default:
      qDebug() << " --- onCommandUndo: default: not handled: " << command.toString();
    }
    */
//! führt die 'undo' action des gegebenen commnds aus.

void XQViewModel::onCommandUndo( XQCommand& command )
{
  qDebug() << " --- onCommandUndo: count: " << XQNode::s_Count;

  static MemCallMap undoCalls
  {
    { XQCommand::cmdToggleSection, &XQViewModel::cmdToggleSection },
    { XQCommand::cmdCut,           &XQViewModel::cmdCutUndo },
    { XQCommand::cmdPaste,         &XQViewModel::cmdPasteUndo },
    { XQCommand::cmdNew,           &XQViewModel::cmdNewUndo },
    { XQCommand::cmdDelete,        &XQViewModel::cmdDeleteUndo },
  };

  try
  {
    MemCall memCall = undoCalls[command.commandType()];
    if( memCall )
      (this->*memCall)( command );
    else
      qDebug() << " --- onCommandUndo: default: not handled: " << command.toString();
  }
  catch( XQException& exception )
  {
    qDebug() << exception.what();
    QMessageBox::critical( nullptr, "Failure", QString("Failure: %1").arg(exception.what()) );
  }
}

// undo-/redo-able stuff

//! markierte knoten entfernen, 'command' enthält die liste

void XQViewModel::cmdCut( XQCommand& command )
{
  // wir gehen rückwärts über alle gemerkten knoten ...
  for (auto it = command.rbegin(); it != command.rend(); ++it)
  {
    // ... holen das erste item, das auch den content node enthält
    //const XQNodeBackup& entry = *it;
    // jetzt löschen, dabei wird die parent-verbindung entfernt
    const XQNodeBackup& entry = *it;

    XQItem& firstItem = xqFirstItem( (*it).itemPos );
    qDebug() << " --- Cut: "  << firstItem.text() << " " << firstItem.row() << " id#" << entry.contentNode->_id;

    entry.contentNode->unlink_self();
    removeRow(entry.itemPos );
  }
}


//! entfernte knoten wieder einfügen , 'command' enthält die liste

void XQViewModel::cmdCutUndo( XQCommand& command )
{
  // die anfangsposition
  int itmPos  = command.first().itemPos;
  // die 'zuständige' section rausfinden
  const XQModelSection& section = _sections.sectionFromRow( itmPos );
  // über alle einträge ...
  for (auto& entry : command )
  {
    const XQNodePtr& savedNode = entry.contentNode;
    // __fix! should not be _contentRoot!
    savedNode->add_me_at( entry.nodePos, _contentRoot );
    XQItemList list = _itemFactory.makeContentRow( section.sheetRootNode, savedNode );

    XQItem& firstItem = *((XQItem*)list[0]);
    qDebug() << " --- Cut Undo: "  << firstItem.text() << " " << firstItem.row() << " id#" << entry.contentNode->_id << " count: " << entry.contentNode.use_count();

    insertRow( entry.itemPos, list );
  }
}


//! clipboard inhalte einfügen

void XQViewModel::cmdPaste( XQCommand& command )
{  
  // selection holen ...
  QItemSelectionModel* selectionModel = treeTable()->selectionModel();
  // ... und löschen
  selectionModel->clearSelection();

  // aktuelles item finden
  const XQItem& item = xqItemFromIndex( command.originIndex() );

  // die neue item position ist unter dem akutellen item
  int insRow  = item.row()+1;
  int nodePos = item.contentNode()->own_pos()+1;

  // die zugehörige section finden
  const XQModelSection& section = _sections.sectionFromRow( insRow-1 );
  // wir pasten das clipboard
  for (auto& entry : _clipBoard )
  {
    // noch ein clone vom clone erzeugen ...
    XQNodePtr newNode = entry.contentNode->clone(section.contentRootNode );
    newNode->clone(section.contentRootNode )->add_me_at( nodePos );
    // ... und damit eine frische item-row erzeugen
    XQItemList list = _itemFactory.makeContentRow( section.sheetRootNode, newNode );
    insertRow( insRow, list );
    // die neue item-row selektieren
    const QModelIndex& selIdx = list[0]->index();
    _treeTable->selectionModel()->select(selIdx, QItemSelectionModel::Select | QItemSelectionModel::Rows);
    // zur nächsten zeile
    insRow++;
    nodePos++;
  }

  // unsere änderungen merken fürs 'undo'
  command.saveNodes( selectionModel->selectedRows() );

}


//! einfügen aus dem clipboard wieder rückgängig machen

void XQViewModel::cmdPasteUndo( XQCommand& command )
{
  command.dumpList("Paste UNDO");
  // wir gehen rückwärts über alle markieren knoten ...
  for (auto it = command.rbegin(); it != command.rend(); ++it)
  {
    // ... holen das erste item, das auch den content node enthält
    const XQNodeBackup& entry = *it;
    XQItem& firstItem = xqFirstItem( (*it).itemPos );
    qDebug() << " --- Cut: "  << firstItem.text() << " " << firstItem.row();
    // jetzt löschen
    entry.contentNode->unlink_self();
    removeRow(entry.itemPos );
  }
}


// don't clone into clipboard, remove items

//! entfernen der selection ohne copy in clipboard.

void XQViewModel::cmdDelete( XQCommand& command )
{
  // wir gehen rückwärts über alle markieren knoten ...
  for (auto it = command.rbegin(); it != command.rend(); ++it)
  {
    // ... holen das erste item, das auch den content node enthält
    const XQNodeBackup& entry = *it;
    XQItem& firstItem = xqFirstItem( (*it).itemPos );
    qDebug() << " --- Cut: "  << firstItem.text() << " " << firstItem.row();
    // jetzt löschen
    entry.contentNode->unlink_self();
    removeRow(entry.itemPos );
  }
}

//! macht 'delete' wirder rückgängig.

void XQViewModel::cmdDeleteUndo( XQCommand& command )
{

}


//! legt eine neue, leere zeile an.

void XQViewModel::cmdNew( XQCommand& command )
{

  // __fix
  /*
  const QModelIndex& origin = command.originIndex();
  if( !origin.isValid() )
    throw XQException("cmdNewRow failed: index not valid ");

  XQItem* target = xqItemFromIndex( origin );
  // current data node
  XQNodePtr node = target->contentNode();

  // we create a new data node
  //XQNodePtr newNode = new XQNodePtr( node->tag_name(), node->parent() );
  XQNodePtr newNode = XQNode::make_node( node->tag_name(), node->tag_value(), node->parent() );
  // store node in node->parent()
  //node->add_before_me( newNode );
  // store node also in 'command' to enable undo
  const XQModelSection& section = _sections.sectionFromIndex( origin );

  // create new item row
  XQItemList list = _itemFactory.createGenericRow( newNode, section.sheetRootNode );

  // add it to the treeview ...
  insertRow( origin.row(), list );

  // ... and make it ...
  treeTable()->setCurrentIndex( list[0]->index() );
  // ... editable
  treeTable()->edit( list[0]->index() );
  */
}

//! entfernt die neu angelegte zeile.

void XQViewModel::cmdNewUndo( XQCommand& command )
{
}


//! schaltet eine section sichtbar oder unsichtbar.

void XQViewModel::cmdToggleSection( XQCommand& command )
{
  const QModelIndex& index = command.originIndex();
  Q_ASSERT(index.isValid());

  int fstRow = _sections.firstRow( index );
  int lstRow = _sections.lastRow( index );

  bool hidden =_treeTable->isRowHidden( fstRow, _treeTable->rootIndex() );
  for (int row = fstRow; row < lstRow; ++row )
    _treeTable->setRowHidden( row, _treeTable->rootIndex(), !hidden );

}


//! git die treetable zurück

XQTreeTable* XQViewModel::treeTable()
{
  return _treeTable;
}

//! setzt die treetable als member.

void XQViewModel::setTreeTable(XQTreeTable* mainView )
{
  // store view for direct access: the maintree
  _treeTable = mainView;
  // connect myself as model to the mainview
  _treeTable->setModel(this);
  XQItemDelegate* delegate = new XQItemDelegate( *this );
  _treeTable->setItemDelegate( delegate );

  _contextMenu = new XQContextMenu( mainView );

  connect( _treeTable, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(onShowContextMenu(QPoint)));
  //connect( _treeTable, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(onDoubleClicked(QModelIndex)) );
  connect(_contextMenu, SIGNAL(triggered(QAction*)), this, SLOT(onActionTriggered(QAction*)));

  // __fixme, die view soll über das modelsheet konfiguriert werden!
  setupViewProperties();
}


//! setzt die eigenschaften der TreeTable.

void XQViewModel::setupViewProperties()
{
  _treeTable->setContextMenuPolicy(Qt::CustomContextMenu);
  _treeTable->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed);
  _treeTable->setSelectionBehavior(QAbstractItemView::SelectRows);
  _treeTable->setSelectionMode(QAbstractItemView::ExtendedSelection);
  //_treeTable->setSelectionMode(QAbstractItemView::ContiguousSelection);
  _treeTable->setSelectionModel( new XQSelectionModel(this) );
}


//! gibt den undo-stack zurück.

QUndoStack* XQViewModel::undoStack()
{
  return _undoStack;
}


//! setzt den undo-stack.

void XQViewModel::setUndoStack( QUndoStack* undoStack )
{
  _undoStack = undoStack;
}


//! SLOT, der die erstellung & anzeige es context-menues triggert.

void XQViewModel::onShowContextMenu(const QPoint& point)
{
  initContextMenu();
  _contextMenu->popup(_treeTable->mapToGlobal(point));
}


//! gibt die namen der neuen data-roles zurück.
//! __fix: die alten roles fehlen hier!

QHash<int, QByteArray> XQViewModel::roleNames() const
{

  QHash<int, QByteArray> roles;
  roles[XQItem::ContentRole] = "content";
  roles[XQItem::ItemTypeRole] = "itemType";
  roles[XQItem::RenderStyleRole] = "renderStyle";
  roles[XQItem::EditorTypeRole] = "editorType";
  roles[XQItem::UnitTypeRole] = "unitType";
  roles[XQItem::FixedChoicesRole] = "fixedChoices";
  roles[XQItem::ContentNodeRole] = "contentNode";
  roles[XQItem::SheetNodeRole] = "sheetNode";
  roles[XQItem::TypeKeyRole] = "typeKey";

  return roles;

}

