/**
   * Returns the cached recent messages history.
   *
   * @return
   * @throws IOException
   */
  private History getHistory() throws IOException {
    synchronized (historyID) {
      HistoryService historyService =
          MessageHistoryActivator.getMessageHistoryService().getHistoryService();

      if (history == null) {
        history = historyService.createHistory(historyID, recordStructure);

        // lets check the version if not our version, re-create
        // history (delete it)
        HistoryReader reader = history.getReader();
        boolean delete = false;
        QueryResultSet<HistoryRecord> res = reader.findLast(1);
        if (res != null && res.hasNext()) {
          HistoryRecord hr = res.next();
          if (hr.getPropertyValues().length >= 4) {
            if (!hr.getPropertyValues()[3].equals(RECENT_MSGS_VER)) delete = true;
          } else delete = true;
        }

        if (delete) {
          // delete it
          try {
            historyService.purgeLocallyStoredHistory(historyID);

            history = historyService.createHistory(historyID, recordStructure);
          } catch (IOException ex) {
            logger.error("Cannot delete recent_messages history", ex);
          }
        }
      }

      return history;
    }
  }
  /**
   * Searches for contact ids in history of recent messages.
   *
   * @param provider
   * @param after
   * @return
   */
  List<String> getRecentContactIDs(String provider, Date after) {
    List<String> res = new ArrayList<String>();

    try {
      History history = getHistory();

      if (history != null) {
        Iterator<HistoryRecord> recs = history.getReader().findLast(NUMBER_OF_MSGS_IN_HISTORY);
        SimpleDateFormat sdf = new SimpleDateFormat(HistoryService.DATE_FORMAT);

        while (recs.hasNext()) {
          HistoryRecord hr = recs.next();

          String contact = null;
          String recordProvider = null;
          Date timestamp = null;

          for (int i = 0; i < hr.getPropertyNames().length; i++) {
            String propName = hr.getPropertyNames()[i];

            if (propName.equals(STRUCTURE_NAMES[0])) recordProvider = hr.getPropertyValues()[i];
            else if (propName.equals(STRUCTURE_NAMES[1])) contact = hr.getPropertyValues()[i];
            else if (propName.equals(STRUCTURE_NAMES[2])) {
              try {
                timestamp = sdf.parse(hr.getPropertyValues()[i]);
              } catch (ParseException e) {
                timestamp = new Date(Long.parseLong(hr.getPropertyValues()[i]));
              }
            }
          }

          if (recordProvider == null || contact == null) continue;

          if (after != null && timestamp != null && timestamp.before(after)) continue;

          if (recordProvider.equals(provider)) res.add(contact);
        }
      }
    } catch (IOException ex) {
      logger.error("cannot create recent_messages history", ex);
    }

    return res;
  }
  /** Adds recent message in history. */
  private void saveRecentMessageToHistory(ComparableEvtObj msc) {
    synchronized (historyID) {
      // and create it
      try {
        History history = getHistory();
        HistoryWriter writer = history.getWriter();

        SimpleDateFormat sdf = new SimpleDateFormat(HistoryService.DATE_FORMAT);

        writer.addRecord(
            new String[] {
              msc.getProtocolProviderService().getAccountID().getAccountUniqueID(),
              msc.getContactAddress(),
              sdf.format(msc.getTimestamp()),
              RECENT_MSGS_VER
            },
            NUMBER_OF_MSGS_IN_HISTORY);
      } catch (IOException ex) {
        logger.error("cannot create recent_messages history", ex);
        return;
      }
    }
  }
  /** Updates recent message in history. */
  private void updateRecentMessageToHistory(final ComparableEvtObj msg) {
    synchronized (historyID) {
      // and create it
      try {
        History history = getHistory();

        HistoryWriter writer = history.getWriter();

        writer.updateRecord(
            new HistoryWriter.HistoryRecordUpdater() {
              HistoryRecord hr;

              @Override
              public void setHistoryRecord(HistoryRecord historyRecord) {
                this.hr = historyRecord;
              }

              @Override
              public boolean isMatching() {
                boolean providerFound = false;
                boolean contactFound = false;
                for (int i = 0; i < hr.getPropertyNames().length; i++) {
                  String propName = hr.getPropertyNames()[i];

                  if (propName.equals(STRUCTURE_NAMES[0])) {
                    if (msg.getProtocolProviderService()
                        .getAccountID()
                        .getAccountUniqueID()
                        .equals(hr.getPropertyValues()[i])) {
                      providerFound = true;
                    }
                  } else if (propName.equals(STRUCTURE_NAMES[1])) {
                    if (msg.getContactAddress().equals(hr.getPropertyValues()[i])) {
                      contactFound = true;
                    }
                  }
                }

                return contactFound && providerFound;
              }

              @Override
              public Map<String, String> getUpdateChanges() {
                HashMap<String, String> map = new HashMap<String, String>();
                SimpleDateFormat sdf = new SimpleDateFormat(HistoryService.DATE_FORMAT);

                for (int i = 0; i < hr.getPropertyNames().length; i++) {
                  String propName = hr.getPropertyNames()[i];

                  if (propName.equals(STRUCTURE_NAMES[0])) {
                    map.put(
                        propName,
                        msg.getProtocolProviderService().getAccountID().getAccountUniqueID());
                  } else if (propName.equals(STRUCTURE_NAMES[1])) {
                    map.put(propName, msg.getContactAddress());
                  } else if (propName.equals(STRUCTURE_NAMES[2])) {
                    map.put(propName, sdf.format(msg.getTimestamp()));
                  } else if (propName.equals(STRUCTURE_NAMES[3]))
                    map.put(propName, RECENT_MSGS_VER);
                }

                return map;
              }
            });
      } catch (IOException ex) {
        logger.error("cannot create recent_messages history", ex);
        return;
      }
    }
  }
/**
 * The source contact service. The will show most recent messages.
 *
 * @author Damian Minkov
 */
public class MessageSourceService extends MetaContactListAdapter
    implements ContactSourceService,
        ContactPresenceStatusListener,
        ContactCapabilitiesListener,
        ProviderPresenceStatusListener,
        SubscriptionListener,
        LocalUserChatRoomPresenceListener,
        MessageListener,
        ChatRoomMessageListener,
        AdHocChatRoomMessageListener {
  /** The logger for this class. */
  private static Logger logger = Logger.getLogger(MessageSourceService.class);

  /** The display name of this contact source. */
  private final String MESSAGE_HISTORY_NAME;

  /** The type of the source service, the place to be shown in the ui. */
  private int sourceServiceType = CONTACT_LIST_TYPE;

  /**
   * Whether to show recent messages in history or in contactlist. By default we show it in
   * contactlist.
   */
  private static final String IN_HISTORY_PROPERTY =
      "net.java.sip.communicator.impl.msghistory.contactsrc.IN_HISTORY";

  /** Property to control number of recent messages. */
  private static final String NUMBER_OF_RECENT_MSGS_PROP =
      "net.java.sip.communicator.impl.msghistory.contactsrc.MSG_NUMBER";

  /** Property to control version of recent messages. */
  private static final String VER_OF_RECENT_MSGS_PROP =
      "net.java.sip.communicator.impl.msghistory.contactsrc.MSG_VER";

  /** Property to control messages type. Can query for message sub type. */
  private static final String IS_MESSAGE_SUBTYPE_SMS_PROP =
      "net.java.sip.communicator.impl.msghistory.contactsrc.IS_SMS_ENABLED";

  /**
   * The number of recent messages to store in the history, but will retrieve just
   * <tt>numberOfMessages</tt>
   */
  private static final int NUMBER_OF_MSGS_IN_HISTORY = 100;

  /** Number of messages to show. */
  private int numberOfMessages = 10;

  /** The structure to save recent messages list. */
  private static final String[] STRUCTURE_NAMES =
      new String[] {"provider", "contact", "timestamp", "ver"};

  /** The current version of recent messages. When changed the recent messages are recreated. */
  private static String RECENT_MSGS_VER = "2";

  /** The structure. */
  private static final HistoryRecordStructure recordStructure =
      new HistoryRecordStructure(STRUCTURE_NAMES);

  /** Recent messages history ID. */
  private static final HistoryID historyID =
      HistoryID.createFromRawID(new String[] {"recent_messages"});

  /** The cache for recent messages. */
  private History history = null;

  /** List of recent messages. */
  private final List<ComparableEvtObj> recentMessages = new LinkedList<ComparableEvtObj>();

  /** Date of the oldest shown message. */
  private Date oldestRecentMessage = null;

  /** The last query created. */
  private MessageSourceContactQuery recentQuery = null;

  /** The message subtype if any. */
  private boolean isSMSEnabled = false;

  /** Message history service that has created us. */
  private MessageHistoryServiceImpl messageHistoryService;

  /** Constructs MessageSourceService. */
  MessageSourceService(MessageHistoryServiceImpl messageHistoryService) {
    this.messageHistoryService = messageHistoryService;

    ConfigurationService conf = MessageHistoryActivator.getConfigurationService();

    if (conf.getBoolean(IN_HISTORY_PROPERTY, false)) {
      sourceServiceType = HISTORY_TYPE;
    }

    MESSAGE_HISTORY_NAME =
        MessageHistoryActivator.getResources().getI18NString("service.gui.RECENT_MESSAGES");

    numberOfMessages = conf.getInt(NUMBER_OF_RECENT_MSGS_PROP, numberOfMessages);

    isSMSEnabled = conf.getBoolean(IS_MESSAGE_SUBTYPE_SMS_PROP, isSMSEnabled);

    RECENT_MSGS_VER = conf.getString(VER_OF_RECENT_MSGS_PROP, RECENT_MSGS_VER);

    MessageSourceContactPresenceStatus.MSG_SRC_CONTACT_ONLINE.setStatusIcon(
        MessageHistoryActivator.getResources()
            .getImageInBytes("service.gui.icons.SMS_STATUS_ICON"));
  }

  /**
   * Returns the display name of this contact source.
   *
   * @return the display name of this contact source
   */
  @Override
  public String getDisplayName() {
    return MESSAGE_HISTORY_NAME;
  }

  /**
   * Returns default type to indicate that this contact source can be queried by default filters.
   *
   * @return the type of this contact source
   */
  @Override
  public int getType() {
    return sourceServiceType;
  }

  /**
   * Returns the index of the contact source in the result list.
   *
   * @return the index of the contact source in the result list
   */
  @Override
  public int getIndex() {
    return 0;
  }

  /**
   * Creates query for the given <tt>searchString</tt>.
   *
   * @param queryString the string to search for
   * @return the created query
   */
  @Override
  public ContactQuery createContactQuery(String queryString) {
    recentQuery = (MessageSourceContactQuery) createContactQuery(queryString, numberOfMessages);

    return recentQuery;
  }

  /**
   * Updates the contact sources in the recent query if any. Done here in order to sync with
   * recentMessages instance, and to check for already existing instances of contact sources.
   * Normally called from the query.
   */
  public void updateRecentMessages() {
    if (recentQuery == null) return;

    synchronized (recentMessages) {
      List<SourceContact> currentContactsInQuery = recentQuery.getQueryResults();

      for (ComparableEvtObj evtObj : recentMessages) {
        // the contains will use the correct equals method of
        // the object evtObj
        if (!currentContactsInQuery.contains(evtObj)) {
          MessageSourceContact newSourceContact =
              new MessageSourceContact(evtObj.getEventObject(), MessageSourceService.this);
          newSourceContact.initDetails(evtObj.getEventObject());

          recentQuery.addQueryResult(newSourceContact);
        }
      }
    }
  }

  /**
   * Searches for entries in cached recent messages in history.
   *
   * @param provider the provider which contact messages we will search
   * @param isStatusChanged is the search because of status changed
   * @return entries in cached recent messages in history.
   */
  private List<ComparableEvtObj> getCachedRecentMessages(
      ProtocolProviderService provider, boolean isStatusChanged) {
    String providerID = provider.getAccountID().getAccountUniqueID();
    List<String> recentMessagesContactIDs =
        getRecentContactIDs(
            providerID, recentMessages.size() < numberOfMessages ? null : oldestRecentMessage);

    List<ComparableEvtObj> cachedRecentMessages = new ArrayList<ComparableEvtObj>();

    for (String contactID : recentMessagesContactIDs) {
      Collection<EventObject> res =
          messageHistoryService.findRecentMessagesPerContact(
              numberOfMessages, providerID, contactID, isSMSEnabled);

      processEventObjects(res, cachedRecentMessages, isStatusChanged);
    }

    return cachedRecentMessages;
  }

  /**
   * Process list of event objects. Checks whether message source contact already exist for this
   * event object, if yes just update it with the new values (not sure whether we should do this, as
   * it may bring old messages) and if status of provider is changed, init its details, updates its
   * capabilities. It still adds the found messages source contact to the list of the new contacts,
   * as later we will detect this and fire update event. If nothing found a new contact is created.
   *
   * @param res list of event
   * @param cachedRecentMessages list of newly created source contacts or already existed but
   *     updated with corresponding event object
   * @param isStatusChanged whether provider status changed and we are processing
   */
  private void processEventObjects(
      Collection<EventObject> res,
      List<ComparableEvtObj> cachedRecentMessages,
      boolean isStatusChanged) {
    for (EventObject obj : res) {
      ComparableEvtObj oldMsg = findRecentMessage(obj, recentMessages);

      if (oldMsg != null) {
        oldMsg.update(obj); // update

        if (isStatusChanged && recentQuery != null) recentQuery.updateCapabilities(oldMsg, obj);

        // we still add it to cachedRecentMessages
        // later we will find it is duplicate and will fire
        // update event
        if (!cachedRecentMessages.contains(oldMsg)) cachedRecentMessages.add(oldMsg);

        continue;
      }

      oldMsg = findRecentMessage(obj, cachedRecentMessages);

      if (oldMsg == null) {
        oldMsg = new ComparableEvtObj(obj);

        if (isStatusChanged && recentQuery != null) recentQuery.updateCapabilities(oldMsg, obj);

        cachedRecentMessages.add(oldMsg);
      }
    }
  }

  /**
   * Access for source contacts impl.
   *
   * @return
   */
  boolean isSMSEnabled() {
    return isSMSEnabled;
  }

  /**
   * Add the ComparableEvtObj, newly added will fire new, for existing fire update and when trimming
   * the list to desired length fire remove for those that were removed
   *
   * @param contactsToAdd
   */
  private void addNewRecentMessages(List<ComparableEvtObj> contactsToAdd) {
    // now find object to fire new, and object to fire remove
    // let us find duplicates and fire update
    List<ComparableEvtObj> duplicates = new ArrayList<ComparableEvtObj>();
    for (ComparableEvtObj msgToAdd : contactsToAdd) {
      if (recentMessages.contains(msgToAdd)) {
        duplicates.add(msgToAdd);

        // save update
        updateRecentMessageToHistory(msgToAdd);
      }
    }
    recentMessages.removeAll(duplicates);

    // now contacts to add has no duplicates, add them all
    boolean changed = recentMessages.addAll(contactsToAdd);

    if (changed) {
      Collections.sort(recentMessages);

      if (recentQuery != null) {
        for (ComparableEvtObj obj : duplicates)
          recentQuery.updateContact(obj, obj.getEventObject());
      }
    }

    if (!recentMessages.isEmpty())
      oldestRecentMessage = recentMessages.get(recentMessages.size() - 1).getTimestamp();

    // trim
    List<ComparableEvtObj> removedItems = null;
    if (recentMessages.size() > numberOfMessages) {
      removedItems =
          new ArrayList<ComparableEvtObj>(
              recentMessages.subList(numberOfMessages, recentMessages.size()));

      recentMessages.removeAll(removedItems);
    }

    if (recentQuery != null) {
      // now fire, removed for all that were in the list
      // and now are removed after trim
      if (removedItems != null) {
        for (ComparableEvtObj msc : removedItems) {
          if (!contactsToAdd.contains(msc)) recentQuery.fireContactRemoved(msc);
        }
      }

      // fire new for all that were added, and not removed after trim
      for (ComparableEvtObj msc : contactsToAdd) {
        if ((removedItems == null || !removedItems.contains(msc)) && !duplicates.contains(msc)) {
          MessageSourceContact newSourceContact =
              new MessageSourceContact(msc.getEventObject(), MessageSourceService.this);
          newSourceContact.initDetails(msc.getEventObject());

          recentQuery.addQueryResult(newSourceContact);
        }
      }

      // if recent messages were changed, indexes have change lets
      // fire event for the last element which will reorder the whole
      // group if needed.
      if (changed) recentQuery.fireContactChanged(recentMessages.get(recentMessages.size() - 1));
    }
  }

  /**
   * When a provider is added, do not block and start executing in new thread.
   *
   * @param provider ProtocolProviderService
   */
  void handleProviderAdded(final ProtocolProviderService provider, final boolean isStatusChanged) {
    new Thread(
            new Runnable() {
              @Override
              public void run() {
                handleProviderAddedInSeparateThread(provider, isStatusChanged);
              }
            })
        .start();
  }

  /**
   * When a provider is added. As searching can be slow especially when handling special type of
   * messages (with subType) this need to be run in new Thread.
   *
   * @param provider ProtocolProviderService
   */
  private void handleProviderAddedInSeparateThread(
      ProtocolProviderService provider, boolean isStatusChanged) {
    // lets check if we have cached recent messages for this provider, and
    // fire events if found and are newer

    synchronized (recentMessages) {
      List<ComparableEvtObj> cachedRecentMessages =
          getCachedRecentMessages(provider, isStatusChanged);

      if (cachedRecentMessages.isEmpty()) {
        // maybe there is no cached history for this
        // let's check
        // load it not from cache, but do a local search
        Collection<EventObject> res =
            messageHistoryService.findRecentMessagesPerContact(
                numberOfMessages, provider.getAccountID().getAccountUniqueID(), null, isSMSEnabled);

        List<ComparableEvtObj> newMsc = new ArrayList<ComparableEvtObj>();

        processEventObjects(res, newMsc, isStatusChanged);

        addNewRecentMessages(newMsc);

        for (ComparableEvtObj msc : newMsc) {
          saveRecentMessageToHistory(msc);
        }
      } else addNewRecentMessages(cachedRecentMessages);
    }
  }

  /**
   * Tries to match the event object to already existing ComparableEvtObj in the supplied list.
   *
   * @param obj the object that we will try to match.
   * @param list the list we will search in.
   * @return the found ComparableEvtObj
   */
  private static ComparableEvtObj findRecentMessage(EventObject obj, List<ComparableEvtObj> list) {
    Contact contact = null;
    ChatRoom chatRoom = null;

    if (obj instanceof MessageDeliveredEvent) {
      contact = ((MessageDeliveredEvent) obj).getDestinationContact();
    } else if (obj instanceof MessageReceivedEvent) {
      contact = ((MessageReceivedEvent) obj).getSourceContact();
    } else if (obj instanceof ChatRoomMessageDeliveredEvent) {
      chatRoom = ((ChatRoomMessageDeliveredEvent) obj).getSourceChatRoom();
    } else if (obj instanceof ChatRoomMessageReceivedEvent) {
      chatRoom = ((ChatRoomMessageReceivedEvent) obj).getSourceChatRoom();
    }

    for (ComparableEvtObj evt : list) {
      if ((contact != null && contact.equals(evt.getContact()))
          || (chatRoom != null && chatRoom.equals(evt.getRoom()))) return evt;
    }

    return null;
  }

  /**
   * A provider has been removed.
   *
   * @param provider the ProtocolProviderService that has been unregistered.
   */
  void handleProviderRemoved(ProtocolProviderService provider) {
    // lets remove the recent messages for this provider, and update
    // with recent messages for the available providers
    synchronized (recentMessages) {
      if (provider != null) {
        List<ComparableEvtObj> removedItems = new ArrayList<ComparableEvtObj>();
        for (ComparableEvtObj msc : recentMessages) {
          if (msc.getProtocolProviderService().equals(provider)) removedItems.add(msc);
        }

        recentMessages.removeAll(removedItems);
        if (!recentMessages.isEmpty())
          oldestRecentMessage = recentMessages.get(recentMessages.size() - 1).getTimestamp();
        else oldestRecentMessage = null;

        if (recentQuery != null) {
          for (ComparableEvtObj msc : removedItems) {
            recentQuery.fireContactRemoved(msc);
          }
        }
      }

      // handleProviderRemoved can be invoked due to stopped
      // history service, if this is the case we do not want to
      // update messages
      if (!this.messageHistoryService.isHistoryLoggingEnabled()) return;

      // lets do the same as we enable provider
      // for all registered providers and finally fire events
      List<ComparableEvtObj> contactsToAdd = new ArrayList<ComparableEvtObj>();
      for (ProtocolProviderService pps : messageHistoryService.getCurrentlyAvailableProviders()) {
        contactsToAdd.addAll(getCachedRecentMessages(pps, true));
      }

      addNewRecentMessages(contactsToAdd);
    }
  }

  /**
   * Searches for contact ids in history of recent messages.
   *
   * @param provider
   * @param after
   * @return
   */
  List<String> getRecentContactIDs(String provider, Date after) {
    List<String> res = new ArrayList<String>();

    try {
      History history = getHistory();

      if (history != null) {
        Iterator<HistoryRecord> recs = history.getReader().findLast(NUMBER_OF_MSGS_IN_HISTORY);
        SimpleDateFormat sdf = new SimpleDateFormat(HistoryService.DATE_FORMAT);

        while (recs.hasNext()) {
          HistoryRecord hr = recs.next();

          String contact = null;
          String recordProvider = null;
          Date timestamp = null;

          for (int i = 0; i < hr.getPropertyNames().length; i++) {
            String propName = hr.getPropertyNames()[i];

            if (propName.equals(STRUCTURE_NAMES[0])) recordProvider = hr.getPropertyValues()[i];
            else if (propName.equals(STRUCTURE_NAMES[1])) contact = hr.getPropertyValues()[i];
            else if (propName.equals(STRUCTURE_NAMES[2])) {
              try {
                timestamp = sdf.parse(hr.getPropertyValues()[i]);
              } catch (ParseException e) {
                timestamp = new Date(Long.parseLong(hr.getPropertyValues()[i]));
              }
            }
          }

          if (recordProvider == null || contact == null) continue;

          if (after != null && timestamp != null && timestamp.before(after)) continue;

          if (recordProvider.equals(provider)) res.add(contact);
        }
      }
    } catch (IOException ex) {
      logger.error("cannot create recent_messages history", ex);
    }

    return res;
  }

  /**
   * Returns the cached recent messages history.
   *
   * @return
   * @throws IOException
   */
  private History getHistory() throws IOException {
    synchronized (historyID) {
      HistoryService historyService =
          MessageHistoryActivator.getMessageHistoryService().getHistoryService();

      if (history == null) {
        history = historyService.createHistory(historyID, recordStructure);

        // lets check the version if not our version, re-create
        // history (delete it)
        HistoryReader reader = history.getReader();
        boolean delete = false;
        QueryResultSet<HistoryRecord> res = reader.findLast(1);
        if (res != null && res.hasNext()) {
          HistoryRecord hr = res.next();
          if (hr.getPropertyValues().length >= 4) {
            if (!hr.getPropertyValues()[3].equals(RECENT_MSGS_VER)) delete = true;
          } else delete = true;
        }

        if (delete) {
          // delete it
          try {
            historyService.purgeLocallyStoredHistory(historyID);

            history = historyService.createHistory(historyID, recordStructure);
          } catch (IOException ex) {
            logger.error("Cannot delete recent_messages history", ex);
          }
        }
      }

      return history;
    }
  }

  /**
   * Returns the index of the source contact, in the list of recent messages.
   *
   * @param messageSourceContact
   * @return
   */
  int getIndex(MessageSourceContact messageSourceContact) {
    synchronized (recentMessages) {
      for (int i = 0; i < recentMessages.size(); i++)
        if (recentMessages.get(i).equals(messageSourceContact)) return i;

      return -1;
    }
  }

  /**
   * Creates query for the given <tt>searchString</tt>.
   *
   * @param queryString the string to search for
   * @param contactCount the maximum count of result contacts
   * @return the created query
   */
  @Override
  public ContactQuery createContactQuery(String queryString, int contactCount) {
    if (!StringUtils.isNullOrEmpty(queryString)) return null;

    recentQuery = new MessageSourceContactQuery(MessageSourceService.this);

    return recentQuery;
  }

  /**
   * Updates contact source contacts with status.
   *
   * @param evt the ContactPresenceStatusChangeEvent describing the status
   */
  @Override
  public void contactPresenceStatusChanged(ContactPresenceStatusChangeEvent evt) {
    if (recentQuery == null) return;

    synchronized (recentMessages) {
      for (ComparableEvtObj msg : recentMessages) {
        if (msg.getContact() != null && msg.getContact().equals(evt.getSourceContact())) {
          recentQuery.updateContactStatus(msg, evt.getNewStatus());
        }
      }
    }
  }

  @Override
  public void providerStatusChanged(ProviderPresenceStatusChangeEvent evt) {
    if (!evt.getNewStatus().isOnline() || evt.getOldStatus().isOnline()) return;

    handleProviderAdded(evt.getProvider(), true);
  }

  @Override
  public void providerStatusMessageChanged(PropertyChangeEvent evt) {}

  @Override
  public void localUserPresenceChanged(LocalUserChatRoomPresenceChangeEvent evt) {
    if (recentQuery == null) return;

    ComparableEvtObj srcContact = null;

    synchronized (recentMessages) {
      for (ComparableEvtObj msg : recentMessages) {
        if (msg.getRoom() != null && msg.getRoom().equals(evt.getChatRoom())) {
          srcContact = msg;
          break;
        }
      }
    }

    if (srcContact == null) return;

    String eventType = evt.getEventType();

    if (LocalUserChatRoomPresenceChangeEvent.LOCAL_USER_JOINED.equals(eventType)) {
      recentQuery.updateContactStatus(srcContact, ChatRoomPresenceStatus.CHAT_ROOM_ONLINE);
    } else if ((LocalUserChatRoomPresenceChangeEvent.LOCAL_USER_LEFT.equals(eventType)
        || LocalUserChatRoomPresenceChangeEvent.LOCAL_USER_KICKED.equals(eventType)
        || LocalUserChatRoomPresenceChangeEvent.LOCAL_USER_DROPPED.equals(eventType))) {
      recentQuery.updateContactStatus(srcContact, ChatRoomPresenceStatus.CHAT_ROOM_OFFLINE);
    }
  }

  /**
   * Handles new events.
   *
   * @param obj the event object
   * @param provider the provider
   * @param id the id of the source of the event
   */
  private void handle(EventObject obj, ProtocolProviderService provider, String id) {
    // check if provider - contact exist update message content
    synchronized (recentMessages) {
      ComparableEvtObj existingMsc = null;
      for (ComparableEvtObj msc : recentMessages) {
        if (msc.getProtocolProviderService().equals(provider)
            && msc.getContactAddress().equals(id)) {
          // update
          msc.update(obj);
          updateRecentMessageToHistory(msc);

          existingMsc = msc;
        }
      }

      if (existingMsc != null) {
        Collections.sort(recentMessages);
        oldestRecentMessage = recentMessages.get(recentMessages.size() - 1).getTimestamp();

        if (recentQuery != null) {
          recentQuery.updateContact(existingMsc, existingMsc.getEventObject());
          recentQuery.fireContactChanged(existingMsc);
        }

        return;
      }

      // if missing create source contact
      // and update recent messages, trim and sort
      MessageSourceContact newSourceContact =
          new MessageSourceContact(obj, MessageSourceService.this);
      newSourceContact.initDetails(obj);
      // we have already checked for duplicate
      ComparableEvtObj newMsg = new ComparableEvtObj(obj);
      recentMessages.add(newMsg);

      Collections.sort(recentMessages);
      oldestRecentMessage = recentMessages.get(recentMessages.size() - 1).getTimestamp();

      // trim
      List<ComparableEvtObj> removedItems = null;
      if (recentMessages.size() > numberOfMessages) {
        removedItems =
            new ArrayList<ComparableEvtObj>(
                recentMessages.subList(numberOfMessages, recentMessages.size()));

        recentMessages.removeAll(removedItems);
      }

      // save
      saveRecentMessageToHistory(newMsg);

      // no query nothing to fire
      if (recentQuery == null) return;

      // now fire
      if (removedItems != null) {
        for (ComparableEvtObj msc : removedItems) {
          recentQuery.fireContactRemoved(msc);
        }
      }

      recentQuery.addQueryResult(newSourceContact);
    }
  }

  /** Adds recent message in history. */
  private void saveRecentMessageToHistory(ComparableEvtObj msc) {
    synchronized (historyID) {
      // and create it
      try {
        History history = getHistory();
        HistoryWriter writer = history.getWriter();

        SimpleDateFormat sdf = new SimpleDateFormat(HistoryService.DATE_FORMAT);

        writer.addRecord(
            new String[] {
              msc.getProtocolProviderService().getAccountID().getAccountUniqueID(),
              msc.getContactAddress(),
              sdf.format(msc.getTimestamp()),
              RECENT_MSGS_VER
            },
            NUMBER_OF_MSGS_IN_HISTORY);
      } catch (IOException ex) {
        logger.error("cannot create recent_messages history", ex);
        return;
      }
    }
  }

  /** Updates recent message in history. */
  private void updateRecentMessageToHistory(final ComparableEvtObj msg) {
    synchronized (historyID) {
      // and create it
      try {
        History history = getHistory();

        HistoryWriter writer = history.getWriter();

        writer.updateRecord(
            new HistoryWriter.HistoryRecordUpdater() {
              HistoryRecord hr;

              @Override
              public void setHistoryRecord(HistoryRecord historyRecord) {
                this.hr = historyRecord;
              }

              @Override
              public boolean isMatching() {
                boolean providerFound = false;
                boolean contactFound = false;
                for (int i = 0; i < hr.getPropertyNames().length; i++) {
                  String propName = hr.getPropertyNames()[i];

                  if (propName.equals(STRUCTURE_NAMES[0])) {
                    if (msg.getProtocolProviderService()
                        .getAccountID()
                        .getAccountUniqueID()
                        .equals(hr.getPropertyValues()[i])) {
                      providerFound = true;
                    }
                  } else if (propName.equals(STRUCTURE_NAMES[1])) {
                    if (msg.getContactAddress().equals(hr.getPropertyValues()[i])) {
                      contactFound = true;
                    }
                  }
                }

                return contactFound && providerFound;
              }

              @Override
              public Map<String, String> getUpdateChanges() {
                HashMap<String, String> map = new HashMap<String, String>();
                SimpleDateFormat sdf = new SimpleDateFormat(HistoryService.DATE_FORMAT);

                for (int i = 0; i < hr.getPropertyNames().length; i++) {
                  String propName = hr.getPropertyNames()[i];

                  if (propName.equals(STRUCTURE_NAMES[0])) {
                    map.put(
                        propName,
                        msg.getProtocolProviderService().getAccountID().getAccountUniqueID());
                  } else if (propName.equals(STRUCTURE_NAMES[1])) {
                    map.put(propName, msg.getContactAddress());
                  } else if (propName.equals(STRUCTURE_NAMES[2])) {
                    map.put(propName, sdf.format(msg.getTimestamp()));
                  } else if (propName.equals(STRUCTURE_NAMES[3]))
                    map.put(propName, RECENT_MSGS_VER);
                }

                return map;
              }
            });
      } catch (IOException ex) {
        logger.error("cannot create recent_messages history", ex);
        return;
      }
    }
  }

  @Override
  public void messageReceived(MessageReceivedEvent evt) {
    if (isSMSEnabled && evt.getEventType() != MessageReceivedEvent.SMS_MESSAGE_RECEIVED) {
      return;
    }

    handle(evt, evt.getSourceContact().getProtocolProvider(), evt.getSourceContact().getAddress());
  }

  @Override
  public void messageDelivered(MessageDeliveredEvent evt) {
    if (isSMSEnabled && !evt.isSmsMessage()) return;

    handle(
        evt,
        evt.getDestinationContact().getProtocolProvider(),
        evt.getDestinationContact().getAddress());
  }

  /**
   * Not used.
   *
   * @param evt the <tt>MessageFailedEvent</tt> containing the ID of the
   */
  @Override
  public void messageDeliveryFailed(MessageDeliveryFailedEvent evt) {}

  @Override
  public void messageReceived(ChatRoomMessageReceivedEvent evt) {
    if (isSMSEnabled) return;

    // ignore non conversation messages
    if (evt.getEventType() != ChatRoomMessageReceivedEvent.CONVERSATION_MESSAGE_RECEIVED) return;

    handle(
        evt, evt.getSourceChatRoom().getParentProvider(), evt.getSourceChatRoom().getIdentifier());
  }

  @Override
  public void messageDelivered(ChatRoomMessageDeliveredEvent evt) {
    if (isSMSEnabled) return;

    handle(
        evt, evt.getSourceChatRoom().getParentProvider(), evt.getSourceChatRoom().getIdentifier());
  }

  /**
   * Not used.
   *
   * @param evt the <tt>ChatroomMessageDeliveryFailedEvent</tt> containing
   */
  @Override
  public void messageDeliveryFailed(ChatRoomMessageDeliveryFailedEvent evt) {}

  @Override
  public void messageReceived(AdHocChatRoomMessageReceivedEvent evt) {
    // TODO
  }

  @Override
  public void messageDelivered(AdHocChatRoomMessageDeliveredEvent evt) {
    // TODO
  }

  /**
   * Not used.
   *
   * @param evt the <tt>AdHocChatroomMessageDeliveryFailedEvent</tt>
   */
  @Override
  public void messageDeliveryFailed(AdHocChatRoomMessageDeliveryFailedEvent evt) {}

  @Override
  public void subscriptionCreated(SubscriptionEvent evt) {}

  @Override
  public void subscriptionFailed(SubscriptionEvent evt) {}

  @Override
  public void subscriptionRemoved(SubscriptionEvent evt) {}

  @Override
  public void subscriptionMoved(SubscriptionMovedEvent evt) {}

  @Override
  public void subscriptionResolved(SubscriptionEvent evt) {}

  /**
   * If a contact is renamed update the locally stored message if any.
   *
   * @param evt the <tt>ContactPropertyChangeEvent</tt> containing the source
   */
  @Override
  public void contactModified(ContactPropertyChangeEvent evt) {
    if (!evt.getPropertyName().equals(ContactPropertyChangeEvent.PROPERTY_DISPLAY_NAME)) return;

    Contact contact = evt.getSourceContact();

    if (contact == null) return;

    for (ComparableEvtObj msc : recentMessages) {
      if (contact.equals(msc.getContact())) {
        if (recentQuery != null)
          recentQuery.updateContactDisplayName(msc, contact.getDisplayName());

        return;
      }
    }
  }

  /**
   * Indicates that a MetaContact has been modified.
   *
   * @param evt the MetaContactListEvent containing the corresponding contact
   */
  public void metaContactRenamed(MetaContactRenamedEvent evt) {
    for (ComparableEvtObj msc : recentMessages) {
      if (evt.getSourceMetaContact().containsContact(msc.getContact())) {
        if (recentQuery != null) recentQuery.updateContactDisplayName(msc, evt.getNewDisplayName());
      }
    }
  }

  @Override
  public void supportedOperationSetsChanged(ContactCapabilitiesEvent event) {
    Contact contact = event.getSourceContact();

    if (contact == null) return;

    for (ComparableEvtObj msc : recentMessages) {
      if (contact.equals(msc.getContact())) {
        if (recentQuery != null) recentQuery.updateCapabilities(msc, contact);

        return;
      }
    }
  }

  /** Permanently removes all locally stored message history, remove recent contacts. */
  public void eraseLocallyStoredHistory() throws IOException {
    List<ComparableEvtObj> toRemove = null;
    synchronized (recentMessages) {
      toRemove = new ArrayList<ComparableEvtObj>(recentMessages);

      recentMessages.clear();
    }

    if (recentQuery != null) {
      for (ComparableEvtObj msc : toRemove) {
        recentQuery.fireContactRemoved(msc);
      }
    }
  }

  /**
   * Permanently removes locally stored message history for the metacontact, remove any recent
   * contacts if any.
   */
  public void eraseLocallyStoredHistory(MetaContact contact) throws IOException {
    List<ComparableEvtObj> toRemove = null;
    synchronized (recentMessages) {
      toRemove = new ArrayList<ComparableEvtObj>();
      Iterator<Contact> iter = contact.getContacts();
      while (iter.hasNext()) {
        Contact item = iter.next();
        String id = item.getAddress();
        ProtocolProviderService provider = item.getProtocolProvider();

        for (ComparableEvtObj msc : recentMessages) {
          if (msc.getProtocolProviderService().equals(provider)
              && msc.getContactAddress().equals(id)) {
            toRemove.add(msc);
          }
        }
      }

      recentMessages.removeAll(toRemove);
    }
    if (recentQuery != null) {
      for (ComparableEvtObj msc : toRemove) {
        recentQuery.fireContactRemoved(msc);
      }
    }
  }

  /**
   * Permanently removes locally stored message history for the chatroom, remove any recent contacts
   * if any.
   */
  public void eraseLocallyStoredHistory(ChatRoom room) {
    ComparableEvtObj toRemove = null;
    synchronized (recentMessages) {
      for (ComparableEvtObj msg : recentMessages) {
        if (msg.getRoom() != null && msg.getRoom().equals(room)) {
          toRemove = msg;
          break;
        }
      }

      if (toRemove == null) return;

      recentMessages.remove(toRemove);
    }

    if (recentQuery != null) recentQuery.fireContactRemoved(toRemove);
  }

  /** Object used to cache recent messages. */
  private class ComparableEvtObj implements Comparable<ComparableEvtObj> {
    private EventObject eventObject;

    /** The protocol provider. */
    private ProtocolProviderService ppService = null;

    /** The address. */
    private String address = null;

    /** The timestamp. */
    private Date timestamp = null;

    /** The contact instance. */
    private Contact contact = null;

    /** The room instance. */
    private ChatRoom room = null;

    /**
     * Constructs.
     *
     * @param source used to extract initial values.
     */
    ComparableEvtObj(EventObject source) {
      update(source);
    }

    /**
     * Extract values from <tt>EventObject</tt>.
     *
     * @param source
     */
    public void update(EventObject source) {
      this.eventObject = source;

      if (source instanceof MessageDeliveredEvent) {
        MessageDeliveredEvent e = (MessageDeliveredEvent) source;

        this.contact = e.getDestinationContact();

        this.address = contact.getAddress();
        this.ppService = contact.getProtocolProvider();
        this.timestamp = e.getTimestamp();
      } else if (source instanceof MessageReceivedEvent) {
        MessageReceivedEvent e = (MessageReceivedEvent) source;

        this.contact = e.getSourceContact();

        this.address = contact.getAddress();
        this.ppService = contact.getProtocolProvider();
        this.timestamp = e.getTimestamp();
      } else if (source instanceof ChatRoomMessageDeliveredEvent) {
        ChatRoomMessageDeliveredEvent e = (ChatRoomMessageDeliveredEvent) source;

        this.room = e.getSourceChatRoom();

        this.address = room.getIdentifier();
        this.ppService = room.getParentProvider();
        this.timestamp = e.getTimestamp();
      } else if (source instanceof ChatRoomMessageReceivedEvent) {
        ChatRoomMessageReceivedEvent e = (ChatRoomMessageReceivedEvent) source;

        this.room = e.getSourceChatRoom();

        this.address = room.getIdentifier();
        this.ppService = room.getParentProvider();
        this.timestamp = e.getTimestamp();
      }
    }

    @Override
    public String toString() {
      return "ComparableEvtObj{" + "address='" + address + '\'' + ", ppService=" + ppService + '}';
    }

    /**
     * The timestamp of the message.
     *
     * @return the timestamp of the message.
     */
    public Date getTimestamp() {
      return timestamp;
    }

    /**
     * The contact.
     *
     * @return the contact.
     */
    public Contact getContact() {
      return contact;
    }

    /**
     * The room.
     *
     * @return the room.
     */
    public ChatRoom getRoom() {
      return room;
    }

    /**
     * The protocol provider.
     *
     * @return the protocol provider.
     */
    public ProtocolProviderService getProtocolProviderService() {
      return ppService;
    }

    /**
     * The address.
     *
     * @return the address.
     */
    public String getContactAddress() {
      if (this.address != null) return this.address;

      return null;
    }

    /**
     * The event object.
     *
     * @return the event object.
     */
    public EventObject getEventObject() {
      return eventObject;
    }

    /**
     * Compares two ComparableEvtObj.
     *
     * @param o the object to compare with
     * @return 0, less than zero, greater than zero, if equals, less or greater.
     */
    @Override
    public int compareTo(ComparableEvtObj o) {
      if (o == null || o.getTimestamp() == null) return 1;

      return o.getTimestamp().compareTo(getTimestamp());
    }

    /**
     * Checks if equals, and if this event object is used to create a MessageSourceContact, if the
     * supplied <tt>Object</tt> is instance of MessageSourceContact.
     *
     * @param o the object to check.
     * @return <tt>true</tt> if equals.
     */
    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || (!(o instanceof MessageSourceContact) && getClass() != o.getClass()))
        return false;

      if (o instanceof ComparableEvtObj) {
        ComparableEvtObj that = (ComparableEvtObj) o;

        if (!address.equals(that.address)) return false;
        if (!ppService.equals(that.ppService)) return false;
      } else if (o instanceof MessageSourceContact) {
        MessageSourceContact that = (MessageSourceContact) o;

        if (!address.equals(that.getContactAddress())) return false;
        if (!ppService.equals(that.getProtocolProviderService())) return false;
      } else return false;

      return true;
    }

    @Override
    public int hashCode() {
      int result = address.hashCode();
      result = 31 * result + ppService.hashCode();
      return result;
    }
  }
}