/***************************************************************************
                          contactlistmodelitem.cpp  -  description
                             -------------------
    begin                : Sat Feb 16 2008
    copyright            : (C) 2008 by Valerio Pilo
    email                : valerio@kmess.org
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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 "contactlistmodelitem.h"

#include "../contact/contact.h"
#include "../contact/contactextension.h"
#include "../contact/group.h"
#include "../contact/specialgroups.h"
#include "../kmessdebug.h"



/**
 * Root constructor
 *
 * Creates a root tree node. It can contain only Groups.
 */
ContactListModelItem::ContactListModelItem()
 : parentNode_(0)
 , object_(0)
 , itemType_(ItemRoot)
{
}



/**
 * Contact constructor
 *
 * Creates a Contact tree leaf. It cannot contain anything.
 */
ContactListModelItem::ContactListModelItem( Contact *contact, ContactListModelItem *parent )
 : parentNode_(parent)
 , object_(contact)
 , itemType_(ItemContact)
{
  // Add this item as a child of the parent
  parent->appendChild( this );
}



/**
 * Group constructor
 *
 * Creates a Group tree node. It can only contain Contacts.
 */
ContactListModelItem::ContactListModelItem( Group *group, ContactListModelItem *parent )
 : parentNode_(parent)
 , object_(group)
 , itemType_(ItemGroup)
{
  // Add this node as a child of the parent
  parent->appendChild( this );
}



/**
 * Destructor
 *
 * Since the objects associated to the items are directly managed by ContactList, their deletion
 * is performed by the Contact List itself.
 */
ContactListModelItem::~ContactListModelItem()
{
  qDeleteAll( childItems_ );
  childItems_.clear();

  // Delete this item from the parent's children
  if( parentNode_ )
  {
    parentNode_->removeChild( this );
  }
}



/**
 * Add a new child item to this one
 *
 * Insert the given item as a child of this node. A root item can only contain Groups, Groups can only
 * contain Contacts. There cannot be duplicate items within the same parent.
 *
 * @param item  Item to add as child
 */
void ContactListModelItem::appendChild( ContactListModelItem *item )
{
  // Only add groups to the root, and contacts to groups
  if( ( itemType_ == ItemRoot    && item->getType() != ItemGroup )
  ||  ( itemType_ == ItemGroup   && item->getType() != ItemContact )
  ||    itemType_ == ItemContact )
  {
    kWarning() << "Cannot insert this item:" << item << "into this one:" << this;
    return;
  }

  // Also, don't add items which are already present
  if( childItems_.contains( item ) )
  {
    kWarning() << "Tried to add twice item:" << item;
    return;
  }

  childItems_.append( item );
}



/**
 * Return the row-th child of this item
 *
 * @param row  Requested row
 * @return Item at the requested position, or a null pointer
 */
ContactListModelItem *ContactListModelItem::child( int row )
{
  if( row < 0 || row >= childItems_.count() )
  {
    return 0;
  }

  return childItems_.at( row );
}



/**
 * Return a child Contact item
 *
 * This method can only be called on a Group node.
 *
 * @param handle  Handle of the contact to find
 * @return        Item of the contact with the requested handle, or null
 */
ContactListModelItem *ContactListModelItem::childContact( const QString &handle )
{
  // Prevent incorrect usage
  if( itemType_ != ItemGroup )
  {
    kWarning() << "Called childContact() on a non-group item:" << this;
    return 0;
  }

  foreach( ContactListModelItem *item, childItems_ )
  {
    if(    item->getType() == ItemContact
        && static_cast<Contact*>( item->object() )->getHandle() == handle )
    {
      return item;
    }
  }

  return 0;
}



/**
 * Return all Contact items in the tree matching a contact handle
 *
 * This method is made so that Contacts added in different Groups are all found (for example,
 * a Contact may be in the Online group and in an user-defined group too).
 * You cannot call this method from a Contact leaf. Calling it from a Group node will return
 * a list with 0 or 1 items, depending if the Contact was found or not.
 *
 * @param handle  Handle of the contact to find
 * @return List of items with the requested Contact
 */
ModelItemList ContactListModelItem::childContacts( const QString &handle )
{
  ModelItemList list;

  if( itemType_ == ItemRoot )
  {
    foreach( ContactListModelItem *item, childItems_ )
    {
      list << item->childContacts( handle );
    }
  }
  else if( itemType_ == ItemGroup )
  {
    foreach( ContactListModelItem *item, childItems_ )
    {
      if( item->getType() != ItemContact )
      {
        kWarning() << "Invalid item in group" << this << ":" << item;
        return ModelItemList();
      }

      if( static_cast<Contact*>( item->object() )->getHandle() == handle )
      {
        list << item;
        break; // Only one Contact for each Group :)
      }
    }
  }
  else
  {
    kWarning() << "Called childContacts() from a Contact item:" << this;
    return ModelItemList();
  }

  return list;
}



/**
 * Return a child Group identified by its ID
 *
 * Can only be called from the root node.
 *
 * @param id  Id of the requested group
 * @return Node of the group or null
 */
ContactListModelItem *ContactListModelItem::childGroup( const QString &id )
{
  // Prevent incorrect usage
  if( itemType_ != ItemRoot )
  {
    kWarning() << "Called childGroup() on a non-root item:" << this;
    return 0;
  }

  foreach( ContactListModelItem *item, childItems_ )
  {
    if( item->getType() != ItemGroup )
    {
      kWarning() << "Invalid item in root" << this << ":" << item;
      return 0;
    }

    if( static_cast<Group*>( item->object() )->getId() == id )
    {
      return item;
    }
  }

  return 0;
}



// Return all Groups children
ModelItemList ContactListModelItem::childGroups()
{
  if( itemType_ != ItemRoot )
  {
    return ModelItemList();
  }

  ModelItemList list;
  foreach( ContactListModelItem *item, childItems_ )
  {
    if( item->getType() == ItemGroup )
    {
      list << item;
    }
  }

  return list;
}



/**
 * Return the number of children of this item
 *
 * @return int
 */
int ContactListModelItem::childCount() const
{
  return childItems_.count();
}



/**
 * Return the number of data columns
 *
 * We only have and use one. Different data is returned depending on the given role.
 *
 * @return int
 */
int ContactListModelItem::columnCount() const
{
  return 1;
}



/**
 * Return a collection of data from this item
 *
 * The role selects which different piece of data will be extracted from the item;
 * DisplayRole gets you the full map of item data, SearchRole a string to perform
 * filtering during search, SortRole a numeric value used to sort the items. Other
 * roles can be easily added if needed :)
 *
 * @param role  Specify the kind of item data to obtain
 * @return QVariant containing int, QMap or QString for the three columns respectively.
 */
const QVariant ContactListModelItem::data( int role ) const
{
  switch( role )
  {
    // Output the sorting information
    case SortRole:
    {
      int sortIndex;

      if( itemType_ == ItemContact )
      {
        const Contact *contact = static_cast<Contact*>( object_ );
        sortIndex = 8000; // default sortIndex

        if( contact == 0 )
        {
           return QVariant( sortIndex );
        }

        // Contacts are sorted by their status
        switch( contact->getStatus() )
        {
          case STATUS_ONLINE:        sortIndex = 1000; break;
          case STATUS_BE_RIGHT_BACK: sortIndex = 2000; break;
          case STATUS_IDLE:          sortIndex = 3000; break;
          case STATUS_AWAY:          sortIndex = 4000; break;
          case STATUS_OUT_TO_LUNCH:  sortIndex = 5000; break;
          case STATUS_ON_THE_PHONE:  sortIndex = 6000; break;
          case STATUS_BUSY:          sortIndex = 7000; break;
          case STATUS_INVISIBLE:
          case STATUS_OFFLINE:
          default:                   sortIndex = 8000; break;
        }

        // Add to the index the first character of the friendly name, so that
        // when two contacts have the same status, there's a (rough but fast)
        // sorting method
        const QString& sortChar( contact->getFriendlyName().mid( 0, 1 ) );
        if( ! sortChar.isEmpty() )
        {
          sortIndex += sortChar.at( 0 ).toUpper().toAscii();
        }
      }
      else if( itemType_ == ItemGroup )
      {
        const Group *group = static_cast<Group*>( object_ );
        sortIndex = 32766; // default sortIndex

        if( group == 0 )
        {
          return QVariant( sortIndex );
        }

        const QString& groupId( group->getId() );

        // These numbers are set so that the groups always come after the contacts in the root group

        // Non-special groups must be the first
        if( ! group->isSpecialGroup() )
        {
          sortIndex = 10000 + group->getSortPosition();
        }
        // The Individuals group must come right after them
        else if( groupId == SpecialGroups::INDIVIDUALS )
        {
          sortIndex =  32762;
        }
        // The Online group must come right after it
        else if( groupId == SpecialGroups::ONLINE )
        {
          sortIndex =  32763;
        }
        // The Offline group must follow Online
        else if( groupId == SpecialGroups::OFFLINE )
        {
          sortIndex =  32764;
        }
        // Then the remaining special groups, Allowed
        else if( groupId == SpecialGroups::ALLOWED )
        {
          sortIndex = 32765;
        }
        // And Removed
        else
        {
          sortIndex = 32766;
        }
      }
      else
      {
        // Avoid crashing without hints
        kWarning() << "Called data( SortRole ) on an invalid item: (see below)";
        kWarning() << this;
        return QVariant( 0 );
      }

      return QVariant( sortIndex );
    }
    break;

    // Return a string for full contact list search
    case SearchRole:
    {
      if( itemType_ == ItemContact )
      {
        const Contact *contact = static_cast<Contact*>( object_ );
        if( contact == 0 )
        {
          return QVariant( QString() );
        }

        // Enable searching by email, friendly name, personal message and music
        QString searchBy( contact->getHandle()             + " " +
                          contact->getFriendlyName()       + " " +
                          contact->getPersonalMessage()    + " " +
                          contact->getCurrentMediaString() );

        // Enable searching by email, true friendly name, and alias
        if( contact->getExtension() && contact->getExtension()->getUseAlternativeName() )
        {
          searchBy += " " + contact->getTrueFriendlyName();
        }

        return QVariant( searchBy );
      }
      else if( itemType_ == ItemGroup )
      {
        Group *group = static_cast<Group*>( object_ );
        if( group == 0 )
        {
          return QVariant( QString() );
        }

        return QVariant( group->getName() );
      }
      else
      {
        // Avoid crashing without hints
        kWarning() << "Called data( SearchRole ) on an invalid item: (see below)";
        kWarning() << this;
        return QVariant( QString() );
      }
    }
    break;

    case DataRole:
    {
      ModelDataList itemData;

      // Fill the returned data map with a selection of useful contact information
      if( itemType_ == ItemContact )
      {
        Contact *contact = static_cast<Contact*>( object_ );
        if( contact == 0 )
        {
          return QVariant( ModelDataList() );
        }

        itemData[ "type"                     ] = ItemContact;
        itemData[ "handle"                   ] = contact->getHandle();
        itemData[ "friendly"                 ] = contact->getFriendlyName( STRING_CLEANED );
        itemData[ "friendlyFormatted"        ] = contact->getFriendlyName( STRING_FORMATTED );
        itemData[ "trueFriendly"             ] = contact->getTrueFriendlyName( STRING_CLEANED );
        itemData[ "trueFriendlyFormatted"    ] = contact->getTrueFriendlyName( STRING_FORMATTED );
        itemData[ "personalMessage"          ] = contact->getPersonalMessage( STRING_CLEANED );
        itemData[ "personalMessageFormatted" ] = contact->getPersonalMessage( STRING_FORMATTED );
        itemData[ "displayPicture"           ] = contact->getContactPicturePath();
        itemData[ "groups"                   ] = contact->getGroupIds();
        itemData[ "mediaType"                ] = contact->getCurrentMediaType();
        itemData[ "mediaString"              ] = contact->getCurrentMediaString();
        itemData[ "status"                   ] = (int) contact->getStatus();
        itemData[ "isBlocked"                ] = contact->isBlocked();
        itemData[ "isReverse"                ] = contact->isReverse();
        itemData[ "clientName"               ] = contact->getClientName();
        itemData[ "lastSeen"                 ] = contact->getExtension()->getLastSeen();
        itemData[ "lastMessage"              ] = contact->getExtension()->getLastMessageDate();

        if( parentNode_->object() )
        {
          Group *group = static_cast<Group*>( parentNode_->object() );
          if( group != 0 )
          {
            itemData[ "group"            ] = group->getId();
            itemData[ "isInSpecialGroup" ] = group->isSpecialGroup();
          }
        }
        else
        {
          itemData[ "group"            ] = QString();
          itemData[ "isInSpecialGroup" ] = false;
        }
      }
      else if( itemType_ == ItemGroup )
      {
        // Get the item's group to retrieve its data
        Group *group = static_cast<Group*>( object_ );
        if( group == 0 )
        {
          return QVariant( ModelDataList() );
        }

        // Find out how many of the group's contacts are online
        uint numOnlineContacts = 0;
        foreach( ContactListModelItem *item, childItems_ )
        {
          if( item->getType() != ItemContact )
          {
            continue;
          }

          Contact *contact = static_cast<Contact*>( item->object() );

          if( contact && contact->isOnline() )
          {
            numOnlineContacts++;
          }
        }

        itemData[ "type"           ] = ItemGroup;
        itemData[ "id"             ] = group->getId();
        itemData[ "name"           ] = group->getName();
        itemData[ "onlineContacts" ] = QString::number( numOnlineContacts );
        itemData[ "totalContacts"  ] = QString::number( childCount() );
        itemData[ "isExpanded"     ] = group->isExpanded();
        itemData[ "isSpecialGroup" ] = group->isSpecialGroup();
      }
      else if( itemType_ == ItemRoot )
      {
        itemData[ "type"           ] = ItemRoot;
      }
      else
      {
        // Avoid crashing without hints
        kWarning() << "Called data( DisplayRole ) on an invalid item: (see below)";
        kWarning() << this;
        return QVariant( ModelDataList() );
      }

      return QVariant( itemData );
    }
    break;

  default:
    // Called with an invalid role, do nothing
    break;
  }

  return QVariant();
}



/**
 * Return the type of the internal stored object
 *
 * @return ItemType
 */
ContactListModelItem::ItemType ContactListModelItem::getType() const
{
  return itemType_;
}



/**
 * Move this item to another parent
 *
 * @param newParent   New parent node. Can not be null
 */
void ContactListModelItem::moveTo( ContactListModelItem *newParent )
{
  if( itemType_ == ItemRoot )
  {
    kWarning() << "Called moveTo() on a root item! Item:" << this;
    return;
  }

  if( newParent == 0 )
  {
    kWarning() << "Called moveTo() with an empty new parent! Item:" << this;
    return;
  }

  if( parentNode_ == 0 )
  {
    kWarning() << "Called moveTo() on a parentless item! Item:" << this;
    return;
  }

  // Remove ourselves from the old node
  parentNode_->removeChild( this );

  // Add ourselves to the new node
  newParent->appendChild( this );

  // Change the internal parent pointer
  parentNode_ = newParent;
}



/**
 * Return the stored object
 *
 * The object should be casted accordingly to the result of a getType() call.
 *
 * @return void*
 */
void *ContactListModelItem::object() const
{
  return object_;
}



/**
 * Get the parent node of this item
 *
 * @return Parent node
 */
ContactListModelItem *ContactListModelItem::parent() const
{
  return parentNode_;
}



/**
 * Remove a child item
 *
 * The child's object itself is not deleted, but is removed from the parent node only.
 * You have to manually call delete on it after removal.
 *
 * @param child  The item to delete
 * @return int   Number of deleted items (usually 0 or 1)
 */
int ContactListModelItem::removeChild( ContactListModelItem *child )
{
  return childItems_.removeAll( child );
}



/**
 * Get this item's position within the parent node
 *
 * @return int
 */
int ContactListModelItem::row() const
{
  if( itemType_ == ItemRoot || ! parentNode_ || parentNode_->childItems_.isEmpty() )
  {
    kWarning() << "Called row() on a root item or an item with invalid parent! Item:" << this;
    return 0;
  }

  return parentNode_->childItems_.indexOf( const_cast<ContactListModelItem*>( this ) );
}



/**
 * Custom operator to display nodes and items in the debug output
 *
 * @param out   Output debugging stream
 * @param item  Item to print
 * @return QDebug  The debug stream itself
 */
QDebug operator<<( QDebug out, const ContactListModelItem *item )
{
  QString type;
  Contact *contact;
  Group *group;

  switch( item->getType() )
  {
    case ContactListModelItem::ItemRoot:
      type = "Root node";
      break;

    case ContactListModelItem::ItemGroup:
      group = static_cast<Group *>( item->object() );
      if( group )
      {
        type = "Group node, id " + group->getId();
      }
      else
      {
        type = "Group node, invalid object";
      }
      break;

    case ContactListModelItem::ItemContact:
      contact = static_cast<Contact *>( item->object() );

      if( contact )
      {
        type = "Contact node, handle " + contact->getHandle();
      }
      else
      {
        type = "Contact node, invalid object";
      }

      if( item->parent()->getType() == ContactListModelItem::ItemRoot )
      {
        type += " in root node";
      }
      // Only the root node should have no parent, this is symptom of an error
      else if( ! item->parent() )
      {
        type += " without parent (!)";
      }
      else
      {
        group = static_cast<Group *>( item->parent()->object() );
        if( group )
        {
          type += " in node " + group->getId();
        }
        else
        {
          type += " within invalid node (!)";
        }
      }
      break;

    default:
      type = "Unknown node type";
      break;
  }

  out << type << "(at address" << QString::number( (qlonglong)item, 16 ) << ")";

  return out;
}


