/**
    contact list management

    Copyright (c) 2020-2023 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 "keygen.h"
#include "network.h"
#include "mainline.h"
#include "contact.h"
#include "param.h"
#include "proto.h"
#include "proxy.h"
#include "server.h"
#include "client.h"
#include "msg.h"
#include "xfer.h"
#include "api.h"

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

#include <sys/stat.h>

#ifndef _WIN32
#include <arpa/inet.h>

#ifndef S_ISLNK
#define S_ISLNK(mode) ((mode) & (_S_IFLNK))
#endif
#else
#define S_ISLNK(mode) true

#undef FILE_EXTENSION_MD5
#define FILE_EXTENSION_MD5 NULL
#endif

#define SIM_MODULE SIM_MODULE_CONTACT

#if CONTACT_ADDRESS_LENGTH * 8 / 5 + 6 != CONTACT_ADDRESS_SIZE || \
  SIM_MAX_NICK_LENGTH + 4 >= CONTACT_ADDRESS_SIZE || SIM_MAX_NICK_LENGTH < 21
#error something is rotten in the state of denmark
#endif

#define CONTACT_KEY_ARRAY "list"
#define CONTACT_KEY_PROXY "proxy"
#define CONTACT_KEY_DELETED "deleted"

#define FILE_CONTACTS_TXT ".txt"

#define CONTACT_MIN_ID 10

const char *contact_right_names[63] = {
  "verify", "utf", "audio", "xfer", "typing", "type", "cancel-y", "cancel-n", "cancel", NULL, NULL, NULL,
  NULL, NULL, NULL, NULL, "verified", "renamed", "info", "new", NULL, NULL, NULL, NULL,
  "edit-y", "edit-n", "echo-y", "echo-n", "utf-y", "utf-n", "audio-y", "audio-n", "xfer-y", "xfer-n", NULL, NULL,
  NULL, NULL, NULL, NULL, "typing-y", "typing-n", "history-y", "history-n", NULL, NULL, NULL, NULL,
  "sound-on-y", "sound-on-n", "sound-off-y", "sound-off-n", NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
  "type-y", "type-n", "deleted"
};

const char *contact_auth_names[5] = { "R", "", "b", "n", "a" };
const char *contact_status_names[7] = { "idle", "invisible", "off", "on", "away", "busy", "hide" };

#define CONTACT_STRUCT_KEYS(RECEIVED, SENT, TIME, COUNT) \
  { CONTACT_KEY_##RECEIVED, CONTACT_KEY_##SENT, CONTACT_KEY_##TIME, CONTACT_KEY_##COUNT }

const char *contact_stat_names[15][4] = {
  CONTACT_STRUCT_KEYS (ALL_CLIENT_RECEIVED, ALL_CLIENT_SENT, ALL_CLIENT_TIME, ALL_CLIENT_COUNT),
  CONTACT_STRUCT_KEYS (THIS_CLIENT_RECEIVED, THIS_CLIENT_SENT, THIS_CLIENT_TIME, THIS_CLIENT_COUNT),
  CONTACT_STRUCT_KEYS (ALL_SOCKET_RECEIVED, ALL_SOCKET_SENT, ALL_SOCKET_TIME, ALL_SOCKET_COUNT),
  CONTACT_STRUCT_KEYS (THIS_SOCKET_RECEIVED, THIS_SOCKET_SENT, THIS_SOCKET_TIME, THIS_SOCKET_COUNT),
  CONTACT_STRUCT_KEYS (ALL_OUTPUT_RECEIVED, ALL_OUTPUT_SENT, ALL_OUTPUT_TIME, ALL_OUTPUT_COUNT),
  CONTACT_STRUCT_KEYS (THIS_OUTPUT_RECEIVED, THIS_OUTPUT_SENT, THIS_OUTPUT_TIME, THIS_OUTPUT_COUNT),
  CONTACT_STRUCT_KEYS (ALL_INPUT_RECEIVED, ALL_INPUT_SENT, ALL_INPUT_TIME, ALL_INPUT_COUNT),
  CONTACT_STRUCT_KEYS (THIS_INPUT_RECEIVED, THIS_INPUT_SENT, THIS_INPUT_TIME, THIS_INPUT_COUNT),
  CONTACT_STRUCT_KEYS (FRAMES_MINE_READ, FRAMES_MINE_WRITE, FRAMES_MINE_ALL, FRAMES_MINE_LOST),
  CONTACT_STRUCT_KEYS (FRAMES_PEER_READ, FRAMES_PEER_WRITE, FRAMES_PEER_ALL, FRAMES_PEER_LOST),
  CONTACT_STRUCT_KEYS (MS_TO_CLIENT, MS_TO_PROXY, MS_FROM_CLIENT, MS_FROM_PROXY),
  { "DCR", "DCS", "DCT", "DCC" },
  { "DSR", "DSS", "DST", "DSC" },
  { "DOR", "DOS", "DOT", "DOC" },
  { "DIR", "DIS", "DIT", "DIC" }
};

const unsigned contact_stat_indexes[4] = {
  CONTACT_STATS_CLIENTS, CONTACT_STATS_SOCKETS, CONTACT_STATS_OUTPUTS, CONTACT_STATS_INPUTS
};

const unsigned contact_stat_deleted[4] = {
  PROXY_STATS_DELETED_CLIENTS, PROXY_STATS_DELETED_SOCKETS, PROXY_STATS_DELETED_OUTPUTS, PROXY_STATS_DELETED_INPUTS
};

static const unsigned contact_stat_index[4] = {
  CONTACT_STATS_CLIENT, CONTACT_STATS_SOCKET, CONTACT_STATS_OUTPUT, CONTACT_STATS_INPUT
};

static const unsigned contact_stat_proxy[4] = {
  PROXY_STATS_THIS, PROXY_STATS_SOCKET, PROXY_STATS_OUTPUT, PROXY_STATS_INPUT
};

static const unsigned contact_stat_proxies[4] = {
  PROXY_STATS_ALL, PROXY_STATS_SOCKETS, PROXY_STATS_OUTPUTS, PROXY_STATS_INPUTS
};

static const char contact_address_encoding[] = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
static int contact_address_decoding[256];

struct _contacts contact_list;
static unsigned contact_request_count;
static simnumber contact_request_tick;

void contact_init (void) {
  int i = 0;
  const simbyte *p;
  memset (contact_address_decoding, 0, sizeof (contact_address_decoding));
  for (p = (const simbyte *) contact_address_encoding; *p; p++) {
    int lo = tolower (*p);
    contact_address_decoding[*p] = ++i;
    if (lo >= 0 && lo < 256)
      contact_address_decoding[lo] = i;
  }
  memset (&contact_list, 0, sizeof (contact_list));
  contact_list.array = array_new_strings (1);
  contact_list.array.len = 0;
  contact_list.table = table_new (1);
  contact_list.file = nil ();
  contact_request_tick = contact_request_count = 0;
}

void contact_uninit (void) {
  ARRAY_FREE (&contact_list.array, nil ());
  TABLE_FREE (&contact_list.table, nil ());
  TABLE_FREE (&contact_list.file, nil ());
  contact_list.me = NULL;
  contact_list.first = 0;
}

simtype sim_contact_convert_to_address (const simtype key, char type) {
  char *p;
  simtype val = *(simtype *) &key, address, hash;
  if (val.len != CONTACT_ADDRESS_LENGTH)
    return nil ();
  *(p = (address = string_new (CONTACT_ADDRESS_SIZE - 1)).ptr) = type;
  hash = sim_crypt_md_hash (sim_crypt_md_new (CRYPT_MD_SHA), val, nil (), pointer_new_len (p, 1));
  while (val.len) {
    *++p = contact_address_encoding[val.str[0] >> 3];
    *++p = contact_address_encoding[(val.str[0] & 7) << 2 | val.str[1] >> 6];
    *++p = contact_address_encoding[val.str[1] >> 1 & 31];
    *++p = contact_address_encoding[(val.str[1] & 1) << 4 | val.str[2] >> 4];
    *++p = contact_address_encoding[(val.str[2] & 15) << 1 | val.str[3] >> 7];
    *++p = contact_address_encoding[val.str[3] >> 2 & 31];
    *++p = contact_address_encoding[(val.str[3] & 3) << 3 | val.str[4] >> 5];
    *++p = contact_address_encoding[val.str[4] & 31];
    val.str += 5;
    val.len -= 5;
  }
  p[1] = contact_address_encoding[hash.str[0] >> 3];
  p[2] = contact_address_encoding[(hash.str[0] & 7) << 2 | hash.str[1] >> 6];
  p[3] = contact_address_encoding[hash.str[1] >> 1 & 31];
  p[4] = contact_address_encoding[(hash.str[1] & 1) << 4 | hash.str[2] >> 4];
  p[5] = 0;
  string_free (hash);
  return address;
}

simtype sim_contact_convert_address (const char *address, char type) {
  simbyte *p;
  simtype key;
  if (strlen (address) != CONTACT_ADDRESS_SIZE - 1 || toupper (*address++) != type)
    return nil ();
  p = (key = string_buffer_new (CONTACT_ADDRESS_LENGTH + 3)).str;
  for (;;) {
    int c0 = contact_address_decoding[(simbyte) address[0]] - 1;
    int c1 = contact_address_decoding[(simbyte) address[1]] - 1;
    int c2 = contact_address_decoding[(simbyte) address[2]] - 1;
    int c3 = contact_address_decoding[(simbyte) address[3]] - 1;
    if (c0 < 0 || c1 < 0 || c2 < 0 || c3 < 0)
      break;
    *p++ = (simbyte) (c0 << 3 | c1 >> 2);
    *p++ = (simbyte) (c1 << 6 | c2 << 1 | c3 >> 4);
    *p = (simbyte) (c3 << 4);
    if (! address[4]) {
      simtype ptr = pointer_new_len (key.str, CONTACT_ADDRESS_LENGTH);
      simtype hash = sim_crypt_md_hash (sim_crypt_md_new (CRYPT_MD_SHA), ptr, nil (), pointer_new_len (&type, 1));
      simbool ok = hash.str[0] == p[-2] && hash.str[1] == p[-1] && (hash.str[2] & 240) == p[0];
      string_free (hash);
      if (! ok)
        break;
      return string_buffer_truncate (key, CONTACT_ADDRESS_LENGTH);
    }
    if ((c0 = contact_address_decoding[(simbyte) address[4]] - 1) < 0)
      break;
    if ((c1 = contact_address_decoding[(simbyte) address[5]] - 1) < 0)
      break;
    if ((c2 = contact_address_decoding[(simbyte) address[6]] - 1) < 0)
      break;
    if ((c3 = contact_address_decoding[(simbyte) address[7]] - 1) < 0)
      break;
    *p++ |= (simbyte) (c0 >> 1);
    *p++ = (simbyte) (c0 << 7 | c1 << 2 | c2 >> 3);
    *p++ = (simbyte) (c2 << 5 | c3);
    address += 8;
  }
  string_buffer_free (key);
  return nil ();
}

simnumber sim_contact_convert_to_id (const char *address, const simtype key) {
  unsigned i;
  simtype addr = address ? sim_contact_convert_address (address, 'S') : nil ();
  simtype hash = sim_crypt_md_hash (sim_crypt_md_new (CRYPT_MD_RIPEMD), addr, key, nil ());
  simunsigned id = 0;
  for (i = 0; i < sizeof (id); i++)
    id = id << 8 | hash.str[i];
  string_free (hash);
  string_free (addr);
  if (! key.len)
    LOG_FATAL_ (SIM_OK, "public key not available\n");
  return addr.typ == SIMNIL ? 0 : id;
}

simnumber sim_contact_convert_to_id_ (const char *address) {
  simnumber id = 0;
  if (contact_list.me) {
    simcontact contact;
    simtype addr = sim_contact_test_address (address, "S");
    sim_protect_ ();
    if ((contact = contact_list_find_id (id = CONTACT_CONVERT_TO_ID (addr.ptr))) != NULL)
      if (string_check_diff (addr, contact->addr))
        id = 0;
    sim_unprotect_ ();
    string_free (addr);
  }
  return id;
}

simtype sim_contact_test_address (const char *address, const char *type) {
  unsigned len;
  simtype key;
  const char *ch;
  char addr[CONTACT_ADDRESS_SIZE + 1];
  if (! address)
    return nil ();
  if ((key = sim_contact_convert_address (address, *type)).typ != SIMNIL) {
    string_free (key);
    return sim_convert_string_to_upcase (address);
  }
  if ((len = strlen (address)) == CONTACT_ADDRESS_SIZE) {
    for (len = 0; len < CONTACT_ADDRESS_SIZE; len++) { /* try to remove inserted char */
      memcpy (addr, address, len);
      memcpy (addr + len, address + len + 1, CONTACT_ADDRESS_SIZE - len);
      if ((key = sim_contact_convert_address (addr, *type)).typ != SIMNIL) {
      done:
        string_free (key);
        LOG_DEBUG_ ("corrected address %s to %s\n", address, addr);
        return sim_convert_string_to_upcase (addr);
      }
    }
  } else if (len == CONTACT_ADDRESS_SIZE - 2) {
    for (len = 0; len < CONTACT_ADDRESS_SIZE - 1; len++) /* try to guess deleted char */
      for (ch = len ? contact_address_encoding : type; *ch;) {
        memcpy (addr, address, len);
        addr[len] = *ch++;
        memcpy (addr + len + 1, address + len, CONTACT_ADDRESS_SIZE - 1 - len);
        if ((key = sim_contact_convert_address (addr, *type)).typ != SIMNIL)
          goto done;
      }
  } else if (len == CONTACT_ADDRESS_SIZE - 1) {
    for (len = 0; len < CONTACT_ADDRESS_SIZE - 2; len++) { /* try to correct swapped char */
      memcpy (addr, address, CONTACT_ADDRESS_SIZE);
      addr[len] = address[len + 1];
      addr[len + 1] = address[len];
      if ((key = sim_contact_convert_address (addr, *type)).typ != SIMNIL)
        goto done;
    }
    for (len = 0; len < CONTACT_ADDRESS_SIZE - 1; len++) /* try to guess replaced char */
      for (ch = len ? contact_address_encoding : type; *ch;) {
        memcpy (addr, address, CONTACT_ADDRESS_SIZE);
        addr[len] = *ch++;
        if ((key = sim_contact_convert_address (addr, *type)).typ != SIMNIL)
          goto done;
      }
  }
  return nil ();
}

static simbool sim_contact_check_nick (const simtype nick, const char *except) {
  simbool ok = true;
  if (nick.len && (nick.str[0] == '@' || except)) {
    simnumber id;
    simtype str = string_copy_len (nick.str + (nick.str[0] == '@'), nick.len - (nick.str[0] == '@'));
    if (nick.str[0] == '@') {
      ok = ! sim_convert_string_to_simunsigned (str.ptr, &id);
    } else if (strlen (except) < str.len && ! strcmp ((char *) str.str + str.len - strlen (except), except)) {
      str.str[str.len - strlen (except)] = 0;
      ok = ! sim_convert_string_to_simunsigned (str.ptr, &id);
    }
    string_free (str);
  }
  return ok;
}

void contact_list_init (void) {
  unsigned i, j;
  int *permutation = sim_new ((contact_list.array.len + 1) * sizeof (*permutation));
  for (i = 1; i <= contact_list.array.len; i++) {
    permutation[i] = permutation[j = (unsigned) (random_get_number (random_public, i - 1)) + 1];
    permutation[j] = i;
  }
  contact_list.first = contact_list.array.len ? permutation[1] : 0;
  for (i = 1; i <= contact_list.array.len; i++) {
    simcontact contact = contact_list.array.arr[permutation[i]].ptr;
    contact->next = i == contact_list.array.len ? 0 : permutation[i + 1];
  }
  sim_free (permutation, (contact_list.array.len + 1) * sizeof (*permutation));
}

simbool contact_list_check_request (simnumber tick) {
  simbool ok;
  if (! contact_request_count || tick - contact_request_tick >= 86400000) {
    contact_request_tick = tick;
    contact_request_count = 0;
  }
  if ((ok = contact_request_count < (unsigned) param_get_number ("contact.requests")) == true) {
    contact_request_count++;
  } else
    LOG_DEBUG_ ("maximal number of contact requests exceeded (%lld seconds left)\n",
                (contact_request_tick - tick) / 1000 + 86400);
  return ok;
}

void contact_list_set_info (int bit) {
  unsigned i;
  if (! (param_get_number ("client.info") & bit))
    for (i = 1; i <= contact_list.array.len; i++) {
      simcontact contact = contact_list.array.arr[i].ptr;
      if (contact->auth != CONTACT_AUTH_NEW && contact->auth != CONTACT_AUTH_REVOKED)
        if (contact_list.me && contact != contact_list.system)
          contact->flags |= CONTACT_FLAG_INFO;
    }
}

int contact_list_count (simbool authorized) {
  unsigned i;
  int count = 0;
  for (i = 1; i <= contact_list.array.len; i++) {
    simcontact contact = contact_list.array.arr[i].ptr;
    if ((! authorized || contact->auth >= CONTACT_AUTH_ACCEPTED) && contact->auth != CONTACT_AUTH_FORGET)
      count++;
  }
  return count;
}

int contact_list_load_txt_ (const char *pathname) {
  int err = SIM_OK;
  FILE *f;
  simtype name;
  if (pathname) {
    name = pointer_new (pathname);
  } else if ((name = sim_file_name_new (FILE_CONTACTS FILE_CONTACTS_TXT, FILE_DIRECTORY_USER)).typ == SIMNIL)
    return SIM_OK;
  if ((f = sim_file_fopen (name.ptr, "rt")) == NULL) {
    simtype bak = string_cat (name.ptr, SIM_FILE_BACKUP);
    LOG_NOTE_ ("open %s error %d\n", pathname ? pathname : FILE_CONTACTS FILE_CONTACTS_TXT, errno);
    if ((f = sim_file_fopen (bak.ptr, "rt")) == NULL)
      err = errno ? errno : EINVAL;
    string_free (bak);
  }
  string_free (name);
  if (f) {
    int added = 0;
    while (err == SIM_OK && ! feof (f)) {
      char buf[BUFSIZ + 1];
      errno = 0;
      if (fgets (buf, BUFSIZ, f)) {
        unsigned len;
        buf[BUFSIZ] = 0;
        if ((len = strlen (buf)) != 0 && buf[len - 1] == '\n')
          buf[--len] = 0;
        if (len >= CONTACT_ADDRESS_SIZE - 1) {
          simcontact contact, newcontact;
          buf[CONTACT_ADDRESS_SIZE - 1] = 0;
          len -= CONTACT_ADDRESS_SIZE - 1;
          contact = contact_list_find_address (buf);
          if (! contact || (pathname && contact->auth != CONTACT_AUTH_BLOCKED))
            if (contact_new_ (buf, CONTACT_AUTH_USER, &newcontact) == SIM_OK && ! contact) {
              contact = newcontact;
              added++;
            }
          if (contact && ! contact->own.len && ! (contact->flags & CONTACT_FLAG_RENAMED) && len > 1)
            contact_rename (contact, pointer_new_len (buf + CONTACT_ADDRESS_SIZE, len - 1), true);
        } else
          LOG_WARN_ ("fgets bad line: %s\n", buf);
      } else if (ferror (f)) {
        err = errno ? errno : EPERM;
        LOG_ERROR_ ("fgets %s error %d\n", pathname, errno);
      }
    }
    fclose (f);
    if (added)
      LOG_DEBUG_ ("added %u contacts\n", added);
  } else
    LOG_WARN_ ("open %s error %d\n", pathname ? pathname : FILE_CONTACTS FILE_CONTACTS_TXT, err);
  return err;
}

int contact_list_save_txt (const char *pathname, int mode) {
  int err = SIM_OK;
  simtype name, bak;
  sim_error_reset_text ();
  if (pathname) {
    name = pointer_new (pathname);
  } else if ((name = sim_file_name_new (FILE_CONTACTS FILE_CONTACTS_TXT, FILE_DIRECTORY_USER)).typ == SIMNIL)
    return SIM_OK;
  bak = string_cat (name.ptr, SIM_FILE_BACKUP);
  if (! pathname)
    mode = param_get_number ("contact.txt");
  if (mode != CONTACT_TXT_DEFAULT || ! pathname) {
    file_wipe (NULL, bak.ptr, sim_file_write);
    if (mode != CONTACT_TXT_DEFAULT) {
      FILE *f;
      sim_file_rename (name.ptr, bak.ptr);
      if ((f = sim_file_fopen (name.ptr, "wt")) != NULL) {
        unsigned i;
        for (i = 1; err == SIM_OK && i <= contact_list.array.len; i++) {
          simcontact contact = contact_list.array.arr[i].ptr;
          if (contact->auth >= CONTACT_AUTH_ACCEPTED || contact->auth == CONTACT_AUTH_BLOCKED)
            if (contact != contact_list.me && contact != contact_list.system) {
              if (fputs (contact->addr, f) >= 0) {
                if (mode == CONTACT_TXT_NICK && sim_contact_check_nick (contact->nick, NULL))
                  if (fputc (' ', f) < 0 || fputs (contact->nick.ptr, f) < 0)
                    err = errno ? errno : ENOSPC;
                if (err == SIM_OK && fputc ('\n', f) < 0)
                  err = errno ? errno : ENOSPC;
              } else
                err = errno ? errno : ENOSPC;
            }
        }
        if (fclose (f) && err == SIM_OK)
          err = errno ? errno : ENOSPC;
      } else
        err = errno ? errno : ENOSPC;
      if (err != SIM_OK) {
        file_wipe (NULL, name.ptr, sim_file_write);
        sim_file_rename (bak.ptr, name.ptr);
        sim_error_set_text (" [", pathname ? pathname : FILE_CONTACTS FILE_CONTACTS_TXT, "]", err);
      }
    } else
      file_wipe (NULL, name.ptr, sim_file_write);
  }
  string_free (bak);
  string_free (name);
  return err || pathname ? err : contact_list_save ();
}

static void contact_list_load_stat (const simtype file, simbool deleted) {
  unsigned i, j;
  simtype table = nil ();
  if (file.typ != SIMNIL)
    table = table_get_table (file, deleted ? CONTACT_KEY_DELETED : CONTACT_KEY_PROXY);
  for (j = 0; j < SIM_ARRAY_SIZE (contact_stat_indexes); j++)
    for (i = 0; i <= CONTACT_STAT_COUNT; i++) {
      simnumber n = table.typ == SIMNIL ? 0 : table_get_number (table, contact_stat_names[contact_stat_indexes[j]][i]);
      proxy_set_stat (n, deleted ? contact_stat_deleted[j] : contact_stat_proxies[j], i);
      proxy_set_stat (0, contact_stat_proxy[j], i);
    }
}

static void contact_list_save_stat (simtype file, simnumber deleted[4][CONTACT_STAT_COUNT + 1]) {
  unsigned i, j;
  const char *key = deleted ? CONTACT_KEY_DELETED : CONTACT_KEY_PROXY;
  simtype table = table_get_table (file, key);
  if (table.typ == SIMNIL)
    table_add (file, key, table = table_new (9));
  for (i = 0; i <= CONTACT_STAT_COUNT; i++) {
    simnumber stats[SIM_ARRAY_SIZE (contact_stat_indexes)];
    if (! deleted) {
      simcustomer proxy = proxy_get_proxy (NULL, NULL, NULL);
      for (j = 0; j < SIM_ARRAY_SIZE (stats); j++)
        stats[j] = proxy_get_stat (contact_stat_proxies[j], i) + proxy_get_stat (contact_stat_proxy[j], i);
      stats[0] += proxy_get_stat (PROXY_STATS_NOW, i);
      if (proxy)
        stats[2] += socket_get_stat (proxy->sock, i, false);
      stats[3] += proxy_get_stat_contact (NULL, i);
    } else
      for (j = 0; j < SIM_ARRAY_SIZE (stats); j++)
        stats[j] = proxy_get_stat (contact_stat_deleted[j], i) + deleted[j][i];
    for (j = 0; j < SIM_ARRAY_SIZE (stats); j++)
      table_set_number (table, contact_stat_names[contact_stat_indexes[j]][i], stats[j]);
  }
}

int contact_list_load (const simtype address) {
  simtype file;
  int err = file_load (FILE_CONTACTS, FILE_TYPE_ENCRYPTED, &file), cipher;
  array_free (contact_list.array);
  table_free (contact_list.table);
  if (err == SIM_OK) {
    unsigned i, j, k, n = 0;
    simtype array = table_get_array_table (file, CONTACT_KEY_ARRAY);
    random_init_entropy (file);
    contact_list.array = array_new_strings (array.len);
    contact_list.table = table_new (array.len * 3);
    contact_list.auth = CONTACT_AUTH_ACCEPTED;
    for (i = 1; i <= array.len; i++) {
      const char **names = &CONTACT_LOOKUP_STATUS_NAME (SIM_STATUS_INVISIBLE);
      const unsigned len = SIM_STATUS_MAX - SIM_STATUS_INVISIBLE + 1;
      simnumber num;
      simtype table = array.arr[i], tmp, str, key = table_detach_string (table, CONTACT_KEY_ID);
      simcontact contact = (tmp = string_new (sizeof (*contact))).ptr;
      simtype location = table_detach_string (table, CONTACT_KEY_LOCATION);
      simtype ip = table_detach_array_string (table, CONTACT_KEY_IP4);
      memset (contact, 0, sizeof (*contact));
      str = table_detach_string (table, CONTACT_KEY_AUTH);
      contact->auth = sim_convert_string_to_enum (str.typ == SIMNIL ? "" : str.ptr, CONTACT_AUTH_REVOKED,
                                                  CONTACT_AUTH_BLOCKED, SIM_ARRAY (contact_auth_names));
      STRING_FREE (&str, table_detach_string (table, CONTACT_KEY_STATUS));
      contact->status = sim_convert_string_to_enum (str.ptr, SIM_STATUS_INVISIBLE, SIM_STATUS_INVISIBLE, names, len);
      STRING_FREE (&str, table_detach_array_string (table, CONTACT_KEY_FLAGS));
      num = sim_convert_strings_to_flags (str, SIM_ARRAY (contact_right_names)) ^ CONTACT_FLAGS_DEFAULT;
      contact->flags = num & ~(CONTACT_FLAG_TYPE | CONTACT_FLAG_TYPE_Y | CONTACT_FLAG_TYPE_N | CONTACT_FLAG_NO_STATS);
      array_free (str);
      msg_init (contact, table_detach_number (table, CONTACT_KEY_MSG_HANDLE));
      contact->xfer.hash = nil ();
      contact->xfer.last = &contact->xfers;
      contact->addr = sim_contact_convert_to_address (contact->key = key, 'S').ptr;
      contact->id = sim_contact_convert_to_id (contact->addr, address);
      contact->nick = table_detach_string (table, CONTACT_KEY_NICK);
      if ((contact->own = table_detach_string (table, CONTACT_KEY_OWN_NICK)).typ == SIMNIL)
        contact->own = pointer_new ("");
      contact->line = table_detach_string (table, CONTACT_KEY_LINE);
      contact->revocation = table_detach_string (table, CONTACT_KEY_REVOCATION);
      contact->hosts = table_detach_array_string (table, CONTACT_KEY_HOSTS);
      contact->versions = table_detach_array_string (table, CONTACT_KEY_VERSIONS);
      num = table_detach_number (table, CONTACT_KEY_EDIT);
      contact->edit = SIM_CHECK_POSITIVE (num) ? (int) num : 0;
      while (contact->hosts.len > SIM_MAX_HOST_COUNT)
        string_free (array_detach (contact->hosts, contact->hosts.len--));
      /*contact->ip->ip = contact->ip->port = 0;*/
      for (j = 0; j < ip.len && j < SIM_ARRAY_SIZE (contact->ip); j++) {
        struct _network_ip_port *node = ip.arr[j + 1].ptr;
        contact->ip[j].ip = ntohl (node->ip), contact->ip[j].port = ntohs (node->port);
      }
      contact->ips = j;
      array_free (ip);
      contact->seen[CONTACT_SEEN_RECEIVE] = table_detach_number (table, CONTACT_KEY_SEEN_RECEIVE);
      contact->seen[CONTACT_SEEN_STATUS] = table_detach_number (table, CONTACT_KEY_SEEN_STATUS);
      contact->seen[CONTACT_SEEN_ACTIVITY] = table_detach_number (table, CONTACT_KEY_SEEN_ACTIVITY);
      num = table_detach_number (table, CONTACT_KEY_LOGON);
      contact->logon = SIM_CHECK_POSITIVE (num) ? (int) num : 0;
      cipher = crypt_cipher_find ((key = table_detach_string (table, CONTACT_KEY_CIPHER)).ptr, 0, NULL);
      string_free (key);
      contact->location = sim_network_parse_ip (location.ptr);
      string_free (location);
      contact->rcvcipher = cipher < 0 ? NULL : sim_crypt_cipher_get (cipher, NULL, NULL);
      contact->ec = table_detach_string (table, CONTACT_KEY_EC);
      contact->rsa = table_detach_string (table, CONTACT_KEY_RSA);
      for (num = k = 0; k < SIM_ARRAY_SIZE (contact_stat_indexes); k++) {
        unsigned l = contact_stat_indexes[k];
        for (j = 0; j <= CONTACT_STAT_COUNT; j++)
          num += contact->stats[l][j] = table_detach_number (table, contact_stat_names[l][j]);
      }
      if (contact->auth == CONTACT_AUTH_NEW || (! num && contact->auth == CONTACT_AUTH_DELETED))
        contact->flags |= CONTACT_FLAG_NO_STATS;
      key = pointer_new_len (&contact->id, sizeof (contact->id));
      if (contact->id > -CONTACT_MIN_ID && contact->id < CONTACT_MIN_ID) {
        LOG_ERROR_ ("load %u@%020llu skipped '%s'\n", i, contact->id, contact->nick.str);
        string_free (tmp);
      } else if (table_add_key (contact_list.table, key, tmp).typ == SIMNIL) {
        if ((j = table_count (table)) < table.len) {
          contact->file = table_copy_strings (table, j);
        } else
          contact->file = array_detach (array, i);
        contact_list.array.arr[++n] = pointer_new_len (tmp.str, tmp.len);
      } else
        LOG_ERROR_ ("load %u@%020llu duplicate '%s'\n", i, contact->id, contact->nick.str);
    }
    contact_list.array.len = n;
    LOG_DEBUG_ ("loaded %d/%u contacts\n", contact_list_count (false), array.len);
    table_delete (file, CONTACT_KEY_ARRAY);
  } else {
    contact_list.array = array_new_strings (1);
    contact_list.array.len = 0;
    contact_list.table = table_new (1);
  }
  contact_list_load_stat (contact_list.file = file, false);
  contact_list_load_stat (file, true);
  contact_request_count = 0;
  contact_list.first = 0;
  return err;
}

int contact_list_save (void) {
  int err, auth;
  unsigned i, j, l = 0, n = table_count (contact_list.table);
  simtype table = contact_list.file.typ == SIMNIL ? table_new (1) : contact_list.file;
  simtype array = array_new_tables (contact_list.array.len);
  simnumber deleted[4][CONTACT_STAT_COUNT + 1];
  memset (deleted, 0, sizeof (deleted));
  LOG_DEBUG_ ("saving %d/%u contacts\n", contact_list_count (false), contact_list.array.len);
  if (array.len != n)
    LOG_ERROR_ ("save %d contacts (instead of %d)\n", array.len, n);
  for (n = 1; n <= array.len; n++) {
    simcontact contact = contact_list.array.arr[n].ptr;
    simtype tmp = nil ();
    if (contact_list_find_id (contact->id) != contact)
      LOG_ERROR_ ("save %u@%020llu not found '%s'\n", n, contact->id, contact->nick.str);
    if (contact->auth != CONTACT_AUTH_FORGET) {
      simunsigned flags = (contact->flags & ~CONTACT_FLAG_NO_STATS) ^ CONTACT_FLAGS_DEFAULT;
      i = table_count (contact->file);
      tmp = array.arr[++l] = table_copy (contact->file, i < 9 ? 9 : i);
      table_add_pointer_len (tmp, CONTACT_KEY_ID, contact->key.str, contact->key.len);
      table_add_pointer_len (tmp, CONTACT_KEY_NICK, contact->nick.str, contact->nick.len);
      if ((auth = contact != contact_list.me ? contact->auth : contact_list.auth) != CONTACT_AUTH_DELETED)
        table_add_pointer (tmp, CONTACT_KEY_AUTH, contact_auth_names[auth - CONTACT_AUTH_REVOKED]);
      if (contact != contact_list.system)
        table_add_number (tmp, CONTACT_KEY_SEEN_STATUS, contact->seen[CONTACT_SEEN_STATUS]);
      if (contact->auth > CONTACT_AUTH_DELETED) {
        simtype ip = array_new_strings (contact == contact_list.system ? 0 : contact->ips);
        if (contact != contact_list.system)
          table_add_number (tmp, CONTACT_KEY_SEEN_RECEIVE, contact->seen[CONTACT_SEEN_RECEIVE]);
        table_add_number (tmp, CONTACT_KEY_SEEN_ACTIVITY, contact->seen[CONTACT_SEEN_ACTIVITY]);
        table_add_pointer (tmp, CONTACT_KEY_STATUS, CONTACT_LOOKUP_STATUS_NAME (contact->status));
        if (flags &= ~(CONTACT_FLAG_TYPE | CONTACT_FLAG_TYPE_Y | CONTACT_FLAG_TYPE_N))
          table_add (tmp, CONTACT_KEY_FLAGS, sim_convert_flags_to_strings (flags, SIM_ARRAY (contact_right_names)));
        table_add_pointer_len (tmp, CONTACT_KEY_OWN_NICK, contact->own.str, contact->own.len);
        if (contact->line.typ != SIMNIL)
          table_add_pointer_len (tmp, CONTACT_KEY_LINE, contact->line.str, contact->line.len);
        if (contact->hosts.len)
          table_add (tmp, CONTACT_KEY_HOSTS, array_copy (contact->hosts));
        if (contact->versions.typ != SIMNIL)
          table_add (tmp, CONTACT_KEY_VERSIONS, array_copy (contact->versions));
        if (contact->edit)
          table_add_number (tmp, CONTACT_KEY_EDIT, contact->edit);
        for (i = 0; i < ip.len; i++) {
          struct _network_ip_port *node = (ip.arr[i + 1] = string_new (CONTACT_SIZE_IP_PORT)).ptr;
          node->ip = htonl (contact->ip[i].ip), node->port = htons (contact->ip[i].port);
        }
        table_add (tmp, CONTACT_KEY_IP4, ip);
        table_add_number (tmp, CONTACT_KEY_LOGON, contact->logon);
        table_add_number (tmp, CONTACT_KEY_MSG_HANDLE, contact->msgs.msgnum);
        if (contact != contact_list.system) {
          if (contact->location)
            table_add (tmp, CONTACT_KEY_LOCATION, string_copy (network_convert_ip (contact->location)));
          if (contact->rcvcipher)
            table_add_pointer (tmp, CONTACT_KEY_CIPHER, contact->rcvcipher);
          if (contact->ec.typ != SIMNIL)
            table_add_pointer_len (tmp, CONTACT_KEY_EC, contact->ec.str, contact->ec.len);
          if (contact->rsa.typ != SIMNIL)
            table_add_pointer_len (tmp, CONTACT_KEY_RSA, contact->rsa.str, contact->rsa.len);
        }
      } else if (contact->revocation.typ != SIMNIL)
        table_add_pointer_len (tmp, CONTACT_KEY_REVOCATION, contact->revocation.str, contact->revocation.len);
    }
    for (i = 0; i <= CONTACT_STAT_COUNT; i++) {
      simnumber stats[SIM_ARRAY_SIZE (contact_stat_indexes)];
      for (j = 0; j < SIM_ARRAY_SIZE (stats); j++)
        stats[j] = contact->stats[contact_stat_indexes[j]][i] + contact->stats[contact_stat_index[j]][i];
      stats[0] += client_get_stat (contact, i, false);
      stats[2] += client_get_stat (contact, i, true);
      stats[3] += proxy_get_stat_contact (contact, i);
      for (j = 0; j < SIM_ARRAY_SIZE (stats); j++) {
        if (contact->auth >= CONTACT_AUTH_DELETED && contact != contact_list.system)
          if (contact->auth != CONTACT_AUTH_DELETED || ! (contact->flags & CONTACT_FLAG_NO_STATS)) {
            table_add_pointer_len (tmp, CONTACT_KEY_OWN_NICK, contact->own.str, contact->own.len);
            if (stats[j])
              table_add_number (tmp, contact_stat_names[contact_stat_indexes[j]][i], stats[j]);
            continue;
          }
        deleted[j][i] += stats[j];
      }
    }
  }
  array.len = l;
  table_add (table, CONTACT_KEY_ARRAY, array);
  contact_list_save_stat (table, deleted);
  contact_list_save_stat (table, NULL);
  err = file_save (table, FILE_CONTACTS, FILE_TYPE_ENCRYPTED);
  table_delete (contact_list.file = table, CONTACT_KEY_ARRAY);
  return err;
}

void contact_list_free (void) {
  unsigned i;
  for (i = 1; i <= contact_list.array.len; i++) {
    simcontact contact = contact_list.array.arr[i].ptr;
    simtype saved = random_get_state (random_public);
    if (saved.typ != SIMNIL && contact->msgs.queue)
      msg_save (contact);
    table_free (saved);
    msg_uninit (contact);
    table_free (contact->xfer.hash);
    sim_free_string ((char *) contact->addr);
    string_free (contact->key);
    string_free (contact->nick);
    string_free (contact->own);
    string_free (contact->line);
    string_free (contact->revocation);
    array_free (contact->hosts);
    array_free (contact->versions);
    string_free (contact->ec);
    string_free (contact->rsa);
    sim_free_string (contact->insecure);
    table_free (contact->file);
    if (contact->probehost.ptr)
      string_free (contact->probehost);
  }
  contact_uninit ();
  contact_list.array = array_new_strings (1);
  contact_list.array.len = 0;
  contact_list.table = table_new (1);
}

simcontact contact_list_find_id (simnumber id) {
  return table_get_key_pointer (contact_list.table, pointer_new_len (&id, sizeof (id)));
}

simcontact contact_list_find_address (const char *address) {
  simcontact contact = contact_list_find_id (CONTACT_CONVERT_TO_ID (address));
  return contact && ! strcmp (contact->addr, address) ? contact : NULL;
}

static simcontact contact_list_find_nick (const char *nick, simcontact except) {
  unsigned i;
  simnumber id;
  for (i = 1; i <= contact_list.array.len; i++) {
    simcontact contact = contact_list.array.arr[i].ptr;
    if (contact != except && ! string_check_diff (contact->nick, nick))
      return contact;
  }
  return *nick == '@' && sim_convert_string_to_simunsigned (nick + 1, &id) ? contact_list_find_id (id) : NULL;
}

int sim_contact_search_ (simnumber id, simbool restart) {
  simcontact contact;
  int ret = MAIN_SEARCH_NONE;
  sim_protect_ ();
  if ((contact = contact_list_find_id (id)) != NULL)
    ret = main_search_create (contact->addr, 0, MAIN_MODE_ACTIVE, restart);
  sim_unprotect_ ();
  return ret;
}

const char *sim_contact_get_nick_ (simnumber id, char nick[SIM_SIZE_NICK]) {
  char *ret = NULL;
  simcontact contact;
  sim_protect_ ();
  if ((contact = id ? contact_list_find_id (id) : contact_list.me) != NULL) {
    memcpy (ret = nick, contact->nick.str, contact->nick.len + 1);
  } else
    *nick = 0;
  sim_unprotect_ ();
  return ret;
}

const char *contact_get_nick (simnumber id, const char *address) {
  static char contact_nick_buffer[23];
  simcontact contact = id != CONTACT_ID_SELF ? contact_list_find_id (id) : contact_list.me;
  if (contact && (! address || ! strcmp (contact->addr, address))) {
    if (contact->nick.ptr)
      return contact->nick.ptr;
  } else if (! id || id == CONTACT_ID_SELF)
    return address;
  sprintf (contact_nick_buffer, "@@%020" SIM_FORMAT_64 "u", id);
  return contact_nick_buffer;
}

simnumber sim_contact_find_ (const char *nick) {
  simnumber id;
  simcontact contact;
  PTH_PROTECT_UNPROTECT_ (id = (contact = nick ? contact_list_find_nick (nick, NULL) : NULL) != NULL ? contact->id : 0);
  return id;
}

int contact_find_host (simcontact contact, const char *host, int port) {
  unsigned i;
  simtype hoststr = nil ();
  for (i = 1; i <= contact->hosts.len; i++) {
    int p;
    STRING_FREE (&hoststr, sim_network_parse_host_port (contact->hosts.arr[i].ptr, &p));
    if (! string_check_diff (hoststr, host) && (! port || port == p))
      break;
  }
  string_free (hoststr);
  return i > contact->hosts.len ? 0 : i;
}

void contact_add_host (simcontact contact, const char *host, int port) {
  if (host) {
    simtype buf = string_buffer_new (strlen (host) + 12);
    int i = contact_find_host (contact, host, 0);
    if (i) {
      array_delete (&contact->hosts, i, 1);
    } else if (contact->hosts.len >= SIM_MAX_HOST_COUNT) {
      array_delete (&contact->hosts, 1, 1);
    } else if (contact->hosts.typ == SIMNIL)
      contact->hosts = array_new_strings (0);
    sprintf (buf.ptr, "%s:%d", host, port);
    array_append (&contact->hosts, string_buffer_truncate (buf, strlen (buf.ptr)));
  } else
    ARRAY_FREE (&contact->hosts, nil ());
}

static void contact_set_right (simcontact contact, simnumber flags, simnumber right) {
  if (flags &= right) {
    contact->flags &= ~right;
    if (flags != right)
      contact->flags |= flags;
  }
}

int contact_set_rights (simcontact contact, const simtype info) {
  simbool audio, xfer, utf, edit;
  simnumber echo, type = contact->flags & ~(CONTACT_FLAG_TYPE_Y | CONTACT_FLAG_TYPE_N);
  if (info.typ != SIMNUMBER)
    return SIM_API_BAD_TYPE;
  if (info.num & ~(CONTACT_FLAG_HISTORY_Y | CONTACT_FLAG_HISTORY_N | CONTACT_FLAG_EDIT_Y | CONTACT_FLAG_EDIT_N |
                   CONTACT_FLAG_TYPING_Y | CONTACT_FLAG_TYPING_N | CONTACT_FLAG_TYPE_Y | CONTACT_FLAG_TYPE_N |
                   CONTACT_FLAG_UTF_Y | CONTACT_FLAG_UTF_N | CONTACT_FLAG_XFER_Y | CONTACT_FLAG_XFER_N |
                   CONTACT_FLAG_AUDIO_Y | CONTACT_FLAG_AUDIO_N | CONTACT_FLAG_ECHO_Y | CONTACT_FLAG_ECHO_N |
                   CONTACT_FLAG_SOUND_ON_Y | CONTACT_FLAG_SOUND_ON_N |
                   CONTACT_FLAG_SOUND_OFF_Y | CONTACT_FLAG_SOUND_OFF_N))
    return SIM_API_NO_RIGHT;
  if ((info.num & (CONTACT_FLAG_TYPE_Y | CONTACT_FLAG_TYPE_N)) == (CONTACT_FLAG_TYPE_Y | CONTACT_FLAG_TYPE_N))
    return SIM_API_NO_RIGHT;
  audio = CONTACT_CHECK_RIGHT_AUDIO (contact);
  xfer = CONTACT_CHECK_RIGHT_XFER (contact);
  utf = CONTACT_CHECK_RIGHT_UTF (contact);
  edit = CONTACT_CHECK_RIGHT_EDIT (contact);
  echo = CONTACT_CHECK_RIGHT_ECHO (contact);
  contact_set_right (contact, info.num, CONTACT_FLAG_TYPING_Y | CONTACT_FLAG_TYPING_N);
  contact_set_right (contact, info.num, CONTACT_FLAG_UTF_Y | CONTACT_FLAG_UTF_N);
  contact_set_right (contact, info.num, CONTACT_FLAG_XFER_Y | CONTACT_FLAG_XFER_N);
  contact_set_right (contact, info.num, CONTACT_FLAG_AUDIO_Y | CONTACT_FLAG_AUDIO_N);
  contact_set_right (contact, info.num, CONTACT_FLAG_ECHO_Y | CONTACT_FLAG_ECHO_N);
  contact_set_right (contact, info.num, CONTACT_FLAG_SOUND_OFF_Y | CONTACT_FLAG_SOUND_OFF_N);
  contact_set_right (contact, info.num, CONTACT_FLAG_SOUND_ON_Y | CONTACT_FLAG_SOUND_ON_N);
  contact_set_right (contact, contact->flags & CONTACT_FLAG_TYPING ? info.num : CONTACT_FLAG_TYPE_N,
                     CONTACT_FLAG_TYPE_Y | CONTACT_FLAG_TYPE_N);
  contact_set_right (contact, info.num, CONTACT_FLAG_HISTORY_Y | CONTACT_FLAG_HISTORY_N);
  contact_set_right (contact, info.num, CONTACT_FLAG_EDIT_Y | CONTACT_FLAG_EDIT_N);
  if (CONTACT_CHECK_RIGHT_XFER (contact) != xfer) {
    xfer_send_pause (contact, 0);
    return SIM_NO_ERROR;
  }
  if (CONTACT_CHECK_RIGHT_AUDIO (contact) != audio || CONTACT_CHECK_RIGHT_UTF (contact) != utf)
    return SIM_NO_ERROR;
  if (CONTACT_CHECK_RIGHT_EDIT (contact) != edit || CONTACT_CHECK_RIGHT_ECHO (contact) != echo)
    return SIM_NO_ERROR; /* SIM_NO_ERROR: save and notify. else save or notify but not both */
  return (contact->flags & ~(CONTACT_FLAG_TYPE_Y | CONTACT_FLAG_TYPE_N)) != type ? SIM_OK : SIM_KEY_NO_SAVE;
}

int contact_set_verified (simcontact contact, const simtype verify, const simtype key) {
  simnumber verified = contact->flags & CONTACT_FLAG_VERIFIED;
  if (verify.typ != SIMPOINTER && verify.typ != SIMSTRING)
    return SIM_API_BAD_TYPE;
  if (verify.len) {
    simbool diff;
    simtype addr;
    if (contact->auth < CONTACT_AUTH_NEW)
      return SIM_CONTACT_BLOCKED;
    if (contact->ec.typ == SIMNIL || contact->rsa.typ == SIMNIL)
      return SIM_CONTACT_NO_KEY;
    if (key.typ == SIMNIL)
      return SIM_CLIENT_UNKNOWN;
    diff = string_check_diff_len (addr = sim_contact_test_address (verify.ptr, "W"), key.str, key.len);
    string_free (addr);
    if (diff)
      return SIM_CONTACT_BAD_VERIFY;
    contact->flags |= CONTACT_FLAG_VERIFIED;
  } else
    contact->flags &= ~CONTACT_FLAG_VERIFIED;
  return (contact->flags & CONTACT_FLAG_VERIFIED) == verified ? SIM_OK : SIM_NO_ERROR;
}

int contact_set_keys (simcontact contact, const simtype ec, const simtype rsa) {
  if (! contact)
    return SIM_CONTACT_UNKNOWN;
  if (contact->ec.typ != SIMNIL || contact->rsa.typ != SIMNIL) {
    int err = SIM_OK;
    if (string_check_diff_len (contact->ec, ec.str, ec.len)) {
      LOG_SIMTYPE_ (SIM_MODULE_CRYPTO, SIM_LOG_ERROR, ec, LOG_BIT_BIN,
                    "falsified EC public key '%s' ", contact->nick.str);
      err = SIM_CRYPT_BAD_KEY;
    }
    if (string_check_diff_len (contact->rsa, rsa.str, rsa.len)) {
      LOG_SIMTYPE_ (SIM_MODULE_CRYPTO, SIM_LOG_ERROR, rsa, LOG_BIT_BIN,
                    "falsified RSA public key '%s' ", contact->nick.str);
      err = SIM_CRYPT_BAD_KEY;
    }
    return event_test_error_crypto (contact, contact->addr, err);
  }
  contact->ec = string_copy_string (ec);
  contact->rsa = string_copy_string (rsa);
  if (contact != contact_list.me && contact != contact_list.system)
    if (event_test_error (contact, SIM_EVENT_ERROR_FILE_SAVE, contact_list_save ()) != SIM_OK) {
      STRING_FREE (&contact->ec, nil ());
      STRING_FREE (&contact->rsa, nil ());
    }
  return SIM_OK;
}

void contact_set_status (simcontact contact, int status, simnumber flags) {
  int oldstatus = contact->status;
  simnumber oldflags = contact->flags, newflags;
  if (status == SIM_STATUS_INVISIBLE && oldstatus == SIM_STATUS_OFF) {
    status = oldstatus;
  } else if (status == SIM_STATUS_OFF && oldstatus == SIM_STATUS_INVISIBLE)
    status = oldstatus;
  if (contact != contact_list.me)
    contact->flags = (contact->flags & ~CONTACT_FLAGS_EVENT) | (flags & CONTACT_FLAGS_EVENT);
  if (param_get_number ("rights.typing") <= 0)
    contact->flags &= ~CONTACT_FLAG_TYPE;
  contact->flags = ((newflags = contact->flags) & ~CONTACT_FLAGS_NO_EVENT) | (flags & CONTACT_FLAGS_NO_EVENT);
  if (! (newflags & CONTACT_FLAG_TYPE) && oldflags & CONTACT_FLAG_TYPE && contact->client)
    contact->client->typingping = 0;
  if ((oldstatus != status || newflags != oldflags) && contact != contact_list.me) {
    simtype event = table_new_name (2, SIM_EVENT_STATUS);
    table_add_number (event, SIM_EVENT_STATUS, oldstatus);
    if (oldstatus != status)
      table_add_number (event, CONTACT_KEY_STATUS, contact->status = status);
    if (newflags != oldflags) {
      newflags = contact->msgs.flags & CONTACT_MSG_OVERFLOW ? contact->flags & ~CONTACT_FLAGS_OVERFLOW : contact->flags;
      table_add_number (event, CONTACT_KEY_FLAGS, newflags & CONTACT_FLAGS_EVENT);
    }
    event_send (contact, event);
  }
  if (status != SIM_STATUS_INVISIBLE && status != SIM_STATUS_OFF)
    if (oldstatus == SIM_STATUS_INVISIBLE || oldstatus == SIM_STATUS_OFF) {
      SIM_FREE_STRING (&contact->insecure, NULL);
      contact->sndcipher = NULL;
      if (contact->client)
        server_set_ciphers (contact->client);
    }
}

simbool contact_set_ip (simcontact contact, unsigned ip, int port, int mode) {
  if (ip && port) {
    int i, j = mode == CONTACT_IP_FIND ? param_get_number ("contact.ips") : contact->ips;
    for (i = 0; i < contact->ips && i < j; i++)
      if (contact->ip[i].ip == ip) {
        if (port == SIM_PROTO_PORT)
          port = contact->ip[i].port;
        if (contact->ip[i].port == port || contact->ip[i].port == SIM_PROTO_PORT) {
          if (mode == CONTACT_IP_FIND)
            return true;
          memmove (&contact->ip[i], &contact->ip[i + 1], (--contact->ips - i) * sizeof (*contact->ip));
          mode = CONTACT_IP_ADD;
        }
      }
    if (mode == CONTACT_IP_ADD) {
      memmove (&contact->ip[1], &contact->ip[0], sizeof (contact->ip) - sizeof (*contact->ip));
      if ((unsigned) contact->ips < SIM_ARRAY_SIZE (contact->ip))
        contact->ips++;
      contact->ip->ip = ip, contact->ip->port = (unsigned short) port;
    }
  } else
    LOG_ERROR_ ("invalid ip port = %d '%s'\n", port, contact->nick.str);
  return mode == CONTACT_IP_ADD;
}

int contact_set_line (simcontact contact, const simtype line) {
  int err = SIM_MSG_BAD_LENGTH;
  unsigned len = line.len;
  if (len <= SIM_MAX_LINE_SIZE) {
    string_free (sim_convert_utf_to_int (line.ptr, &len));
    if (! len && line.len) {
      err = SIM_MSG_BAD_CHAR;
    } else if ((err = string_check_diff_len (contact->line, line.str, line.len) ? SIM_NO_ERROR : SIM_OK) != SIM_OK) {
      if (contact == contact_list.me)
        contact_list_set_info (2);
      STRING_FREE (&contact->line, string_copy_string (line));
    }
  }
  return err;
}

void contact_set_nick (simcontact contact, const simtype nick, const simtype line, simnumber edit) {
  if (contact != contact_list.me) {
    int err, num = SIM_CHECK_POSITIVE (edit) ? (int) edit : 0;
    simtype event = table_new_name (2, SIM_EVENT_STATUS);
    if (nick.typ != SIMNIL) {
      simtype oldnick = string_copy_string (contact->nick);
      if (! string_check_diff_len (contact->own, nick.str, nick.len)) {
        string_free (oldnick);
      } else if (contact_rename (contact, nick, true) == SIM_OK) {
        if (contact->flags & CONTACT_FLAG_RENAMED) {
          STRING_FREE (&contact->nick, oldnick);
        } else if (string_check_diff_len (nick, oldnick.str, oldnick.len)) {
          table_add (event, CONTACT_KEY_OWN_NICK, oldnick);
          table_add_pointer_len (event, CONTACT_KEY_NICK, nick.str, nick.len);
        } else
          string_free (oldnick);
        STRING_FREE (&contact->own, string_copy_string (nick));
      } else {
        string_free (oldnick);
        LOG_WARN_ ("invalid nick %s '%s'\n", nick.str, contact->nick.str);
      }
    }
    if (line.typ != SIMNIL && (err = contact_set_line (contact, line)) != SIM_OK) {
      if (err == SIM_NO_ERROR) {
        table_add_pointer_len (event, CONTACT_KEY_LINE, line.str, line.len);
      } else
        LOG_WARN_ ("invalid info line %.*s '%s'\n", line.len, line.str, contact->nick.str);
    }
    if (edit != num) {
      LOG_WARN_ ("invalid edit number %lld '%s'\n", edit, contact->nick.str);
    } else if (contact->edit != num)
      table_add_number (event, CONTACT_KEY_EDIT, contact->edit = num);
    if (table_count (event)) {
      table_add_number (event, SIM_EVENT_STATUS, contact->status);
      event_send (contact, event);
    } else
      table_free (event);
  }
}

int contact_set_revocation (simcontact contact, const simtype reason) {
  int err = SIM_CONTACT_REVOKED, old = contact->auth;
  if (contact_reject (contact, CONTACT_AUTH_REVOKED) != CONTACT_AUTH_REVOKED) {
    unsigned len = reason.len;
    if (len <= SIM_MAX_REVOKE_SIZE) {
      string_free (sim_convert_utf_to_int (reason.ptr, &len));
      if (len && contact->revocation.typ == SIMNIL)
        contact->revocation = string_copy_string (reason);
    }
    event_test_error (contact, SIM_EVENT_ERROR_FILE_SAVE, err = contact_list_save_txt (NULL, CONTACT_TXT_DEFAULT));
    if (err != SIM_OK) {
      STRING_FREE (&contact->revocation, nil ());
      contact->auth = old;
    } else {
      LOG_WARN_ ("REVOKED %s '%s'\n", len || ! reason.len ? reason.str : NULL, contact->nick.str);
      xfer_save ();
    }
  }
  return err;
}

static const char *sim_contact_new_link (char *pathname, const simtype nick, simtype *name) {
  char *s = strrchr (pathname, *FILE_DIRECTORY_SLASH);
#ifdef _WIN32
  if (s)
    *s = 0;
  *name = string_concat (pathname, FILE_DIRECTORY_SLASH, nick.ptr, ".lnk", NULL);
  if (s)
    *s = *FILE_DIRECTORY_SLASH;
  s = pathname;
#else
  *name = string_cat_len (pathname, s ? (unsigned) (++s - pathname) : strlen (pathname), nick.ptr, nick.len);
#endif
  return s;
}

void contact_new_link (simcontact contact, char *pathname, const simtype oldnick, const simtype newnick) {
  simbool mode;
  simtype name;
  struct stat buf;
  if (! contact) {
    if (oldnick.typ != SIMNIL && sim_contact_check_nick (oldnick, FILE_EXTENSION_MD5)) {
      const char *s = sim_contact_new_link (pathname, oldnick, &name);
      if (! sim_file_lstat (name.ptr, &buf) && S_ISLNK (buf.st_mode)) {
        simtype tmp = sim_file_read_link (name.ptr);
        if (! s || strcmp (tmp.ptr, s) || sim_file_remove (name.ptr))
          LOG_WARN_ ("remove #%s error %d %s '%s'\n", s, errno, tmp.str, oldnick.str);
        string_free (tmp);
      }
      string_free (name);
    }
    if (newnick.typ != SIMNIL && sim_contact_check_nick (newnick, FILE_EXTENSION_MD5)) {
      const char *s = sim_contact_new_link (pathname, newnick, &name);
#ifdef _WIN32
      if (sim_file_lstat (name.ptr, &buf))
#endif
        if (! s || ! sim_file_create_link (s, name.ptr))
          LOG_WARN_ ("link #%s error %d '%s'\n", s, errno, newnick.str);
      string_free (name);
    }
  } else
    for (mode = false; mode <= true; mode++) {
      if ((name = xfer_new_dir_name (contact, mode)).typ != SIMNIL)
        if (newnick.typ == SIMNIL || (param_get_number ("contact.links") && ! sim_file_lstat (name.ptr, &buf)))
          contact_new_link (NULL, name.ptr, oldnick, newnick);
      string_free (name);
    }
}

int contact_new_ (const char *address, int auth, simcontact *contact) {
  struct _contact *newcontact, tmp;
  int err, max = auth >= CONTACT_AUTH_SYSTEM ? 0 : param_get_number ("contact.max");
  simtype tmpcontact;
  *contact = NULL;
  if (! address)
    return SIM_CONTACT_BAD_ADDRESS;
  memset (&tmp, 0, sizeof (tmp));
  tmp.auth = auth > CONTACT_AUTH_ACCEPTED ? CONTACT_AUTH_ACCEPTED : auth;
  if ((tmp.key = sim_contact_convert_address (address, 'S')).typ == SIMNIL)
    return SIM_CONTACT_BAD_ADDRESS;
  tmp.id = sim_contact_convert_to_id (address, auth == CONTACT_AUTH_SELF ? tmp.key : contact_list.me->key);
  if (tmp.id > -CONTACT_MIN_ID && tmp.id < CONTACT_MIN_ID) {
    string_free (tmp.key);
    return SIM_CONTACT_BAD_ID;
  }
  if ((*contact = newcontact = contact_list_find_id (tmp.id)) != NULL) {
    int i, newauth = newcontact != contact_list.me ? newcontact->auth : contact_list.auth;
    string_free (tmp.key);
    if (strcmp (newcontact->addr, address)) {
      *contact = NULL;
      return SIM_CONTACT_BAD_ID;
    }
    if (auth == CONTACT_AUTH_SYSTEM || newauth == tmp.auth)
      return SIM_CONTACT_EXISTS;
    if (newauth == CONTACT_AUTH_REVOKED)
      return SIM_CONTACT_REVOKED;
    if (newauth == CONTACT_AUTH_NEW) {
      for (i = 0; i < newcontact->ips && i < param_get_number ("contact.ips"); i++)
        client_probe (newcontact, newcontact->ip[i].ip, newcontact->ip[i].port, NULL, CLIENT_PROBE_ACCEPT);
      newcontact->ips = 0;
      if (contact_request_count)
        contact_request_count--; /* allow one more contact request for the day */
      newcontact->flags |= CONTACT_FLAG_INFO;
    } else
      newcontact->flags &= ~CONTACT_FLAG_NO_STATS;
    if (newcontact->auth <= CONTACT_AUTH_DELETED)
      newcontact->seen[CONTACT_SEEN_STATUS] = 0;
    newcontact->auth = tmp.auth;
    if (newcontact == contact_list.me) {
      contact_list.auth = tmp.auth;
    } else if (newauth == CONTACT_AUTH_FORGET)
      contact_new_link (newcontact, NULL, nil (), newcontact->nick);
    return SIM_OK;
  }
  if (max && contact_list_count (false) >= max) {
    string_free (tmp.key);
    return SIM_CONTACT_OVERFLOW;
  }
  msg_init (&tmp, 0);
  tmp.status = SIM_STATUS_OFF;
  tmp.flags = CONTACT_FLAGS_DEFAULT | (auth == CONTACT_AUTH_NEW ? CONTACT_FLAG_NO_STATS : 0);
  if (auth >= CONTACT_AUTH_ACCEPTED && ! (contact_list.test & 0x40000000) && auth < CONTACT_AUTH_SYSTEM)
    tmp.flags |= CONTACT_FLAG_INFO | (auth == CONTACT_AUTH_ACCEPTED ? 0 : CONTACT_FLAG_NEW);
  tmp.addr = string_copy (address).ptr;
  tmp.nick = sim_convert_simunsigned_to_string ("@", tmp.id, NULL);
  tmp.own = pointer_new ("");
  tmp.rsa = tmp.ec = tmp.versions = tmp.hosts = tmp.revocation = tmp.line = nil ();
  tmp.file = table_new (1);
  if (! contact_list.array.len) {
    contact_list.first = 1;
  } else if (auth <= CONTACT_AUTH_USER) {
    unsigned n = (unsigned) (random_get_number (random_public, contact_list.array.len - 1)) + 1;
    tmp.next = (newcontact = contact_list.array.arr[n].ptr)->next;
    newcontact->next = contact_list.array.len + 1;
  } /* else RNG not initialized yet but contact_list_init is yet to be called */
  *contact = newcontact = (tmpcontact = string_copy_len (&tmp, sizeof (tmp))).ptr;
  newcontact->xfer.hash = nil ();
  newcontact->xfer.last = &newcontact->xfers;
  array_append (&contact_list.array, pointer_new_len (tmpcontact.str, tmpcontact.len));
  table_add_key (contact_list.table, pointer_new_len (&newcontact->id, sizeof (newcontact->id)), tmpcontact);
  if ((err = auth > CONTACT_AUTH_USER ? SIM_OK : file_load_msg_ (newcontact, -1)) == SIM_OK) {
    msg_ack (newcontact, 0, -1);
  } else if (err != SIM_FILE_START)
    LOG_WARN_ ("xml @%020llu error %d\n", tmp.id, err);
  return SIM_OK;
}

int contact_rename (simcontact contact, const simtype nick, simbool always) {
  int count = 0;
  unsigned len = nick.len;
  simtype newnick;
  if (! len || len > SIM_SIZE_NICK)
    return SIM_CONTACT_BAD_NICK;
  string_free (sim_convert_utf_to_int (nick.ptr, &len));
  if (! len || len > SIM_MAX_NICK_LENGTH || ! sim_contact_check_nick (nick, NULL))
    return SIM_CONTACT_BAD_NICK;
  memcpy ((newnick = string_buffer_new (nick.len + 4)).str, nick.str, (len = nick.len) + 1);
  if (contact_list_find_nick (newnick.ptr, contact) || ! strcmp (newnick.ptr, CONTACT_VIP_TEST) ||
      (! strcmp (newnick.ptr, CONTACT_VIP_SYSTEM) && contact != contact_list.system))
    do {
      if (! always || count >= 999) {
        string_buffer_free (newnick);
        return SIM_CONTACT_EXISTS;
      }
      sprintf ((char *) newnick.str + nick.len, "%d", ++count);
      len = strlen (newnick.ptr);
    } while (contact_list_find_nick (newnick.ptr, contact));
  newnick = string_buffer_truncate (newnick, len);
  if (contact != contact_list.me && contact->auth != CONTACT_AUTH_FORGET)
    contact_new_link (contact, NULL, contact->nick, newnick);
  if (contact == contact_list.me && string_check_diff (contact->nick, newnick.ptr))
    contact_list_set_info (1);
  STRING_FREE (&contact->nick, newnick);
  return SIM_OK;
}

int contact_reject (simcontact contact, int auth) {
  int old = CONTACT_AUTH_FORGET;
  if (contact && ((contact != contact_list.me && contact != contact_list.system) || auth != old))
    if ((old = contact->auth) != CONTACT_AUTH_REVOKED) {
      if (contact == contact_list.me) {
        old = contact_list.auth;
        contact_list.auth = auth;
      } else {
        if ((contact->auth = auth) <= CONTACT_AUTH_DELETED)
          contact->seen[CONTACT_SEEN_STATUS] = time (NULL);
        if (auth == CONTACT_AUTH_FORGET) {
          contact_new_link (contact, NULL, contact->nick, nil ());
        } else if (old == CONTACT_AUTH_FORGET)
          contact_new_link (contact, NULL, nil (), contact->nick);
      }
    }
  return old;
}

void contact_cancel (simcontact contact, int error) {
#ifdef DONOT_DEFINE
  simcustomer server = proxy_find_server (contact->addr);
  if (server)
    proxy_cancel_server (server, error);
#endif
  client_cancel_contact (contact, error);
  contact_set_status (contact, SIM_STATUS_INVISIBLE, contact->flags);
}

#define CONTACT_CASE_PROBE(probe) \
  ((probe) == CONTACT_PROBE_CONNECT || (probe) == CONTACT_PROBE_DISCONNECT ? CLIENT_PROBE_LOGON : CLIENT_PROBE_REVERSE)

int contact_probe (simcontact contact, unsigned ip, int port, const char *host, int probe) {
  int i = probe == CONTACT_PROBE_VERIFY ? CONTACT_PROBE_DUPLICATE : probe;
  simnumber tick = system_get_tick ();
  if (probe == CONTACT_PROBE_RESOLVE && contact->probehost.ptr)
    STRING_FREE (&contact->probehost, nil ());
  if (tick - contact->probetick[i] < param_get_number ("contact.probe") * 1000) {
    static const char *contact_probe_names[] = {
      "CONNECT", "DISCONNECT", "RECONNECT", "HANDSHAKE", "RESOLVE", "DUPLICATE", "VERIFY"
    };
    if (probe == CONTACT_PROBE_RESOLVE)
      contact->probehost = string_copy (host);
    if (probe != CONTACT_PROBE_VERIFY)
      contact->probe[i].ip = ip, contact->probe[i].port = (unsigned short) port;
    LOG_DEBUG_ ("probe %s %s:%d SKIP '%s'\n", contact_probe_names[probe], network_convert_ip (ip), port,
                contact->nick.str);
    return SIM_NO_ERROR;
  }
  contact->probe[i].port = 0;
  contact->probetick[i] = tick;
  return client_probe (contact, ip, ip || host ? port : 0, host, CONTACT_CASE_PROBE (probe));
}

void contact_periodic (void) {
  unsigned i, n, timeout = param_get_number ("contact.probe") * 1000;
  simunsigned tick = system_get_tick ();
  for (n = 1; n <= contact_list.array.len; n++) {
    simcontact contact = contact_list.array.arr[n].ptr;
    for (i = 0; i < SIM_ARRAY_SIZE (contact->probe); i++)
      if (contact->probe[i].port && tick - contact->probetick[i] >= timeout) {
        char *host = i == CONTACT_PROBE_RESOLVE ? contact->probehost.ptr : NULL;
        unsigned ip = contact->probe[i].ip;
        client_probe (contact, ip, ip || host ? contact->probe[i].port : 0, host, CONTACT_CASE_PROBE (i));
        contact->probe[i].port = 0;
        contact->probetick[i] = tick;
        if (i == CONTACT_PROBE_RESOLVE && contact->probehost.ptr)
          STRING_FREE (&contact->probehost, nil ());
      }
    msg_periodic (contact, tick);
  }
}

void contact_log_list (const char *module, int level, int auth) {
  simtype key;
  simwalker ctx;
  if (table_walk_first (&ctx, contact_list.table))
    while ((key = table_walk_next_string (&ctx, NULL)).typ != SIMNIL) {
      simcontact contact = key.ptr;
      int i = contact != contact_list.me ? contact->auth : contact_list.auth;
      simnumber seen;
      if (i == CONTACT_AUTH_FORGET && auth != CONTACT_LOG_LONG)
        continue;
      if (auth == CONTACT_LOG_SHORT || auth == CONTACT_LOG_LONG) {
        log_any_ (module, level, "%c", "*/-_+ "[i - CONTACT_AUTH_FORGET]);
      } else if (i != auth && (i > CONTACT_AUTH_BLOCKED || auth != CONTACT_AUTH_DELETED))
        continue;
      log_any_ (module, level, "%s", contact->nick.str);
      if (auth == CONTACT_LOG_LONG && contact->own.len)
        if (string_check_diff_len (contact->nick, contact->own.str, contact->own.len))
          log_any_ (module, level, " [%s]", contact->own.str);
      log_any_ (module, level, " %s", CONTACT_LOOKUP_STATUS_NAME (contact->status));
      if (auth == CONTACT_LOG_LONG)
        log_any_ (module, level, " %s %s", network_convert_ip (contact->location),
                  contact->rcvcipher ? contact->rcvcipher : "none");
      if (contact->seen[CONTACT_SEEN_RECEIVE] > contact->seen[CONTACT_SEEN_STATUS]) {
        seen = contact->seen[CONTACT_SEEN_RECEIVE];
      } else
        seen = contact->seen[CONTACT_SEEN_STATUS];
      if (seen && (seen = time (NULL) - seen) >= 0)
        log_any_ (module, level, " (seen %lld:%02d:%02d ago)", seen / 3600, (int) (seen / 60 % 60), (int) (seen % 60));
      for (i = 0; i < contact->ips; i++)
        log_any_ (module, level, " %s:%d", network_convert_ip (contact->ip[i].ip), contact->ip[i].port);
      if (auth == CONTACT_LOG_LONG) {
        if (contact->hosts.typ != SIMNIL)
          log_simtype_ (module, level, contact->hosts, LOG_BIT_CONT, " ");
        if (contact->connected)
          log_any_ (module, level, " count=%d", contact->connected);
        log_any_ (module, level, " logon=%d hs=%d", contact->logon, contact->handshake);
      }
      if (contact->xfer.hash.typ != SIMNIL)
        log_any_ (module, level, " XFERS=%u", contact->xfer.hash.len);
      if (contact->msgs.flags & CONTACT_MSG_NO_FILES)
        log_any_ (module, level, " NO-FILES");
      if (contact->msgs.flags & CONTACT_MSG_NO_SPACE)
        log_any_ (module, level, " NO-SPACE");
      if (contact->msgs.flags & CONTACT_MSG_NO_TRAVERSE)
        log_any_ (module, level, " NO-TRAVERSE");
      log_any_ (module, level, " @%020llu", contact->id);
      if (auth == CONTACT_LOG_LONG)
        log_any_ (module, level, " [%s]", contact->addr);
      log_any_ (module, level, "\n");
    }
}

int contact_log_stats (const char *module, int level, const char *key, simnumber id) {
  unsigned i, j;
  if (id) {
    unsigned m = ! key || ! *key ? CONTACT_STATS_INPUT : CONTACT_STATS_PEER;
    simcontact contact = NULL;
    if (id == CONTACT_ID_TEST) {
      if (m == CONTACT_STATS_PEER)
        m = SIM_ARRAY_SIZE (contact_stat_names) - 1;
    } else if ((contact = contact_list_find_id (id)) == NULL)
      return SIM_CONTACT_UNKNOWN;
    for (i = 0; i <= m; i++)
      for (j = 0; j <= CONTACT_STAT_COUNT; j++) {
        simnumber n = contact ? contact->stats[i][j] : main_get_stat (i, j);
        if (n && (! key || ! *key || ! strncmp (contact_stat_names[i][j], key, strlen (key)))) {
          log_any_ (module, level, "'%s' %s = %lld\n",
                    contact ? (char *) contact->nick.str : CONTACT_VIP_TEST, contact_stat_names[i][j], n);
        } else if (key && ! strcmp (key, "RESET")) {
          if (contact) {
            contact->stats[i][j] = 0;
          } else if (i >= CONTACT_STATS_MINE - PROXY_STATS_ALL)
            proxy_set_stat (0, i - CONTACT_STATS_MINE + PROXY_STATS_ALL, j);
        }
      }
  } else {
    if (! key || ! *key)
      contact_log_stats (module, level, "D", CONTACT_ID_TEST);
    contact_log_stats (module, level, key, CONTACT_ID_TEST);
    for (i = 1; i <= contact_list.array.len; i++)
      contact_log_stats (module, level, key, ((simcontact) contact_list.array.arr[i].ptr)->id);
  }
  return SIM_OK;
}
