/**
    chat messages and history

    Copyright (c) 2020-2022 The Creators of Simphone

    See the file COPYING.LESSER.txt for copying permission.
**/

#include "config.h"
#include "spth.h"

#include "logger.h"
#include "table.h"
#include "error.h"
#include "utils.h"
#include "system.h"
#include "crypto.h"
#include "file.h"
#include "socket.h"
#include "contact.h"
#include "param.h"
#include "proto.h"
#include "client.h"
#include "msg.h"
#include "xfer.h"
#include "audio.h"
#include "api.h"

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <errno.h>

#ifdef _WIN32
#include <io.h>
#ifdef _MSC_VER
#define lseek _lseeki64
#endif
#else
#if HAVE_MALLOC_USABLE_SIZE
#if HAVE_MALLOC_NP_H
#include <malloc_np.h>
#else
#include <malloc.h>
#endif
#endif /* HAVE_MALLOC_USABLE_SIZE */

#include <unistd.h>
#endif

#define SIM_MODULE "msg"

#define FILE_DIRECTORY_PENDING "pending" FILE_DIRECTORY_SLASH
#define MSG_KEY_PENDING "pending"

#define MSG_XML_HEAD "<"                                                         \
                     "!"                                                         \
                     "DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\"\n"       \
                     "\"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">\n"       \
                     "<html xmlns=\"http://www.w3.org/1999/xhtml\"><head>\n"     \
                     "<title>%" SIM_FORMAT_64 "u</title>\n<meta http-equiv=\""   \
                     "Content-Type\" content=\"text/html; charset=utf-8\"/>\n"   \
                     "<meta name=\"author\" content=\"%" SIM_FORMAT_64 "u\"/>\n" \
                     "<meta name=\"generator\" content=\"%s %s\"/>\n</head>" MSG_XML_BODY "\n"
#define MSG_XML_COMMENT "!"

#define MSG_XML_UTC_OPEN "<" MSG_XML_COMMENT "-- <" MSG_HISTORY_UTC ">"
#define MSG_XML_UTC_CLOSE "</" MSG_HISTORY_UTC ">"

static const char *msg_type_names[] = { "", "s", "x" };

struct msg_queue { /* msg queue item */
  pth_message_t header;
  int idx;
};

int msg_fd_count = 0;

#define MSG_NEW_NICK(nick, string, length) \
  (string_check_diff_len (nick, string, length) ? string_copy_string (nick) : pointer_new_len (string, length))
#define MSG_GET_NAK(client) \
  param_get_number ((client) && (client)->xfer.current && SOCKET_CHECK_PROXY ((client)->sock) ? "xfer.nak" : "msg.nak")

#define MSG_CHECK_UNACKED(status) \
  ((status) != SIM_MSG_INCOMING && (status) != SIM_MSG_DELIVERED && (status) != SIM_MSG_ACKNOWLEDGED)
#define MSG_CHECK_UNSENT(status) \
  ((status) == SIM_MSG_UNSENT || (status) == SIM_MSG_UNDELIVERED || (status) == SIM_MSG_NOTDELIVERED)
#define MSG_CHECK_NOTSENT(status) ((status) == SIM_MSG_NOTSENT || (status) == SIM_MSG_NOTDELIVERED)
#define MSG_CHECK_REMOVED(message) ((message)->text.typ == SIMNIL)

simtype sim_msg_convert_to_xml (const struct _message *message, unsigned datetime,
                                char output[512], unsigned *spaces, unsigned *nicklen) {
  static const char msg_padding[SIM_MAX_NICK_LENGTH + 4];
  unsigned len;
  simtype table = table_new (0), comment = table_new (0);
  table_add_number (comment, MSG_HISTORY_UTC, message->sndtime);
  if (message->rcvtime)
    table_add_number (comment, MSG_HISTORY_TIME, message->rcvtime);
  if (message->handle)
    table_add_number (comment, MSG_HISTORY_HANDLE, message->handle);
  if (message->oldhandle) {
    const char *key = message->type == SIM_MSG_TYPE_SYSTEM ? MSG_HISTORY_XFER : MSG_HISTORY_EDIT;
    table_add_number (comment, key, message->oldhandle);
  }
  len = sim_convert_time_to_string (datetime, output);
  output[len++] = ' ';
  memcpy (output + len, message->nick.str, message->nick.len);
  len += message->nick.len; /* should actually truncate the nick to SIM_MAX_NICK_LENGTH characters (not bytes) */
  output[len++] = ':';
  output[len++] = ' ';
  table_add_pointer_len (table, MSG_HISTORY_DATE, output, len);
  *nicklen = message->nick.len;
  string_free (sim_convert_utf_to_int (message->nick.ptr, nicklen));
  len = *nicklen > SIM_MAX_NICK_LENGTH + 3 ? 1 : SIM_MAX_NICK_LENGTH + 4 - *nicklen;
  table_add_pointer_len (table, MSG_XML_COMMENT "--", msg_padding, len);
  table_add_pointer_len (table, MSG_HISTORY_MSG, message->text.str, message->text.len);
  table_add (table, MSG_XML_COMMENT "-- ", comment);
  *spaces = SIM_STRING_GET_LENGTH_CONST ("<" MSG_HISTORY_SYSTEM ">") +
            SIM_STRING_GET_LENGTH_CONST ("<" MSG_HISTORY_INCOMING ">");
  if (message->type == SIM_MSG_TYPE_SYSTEM) {
    simtype underline = table;
    table_add (table = table_new (1), MSG_HISTORY_SYSTEM, underline);
    *spaces -= SIM_STRING_GET_LENGTH_CONST ("<" MSG_HISTORY_SYSTEM ">");
  }
  if (message->status == SIM_MSG_INCOMING) {
    simtype bold = table;
    table_add (table = table_new (1), MSG_HISTORY_INCOMING, bold);
    *spaces -= SIM_STRING_GET_LENGTH_CONST ("<" MSG_HISTORY_INCOMING ">");
  }
  return table;
}

static simtype msg_convert_to_table (const struct _message *message, time_t sendtime) {
  simtype table = table_new (2);
  if (message->type != SIM_MSG_TYPE_UTF)
    table_add_pointer (table, SIM_CMD_MSG_TYPE, msg_type_names[message->type]);
  if (sendtime)
    table_add_number (table, SIM_CMD_MSG_TIME, sendtime);
  table_add_number (table, SIM_CMD_MSG_HANDLE, message->handle);
  if (message->oldhandle)
    table_add_number (table, SIM_CMD_MSG_EDIT, message->oldhandle);
  table_add_pointer_len (table, SIM_CMD_MSG_TEXT, message->text.str, message->text.len);
  return table;
}

static simbool msg_convert_table (simtype table, simtype text, time_t recvtime, int status, struct _message *message) {
  simtype type = table_get (table, SIM_CMD_MSG_TYPE);
  simnumber msgtime = table_get_number (table, SIM_CMD_MSG_TIME);
  const char *s = type.typ == SIMSTRING || type.typ == SIMPOINTER ? type.ptr : "";
  int l, msgtype = sim_convert_string_to_enum (s, 0, SIM_MSG_TYPE_HTML, SIM_ARRAY (msg_type_names));
  if ((message->status = (simbyte) status) == SIM_MSG_INCOMING && msgtime > recvtime)
    msgtime = recvtime;
  message->type = SIM_MSG_TYPE_UTF;
  message->status = (simbyte) status;
  message->sndtime = msgtime < 0 || msgtime != (unsigned) msgtime ? 0 : (unsigned) msgtime;
  message->rcvtime = recvtime;
  message->offset = 0;
  message->handle = table_get_number (table, SIM_CMD_MSG_HANDLE);
  message->oldhandle = table_get_number (table, SIM_CMD_MSG_EDIT);
  message->text = text;
  if ((type.typ != SIMNIL && type.typ != SIMSTRING && type.typ != SIMPOINTER) || text.typ == SIMNIL)
    return false;
  if (msgtype == SIM_MSG_TYPE_SYSTEM) {
    message->type = SIM_MSG_TYPE_SYSTEM;
    l = message->oldhandle ? SIM_STRING_GET_LENGTH_CONST ("FILE SEND ") : SIM_STRING_GET_LENGTH_CONST ("CALL FAILED ");
    if (strncmp (text.ptr, message->oldhandle ? "FILE SEND " : "CALL FAILED ", l))
      return false;
    if (message->oldhandle) {
      if (message->handle)
        return false;
    } else if (atoi ((char *) &text.str[l]) >= 0) {
      string_free (text);
      message->text = string_copy ("CALL FAILED -999"); /* msg_new_text ("CALL", "FAILED", SIM_NO_ERROR); */
    }
  } else if (msgtype != SIM_MSG_TYPE_UTF)
    return false;
  return true;
}

static simtype msg_new_table (const struct _message *message) {
  simtype table = nil ();
  if (! MSG_CHECK_REMOVED (message)) {
    time_t delta = time (NULL) - message->sndtime;
    table_add_pointer (table = msg_convert_to_table (message, (int) delta < 0 ? 0 : delta), SIM_CMD, SIM_CMD_MSG);
    if (message->type == SIM_MSG_TYPE_SYSTEM && ! SIM_STRING_CHECK_DIFF_CONST (message->text.ptr, "CALL FAILED "))
      if (atoi ((char *) message->text.str + SIM_STRING_GET_LENGTH_CONST ("CALL FAILED ")) >= 0)
        table_set_pointer (table, SIM_CMD_MSG_TEXT, "CALL FAILED -999"); /* SIM_NO_ERROR */
  }
  return table;
}

static simtype msg_new_text (const char *tag, const char *text, simnumber number) {
  char buf[23];
  sprintf (buf, " %" SIM_FORMAT_64 "d ", number);
  if (! SIM_STRING_CHECK_DIFF_CONST (tag, "FILE "))
    return string_concat (tag, buf, text, NULL);
  buf[strlen (buf) - 1] = 0;
  return number ? string_concat (tag, text, buf, NULL) : string_cat (tag, text);
}

static void msg_set_status (simclient client, int idx, simbool sent) {
  struct _message *msg = &client->contact->msgs.queue[idx];
  if (! sent) {
    event_send_value (client->contact, SIM_EVENT_SENT, SIM_EVENT_MSG, array_new_type (number_new (idx)));
    msg->status = SIM_MSG_DELIVERED;
  } else if (msg->status != SIM_MSG_UNSENT) {
    client->contact->msgs.flags |= CONTACT_MSG_UNSENT;
    msg->status = client->sock->err == SIM_OK ? SIM_MSG_SENT : SIM_MSG_NOTSENT;
  } else
    msg->status = client->sock->err == SIM_OK ? SIM_MSG_UNDELIVERED : SIM_MSG_NOTDELIVERED;
}

static void msg_set_sizes (simcontact contact, simnumber sizes) {
  int flag = 0;
  simnumber maxsize = contact->msgs.maxsize;
  if (! maxsize) {
    maxsize = sizes + 1;
  } else if (contact->msgs.flags & CONTACT_MSG_OVERFLOW && contact->msgs.queue)
    maxsize -= SIM_POINTER_SIZE (contact->msgs.queue, sizeof (*contact->msgs.queue) * (contact->msgs.size + 1));
  if ((contact->msgs.sizes = sizes) >= maxsize)
    flag = CONTACT_MSG_OVERFLOW;
  if ((contact->msgs.flags & CONTACT_MSG_OVERFLOW) != flag) {
    simnumber flags = contact->flags & CONTACT_FLAGS_EVENT;
    if ((contact->msgs.flags = (contact->msgs.flags & ~CONTACT_MSG_OVERFLOW) | flag) & CONTACT_MSG_OVERFLOW)
      flags &= ~CONTACT_FLAGS_OVERFLOW;
    event_send_value (contact, SIM_EVENT_STATUS, CONTACT_KEY_FLAGS, number_new (flags));
    if (contact->client)
      client_send_status (contact->client, CLIENT_SEND_STATUS_ON);
  }
}

static simnumber msg_get_sizes (simcontact contact, simnumber *bytes) {
  simnumber sizes = SIM_POINTER_SIZE (contact->msgs.handles, sizeof (*contact->msgs.handles) * contact->msgs.hashsize);
  *bytes = contact->msgs.queue ? sizeof (*contact->msgs.queue) * (contact->msgs.size + 1) : 0;
  sizes += contact->msgs.queue ? SIM_POINTER_SIZE (contact->msgs.queue, *bytes) : 0;
  *bytes += sizeof (*contact->msgs.handles) * (contact->msgs.hashsize + contact->msgs.size + 1);
  sizes += SIM_POINTER_SIZE (contact->msgs.indexes, sizeof (*contact->msgs.indexes) * (contact->msgs.size + 1));
  return sizes;
}

simtype msg_get_text (simnumber id, unsigned idx, simnumber *duration) {
  simcontact contact = contact_list_find_id (id);
  struct _messages *msgs = contact ? &contact->msgs : NULL;
  struct _message *msg = msgs && idx && idx <= msgs->count ? &msgs->queue[msgs->indexes[idx]] : NULL;
  if (duration)
    *duration = msg ? (simnumber) (msg->rcvtime - msg->sndtime) : -1;
  return ! msg ? nil () : msg->type != SIM_MSG_TYPE_SYSTEM ? pointer_new_len (msg->text.str, msg->text.len) : msg->text;
}

simbool msg_get_ (simclient client, simbool init) {
  struct msg_queue *item = (struct msg_queue *) pth_queue_get (client->msgqueue);
  while (item) {
    unsigned i, len = 0, size = pth_msgport_pending (client->msgqueue) + 1;
    unsigned max = socket_size_client (client->sock, 0, (int *) &i);
    simtype msgs = array_new_tables (size < max / 14 + 1 ? size : max / 14 + 1), many;
    if (CLIENT_CHECK_VERSION (client, 3)) {
      table_add (many = table_new (1), SIM_CMD_MANY_MSG, array_new_tables (0));
    } else
      many = client_new_cmd (SIM_CMD_MANY, SIM_CMD_MANY_MSG, array_new_tables (0), NULL, nil ());
    size = table_size (many, 0);
    table_free (many);
    for (i = 1; i <= msgs.len; i++) {
      int idx = item->idx;
      struct _message *msg = &client->contact->msgs.queue[idx];
      if (idx < 0) {
        sim_free (item, sizeof (*item));
        array_free (msgs);
        return false;
      }
      if (idx) {
        if (msg->status != SIM_MSG_ACKNOWLEDGED) {
          if ((msgs.arr[i] = msg_new_table (msg)).typ != SIMNIL) {
            if (i == 1) {
              table_add_number (msgs.arr[i], SIM_CMD_MSG_ACK, client->contact->msgs.msgnum);
              client->contact->msgs.rcvtick = 0;
            }
            if ((size += (len = table_size (msgs.arr[i], 0))) >= max) {
              if (init) {
                sim_free (item, sizeof (*item)), item = NULL;
                msgs.len = i;
              } else if (table_free (msgs.arr[i]), (msgs.len = i - 1) == 0) {
                sim_free (item, sizeof (*item)), item = NULL;
                LOG_ERROR_ ("message too long (%d bytes) '%s'\n", len, client->contact->nick.str);
              }
              break;
            }
          }
          msg_set_status (client, idx, msgs.arr[i].typ != SIMNIL);
        } else {
          msg->status = SIM_MSG_DELIVERED;
          LOG_NOTE_ ("skipping message #%llu to '%s'\n", msg->handle, client->contact->nick.str);
        }
      }
      sim_free (item, sizeof (*item));
      item = (struct msg_queue *) pth_queue_get (client->msgqueue);
    }
    if (len)
      LOG_DEBUG_ ("sending %d messages (%d left) to '%s'\n",
                  msgs.len, pth_msgport_pending (client->msgqueue), client->contact->nick.str);
    for (i = 1; i <= msgs.len; i++)
      if ((many = array_detach (msgs, i)).typ != SIMNIL) {
        table_set (many, SIM_CMD_MSG_TEXT, string_copy_string (table_get_string (many, SIM_CMD_MSG_TEXT)));
        client_send (client, many);
      }
    array_free (msgs);
    if (item)
      client_send_ (client, nil ());
  }
  return true;
}

unsigned msg_get_hash (simcontact contact, simnumber handle) {
  struct _message *queue = contact->msgs.queue;
  unsigned h = sim_table_hash (&handle, sizeof (handle), contact->msgs.hashsalt) & (contact->msgs.hashsize - 1), i;
  for (i = contact->msgs.handles[h]; i && queue[i].handle != handle; i = queue[i].next) {}
  return i;
}

static void msg_put_hash (simcontact contact, unsigned idx, simbool xfer) {
  simnumber *handle = ! xfer ? &contact->msgs.queue[idx].handle : &contact->msgs.queue[idx].oldhandle;
  unsigned h = sim_table_hash (handle, sizeof (*handle), contact->msgs.hashsalt);
  contact->msgs.queue[idx].next = contact->msgs.handles[h &= contact->msgs.hashsize - 1];
  contact->msgs.handles[h] = idx;
}

int msg_put (simclient client, int idx) {
  int err;
  struct msg_queue *item = sim_new (sizeof (*item));
  item->idx = idx;
  if ((err = pth_queue_put (client->msgqueue, &item->header)) != SIM_OK)
    sim_free (item, sizeof (*item));
  return err;
}

static int msg_put_all (simclient client) {
  int err = SIM_OK, sample = client->contact->msgs.callsample, flag = 0;
  unsigned i;
  simnumber start = MSG_CHECK_CALL (client->contact), pinging = client->contact->msgs.pinging;
  MSG_UNSET_CALL (client->contact);
  if (pinging != MSG_NUMBER_INVALID) {
    client->contact->msgs.pinging = MSG_NUMBER_INVALID;
    err = client_send_cmd (client, SIM_CMD_PING, NULL, nil (), SIM_CMD_PING_PONG_NUMBER, number_new (pinging));
    if (err != SIM_OK)
      return err;
  }
  if (start) {
    client->flags |= CLIENT_FLAG_RINGING;
    if (! (client->contact->flags & CONTACT_FLAG_AUDIO)) {
      event_send_audio (client->contact, CONTACT_AUDIO_OUTGOING, CONTACT_AUDIO_HANGUP);
      event_send_history_system (client->contact, "CALL ", "FAILED", 0, start, SIM_API_NO_RIGHT, true);
    } else if ((err = audio_start (client, sample, AUDIO_CALL_OUTGOING, true)) != SIM_OK)
      return err;
  }
  client->contact->msgs.sndtick = system_get_tick () + MSG_GET_NAK (client) * 1000;
  for (i = 1; i <= client->contact->msgs.len; i++) {
    struct _message *msg = &client->contact->msgs.queue[i];
    if (MSG_CHECK_UNACKED (msg->status) && msg->handle) {
      if (client->contact->flags & CONTACT_FLAG_UTF || msg->type == SIM_MSG_TYPE_SYSTEM) {
        msg->status = MSG_CHECK_UNSENT (msg->status) ? SIM_MSG_UNSENT : SIM_MSG_PENDING;
        if ((err = msg_put (client, i)) != SIM_OK)
          break;
        client->contact->msgs.resent = msg->handle;
      } else
        flag = CONTACT_MSG_NOTPUT;
    }
  }
  client->contact->msgs.flags = (client->contact->msgs.flags & ~CONTACT_MSG_NOTPUT) | flag;
  return err;
}

static void *thread_msg_ (void *arg) {
  simclient client = arg;
  int fd = client->sock->fd, idx = 0;
  LOG_API_DEBUG_ ("$%d '%s'\n", fd, client->contact->nick.str);
  while (idx >= 0 && pth_queue_wait_ (client->msgevent, fd) > 0) {
    simtype table = nil ();
    if (pth_msgport_pending (client->msgqueue) <= 1) {
      struct msg_queue *item = (struct msg_queue *) pth_queue_get (client->msgqueue);
      struct _message *msg = &client->contact->msgs.queue[idx = item->idx];
      sim_free (item, sizeof (*item));
      if (idx > 0) {
        if (msg->status != SIM_MSG_ACKNOWLEDGED) {
          if ((table = msg_new_table (msg)).typ != SIMNIL) {
            table_add_number (table, SIM_CMD_MSG_ACK, client->contact->msgs.msgnum);
            LOG_XTRA_ ("sending message #%llu to '%s'\n", msg->handle, client->contact->nick.str);
            client->contact->msgs.rcvtick = 0;
          }
          msg_set_status (client, idx, table.typ != SIMNIL);
        } else {
          msg->status = SIM_MSG_DELIVERED;
          LOG_NOTE_ ("skipping message #%llu to '%s'\n", msg->handle, client->contact->nick.str);
        }
      }
    } else
      idx = msg_get_ (client, false) ? 0 : -1;
    client_send_ (client, table);
  }
  LOG_API_DEBUG_ ("$%d:$%d\n", fd, client->sock->fd);
  return pth_thread_exit_ (false);
}

int msg_start_thread (simclient client) {
  int err = pth_queue_new (&client->msgqueue, &client->msgevent, client->sock->fd);
  client->msgtid = NULL;
  if (err == SIM_OK) {
    if ((err = msg_put_all (client)) == SIM_OK)
      if ((err = pth_thread_spawn (thread_msg_, client, &client->msgtid, client->sock->fd)) == SIM_OK)
        return SIM_OK;
    pth_queue_free (&client->msgqueue, &client->msgevent, sizeof (struct msg_queue));
  }
  return err;
}

void msg_stop_thread_ (simclient client) {
  if (client->msgtid) {
    msg_put (client, -1);
    if (pth_thread_join_ (&client->msgtid, NULL, thread_msg_, client->sock->fd) == SIM_OK)
      pth_queue_free (&client->msgqueue, &client->msgevent, sizeof (struct msg_queue));
    msg_close (client->contact);
    if (client->contact->msgs.flags & CONTACT_MSG_UNSAVED)
      msg_save (client->contact);
  }
}

void msg_save (simcontact contact) {
  int err = SIM_NO_ERROR;
  unsigned i, j;
  simtype name = sim_convert_simunsigned_to_string (FILE_DIRECTORY_PENDING, contact->id, NULL);
  simtype table = contact->msgs.pending, saved = array_new_tables (contact->msgs.len);
  contact->msgs.flags &= ~CONTACT_MSG_UNSAVED;
  msg_close (contact);
  for (i = j = 1; i <= saved.len; i++) {
    struct _message *msg = &contact->msgs.queue[i];
    if (MSG_CHECK_UNACKED (msg->status)) {
      saved.arr[j] = msg_convert_to_table (msg, msg->sndtime);
      if (msg->rcvtime)
        table_add_number (saved.arr[j], SIM_CMD_MSG_RECEIVED, msg->rcvtime);
      table_add_number (saved.arr[j++], SIM_CMD_MSG_STATUS, msg->status);
    }
  }
  if (table.typ == SIMNIL)
    contact->msgs.pending = table = table_new (1);
  if ((saved.len = j - 1) != 0) {
    table_add (table, MSG_KEY_PENDING, saved);
  } else
    array_free (saved);
  if (table_count (table)) {
    event_test_error (contact, SIM_EVENT_ERROR_FILE_SAVE, err = file_save (table, name.ptr, FILE_TYPE_ENCRYPTED));
    contact->msgs.flags |= CONTACT_MSG_SAVED;
    table_delete (table, MSG_KEY_PENDING);
  } else {
    contact->msgs.flags &= ~CONTACT_MSG_UNSENT; /* not needed? */
    if (contact->msgs.flags & CONTACT_MSG_SAVED) {
      if ((err = file_save (nil (), name.ptr, FILE_TYPE_ENCRYPTED)) == SIM_OK)
        contact->msgs.flags &= ~CONTACT_MSG_SAVED;
      event_test_error (contact, SIM_EVENT_ERROR_FILE_DELETE, err);
    }
  }
  string_free (name);
  if (err == SIM_OK)
    LOG_DEBUG_ ("saved %d unsent messages to '%s'\n", saved.len, contact->nick.str);
}

void msg_periodic (simcontact contact, simunsigned tick) {
  unsigned i, j, k;
  simunsigned timeout;
  if (contact->msgs.flags & CONTACT_MSG_REMOVED)
    msg_close (contact);
  if (contact->msgs.flags & CONTACT_MSG_UNSENT && tick >= (simunsigned) contact->msgs.sndtick) {
    simtype event, notsent;
    contact->msgs.flags &= ~CONTACT_MSG_UNSENT;
    for (i = 1; i <= contact->msgs.len; i++) {
      int status = contact->msgs.queue[i].status;
      if (status == SIM_MSG_SENT || status == SIM_MSG_NOTSENT || status == SIM_MSG_PENDING) {
        for (j = i; j <= contact->msgs.len; j++)
          switch (contact->msgs.queue[j].status) {
            case SIM_MSG_PENDING:
              contact->msgs.queue[j].status = SIM_MSG_UNSENT;
              break;
            case SIM_MSG_SENT:
              contact->msgs.queue[j].status = SIM_MSG_UNDELIVERED;
              break;
            case SIM_MSG_NOTSENT:
              contact->msgs.queue[j].status = SIM_MSG_NOTDELIVERED;
          }
        notsent = array_new_numbers (j - i);
        for (j = k = 0; notsent.len; notsent.len--) {
          struct _message *msg = &contact->msgs.queue[i++];
          if (msg->status != SIM_MSG_INCOMING && msg->status != SIM_MSG_DELIVERED && msg->index) {
            notsent.arr[++j] = number_new (msg->index);
            /*if (! (contact->msgs.flags & CONTACT_MSG_NOTSENT))*/
            k = k || msg->type != SIM_MSG_TYPE_SYSTEM || msg->oldhandle;
          }
        }
        if ((notsent.len = j) != 0)
          contact->msgs.flags |= CONTACT_MSG_NOTSENT;
        table_add_number (event = table_new_name (2, SIM_EVENT_NOTSENT), SIM_EVENT_NOTSENT, k);
        table_add (event, SIM_EVENT_MSG, notsent);
        event_send (contact, event);
        break;
      }
    }
  }
  timeout = contact->msgs.savetimeout ? contact->msgs.savetimeout : param_get_number ("msg.save") * 1000;
  if (tick - contact->msgs.savetick >= timeout) {
    contact->msgs.savetick = tick;
    if (contact->msgs.flags & CONTACT_MSG_UNSAVED) {
      contact->msgs.savetimeout = timeout * 2;
      LOG_DEBUG_ ("save time = %lld '%s'\n", contact->msgs.savetimeout / 1000, contact->nick.str);
      msg_save (contact);
      if (contact->msgs.flags & CONTACT_MSG_SAVED)
        return;
    }
    if (contact->msgs.savetimeout)
      LOG_DEBUG_ ("save time = %d '%s'\n", param_get_number ("msg.save"), contact->nick.str);
    contact->msgs.savetimeout = 0;
  }
}

static int msg_write (simcontact contact, const char *message, unsigned length) {
  int fd = contact->msgs.fd, ret = sim_file_write (fd, message, length);
  if (ret == (int) length)
    return SIM_OK;
  if (ret < 0) {
    LOG_WARN_ ("history write error %d '%s'\n", errno, contact->nick.str);
    return errno ? errno : ENOSPC;
  }
  if (ret) {
    lseek (fd, -ret, SEEK_CUR);
    sim_file_truncate (fd);
  }
  LOG_WARN_ ("history write failed '%s'\n", contact->nick.str);
  return SIM_FILE_NO_SPACE;
}

static int event_test_error_history (simcontact contact, const char *type, int error) {
  if (error != SIM_OK) {
    const char *info;
    int err = sim_error_save_text (&info);
    simtype name = sim_convert_simunsigned_to_string (FILE_DIRECTORY_HISTORY, contact->id, FILE_EXTENSION_HISTORY);
    sim_error_set_text (" [", name.ptr, "]", error);
    event_test_error (contact, type, error);
    string_free (name);
    sim_error_load_text (info, err);
  }
  return error;
}

void msg_close (simcontact contact) {
  int err = SIM_OK;
  if (contact->msgs.fd >= 0) {
    if (! (contact->msgs.flags & (CONTACT_MSG_APPEND | CONTACT_MSG_REMOVED)))
      err = msg_write (contact, MSG_XML_TAIL "\n", SIM_STRING_GET_LENGTH_CONST (MSG_XML_TAIL "\n"));
#ifndef _WIN32
    SOCKET_DELETE (contact->msgs.fd);
    msg_fd_count--;
#endif
    if (sim_file_close (contact->msgs.fd)) {
      LOG_WARN_ ("history close error %d '%s'\n", errno, contact->nick.str);
      if (err == SIM_OK)
        err = errno ? errno : ENOSPC;
    }
    contact->msgs.fd = -1;
    contact->msgs.flags &= ~CONTACT_MSG_REMOVED;
  }
  event_test_error_history (contact, SIM_EVENT_ERROR_FILE_CLOSE, err);
}

static int msg_open (simcontact contact, struct _message *message) {
  int fd, err = SIM_OK;
  unsigned spaces = 6, len;
  simnumber offset = -1, ofs;
  simtype table, str;
  char buf[512];
  if (message && ! CONTACT_CHECK_RIGHT_HISTORY (contact))
    return SIM_OK;
  if ((fd = contact->msgs.fd) < 0) {
    simtype name = sim_convert_simunsigned_to_string (FILE_DIRECTORY_HISTORY, contact->id, FILE_EXTENSION_HISTORY);
#ifndef _WIN32
    if (msg_fd_count >= socket_max_limit[SOCKET_MAX_FDS]) {
      unsigned i;
      LOG_INFO_ ("history fds = %d\n", msg_fd_count);
      for (i = 1; i <= contact_list.array.len; i++)
        msg_close (contact_list.array.arr[i].ptr);
    }
#endif
    contact->msgs.flags &= ~CONTACT_MSG_APPEND;
    fd = sim_file_create (name.ptr, FILE_BIT_READ | FILE_BIT_WRITE | FILE_BIT_CREATE | FILE_BIT_TEXT);
    if (fd < 0 && errno) {
      if ((fd = sim_file_create (name.ptr, FILE_BIT_APPEND | FILE_BIT_WRITE | FILE_BIT_TEXT)) < 0) {
        LOG_WARN_ ("history create error %d '%s'\n", errno, contact->nick.str);
      } else
        contact->msgs.flags |= CONTACT_MSG_APPEND;
    }
    err = errno;
    string_free (name);
    if ((contact->msgs.fd = fd) < 0)
      return message ? event_test_error_history (contact, SIM_EVENT_ERROR_FILE_CREATE, err) : err;
#ifndef _WIN32
    if (SOCKET_ADD (fd).typ != SIMNIL)
      LOG_ERROR_ ("zombie fd:%d '%s'\n", fd, contact->nick.str);
    msg_fd_count++;
#endif
    err = SIM_OK;
    if (message && ! (contact->msgs.flags & CONTACT_MSG_APPEND)) {
      if (lseek (fd, -3, SEEK_END) <= 0) {
        sprintf (buf, MSG_XML_HEAD, contact->id, contact_list.me->id, PACKAGE_NAME, PACKAGE_VERSION);
        if ((err = msg_write (contact, buf, strlen (buf))) != SIM_OK)
          return event_test_error_history (contact, SIM_EVENT_ERROR_FILE_WRITE, err);
      } else if ((ofs = sim_file_read (fd, buf, 3)) > 0) {
        const int l = SIM_STRING_GET_LENGTH_CONST (MSG_XML_TAIL);
        while (ofs-- && (buf[ofs] == '\n' || buf[ofs] == '\r' || buf[ofs] == 0x1A)) {}
        ofs = lseek (fd, ofs - l - 2, SEEK_END);
        if (ofs >= 0 && sim_file_read (fd, buf, l) == l && ! memcmp (buf, MSG_XML_TAIL "\n", l))
          offset = lseek (fd, ofs, SEEK_SET); /* no need to truncate file (shortest possible line longer than XHTML tail) */
      }
    }
  }
  if (! message) {
    if (err == SIM_OK && contact->msgs.fd >= 0)
      contact->msgs.flags |= CONTACT_MSG_REMOVED;
    return contact->msgs.flags & CONTACT_MSG_APPEND ? EPERM : err;
  }
  table = sim_msg_convert_to_xml (message, message->sndtime, buf, &spaces, &len);
  if ((str = sim_convert_type_to_xml (table, MSG_HISTORY_ENTRY)).len) {
    str.str[str.len] = '\n'; /* overwrite the terminating zero */
    if (offset < 0)
      offset = lseek (fd, 0, contact->msgs.flags & CONTACT_MSG_APPEND ? SEEK_END : SEEK_CUR);
    if (! spaces || (err = msg_write (contact, "      ", spaces)) == SIM_OK)
      if ((err = msg_write (contact, str.ptr, str.len + 1)) == SIM_OK && offset >= 0) {
        simtype s = sim_convert_type_to_xml (message->nick, NULL);
        if (s.len >= len)
          message->offset = offset + s.len - len + SIM_MAX_NICK_LENGTH +
                            SIM_STRING_GET_LENGTH_CONST ("<" MSG_HISTORY_ENTRY ">") +
                            SIM_STRING_GET_LENGTH_CONST ("<" MSG_HISTORY_INCOMING ">") +
                            SIM_STRING_GET_LENGTH_CONST ("<" MSG_HISTORY_SYSTEM ">") +
                            SIM_STRING_GET_LENGTH_CONST ("<" MSG_HISTORY_DATE ">") * 2 +
                            1 + SIM_SIZE_TIME + SIM_STRING_GET_LENGTH_CONST (" <-- " MSG_XML_COMMENT " --> ") + 2;
        string_free (s);
      }
    contact->msgs.flags &= ~CONTACT_MSG_REMOVED;
  } else
    LOG_ERROR_SIMTYPE_ (table, 0, "history message invalid ");
  table_free (table);
  string_free (str);
  return event_test_error_history (contact, SIM_EVENT_ERROR_FILE_WRITE, err);
}

static unsigned msg_size (const struct _message *message) {
  unsigned size = message->text.typ == SIMSTRING ? SIM_POINTER_SIZE (message->text.ptr, message->text.len + 1) : 0;
  return message->nick.typ == SIMSTRING ? size + SIM_POINTER_SIZE (message->nick.ptr, message->nick.len + 1) : size;
}

static void msg_append (simcontact contact, simclient client, struct _message *message, unsigned idx) {
  if (contact->msgs.len + 1 > contact->msgs.size) {
    simnumber bytes;
    unsigned newsize = contact->msgs.len * 2 + 1;
    struct _message *queue = sim_new (sizeof (*queue) * (newsize + 1));
    unsigned *indexes;
    memcpy (queue + 1, contact->msgs.queue + 1, sizeof (*queue) * contact->msgs.len);
    contact->msgs.sizes -= msg_get_sizes (contact, &bytes);
    sim_free (contact->msgs.queue, sizeof (*queue) * (contact->msgs.size + 1));
    indexes = sim_new (sizeof (*indexes) * (newsize + 1));
    memcpy (indexes + 1, contact->msgs.indexes + 1, sizeof (*indexes) * contact->msgs.count);
    sim_free (contact->msgs.indexes, sizeof (*indexes) * (contact->msgs.size + 1));
    contact->msgs.queue = queue;
    contact->msgs.indexes = indexes;
    contact->msgs.size = newsize;
    contact->msgs.sizes += msg_get_sizes (contact, &bytes);
  }
  if (message->handle || message->type == SIM_MSG_TYPE_SYSTEM) {
    if (! idx) {
      idx = ++contact->msgs.count;
    } else
      contact->msgs.queue[contact->msgs.indexes[idx]].index = 0;
    contact->msgs.indexes[message->index = idx] = contact->msgs.len + 1;
  }
  if (message->status != SIM_MSG_INCOMING) {
    if (message->status != SIM_MSG_DELIVERED || (message->type != SIM_MSG_TYPE_SYSTEM && idx == (unsigned) -1)) {
      contact->msgs.flags |= CONTACT_MSG_UNSAVED;
      contact->msgs.sndtick = system_get_tick () + MSG_GET_NAK (client) * 1000;
    }
    if (idx == (unsigned) -1) {
      contact->msgs.queue[++contact->msgs.len] = *message;
      return;
    }
    contact->msgs.own = pointer_new_len (message->nick.str, message->nick.len);
  } else
    contact->msgs.nick = pointer_new_len (message->nick.str, message->nick.len);
  message->tonick = contact->msgs.nick.ptr;
  contact->msgs.queue[++contact->msgs.len] = *message;
  if (message->handle)
    msg_put_hash (contact, contact->msgs.len, false);
  msg_set_sizes (contact, msg_size (message) + contact->msgs.sizes);
}

static void msg_append_msg (simcontact contact, struct _message *message, unsigned idx) {
  msg_append (contact, NULL, message, idx);
  msg_open (contact, &contact->msgs.queue[contact->msgs.len]);
  msg_close (contact);
}

static simtype event_new_history (const struct _message *message, unsigned idx) {
  simtype event = table_new_name (2, SIM_EVENT_HISTORY);
  table_add_number (event, SIM_EVENT_HISTORY, idx);
  table_add_number (event, SIM_CMD_MSG_STATUS, message->status);
  table_add_number (event, SIM_CMD_MSG_TIME, 0);
  table_add_number (event, SIM_CMD_MSG_RECEIVED, message->rcvtime);
  table_add_number (event, SIM_CMD_MSG_HANDLE, message->oldhandle);
  table_add_pointer_len (event, SIM_CMD_MSG_TEXT, message->text.str, message->text.len);
  return event;
}

void event_send_history_system (simcontact contact, const char *tag, const char *text,
                                simnumber handle, simnumber recvtime, simnumber number, simbool local) {
  simcontact sender = local ? contact_list.me : contact;
  simtype *nick = local ? &contact->msgs.own : &contact->msgs.nick;
  struct _message msg;
  memset (&msg, 0, sizeof (msg));
  msg.status = local ? SIM_MSG_DELIVERED : SIM_MSG_INCOMING;
  msg.type = SIM_MSG_TYPE_SYSTEM;
  msg.sndtime = time (NULL);
  msg.rcvtime = recvtime;
  msg.oldhandle = handle;
  msg.text = msg_new_text (tag, text, number);
  msg.next = 0;
  msg.nick = MSG_NEW_NICK (sender->nick, nick->str, nick->len);
  if (handle && ! recvtime && local) {
    msg.status = SIM_MSG_NOTSENT;
    contact->msgs.flags |= CONTACT_MSG_UNSENT;
    msg_append (contact, contact->client, &msg, 0);
    msg_put_hash (contact, contact->msgs.len, true);
  } else
    msg_append_msg (contact, &msg, 0);
  event_send (contact, event_new_history (&msg, contact->msgs.count));
}

static void *thread_connect_ (void *arg) {
  int err;
  unsigned count;
  simcontact contact = ((simclient) arg)->contact;
  LOG_API_DEBUG_ ("@%020llu '%s'\n", contact->id, contact->nick.str);
  do {
    simnumber start;
    do {
      count = contact->msgs.len;
      start = MSG_CHECK_CALL (contact);
      if ((err = client_connect_ (arg, contact)) == SIM_OK)
        break;
      client_release (arg);
      arg = client_new (socket_new (sim_new (sizeof (struct _socket))), contact, 0);
      contact->msgs.pinging = MSG_NUMBER_INVALID;
      event_send_name (contact, SIM_EVENT_NET, SIM_EVENT_NET_CONNECT, number_new (err));
    } while (MSG_CHECK_CALL (contact) && MSG_CHECK_CALL (contact) != start);
    if (err != SIM_OK && MSG_CHECK_CALL (contact)) {
      MSG_UNSET_CALL (contact);
      event_send_audio (contact, CONTACT_AUDIO_OUTGOING, CONTACT_AUDIO_HANGUP);
      count += msg_send_system (contact, "CALL ", "FAILED", start, err) == SIM_OK;
    }
  } while (err != SIM_OK && contact->msgs.len != count);
  LOG_API_DEBUG_ ("@%020llu '%s' error %d\n", contact->id, contact->nick.str, err);
  client_release (arg);
  return pth_thread_exit_ (true);
}

int msg_connect (simcontact contact, simbool reconnect) {
  int err = SIM_OK;
  if (simself.state != CLIENT_STATE_RUNNING || simself.status == SIM_STATUS_OFF)
    return SIM_CLIENT_CANCELLED;
  if (contact->auth < CONTACT_AUTH_NEW)
    return SIM_CONTACT_BLOCKED;
  if (! client_find (contact, CLIENT_FLAG_ACCEPTED | CLIENT_FLAG_CONNECTED)) {
    if (! client_find_connecting (contact, false)) {
      simclient client = client_new (socket_new (sim_new (sizeof (struct _socket))), contact, 0);
      if ((err = pth_thread_spawn (thread_connect_, client, NULL, contact->id)) != SIM_OK)
        client_release (client);
    } else if (reconnect) {
      contact->dht.search = system_get_tick ();
      contact->dht.active = true;
    }
  }
  return err;
}

int msg_create (simcontact contact, simtype text, simbyte type, struct _message *message) {
  int err = SIM_MSG_BAD_LENGTH;
  if (text.len <= SIM_MAX_MSG_SIZE) { /* don't know the table size yet */
    unsigned len = text.len;
    string_free (sim_convert_utf_to_int (text.ptr, &len));
    if (! text.len || len) {
      memset (message, 0, sizeof (*message));
      message->type = type;
      message->status = SIM_MSG_NOTSENT;
      message->sndtime = time (NULL);
      do {
        message->handle = random_get_number (random_public, 0x1FFFFFFFFFFFFFFFLL) + 0x2000000000000000LL;
      } while (msg_get_hash (contact, message->handle));
      message->text = text;
      message->next = 0;
      message->nick = MSG_NEW_NICK (contact_list.me->nick, contact->msgs.own.str, contact->msgs.own.len);
      message->tonick = NULL;
      return SIM_OK;
    }
    err = SIM_MSG_BAD_CHAR;
  }
  string_free (text);
  return err;
}

void msg_destroy (const struct _message *message) {
  string_free (message->nick);
  string_free (message->text);
}

void msg_send_ack (simclient client, simnumber tick) {
  if (client->contact->msgs.rcvtick && tick - client->contact->msgs.sndtick >= param_get_number ("msg.ack") * 1000) {
    client->contact->msgs.rcvtick = 0;
    client->contact->msgs.sndtick = tick + MSG_GET_NAK (client) * 1000;
    client_send_cmd (client, SIM_CMD_ACK, SIM_CMD_ACK_HANDLE, number_new (client->contact->msgs.msgnum), NULL, nil ());
    msg_close (client->contact);
  }
}

int msg_send (simcontact contact, struct _message *message, unsigned idx) {
  int err = SIM_OK;
  if (contact == contact_list.me) {
    message->status = SIM_MSG_DELIVERED;
    message->rcvtime = message->sndtime;
    msg_append_msg (contact, message, idx);
    if (! idx)
      idx = contact->msgs.count;
    event_send_value (contact, SIM_EVENT_SENT, SIM_EVENT_MSG, array_new_type (number_new (idx)));
  } else if (contact->auth >= CONTACT_AUTH_NEW && (! idx || contact->edit > 0)) {
    if (! contact->client || ! contact->client->msgtid) { /* not connected or msg_put_all still running */
      if (message->type != SIM_MSG_TYPE_SYSTEM)
        err = msg_connect (contact, false);
      LOG_DEBUG_ ("queueing message #%llu to '%s'\n", message->handle, contact->nick.str);
      msg_append (contact, contact->client, message, idx);
    } else {
      LOG_DEBUG_ ("putting message #%llu to '%s'\n", message->handle, contact->nick.str);
      message->status = SIM_MSG_PENDING;
      msg_append (contact, contact->client, message, idx);
      err = msg_put (contact->client, contact->msgs.len);
    }
    contact->msgs.flags |= CONTACT_MSG_UNSENT;
    contact->seen[CONTACT_SEEN_ACTIVITY] = message->sndtime;
  } else {
    msg_destroy (message);
    err = contact->auth >= CONTACT_AUTH_NEW ? SIM_API_NO_RIGHT : SIM_CONTACT_BLOCKED;
  }
  return err;
}

int msg_send_system (simcontact contact, const char *tag, const char *text, simnumber recvtime, int error) {
  struct _message msg;
  int err = msg_create (contact, msg_new_text (tag, text, error), SIM_MSG_TYPE_SYSTEM, &msg);
  if (err == SIM_OK) {
    msg.rcvtime = recvtime;
    if ((err = msg_send (contact, &msg, 0)) != SIM_CONTACT_BLOCKED) {
      event_send (contact, event_new_history (&msg, contact->msgs.count));
      err = SIM_OK; /* message has been appended even if not put */
    }
  }
  return err;
}

int msg_edit (simcontact contact, struct _message *message, simunsigned idx) {
  int err = SIM_MSG_BAD_INDEX;
  unsigned newidx = (unsigned) idx;
  struct _message *oldmsg = NULL;
  if (idx && idx <= contact->msgs.count) {
    oldmsg = &contact->msgs.queue[newidx = contact->msgs.indexes[newidx]];
    if (oldmsg->status != SIM_MSG_INCOMING && oldmsg->type == SIM_MSG_TYPE_UTF && ! MSG_CHECK_REMOVED (oldmsg))
      err = contact->msgs.flags & CONTACT_MSG_OVERFLOW ? SIM_MSG_OVERFLOW : SIM_OK;
  }
  if (err == SIM_OK) {
    struct _message msg;
    if (! MSG_CHECK_NOTSENT (oldmsg->status)) {
      if (! (contact->flags & CONTACT_FLAG_UTF) || ! CONTACT_CHECK_RIGHT_UTF (contact)) {
        msg_destroy (message);
        return SIM_API_NO_RIGHT;
      }
      message->status = SIM_MSG_SENT;
      message->oldhandle = oldmsg->handle;
      return msg_send (contact, message, (unsigned) idx);
    }
    string_free (message->nick);
    msg = *oldmsg;
    message->status = msg.status;
    message->handle = msg.handle;
    message->nick = pointer_new_len (msg.nick.str, msg.nick.len);
    message->tonick = msg.tonick;
    message->index = msg.index;
    msg.status = SIM_MSG_DELIVERED;
    msg.oldhandle = msg.handle = msg.index = 0;
    msg_append (contact, contact->client, &msg, (unsigned) -1);
    msg_set_sizes (contact, msg_size (message) + contact->msgs.sizes);
    contact->msgs.queue[newidx] = *message;
    contact->seen[CONTACT_SEEN_ACTIVITY] = message->sndtime;
    while (++newidx < contact->msgs.len) {
      struct _message *newmsg = &contact->msgs.queue[newidx];
      if (MSG_CHECK_NOTSENT (newmsg->status)) {
        if (newmsg->type == SIM_MSG_TYPE_SYSTEM && newmsg->rcvtime) /* preserve duration of system message */
          newmsg->rcvtime += message->sndtime - newmsg->sndtime;
        newmsg->sndtime = message->sndtime;
      }
    }
    contact->msgs.flags |= CONTACT_MSG_UNSENT;
    LOG_DEBUG_ ("edited pending message #%llu to '%s'\n", message->handle, contact->nick.str);
  } else
    msg_destroy (message);
  return err;
}

static int msg_read (simcontact contact, unsigned idx, simbool wipe) {
  int err;
  struct _message *msg = &contact->msgs.queue[idx];
  simtype buffer = nil (), buf = nil ();
  simnumber len = 0;
  if (! msg->offset || (buf = sim_convert_type_to_xml (msg->text, NULL)).typ == SIMNIL) {
    err = msg->offset ? SIM_MSG_BAD_CHAR : SIM_OK;
  } else if ((err = msg_open (contact, NULL)) == SIM_OK) {
    err = SIM_MSG_BAD_FILE;
    if ((len = lseek (contact->msgs.fd, msg->offset, SEEK_SET)) == msg->offset) {
#ifdef _WIN32
      buffer = string_buffer_new (buf.len * 2 + SIM_STRING_GET_LENGTH_CONST (MSG_XML_UTC_OPEN MSG_XML_UTC_CLOSE) + 11);
#else
      buffer = string_buffer_new (buf.len + SIM_STRING_GET_LENGTH_CONST (MSG_XML_UTC_OPEN MSG_XML_UTC_CLOSE) + 11);
#endif
      len = sim_file_read (contact->msgs.fd, buffer.str, buffer.len - 1);
      if (len >= buf.len + (simnumber) SIM_STRING_GET_LENGTH_CONST (MSG_XML_UTC_OPEN) &&
          (! buf.len || ! memcmp (buffer.str, buf.str, buf.len))) {
        char *s = (char *) buffer.str + buf.len, *e = s + SIM_STRING_GET_LENGTH_CONST (MSG_XML_UTC_OPEN);
        simnumber n;
        buffer.str[len] = 0;
        if (SIM_STRING_CHECK_DIFF_CONST (s, MSG_XML_UTC_OPEN) || (e = strstr (s = e, MSG_XML_UTC_CLOSE)) == NULL) {
          LOG_INFO_ ("remove message[%u:%lld] #%llu to '%s'\n", idx, msg->offset, msg->handle, contact->nick.str);
        } else if ((*e = 0, sim_convert_string_to_simunsigned (s, &n)) && n == msg->sndtime) {
          int l = (int) len;
          if (wipe)
            memset (buffer.str, ' ', buf.len);
          s -= SIM_STRING_GET_LENGTH_CONST ("<" MSG_HISTORY_UTC ">") + 1;
          memset (s, ' ', e - s + SIM_STRING_GET_LENGTH_CONST (MSG_XML_UTC_CLOSE));
          if (! wipe || (len = lseek (contact->msgs.fd, msg->offset, SEEK_SET)) == msg->offset)
            if (! wipe || (len = sim_file_write (contact->msgs.fd, buffer.str, l)) == l)
              err = SIM_OK;
        } else
          LOG_NOTE_ ("remove message[%u:%lld] #%llu to '%s'\n", idx, msg->offset, msg->handle, contact->nick.str);
      } else
        LOG_DEBUG_ ("remove message[%u:%lld] #%llu to '%s'\n", idx, msg->offset, msg->handle, contact->nick.str);
    }
  }
  if (len < 0 && errno)
    err = errno;
  string_free (buf);
  string_buffer_free (buffer);
  if (err == SIM_OK && wipe) {
    int next = msg->next, status = msg->status;
    LOG_DEBUG_ ("removed message[%u] #%llu to '%s'\n", idx, msg->handle, contact->nick.str);
    if (status != SIM_MSG_INCOMING && status != SIM_MSG_DELIVERED) {
      contact->msgs.flags |= CONTACT_MSG_UNSAVED;
      if (status != SIM_MSG_ACKNOWLEDGED)
        msg->status = SIM_MSG_DELIVERED;
    }
    if (msg->text.typ == SIMSTRING)
      msg_set_sizes (contact, contact->msgs.sizes - SIM_POINTER_SIZE (msg->text.ptr, msg->text.len + 1));
    string_free (msg->text), msg->text = nil ();
    msg->next = next;
    msg->offset = 0;
    if (! msg->handle && msg->oldhandle && msg->type == SIM_MSG_TYPE_SYSTEM && MSG_CHECK_NOTSENT (status))
      err = xfer_send_close (contact, msg->oldhandle, SIM_XFER_CANCELLED);
  }
  return err;
}

int msg_remove (simcontact contact, simunsigned idx) {
  int err = SIM_OK;
  unsigned i, j;
  simbool wipe;
  simnumber handle;
  struct _message *msg;
  if (! idx || idx > contact->msgs.count)
    return SIM_MSG_BAD_INDEX;
  msg = &contact->msgs.queue[i = contact->msgs.indexes[idx]];
  LOG_DEBUG_ ("removing message[%u] #%llu to '%s'\n", i, msg->handle, contact->nick.str);
  if (! msg->oldhandle || msg->type == SIM_MSG_TYPE_SYSTEM)
    return msg_read (contact, i, true);
  if (! param_get_number ("msg.remove"))
    for (handle = msg->oldhandle; handle; handle = contact->msgs.queue[i = j].oldhandle) {
      LOG_DEBUG_ ("removing message[%d] #%llu to '%s'\n", i, handle, contact->nick.str);
      if ((j = msg_get_hash (contact, handle)) >= i || ! j)
        return SIM_MSG_NO_EDIT;
    }
  for (wipe = false; wipe <= true && err == SIM_OK; wipe++) {
    err = msg_read (contact, i = contact->msgs.indexes[idx], true);
    for (handle = msg->oldhandle; handle && err == SIM_OK; handle = contact->msgs.queue[i].oldhandle) {
      if ((j = msg_get_hash (contact, handle)) >= i || ! j)
        break;
      err = msg_read (contact, i = j, wipe);
    }
  }
  return err;
}

static void msg_ack_msg (simcontact contact, unsigned idx, int resent) {
  struct _message *msg = &contact->msgs.queue[idx];
  if (msg->type != SIM_MSG_TYPE_SYSTEM || msg->handle) {
    LOG_WARN_ ("ack #%llu '%s'\n", msg->oldhandle, contact->nick.str);
  } else if (MSG_CHECK_NOTSENT (msg->status)) {
    int status = msg->status;
    msg->status = SIM_MSG_DELIVERED;
    msg->rcvtime = time (NULL);
    if (resent < 0) {
      int err = msg_read (contact, idx, true);
      contact->msgs.flags |= CONTACT_MSG_UNSAVED;
      LOG_ANY_ (err == SIM_OK ? SIM_LOG_DEBUG : SIM_LOG_WARN, "ack #%llu (error %d) '%s'\n",
                msg->oldhandle, err, contact->nick.str);
    } else if (msg_open (contact, msg) != SIM_OK) {
      msg->status = (simbyte) status;
      msg->rcvtime = msg->sndtime;
    } else if (msg->index) {
      simtype event = table_new_name (2, SIM_EVENT_SENT);
      table_add (event, SIM_EVENT_MSG, array_new_type (number_new (msg->index)));
      contact->msgs.flags |= CONTACT_MSG_UNSAVED;
      if (resent && status == SIM_MSG_NOTDELIVERED && contact->msgs.flags & CONTACT_MSG_NOTSENT) {
        table_add_number (event, SIM_EVENT_SENT, -2);
        contact->msgs.flags &= ~CONTACT_MSG_NOTSENT;
        contact->msgs.resent = 0;
      }
      event_send (contact, event);
      LOG_XTRA_ ("ack #%llu '%s'\n", msg->oldhandle, contact->nick.str);
    } else
      LOG_ERROR_ ("ack #%llu '%s'\n", msg->oldhandle, contact->nick.str);
  } else
    LOG_NOTE_ ("ack #%llu '%s'\n", msg->oldhandle, contact->nick.str);
}

void msg_ack (simcontact contact, simnumber handle, int resent) {
  unsigned h = sim_table_hash (&handle, sizeof (handle), contact->msgs.hashsalt) & (contact->msgs.hashsize - 1), i;
  if (handle) {
    for (i = contact->msgs.handles[h]; i; i = contact->msgs.queue[i].next)
      if (contact->msgs.queue[i].oldhandle == handle)
        msg_ack_msg (contact, i, resent);
  } else
    for (i = 1; i <= contact->msgs.len; i++)
      if (! contact->msgs.queue[i].handle && contact->msgs.queue[i].oldhandle)
        msg_ack_msg (contact, i, resent);
}

void msg_recv_ack (simcontact contact, simnumber handle) {
  unsigned i, j = 0, k = 0, n = 0, len = contact->msgs.len;
  int resent = contact->msgs.resent == handle;
  simtype sent, event;
  LOG_ANY_ (contact->msgs.msgack == handle ? SIM_LOG_XTRA : SIM_LOG_DEBUG,
            "ack #%llu '%s'\n", handle, contact->nick.str);
  if (! handle || contact->msgs.msgack == handle)
    return;
  contact->msgs.msgack = handle;
  for (i = 1; i <= len; i++)
    if (contact->msgs.queue[i].handle == handle && contact->msgs.queue[i].status != SIM_MSG_INCOMING)
      j = i;
  if ((sent = array_new_numbers (j)).len) {
    time_t now = time (NULL);
    for (i = 1; i <= j; i++) {
      struct _message *msg = &contact->msgs.queue[i];
      int status = msg->status;
      if (status != SIM_MSG_INCOMING && status != SIM_MSG_DELIVERED && msg->handle)
        if (contact->flags & CONTACT_FLAG_UTF || msg->type == SIM_MSG_TYPE_SYSTEM) {
          unsigned rcvtime = msg->rcvtime;
          if (status == SIM_MSG_PENDING || status == SIM_MSG_UNSENT) {
            msg->status = SIM_MSG_ACKNOWLEDGED;
            LOG_NOTE_ ("dropping message #%llu to '%s'\n", msg->handle, contact->nick.str);
          } else {
            n++;
            msg->status = SIM_MSG_DELIVERED;
          }
          if (msg->type != SIM_MSG_TYPE_SYSTEM)
            msg->rcvtime = now;
          if (msg_open (contact, msg) != SIM_OK) {
            msg->status = (simbyte) status;
            msg->rcvtime = rcvtime;
            j = i - 1;
            break;
          }
          if (msg->index)
            sent.arr[++k] = number_new (msg->index);
          if (contact->msgs.resent == msg->handle) {
            resent = 1;
          } else if (! resent && contact->msgs.flags & CONTACT_MSG_NOTSENT && MSG_CHECK_UNSENT (status))
            resent = -1;
          contact->msgs.flags |= CONTACT_MSG_UNSAVED;
        }
    }
    msg_close (contact);
    if (j == len)
      contact->msgs.flags &= ~CONTACT_MSG_UNSENT;
  }
  if ((sent.len = k) != 0 || resent) {
    if (resent) {
      contact->msgs.flags &= ~CONTACT_MSG_NOTSENT;
      contact->msgs.resent = 0;
    }
    LOG_DEBUG_ ("delivered %d/%d messages (%d left)%s to '%s'\n",
                n, j, len - j, resent > 0 ? " + put" : resent ? " + acked" : "", contact->nick.str);
    table_add_number (event = table_new_name (2, SIM_EVENT_SENT), SIM_EVENT_SENT, resent);
    table_add (event, SIM_EVENT_MSG, sent);
    event_send (contact, event);
  } else
    array_free (sent);
}

int msg_recv (simclient client, simtype text, const simtype table) {
  int err = SIM_OK;
  unsigned idx = 0;
  simcontact contact = client->contact;
  simtype event = nil ();
  struct _message msg;
  msg.nick = MSG_NEW_NICK (contact->nick, contact->msgs.nick.str, contact->msgs.nick.len);
  if (msg_convert_table (table, text, time (NULL), SIM_MSG_INCOMING, &msg)) {
    unsigned len = msg.text.len;
    string_free (sim_convert_utf_to_int (msg.text.ptr, &len));
    if (! msg.text.len || len) {
      msg.sndtime = msg.rcvtime == (unsigned) -1 ? msg.rcvtime : msg.rcvtime - msg.sndtime;
      if (contact->msgs.flags & CONTACT_MSG_OVERFLOW) {
        err = SIM_MSG_OVERFLOW;
      } else if (msg.type == SIM_MSG_TYPE_SYSTEM) {
        if (msg.handle && CONTACT_CHECK_RIGHT_AUDIO (contact) && ! msg.oldhandle) {
          event = event_new_history (&msg, contact->msgs.count + 1);
          if (msg.sndtime) {
            table_set_number (event, SIM_CMD_MSG_TIME, msg.sndtime);
          } else
            table_delete (event, SIM_CMD_MSG_TIME);
        } else
          LOG_NOTE_ ("dropping %ssystem message from '%s'\n", msg.handle ? "" : "no ", contact->nick.str);
      } else if (CONTACT_CHECK_RIGHT_UTF (contact)) {
        if (msg.oldhandle) {
          if (! CONTACT_CHECK_RIGHT_EDIT (contact)) {
            LOG_NOTE_ ("dropping edit message #%llu from '%s'\n", msg.oldhandle, contact->nick.str);
            msg.oldhandle = 0;
          } else {
            if ((idx = msg_get_hash (contact, msg.oldhandle)) != 0)
              idx = contact->msgs.queue[idx].status == SIM_MSG_INCOMING ? contact->msgs.queue[idx].index : 0;
            if (! idx) {
              LOG_NOTE_ ("dropping unknown message #%llu from '%s'\n", msg.oldhandle, contact->nick.str);
              msg.oldhandle = 0;
            } else if (contact->msgs.count - idx > (unsigned) abs (param_get_number ("msg.edit"))) {
              LOG_NOTE_ ("dropping edit message #%llu from '%s'\n", msg.oldhandle, contact->nick.str);
              msg.oldhandle = idx = 0;
            }
          }
        }
        if (msg.handle) {
          if (idx) {
            table_add_number (event = table_new_name (2, SIM_EVENT_EDIT), SIM_EVENT_EDIT, idx);
          } else
            table_add_number (event = table_new_name (2, SIM_EVENT_MSG), SIM_EVENT_MSG, contact->msgs.count + 1);
          table_add_pointer_len (event, SIM_CMD_MSG_TEXT, msg.text.str, msg.text.len);
        } else
          LOG_NOTE_ ("dropping no message from '%s'\n", contact->nick.str);
      } else
        err = SIM_API_NO_RIGHT;
    } else
      LOG_NOTE_ ("dropping bad message from '%s' (type = %d)\n", contact->nick.str, (int) msg.type);
  } else {
    LOG_NOTE_ ("dropping message from '%s' (type = %d)\n", contact->nick.str, (int) msg.type);
    string_free (text);
    msg.text = nil ();
  }
  if (event.typ != SIMNIL) {
    if (msg_get_hash (contact, msg.handle)) {
      LOG_NOTE_ ("dropping duplicate message #%llu from '%s'\n", msg.handle, contact->nick.str);
    } else if (msg_open (contact, &msg) == SIM_OK) {
      msg_append (contact, client, &msg, idx);
      LOG_DEBUG_ ("received %smessage[%u] #%llu from '%s'\n", idx ? "edit " : "",
                  idx ? idx : contact->msgs.count, msg.handle, contact->nick.str);
      contact->seen[CONTACT_SEEN_ACTIVITY] = msg.rcvtime;
      event_send (contact, event);
      msg.nick = msg.text = event = nil ();
    } else
      contact->seen[CONTACT_SEEN_ACTIVITY] = msg.rcvtime;
    table_free (event);
  }
  msg_destroy (&msg);
  if (msg.handle && err == SIM_OK) {
    contact->msgs.rcvtick = system_get_tick ();
    contact->msgs.msgnum = msg.handle;
  }
  return err;
}

int msg_wait_ (simcontact contact) {
  int drain = param_get_number ("msg.drain");
  simnumber tick = system_get_tick ();
  simclient client;
  while ((client = client_find (contact, CLIENT_FLAG_CONNECTED | CLIENT_FLAG_MSG)) != NULL && client->msgqueue) {
    simnumber now = system_get_tick ();
    if (! (client->flags & CLIENT_FLAG_MSG) && ! pth_msgport_pending (client->msgqueue)) {
      LOG_INFO_ ("drain success '%s'\n", contact->nick.str);
      break;
    }
    if (now - tick >= abs (drain)) {
      client->flags |= CLIENT_FLAG_MSG;
      socket_cancel (client->sock, SIM_SOCKET_BLOCKED);
      LOG_WARN_ ("drain error '%s'\n", contact->nick.str);
      if (drain <= 0)
        return SIM_SOCKET_BLOCKED;
    }
    pth_usleep_ (100000); /* cannot proceed while pth queue points to messages */
    if (simself.state != CLIENT_STATE_RUNNING)
      return SIM_CLIENT_CANCELLED;
  }
  return SIM_OK;
}

int msg_load (simcontact contact, simtype messages, unsigned count, simbool reload) {
  int err = SIM_OK;
  unsigned i, j, k, n, newlen, newsize, *indexes;
  simnumber bytes, sizes = 0;
  struct _message *queue = messages.ptr, *oldmsgs;
  simtype table = nil (), name = sim_convert_simunsigned_to_string (FILE_DIRECTORY_PENDING, contact->id, NULL);
  /* remove any initial messages that do not exist */
  for (oldmsgs = queue + 1; count && oldmsgs->text.typ == SIMNIL; count--)
    oldmsgs++;
  if (reload) { /* load old pending messages */
    n = j = count;
    for (i = 1; i <= contact->msgs.len; i++) {
      struct _message *msg = &contact->msgs.queue[i];
      if (! msg->offset && (msg->handle || msg->type == SIM_MSG_TYPE_SYSTEM) && ! MSG_CHECK_REMOVED (msg))
        n++;
    }
    queue = sim_new (sizeof (*queue) * ((newlen = newsize = n) + 1));
    for (i = 1; i <= contact->msgs.len; i++) {
      struct _message *msg = &contact->msgs.queue[i];
      if (! msg->offset && (msg->handle || msg->type == SIM_MSG_TYPE_SYSTEM) && ! MSG_CHECK_REMOVED (msg)) {
        queue[++j] = *msg;
        msg->nick = msg->text = nil ();
      }
    }
  } else if ((err = file_load (name.ptr, FILE_TYPE_ENCRYPTED, &table)) == SIM_OK) {
    simtype pending = table_detach_array_table (table, MSG_KEY_PENDING);
    newsize = (newlen = count) + pending.len;
    contact->msgs.flags |= CONTACT_MSG_SAVED;
    if (pending.len)
      queue = sim_new (sizeof (*queue) * (newsize + 1));
    for (i = 1; i <= pending.len; i++) {
      struct _message *msg = &queue[++newlen];
      simnumber status = table_get_number (pending.arr[i], SIM_CMD_MSG_STATUS);
      simtype text = table_detach_string (pending.arr[i], SIM_CMD_MSG_TEXT);
      int t = status == SIM_MSG_SENT || status == SIM_MSG_UNDELIVERED ? SIM_MSG_UNDELIVERED : SIM_MSG_NOTDELIVERED;
      if (! msg_convert_table (pending.arr[i], text, table_get_number (pending.arr[i], SIM_CMD_MSG_RECEIVED), t, msg)) {
        LOG_NOTE_ ("dropping pending message to '%s' (type = %d)\n", contact->nick.str, (int) msg->type);
        string_free (text);
        newlen--;
      } else
        msg->nick = pointer_new_len (contact_list.me->nick.str, contact_list.me->nick.len);
    }
    array_free (pending);
  } else
    newlen = newsize = count;
  string_free (name);
  if (queue != oldmsgs - 1) { /* prepend supplied messages and free them, else consume them */
    memcpy (queue + 1, oldmsgs, sizeof (*queue) * count);
    string_buffer_free (messages);
  } else if ((newsize = messages.len / sizeof (*queue)) != 0)
    newsize--;
  /* resize message text and set the nicks of all messages */
  contact->msgs.own = contact->msgs.nick = nil ();
  for (i = 1; i <= newlen; i++) {
    struct _message *msg = &queue[i];
    simtype *nick = msg->status == SIM_MSG_INCOMING ? &contact->msgs.nick : &contact->msgs.own;
    if (msg->text.typ != SIMSTRING && msg->text.typ != SIMPOINTER && msg->text.typ != SIMNIL)
      msg->text = string_buffer_truncate (msg->text, strlen (msg->text.ptr));
    if (msg->nick.typ == SIMPOINTER) {
      msg->nick = MSG_NEW_NICK (msg->nick, nick->str, nick->len);
      msg->tonick = NULL;
      *nick = pointer_new_len (msg->nick.str, msg->nick.len);
    } else if (! string_check_diff_len (msg->nick, nick->str, nick->len) || ! msg->nick.len) {
      simtype oldnick = msg->nick;
      if (! (msg->nick = pointer_new_len (nick->str, nick->len)).len)
        string_free (msg->text), msg->text = nil ();
      msg->tonick = oldnick.ptr;
      msg->next = oldnick.len;
    } else {
      msg->tonick = NULL;
      *nick = pointer_new_len (msg->nick.str, msg->nick.len);
    }
  }
  /* free unused nicks and set the "to" nick pointer of all messages */
  contact->msgs.nick = nil ();
  for (i = 1; i <= newlen; i++) {
    struct _message *msg = &queue[i];
    sim_free ((char *) msg->tonick, msg->next + 1); /* string_free */
    if (msg->status == SIM_MSG_INCOMING)
      contact->msgs.nick = pointer_new_len (msg->nick.str, msg->nick.len);
    msg->tonick = contact->msgs.nick.ptr;
  }
  /* free any old messages and initialize new ones */
  msg_uninit (contact);
  if ((contact->msgs.queue = queue) != NULL)
    memset (queue, 0, sizeof (*queue));
  contact->msgs.len = newlen;
  contact->msgs.indexes = indexes = sim_new (sizeof (*indexes) * ((contact->msgs.size = newsize) + 1));
  j = 1;
  for (i = newlen; i; i >>= 1)
    j <<= 1;
  contact->msgs.hashsalt = (unsigned) random_get_number (random_public, 0xFFFFFFFF);
  contact->msgs.hashsize = j > 256 || contact->auth < CONTACT_AUTH_ACCEPTED ? j : 256;
  contact->msgs.handles = sim_new (sizeof (*contact->msgs.handles) * contact->msgs.hashsize);
  memset (contact->msgs.handles, 0, sizeof (*contact->msgs.handles) * contact->msgs.hashsize);
  contact->msgs.pending = table;
  contact->msgs.maxsize = param_get_number ("msg.maxmb") << 20;
  /* build a message index and a handles hash table; previous versions of an edited message get zero indexes */
  for (i = 1; i <= newlen; i++) {
    struct _message *msg = &queue[i];
    sizes += msg_size (msg);
    msg->index = 0;
    if (! msg->handle || msg->text.typ == SIMNIL) {
      msg->next = 0;
      if ((indexes[i] = msg->text.typ != SIMNIL && msg->type == SIM_MSG_TYPE_SYSTEM ? i : 0) == 0) {
        LOG_INFO_ ("dropping no message[%u] #%llu '%s'\n", i, msg->handle, contact->nick.str);
      } else if (msg->oldhandle && MSG_CHECK_NOTSENT (msg->status))
        msg_put_hash (contact, i, true);
    } else if (msg_get_hash (contact, msg->handle) || (msg->type == SIM_MSG_TYPE_SYSTEM && msg->oldhandle)) {
      LOG_NOTE_ ("dropping duplicate message #%llu:#%llu '%s'\n", msg->handle, msg->oldhandle, contact->nick.str);
      indexes[i] = msg->next = 0;
    } else {
      simnumber handle = msg->oldhandle;
      for (indexes[k = i] = 0; handle && (j = msg_get_hash (contact, handle)) != 0; indexes[k = j] = 0)
        handle = queue[j].oldhandle;
      msg_put_hash (contact, indexes[k] = i, false);
    }
  }
  msg_set_sizes (contact, msg_get_sizes (contact, &bytes) + sizes);
  /* delete messages removed by consolidation from index array */
  k = newlen;
  for (i = 1; i <= newlen; i++)
    if (! indexes[j = i]) {
      while (++i <= newlen)
        if (indexes[i])
          indexes[j++] = indexes[i];
      k = j - 1;
      break;
    }
  contact->msgs.count = k;
  /* calculate reverse indexes after having deleted some */
  for (i = 1; i <= k; i++)
    queue[indexes[i]].index = i;
  LOG_DEBUG_ ("loaded %d old + %d/%d messages (%llu bytes) to '%s'\n",
              count, k - count, newlen - count, contact->msgs.sizes, contact->nick.str);
#ifndef DONOT_DEFINE
  for (i = 1; i <= newlen; i++) {
    struct _message *msg = &queue[i];
    if (msg->index && contact->msgs.indexes[msg->index] != i)
      LOG_FATAL_ (SIM_OK, "INDEX %d MISMATCH (WANTED %d, GOT %d) '%s'\n",
                  msg->index, i, contact->msgs.indexes[msg->index], contact->nick.str);
  }
#endif
  return err;
}

simbool msg_check_pending (simcontact contact) {
  unsigned i;
  if (contact->flags & (CONTACT_FLAG_UTF | CONTACT_FLAG_AUDIO) && ! (contact->msgs.flags & CONTACT_MSG_OVERFLOW))
    for (i = 1; i <= contact->msgs.len; i++) {
      struct _message *msg = &contact->msgs.queue[i];
      if (MSG_CHECK_UNACKED (msg->status) && msg->handle)
        if (contact->flags & (msg->type == SIM_MSG_TYPE_SYSTEM ? CONTACT_FLAG_AUDIO : CONTACT_FLAG_UTF))
          return true;
    }
  return false;
}

void msg_init (simcontact contact, simnumber msgnum) {
  contact->msgs.msgnum = msgnum;
  contact->msgs.own = contact->msgs.nick = nil ();
  contact->msgs.pinging = MSG_NUMBER_INVALID;
  contact->msgs.fd = -1;
  contact->msgs.savetick = system_get_tick ();
  contact->msgs.pending = nil ();
}

void msg_uninit (simcontact contact) {
#ifdef DONOT_DEFINE
  unsigned i;
  simnumber bytes;
  for (i = 1; i <= contact->msgs.len; i++) {
    contact->msgs.sizes -= msg_size (&contact->msgs.queue[i]);
    msg_destroy (&contact->msgs.queue[i]);
  }
  contact->msgs.sizes -= msg_get_sizes (contact, &bytes);
#else
  unsigned i;
  for (i = 1; i <= contact->msgs.len; i++)
    msg_destroy (&contact->msgs.queue[i]);
  contact->msgs.sizes = 0;
#endif
  sim_free (contact->msgs.queue, sizeof (*contact->msgs.queue) * (contact->msgs.size + 1));
  sim_free (contact->msgs.indexes, sizeof (*contact->msgs.indexes) * (contact->msgs.size + 1));
  contact->msgs.size = contact->msgs.len = 0;
  contact->msgs.queue = NULL;
  sim_free (contact->msgs.handles, sizeof (*contact->msgs.handles) * contact->msgs.hashsize);
  contact->msgs.hashsalt = contact->msgs.hashsize = contact->msgs.count = 0;
  contact->msgs.handles = contact->msgs.indexes = NULL;
  msg_close (contact);                                                /* just in case */
  table_free (contact->msgs.pending), contact->msgs.pending = nil (); /* DO NOT clear msgs.nick and msgs.own here */
}

int msg_log_queue (const char *module, int level, simnumber id) {
  unsigned i, j;
  simcontact contact;
  if (id) {
    simtype array, table;
    simnumber blocks, bytes, sizes;
    if ((contact = contact_list_find_id (id)) == NULL)
      return SIM_CONTACT_UNKNOWN;
    array = array_new_tables (contact->msgs.len);
    for (i = 1; i <= array.len; i++) {
      struct _message *msg = &contact->msgs.queue[i];
      array.arr[i] = table = msg_convert_to_table (msg, msg->sndtime);
      if (msg->text.typ == SIMNIL)
        table_delete (table, SIM_CMD_MSG_TEXT);
      table_add_number (table, SIM_CMD_MSG_STATUS, msg->status);
      if (msg->offset)
        table_add_number (table, "o", msg->offset);
      if (msg->index) {
        table_add_number (table, "i", msg->index);
        if (contact->msgs.indexes[msg->index] != i)
          LOG_ERROR_ ("INDEX %d MISMATCH (WANTED %d, GOT %d) '%s'\n",
                      msg->index, i, contact->msgs.indexes[msg->index], contact->nick.str);
      }
      if (msg->rcvtime)
        table_add_number (table, SIM_CMD_MSG_RECEIVED, msg->rcvtime);
      if (msg->nick.typ != SIMNIL)
        table_add (table, SIM_CMD_MSG_SENDER, msg->nick);
      if (msg->tonick)
        table_add_pointer (table, SIM_CMD_MSG_NICK, msg->tonick);
    }
    blocks = contact->msgs.queue ? 3 : 2;
    sizes = msg_get_sizes (contact, &bytes);
    for (i = 1; i <= contact->msgs.len; i++) {
      simtype str = contact->msgs.queue[i].text;
      for (j = 0; j <= 1; j++) {
        if (str.typ == SIMSTRING) {
          blocks++;
          bytes += str.len + 1;
          sizes += SIM_POINTER_SIZE (str.ptr, str.len + 1);
        }
        str = contact->msgs.queue[i].nick;
      }
    }
    log_any_ (module, level, "%s:", contact->nick.str);
    if (MSG_CHECK_CALL (contact))
      log_any_ (module, level, " CALLING");
    if (contact->msgs.pinging != MSG_NUMBER_INVALID)
      log_any_ (module, level, " PINGING");
    if (contact->msgs.fd >= 0)
      log_any_ (module, level, contact->msgs.flags & CONTACT_MSG_REMOVED ? " OPENED" : " OPEN");
    if (contact->msgs.flags & CONTACT_MSG_APPEND)
      log_any_ (module, level, " APPEND");
    if (contact->msgs.flags & CONTACT_MSG_UNSENT)
      log_any_ (module, level, " UNSENT");
    if (contact->msgs.flags & CONTACT_MSG_SAVED)
      log_any_ (module, level, " SAVED");
    if (contact->msgs.flags & CONTACT_MSG_UNSAVED)
      log_any_ (module, level, " UNSAVED");
    if (contact->msgs.flags & CONTACT_MSG_NOTSENT)
      log_any_ (module, level, " NOTSENT");
    if (contact->msgs.resent)
      log_any_ (module, level, " RESENT");
    if (contact->msgs.flags & CONTACT_MSG_NOTPUT)
      log_any_ (module, level, " NOTPUT");
    if (contact->msgs.flags & CONTACT_MSG_OVERFLOW)
      log_any_ (module, level, " OVERFLOW");
    log_simtype_ (module, level, array, LOG_BIT_CONT, " ");
    log_any_ (module, level, " %llu blocks (%llu/%llu bytes)\n", blocks, bytes, sizes);
    if (contact->msgs.sizes != sizes)
      LOG_ERROR_ ("SIZE MISMATCH (WANTED %lld, GOT %lld) '%s'\n", contact->msgs.sizes, sizes, contact->nick.str);
    for (i = 1; i <= array.len; i++)
      table_detach (array.arr[i], SIM_CMD_MSG_SENDER);
    array_free (array);
  } else
    for (i = contact_list.first; i; i = contact->next)
      msg_log_queue (module, level, (contact = contact_list.array.arr[i].ptr)->id);
  return SIM_OK;
}
