/**
    simcore API implementation

    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 "sssl.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 "limit.h"
#include "proxy.h"
#include "nat.h"
#include "server.h"
#include "client.h"
#include "msg.h"
#include "xfer.h"
#include "audio.h"
#include "api.h"
#include "console.h"

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

#ifdef _WIN32
void _exit (int);
#endif

#define SIM_MODULE SIM_MODULE_API

#define SIM_CORE_VERSION 1

#define API_UNINITIALIZED 0 /* sim_exit_ has succeeded */
#define API_INITIALIZING 1  /* inside sim_init_ */
#define API_INITIALIZED 2   /* sim_init_ has succeeded */
#define API_GENERATING 3    /* sim_key_generate_ is working */
#define API_LOGGINGIN 4     /* inside sim_init_user_ */
#define API_LOGGEDIN 5      /* sim_init_user_ has succeeded */
#define API_STATUSCHANGE 6  /* inside sim_status_set_ */
#define API_LOGGINGOUT 7    /* inside sim_exit_user_ */
#define API_LOGGEDOUT 8     /* sim_exit_user_ has succeeded */

struct _status_self simself;

static unsigned api_init_mask = SIM_INIT_BIT_DEFAULT;
static simtype api_init_ui_string;
static simbool api_init_ui_autostarted;
static int api_init_version = -1;

static unsigned api_init_state = API_UNINITIALIZED;
static int api_init_error = SIM_OK;
static char *api_init_error_info = NULL;

static simnumber api_proxy_tick, api_proxy_rcvbytes, api_proxy_sndbytes; /* proxy statistics */

static simunsigned api_status_on_tick, api_status_off_tick; /* tick of last status on and last status off */

#define SIM_PROTECT_(...) sim_protect_ (), LOG_API_INFO_ (__VA_ARGS__)
#define SIM_UNPROTECT_VALUE_(ret) (sim_unprotect_ (), ret)
#define SIM_UNPROTECT_(error) ((void) LOG_API_WARN_ (error), SIM_UNPROTECT_VALUE_ (error))

void sim_protect_ (void) {
#ifndef HAVE_LIBPTH
  pth_protect_ ();
#endif
}

void sim_unprotect_ (void) {
#ifndef HAVE_LIBPTH
  pth_unprotect ();
#endif
}

static int status_exit_ (simbool init) {
  int err;
  if (api_init_state == API_LOGGEDIN) {
    api_init_state = API_LOGGINGOUT;
    param_set_number ("client.logon", 0, SIM_PARAM_TEMPORARY);
    simself.state = CLIENT_STATE_STOPPING;
    if ((err = main_exit ()) != SIM_OK)
      event_send_name (NULL, SIM_EVENT_ERROR, SIM_EVENT_ERROR_FILE_SAVE, number_new (err));
    if (simself.status != SIM_STATUS_OFF && simself.status != SIM_STATUS_INVISIBLE) {
      simself.status = SIM_STATUS_INVISIBLE; /* prevent peers from getting non-offline status from me after this point */
      client_logoff (SIM_STATUS_INVISIBLE);
    }
  }
  api_init_state = API_LOGGEDOUT;
  LOG_CODE_NOTE_ ((client_log_clients (SIM_MODULE, SIM_LOG_NOTE, false), client_log_probes (SIM_MODULE, SIM_LOG_NOTE)));
  if ((err = client_uninit_ (init)) == SIM_OK) {
    audio_stop_sound_ (NULL);
    param_close ();
    key_uninit ();
    file_set_password (nil (), true);
    api_init_state = API_INITIALIZED;
  }
  ssl_exit_thread_ ();
  return err;
}

unsigned sim_init_bits (unsigned bits) {
  unsigned old = api_init_mask;
  api_init_mask = bits == SIM_INIT_BIT_DEFAULT ? bits : old | bits;
  return old;
}

simtype sim_init_path (int path) {
  simtype ret = nil ();
  if (path == SIM_PATH_USER) {
    if ((ret = sim_file_name_new ("", FILE_DIRECTORY_USER)).typ == SIMSTRING) {
      simtype tmp = string_copy_len (ret.ptr, ret.len - 1);
      string_free (ret);
      ret = tmp;
    }
  } else if (path == SIM_PATH_EXE && log_init_ () == SIM_OK)
    sim_system_get_exe (&ret);
  return ret;
}

simtype sim_init_path_ (const char *user, simbool create) {
  simtype path = nil (), login;
  if (log_init_ () == SIM_OK && file_set_user (user) != NULL)
    if ((path = sim_init_path (SIM_PATH_USER)).typ != SIMNIL && create) {
      if (user[0] != '.' || (user[1] != *FILE_DIRECTORY_SLASH && user[1] != '/')) {
        sim_file_mkdir ((login = sim_file_name_new ("", FILE_DIRECTORY_LOGIN)).ptr);
        string_free (login);
      }
      sim_file_mkdir (path.ptr);
    }
  return path;
}

int sim_init_move (const char *pathname, const char *prefix) {
  int err = EBADF;
  unsigned len;
  simunsigned i;
  simtype path, name;
  for (len = (name = string_copy (pathname)).len; len--;)
    if (name.str[len] == *FILE_DIRECTORY_SLASH || name.str[len] == '/')
      break;
  name.str[len + 1] = 0; /* remove file name from pathname */
  path = string_cat (name.ptr, prefix);
  for (i = 1; i < 100 && err; i++) {
    string_free (name);
    name = sim_convert_simunsigned_to_string (path.ptr, i, "");
    err = ! sim_file_rename (pathname, name.ptr) ? SIM_OK : errno ? errno : EBADF;
  }
  string_free (name);
  string_free (path);
  return err;
}

simtype sim_init_param_ (const char *user) {
  simtype params = nil ();
  if (api_init_state == API_UNINITIALIZED && log_init_ () == SIM_OK) {
    file_set_user (user);
    if (param_init (false) == SIM_OK) {
      params = param_table;
      param_table = nil ();
      param_uninit ();
    }
  }
  return params;
}

int sim_init_ (const char *user, unsigned version, const char *ui) {
  static simbool system_installed_flag = false;
  int err, err2;
  char *info = NULL;
  if (version > SIM_CORE_VERSION || sizeof (void *) != SIZEOF_VOID_P || sizeof (simtype) != 16)
    return SIM_API_BAD_VERSION;
  if (! ui || strlen (ui) < 4 || strlen (ui) > 80)
    return SIM_API_BAD_PARAM;
  if (api_init_state != API_UNINITIALIZED)
    return SIM_API_INIT;
  api_init_ui_autostarted = false;
  api_init_state = API_INITIALIZING;
  if ((err = log_init_ ()) != SIM_OK)
    return err;
#ifdef _WIN32
  if (system_init_ ((api_init_mask & SIM_INIT_BIT_INSTALL) != 0, NULL) == SIM_OK) {
    _exit (1);
    system_uninit ();
    uninit_log ();
    return SIM_API_INSTALL_CANCELLED;
  }
#else
  system_init_ ((api_init_mask & SIM_INIT_BIT_INSTALL) != 0, NULL);
#endif
  if ((err = error_init ()) != SIM_OK)
    return err;
  api_init_version = version;
  event_init ();
  console_init ();
  contact_init ();
  log_set_level_ (NULL, SIM_LOG_XTRA);
  log_load_ (LOG_NO_EVENT, 0); /* previously logged messages are now LOST */
  user = file_set_user (user);
  err = param_init (true);
  LOG_API_DEBUG_ ("(bits = %u) %u \"%s\" %s\n", api_init_mask, version, ui, user);
  system_log_stack (0);
  if (err != SIM_OK) {
    api_init_state = API_UNINITIALIZED;
  } else if ((err = ssl_init_ (NULL, nil (), nil ())) != SIM_OK)
    param_uninit ();
  if (err != SIM_OK) { /* clean up to a sane state so sim_init_ can be retried */
  quit:
    event_uninit ();
    contact_uninit ();
    console_uninit ();
    api_init_version = -1;
    LOG_API_WARN_ (err);
    return err;
  }
  if (! system_installed_flag) {
    int install = param_get_number ("system.install");
    system_installed_flag = true;
    if (install >= 0) {
      PTH_PROTECT_UNPROTECT_ (err2 = system_init_ (api_init_mask & SIM_INIT_BIT_INSTALL && install > 0, &err));
      if (err2 != SIM_OK) {
        ssl_uninit_ (true);
        api_init_state = API_UNINITIALIZED;
        goto quit;
      }
    }
    info = sim_error_get_text (err);
  }
  sim_free_string (api_init_error_info);
  api_init_error = err;
  api_init_error_info = info;
#ifndef SIM_BOUNDS_CHECK
  if (! (api_init_mask & SIM_INIT_BIT_NOCRASH))
    system_init_crash (param_get_number ("system.crash"));
#endif
  if (! (api_init_mask & SIM_INIT_BIT_NOAUDIO))
    PTH_PROTECT_UNPROTECT_ (audio_init_ (SIM_STATUS_ON));
  api_init_ui_string = string_copy (ui);
  api_init_state = API_INITIALIZED;
  LOG_API_DEBUG_ ("error %d: %s\n", err, info);
  return SIM_OK;
}

int sim_init_user_ (const char *password) {
  int err, offline, i;
  SIM_PROTECT_ ("%s\n", password && *password ? "PASSWORD" : password);
  system_log_stack (0);
  if (api_init_state != API_INITIALIZED)
    return SIM_UNPROTECT_ (SIM_API_INIT);
  api_init_state = API_LOGGINGIN;
  if ((err = key_init_ (password)) == SIM_OK && (err = param_open ()) == SIM_OK && (err = xfer_init ()) == SIM_OK) {
    const unsigned len = SIM_STATUS_MAX - SIM_STATUS_MIN + 1;
    simtype name = sim_file_name_new (FILE_RANDOM, FILE_DIRECTORY_USER), status = param_get_string ("client.status");
    simunsigned ftimes[3];
    if ((offline = param_get_number ("contact.offline")) != 0 && ! sim_file_size (name.ptr, NULL, (simnumber *) ftimes))
      if (offline == 1 || (simunsigned) time (NULL) - ftimes[0] / 1000000000 >= (unsigned) offline)
        for (i = 1; i <= (int) contact_list.array.len; i++) {
          simcontact contact = contact_list.array.arr[i].ptr;
          contact->status = SIM_STATUS_OFF;
        }
    string_free (name);
    random_init_entropy (param_table);
    LOG_NOTE_SIMTYPE_ (param_table, 0, "config ");
    memset (&simself, 0, sizeof (simself));
    simself.status = sim_convert_string_to_enum (status.ptr, SIM_STATUS_MIN, SIM_STATUS_ON, contact_status_names, len);
    api_init_ui_autostarted = param_get_number ("ui.login.autostarted");
    api_status_on_tick = api_status_off_tick = system_get_tick ();
    if ((err = client_init_ ()) == SIM_OK) {
      simtype ec, rsa;
      key_get (true, &ec, &rsa);
      if ((err = contact_set_keys (contact_list.me, ec, rsa)) == SIM_OK)
        if (simself.status != SIM_STATUS_OFF && ! (api_init_mask & SIM_INIT_BIT_NODHT))
          err = main_init ();
      if (err == SIM_OK && contact_list.system) {
        key_get (false, &ec, &rsa);
        if ((err = contact_set_keys (contact_list.system, ec, rsa)) == SIM_OK)
          contact_list.system->flags |= CONTACT_FLAG_VERIFY | CONTACT_FLAG_VERIFIED;
      }
      if (err != SIM_OK) {
        const char *info;
        int err2 = sim_error_save_text (&info);
        client_uninit_ (true);
        sim_error_load_text (info, err2);
      }
    }
    if (err == SIM_OK) {
      event_send_tick_type (SIM_EVENT_HISTORY);
      random_buffer_close (random_private);
      if (! param_get_number ("msg.maxmb")) {
        int maxmb = param_get_max ("msg.maxmb", 1);
        simnumber size = sim_system_get_memory (false) >> 20 >> 2;
        param_set_number ("msg.maxmb", size >= maxmb ? maxmb : size > 0 ? (int) size + 1 : 128, SIM_PARAM_TEMPORARY);
      }
      contact_list_load_txt_ (NULL);
      for (i = 1; i <= (int) contact_list.array.len; i++) {
        simcontact contact = contact_list.array.arr[i].ptr;
        if (contact->msgs.flags & CONTACT_MSG_UNSAVED)
          msg_save (contact);
        err = file_load_msg_ (contact, -1);
        if (err != SIM_OK && err != SIM_FILE_START)
          LOG_WARN_ ("%s error %d '%s'\n", __FUNCTION__, err, contact->nick.str);
      }
      api_init_state = API_LOGGEDIN;
      event_test_net_device (SIM_STATUS_BUSY);
      if (api_init_error != SIM_OK) {
        sim_error_set_text (api_init_error_info, NULL, NULL, api_init_error);
        sim_free_string (api_init_error_info), api_init_error_info = NULL;
        event_send_name (NULL, SIM_EVENT_ERROR, SIM_EVENT_ERROR_INSTALL, number_new (api_init_error));
        api_init_error = SIM_OK;
      }
      api_proxy_rcvbytes = api_proxy_sndbytes = api_proxy_tick = 0;
      err = SIM_OK;
    } else
      api_init_ui_autostarted = false;
  }
  if (err != SIM_OK) {
    param_close ();
    key_uninit ();
    file_set_password (nil (), true);
    api_init_state = API_INITIALIZED;
  }
  return SIM_UNPROTECT_ (err);
}

int sim_exit_user_ (void) {
  int err;
  SIM_PROTECT_ ("\n");
  system_log_stack (system_get_tick ());
  while (api_init_state == API_STATUSCHANGE)
    pth_usleep_ (100000);
  err = api_init_state == API_LOGGEDIN ? status_exit_ (true) : SIM_API_INIT;
  return SIM_UNPROTECT_ (err);
}

int sim_exit_ (void) {
  SIM_PROTECT_ ("\n");
  while (api_init_state == API_STATUSCHANGE)
    pth_usleep_ (100000);
  if (api_init_state == API_LOGGEDIN || api_init_state == API_LOGGINGOUT)
    status_exit_ (false);
  if (api_init_state != API_UNINITIALIZED) {
    if (api_init_state != API_INITIALIZED) {
      int err = SIM_API_INIT;
      if (api_init_state == API_LOGGEDOUT) {
        err = simself.state == CLIENT_STATE_STOPPED ? client_uninit_ (false) : SIM_OK;
        simself.state = CLIENT_STATE_STOPPED;
      }
      file_lock (false);
      return SIM_UNPROTECT_ (err);
    }
    /*err= */ audio_uninit_ (false);
    ssl_uninit_ (false);
    param_uninit ();
    event_unregister ();
    contact_uninit ();
    console_uninit ();
#ifndef SIM_BOUNDS_CHECK
    if (! (api_init_mask & SIM_INIT_BIT_NOCRASH))
      system_init_crash (0);
#endif
    string_free (api_init_ui_string), api_init_ui_string = nil ();
  }
  file_set_user (".");
  system_log_stack (0);
  error_uninit ();
  system_uninit ();
  if (api_init_state == API_INITIALIZED)
    event_uninit ();
  uninit_log ();
  api_init_state = API_UNINITIALIZED;
  return SIM_UNPROTECT_VALUE_ (SIM_OK);
}

int sim_event_register_ (const char *name, simevent *handler, simevent **oldhandler) {
  int err = SIM_OK;
  SIM_PROTECT_ ("%s\n", name);
  if (oldhandler && name) {
    *oldhandler = event_register (name, handler);
  } else
    err = SIM_API_BAD_PARAM;
  return SIM_UNPROTECT_ (err);
}

int sim_key_generate_ (simtype *seed, const simtype entropy, int size, unsigned type) {
  int err = SIM_OK;
  unsigned keytype = type;
  simtype tmp, seeds[2];
  SIM_PROTECT_ ("%d %d %s (len = %u)\n", size, type, seed && seed->typ != SIMNIL ? "SEED" : "", entropy.len);
  sim_error_reset_text ();
  if (api_init_state != API_INITIALIZED)
    return SIM_UNPROTECT_ (SIM_API_INIT);
  if (! seed)
    return SIM_UNPROTECT_ (SIM_API_BAD_PARAM);
  if (entropy.typ != SIMNIL) {
    if ((! size && keytype == SIM_KEY_NONE) || (entropy.typ != SIMPOINTER && entropy.typ != SIMSTRING))
      return SIM_UNPROTECT_ (SIM_API_BAD_TYPE);
    random_buffer_xor (random_private, entropy.str, entropy.len);
  }
  if ((size || type != SIM_KEY_NONE) && ((int) type < SIM_KEY_BP512R1_RSA2K || type > SIM_KEY_BP512R1_RSA16K))
    return SIM_UNPROTECT_ (SIM_API_BAD_TYPE);
  if (size) {
    static const int key_seed_bits[SIM_KEY_BP512R1_RSA16K + 1] = { 128, 128, 192, 256 };
    if (size < key_seed_bits[type] || size > SIM_MAX_SEED_BITS)
      return SIM_UNPROTECT_ (SIM_API_BAD_TYPE);
    if ((tmp = key_generate_seed (size)).typ == SIMNIL)
      return SIM_UNPROTECT_ (SIM_KEY_NO_ENTROPY);
    seeds[0] = convert_seed_to_string (tmp, type, false);
    seeds[1] = convert_seed_to_string (tmp, type, true);
    *seed = string_new (strlen (seeds[0].ptr) + strlen (seeds[1].ptr) + 2);
    strcpy (seed->ptr, seeds[0].ptr);
    strcpy ((char *) (seed->str + strlen (seed->ptr) + 1), seeds[1].ptr);
    seed->str[strlen (seeds[0].ptr) + strlen (seeds[1].ptr) + 2] = 0;
    string_free (seeds[1]);
    string_free (seeds[0]);
  } else {
    if (seed->typ != SIMNIL && seed->typ != SIMSTRING && seed->typ != SIMPOINTER)
      return SIM_UNPROTECT_ (SIM_API_BAD_PARAM);
    tmp = convert_string_to_seed (seed->ptr, &type);
    if (keytype == SIM_KEY_NONE) {
      if ((int) type >= SIM_KEY_BP512R1_RSA2K && type <= SIM_KEY_BP512R1_RSA16K) {
        keytype = type;
      } else if (type == SIM_KEY_NONE) {
        err = SIM_API_BAD_SEED;
        if (tmp.typ == SIMNIL || tmp.len)
          sim_error_set_text (": ", tmp.ptr, NULL, err);
      } else
        err = SIM_KEY_BAD_TYPE;
    } else if (type != SIM_KEY_NONE) {
      err = SIM_API_BAD_PARAM;
    } else if (seed->typ != SIMNIL)
      if (sim_file_check_exists (FILE_KEY) || sim_file_check_exists (FILE_KEY SIM_FILE_BACKUP))
        err = SIM_KEY_EXISTS;
    if (err == SIM_OK) {
      simtype str = seed->typ != SIMNIL ? string_copy (seed->ptr) : nil ();
      err = key_generate_key (&api_init_state, str, type != SIM_KEY_NONE ? string_copy_string (tmp) : nil (), keytype);
    }
    if (err == SIM_OK)
      api_init_state = API_GENERATING;
  }
  string_free (tmp);
  return SIM_UNPROTECT_ (err);
}

int sim_key_set_password_ (const char *password, unsigned bits) {
  int err = SIM_API_INIT, type;
  SIM_PROTECT_ ("%s %u\n", ! password ? NULL : *password ? "PASSWORD" : "NONE", bits);
  if (bits == SIM_PASSWORD_BIT_CHECK) {
    if (password) {
      string_free (convert_string_to_seed (password, (unsigned *) &type));
      err = (unsigned) type != SIM_KEY_NONE ? SIM_OK : SIM_API_BAD_SEED;
    } else if ((type = file_check_password ()) >= 0)
      err = type ? SIM_API_BAD_SEED : SIM_OK;
  } else if (api_init_state == API_LOGGEDIN || api_init_state == API_STATUSCHANGE) {
    if (password) {
      simtype seed = file_set_password (*password ? string_copy (password) : nil (), true), ec, rsa;
      if (seed.typ != SIMNIL) {
        seed = sim_crypt_md_hash (sim_crypt_md_new (CRYPT_MD_RIPEMD), nil (), nil (), seed);
        if (string_check_diff_len (key_get (false, &ec, &rsa), seed.str, seed.len)) {
          file_set_password (nil (), true);
          err = SIM_API_BAD_SEED;
        }
        string_free (seed);
      }
      if (err == SIM_API_INIT)
        err = key_save_ (bits & SIM_PASSWORD_BIT_OVERWRITE ? KEY_MODE_SAVE : KEY_MODE_SAVE | KEY_MODE_LOAD);
    } else if (bits & SIM_PASSWORD_BIT_OVERWRITE) {
      err = key_save_ (KEY_MODE_KILL);
    } else {
      err = SIM_OK;
      if (sim_file_check_exists (FILE_KEY) || sim_file_check_exists (FILE_KEY SIM_FILE_BACKUP))
        err = SIM_KEY_EXISTS;
    }
  }
  return SIM_UNPROTECT_ (err);
}

simunsigned status_get_tick (void) {
  if (simself.state == CLIENT_STATE_RUNNING && simself.status != SIM_STATUS_OFF)
    return system_get_tick () - api_status_on_tick + 1;
  return 0;
}

int sim_status_get (unsigned *flags) {
  /*LOG_API_INFO_ ("%d\n", simself.status);*/
  if (flags)
    *flags = (simself.flags & ~SIM_STATUS_FLAG_IP) | (api_init_mask & SIM_INIT_BIT_NODHT ? SIM_STATUS_FLAG_DHT_OUT : 0);
  return simself.status;
}

int sim_status_get_ (unsigned *flags) {
  int status;
  PTH_PROTECT_UNPROTECT_ (status = sim_status_get (flags));
  return status;
}

int sim_status_set_ (int status) {
  int err = SIM_OK, oldstatus;
  SIM_PROTECT_ ("%d\n", status);
  if (status < SIM_STATUS_MIN || status > SIM_STATUS_MAX)
    return SIM_UNPROTECT_ (SIM_API_BAD_STATUS);
  while (api_init_state == API_STATUSCHANGE)
    pth_usleep_ (100000);
  if (api_init_state != API_LOGGEDIN)
    return SIM_UNPROTECT_ (SIM_API_INIT);
  api_init_state = API_STATUSCHANGE;
  oldstatus = simself.status;
  if (status == SIM_STATUS_OFF && oldstatus != SIM_STATUS_OFF) {
    int clos = param_get_number ("server.close");
    simself.status = SIM_STATUS_OFF;
    api_status_off_tick = system_get_tick ();
    if ((clos = clos >= 0 && (clos || proxy_check_required (false))) != 0) {
      int fd = socket_udp_sock.fd;
      socket_udp_sock.fd = INVALID_SOCKET;
      if ((err = main_exit ()) != SIM_OK)
        event_send_name (NULL, SIM_EVENT_ERROR, SIM_EVENT_ERROR_FILE_SAVE, number_new (err));
      main_uninit_ (false);
      socket_udp_sock.fd = fd;
      err = SIM_OK;
    }
    server_logoff (SIM_CLIENT_OFFLINE, oldstatus);
    if (clos) {
      proxy_send_error (SIM_PROXY_OFFLINE);
      server_uninit_ (false);
    }
  } else if (oldstatus != status) {
    if (oldstatus == SIM_STATUS_OFF) {
      unsigned offline = param_get_number ("contact.offline"), i;
      if (offline && (offline == 1 || system_get_tick () - api_status_off_tick >= offline * 1000))
        for (i = 1; i <= contact_list.array.len; i++) {
          simcontact contact = contact_list.array.arr[i].ptr;
          contact->status = SIM_STATUS_OFF;
        }
      if ((err = server_init_ (false)) == SIM_OK) {
        if ((err = proxy_init (false)) != SIM_OK) {
          server_uninit_ (false);
        } else if (! (api_init_mask & SIM_INIT_BIT_NODHT) && (err = main_init ()) == SIM_MAIN_INIT)
          err = SIM_OK;
      } else if (err == SIM_SERVER_INIT)
        err = proxy_init (false);
    }
    if (err == SIM_OK) {
      if ((simself.status = status) != SIM_STATUS_INVISIBLE) {
        client_cancel_probes (SIM_STATUS_ON);
        client_logon (false);
        client_send_status (NULL, CLIENT_SEND_STATUS_ON);
      } else if (oldstatus != SIM_STATUS_OFF)
        client_logoff (SIM_STATUS_ON);
      if (status != SIM_STATUS_IDLE)
        api_status_on_tick = system_get_tick ();
    }
  }
  if (err == SIM_OK && status != oldstatus && status != SIM_STATUS_OFF) {
    simtype newstatus = pointer_new (contact_status_names[status - SIM_STATUS_MIN]);
    if ((err = param_set_string ("client.status", newstatus, SIM_PARAM_PERMANENT)) == SIM_OK)
      if (event_test_error (NULL, SIM_EVENT_ERROR_FILE_SAVE, err = param_save ()) != SIM_OK)
        if (status != SIM_STATUS_OFF)
          err = SIM_OK;
  }
  api_init_state = API_LOGGEDIN;
  return SIM_UNPROTECT_ (err);
}

int sim_status_exec_ (const char *type, const char *value) {
  int err = SIM_OK;
  SIM_PROTECT_ ("%s\n", type);
  if (! type || api_init_state != API_LOGGEDIN) {
    err = type ? SIM_API_INIT : SIM_API_BAD_TYPE;
  } else if (! strcmp (type, SIM_STATUS_EXEC_DHT_OFF)) {
    event_test_error (NULL, SIM_EVENT_ERROR_FILE_SAVE, main_exit ());
    err = main_uninit_ (false);
  } else if (! strcmp (type, SIM_STATUS_EXEC_DHT_ON)) {
    err = main_init ();
  } else if (! strcmp (type, SIM_STATUS_EXEC_LOG_OFF)) {
    client_logoff (SIM_STATUS_ON);
  } else if (! strcmp (type, SIM_STATUS_EXEC_LOG_ON)) {
    client_logon (false);
  } else if (! strcmp (type, SIM_STATUS_EXEC_CHECK)) {
    client_logon (true);
  } else if (! strcmp (type, SIM_STATUS_EXEC_PROXY_BLOCK)) {
    err = proxy_cancel_proxy (value, SIM_PROXY_BLOCKED);
  } else if (! strcmp (type, SIM_STATUS_EXEC_PROXY_DROP)) {
    err = proxy_cancel_proxy (NULL, SIM_PROXY_DROPPED);
  } else
    err = SIM_API_BAD_TYPE;
  return SIM_UNPROTECT_ (err);
}

static simtype client_convert_keys (simclient client, const simtype ec, const simtype rsa) {
  simtype addr, hash;
  if (! client || ! client->sock->master || (addr = client->sock->master->key).typ == SIMNIL)
    return nil ();
  hash = sim_crypt_md_hash (sim_crypt_md_new (CRYPT_MD_WHIRLPOOL), ec, rsa, pointer_new_len (addr.str, 4));
  addr = sim_contact_convert_to_address (pointer_new_len (hash.str, CONTACT_ADDRESS_LENGTH), 'W');
  string_free (hash);
  return addr;
}

static int contact_accept_ (const char *address, simnumber *id) {
  simcontact contact = NULL;
  int err = contact_new_ (address, CONTACT_AUTH_AUTHORIZE, &contact);
  if (err == SIM_OK || (err == SIM_CONTACT_EXISTS && ! contact->client)) {
    if (contact != contact_list.me) {
      client_probe (contact, 0, 0, NULL, CLIENT_PROBE_LOGON);
      if (! contact->dht.search)
        contact->dht.search = system_get_tick () + param_get_number ("main.research") * 1000;
      contact->dht.active = true;
      if (err == SIM_OK)
        err = contact_list_save_txt (NULL, CONTACT_TXT_DEFAULT);
    } else if (err == SIM_OK)
      err = contact_list_save ();
  }
  if (id)
    *id = contact ? contact->id : 0;
  return err == SIM_CONTACT_EXISTS ? SIM_OK : err;
}

int sim_contact_connect_ (simnumber id) {
  int err = SIM_CONTACT_UNKNOWN;
  simcontact contact;
  SIM_PROTECT_ ("'%s'", contact_get_nick (id, NULL));
  if ((contact = contact_list_find_id (id)) != NULL && contact->auth >= CONTACT_AUTH_NEW) {
    LOG_INFO_ (" %u\n", contact->connected + 1);
    err = msg_connect (contact, true);
    contact->connected++;
  } else
    LOG_INFO_ ("\n");
  return SIM_UNPROTECT_ (err);
}

int sim_contact_disconnect_ (simnumber id) {
  int err = SIM_CONTACT_UNKNOWN;
  simcontact contact;
  SIM_PROTECT_ ("'%s'", contact_get_nick (id, NULL));
  if ((contact = contact_list_find_id (id)) != NULL && contact->auth >= CONTACT_AUTH_NEW) {
    LOG_INFO_ (" %u\n", contact->connected);
    if (contact->connected > 0) {
      contact->connected--;
      err = SIM_OK;
    } else
      err = SIM_CLIENT_UNKNOWN;
  } else
    LOG_INFO_ ("\n");
  return SIM_UNPROTECT_ (err);
}

int sim_contact_ping_ (simnumber id, simnumber number, unsigned bits) {
  int err = SIM_CONTACT_UNKNOWN, err2 = SIM_API_BAD_TYPE;
  simcontact contact;
  SIM_PROTECT_ ("'%s' %lld %u\n", contact_get_nick (id, NULL), number, bits);
  if (! SIM_NUMBER_CHECK (number)) {
    err = SIM_API_BAD_PARAM;
  } else if ((contact = contact_list_find_id (id)) != NULL) {
    if (! contact->client) {
      err = bits & ~SIM_PING_BIT_AUDIO ? SIM_CLIENT_UNKNOWN : SIM_API_BAD_TYPE;
      if ((bits & (SIM_PING_BIT_CONNECT | SIM_PING_BIT_AUDIO)) == SIM_PING_BIT_CONNECT) {
        contact->msgs.pinging = bits & SIM_PING_BIT_TCP ? number : MSG_NUMBER_INVALID;
        err = msg_connect (contact, false);
      }
    } else {
      err = bits & SIM_PING_BIT_PROXY ? proxy_ping (contact->client) : SIM_OK;
      if (err == SIM_OK || err == SIM_SOCKET_BLOCKED) {
        if (! (bits & (SIM_PING_BIT_TCP | SIM_PING_BIT_CONNECT))) {
          err2 = bits & SIM_PING_BIT_AUDIO ? server_ping (contact->client) : bits ? SIM_OK : SIM_API_BAD_TYPE;
        } else if ((bits & (SIM_PING_BIT_AUDIO | SIM_PING_BIT_TCP)) == SIM_PING_BIT_TCP)
          err2 = client_send_cmd (contact->client, SIM_CMD_PING, SIM_CMD_PING_PONG, number_new (0),
                                  SIM_CMD_PING_PONG_NUMBER, number_new (number));
        if (err2 == SIM_OK && bits & SIM_PING_BIT_NAT)
          err2 = nat_traverse_attempt (contact->client);
        if (err2 != SIM_OK)
          err = err2;
      }
    }
  }
  return SIM_UNPROTECT_ (err);
}

simtype sim_contact_get_ (simnumber id, unsigned bits) {
  int err = SIM_OK;
  unsigned i, j, ip;
  simcontact contact;
  simtype tmp = nil (), list, ec, rsa;
  if (id) {
    sim_protect_ (); /*SIM_PROTECT_ ("'%s' %u\n", contact_get_nick (id, NULL), bits);*/
    if ((contact = id != CONTACT_ID_SELF ? contact_list_find_id (id) : contact_list.me) != NULL) {
      simnumber seen = contact->seen[CONTACT_SEEN_STATUS], flags;
      table_add (tmp = table_new_name (9, contact->nick.ptr), CONTACT_KEY_NICK, string_copy_string (contact->nick));
      if (contact->auth > CONTACT_AUTH_DELETED) {
        simclient client = contact->client;
        table_add_number (tmp, CONTACT_KEY_STATUS, contact->status);
        if (bits & CONTACT_BIT_VERIFY && client) {
          key_get (true, &ec, &rsa);
          if ((list = client_convert_keys (client, ec, rsa)).typ != SIMNIL)
            table_add (tmp, CONTACT_KEY_VERIFY, list);
          table_add_pointer (tmp, CONTACT_KEY_ADDRESS, contact->addr);
        }
        if (bits & CONTACT_BIT_LINE)
          table_add (tmp, CONTACT_KEY_LINE, CONTACT_GET_INFO (contact));
        if (bits & CONTACT_BIT_VERSION && contact->versions.typ != SIMNIL)
          table_add (tmp, CONTACT_KEY_VERSIONS, array_copy_strings (contact->versions));
        if (bits & CONTACT_BIT_LATENCY && client)
          for (j = CLIENT_PONG_CLIENT; j <= CLIENT_PING_PROXY; j++)
            table_add_number (tmp, contact_stat_names[CONTACT_STATS_LATENCY][j], proxy_get_latency (client, j));
        if (bits & CONTACT_BIT_DEFAULT) {
          simnumber n = seen > contact->seen[CONTACT_SEEN_RECEIVE] ? seen : contact->seen[CONTACT_SEEN_RECEIVE];
          table_add_number (tmp, CONTACT_KEY_SEEN, n);
          table_add_pointer (tmp, CONTACT_KEY_ADDRESS, contact->addr);
          table_add_number (tmp, CONTACT_KEY_SEEN_ACTIVITY, contact->seen[CONTACT_SEEN_ACTIVITY]);
          if (contact->own.len)
            table_add (tmp, CONTACT_KEY_OWN_NICK, string_copy_string (contact->own));
          if (client) {
            const char *s = client->flags & CLIENT_FLAG_UDP ? CONTACT_AUDIO_UDP : audio_state_names[client->call.state];
            table_add_pointer (tmp, CONTACT_KEY_AUDIO, s);
            table_add_number (tmp, CONTACT_KEY_LATENCY, client->pong);
            if (bits & CONTACT_BIT_LATENCY)
              client_send_cmd (client, SIM_CMD_PING, SIM_CMD_PING_PONG, number_new (client->pong = 0),
                               SIM_CMD_PING_PONG_NUMBER, number_new (SIM_CMD_PING_NUMBER_LATENCY));
          } else if (MSG_CHECK_CALL (contact))
            table_add_pointer (tmp, CONTACT_KEY_AUDIO, CONTACT_AUDIO_OUTGOING);
          table_add_pointer (tmp, CONTACT_KEY_CIPHER, contact->rcvcipher ? contact->rcvcipher : "");
          if (contact->location)
            table_add (tmp, CONTACT_KEY_LOCATION, string_copy (network_convert_ip (contact->location)));
          table_add_number (tmp, CONTACT_KEY_KEY_SIZE, CONTACT_GET_KEY_SIZE (contact));
          if (client && client->param.ip[0])
            table_add (tmp, CONTACT_KEY_IP, string_copy (network_convert_ip (client->param.ip[0])));
        }
        if (bits & CONTACT_BIT_STATS) {
          if (client)
            table_add (tmp, CONTACT_KEY_CONNECT, client_get_state (client, &ip));
          if (contact->hosts.typ != SIMNIL)
            table_add (tmp, CONTACT_KEY_HOSTS, array_copy_strings (contact->hosts));
          for (i = client ? CONTACT_STATS_PEER : CONTACT_STATS_MINE; (int) i >= CONTACT_STATS_CLIENTS; i--)
            for (j = 0; j <= CONTACT_STAT_COUNT; j++) {
              simnumber n = contact->stats[i][j];
              if (i == CONTACT_STATS_INPUT) {
                n += proxy_get_stat_contact (contact, j);
              } else if (i == CONTACT_STATS_OUTPUT || i == CONTACT_STATS_CLIENT)
                n += client_get_stat (contact, j, i == CONTACT_STATS_OUTPUT);
              table_add_number (tmp, contact_stat_names[i][j], n);
            }
        }
        if (contact == contact_list.me)
          table_add_pointer (tmp, CONTACT_KEY_VIP, CONTACT_VIP_MYSELF);
      } else if (bits & CONTACT_BIT_DEFAULT) {
        table_add_number (tmp, CONTACT_KEY_SEEN, seen);
        table_add_pointer (tmp, CONTACT_KEY_ADDRESS, contact->addr);
        if (contact->own.len)
          table_add (tmp, CONTACT_KEY_OWN_NICK, string_copy_string (contact->own));
      }
      if (contact == contact_list.system)
        table_add_pointer (tmp, CONTACT_KEY_VIP, CONTACT_VIP_SYSTEM);
      table_add_number (tmp, CONTACT_KEY_AUTH, contact != contact_list.me ? contact->auth : contact_list.auth);
      flags = contact->flags;
      if (contact->auth < CONTACT_AUTH_NEW) {
        flags &= ~(CONTACT_FLAG_UTF | CONTACT_FLAG_XFER | CONTACT_FLAG_AUDIO | CONTACT_FLAG_TYPING | CONTACT_FLAG_TYPE);
      } else if (contact->msgs.flags & CONTACT_MSG_OVERFLOW)
        flags &= ~CONTACT_FLAGS_OVERFLOW;
      table_add_number (tmp, CONTACT_KEY_FLAGS, flags & ~CONTACT_FLAG_NO_STATS);
      table_add_number (tmp, CONTACT_KEY_EDIT, contact != contact_list.me ? contact->edit : ((simnumber) 1 << 31) - 1);
    } else if (id == CONTACT_ID_TEST) {
      const unsigned len = CONTACT_AUTH_ACCEPTED - CONTACT_AUTH_DELETED + 1;
      int count = audio_device_count ();
      int flags = (count ? CONTACT_FLAG_AUDIO : 0) | CONTACT_FLAG_VERIFY | CONTACT_FLAG_VERIFIED;
      simtype auth = param_get_string ("contact.test");
      table_add_pointer (tmp = table_new (9), CONTACT_KEY_NICK, CONTACT_VIP_TEST);
      table_add_number (tmp, CONTACT_KEY_STATUS, count ? SIM_STATUS_ON : SIM_STATUS_OFF);
      table_add_number (tmp, CONTACT_KEY_FLAGS, flags | CONTACT_FLAG_SOUND_ON_N | CONTACT_FLAG_SOUND_OFF_N);
      table_add_number (tmp, CONTACT_KEY_EDIT, 0);
      table_add_pointer (tmp, CONTACT_KEY_VIP, CONTACT_VIP_TEST);
      flags = sim_convert_string_to_enum (auth.len ? auth.ptr : "", CONTACT_AUTH_DELETED, CONTACT_AUTH_BLOCKED,
                                          contact_auth_names - CONTACT_AUTH_REVOKED + CONTACT_AUTH_DELETED, len);
      table_add_number (tmp, CONTACT_KEY_AUTH, flags);
      if (bits & CONTACT_BIT_LINE)
        table_add_pointer (tmp, CONTACT_KEY_LINE, "This is the system test robot which you can command.");
      if (bits & CONTACT_BIT_DEFAULT) {
        simnumber pong;
        if (contact_list.me)
          table_add_pointer (tmp, CONTACT_KEY_ADDRESS, contact_list.me->addr);
        table_add_number (tmp, CONTACT_KEY_SEEN, count ? time (NULL) : 0);
        proxy_get_ip (NULL, &ip, NULL);
        if (! ip)
          ip = simself.flags & SIM_STATUS_FLAG_IP ? simself.ipin : simself.ipout;
        if (ip)
          table_add (tmp, CONTACT_KEY_IP, string_copy (network_convert_ip (ip)));
        if (proxy_get_proxy (NULL, NULL, &pong)) {
          table_add_number (tmp, CONTACT_KEY_LATENCY, pong);
          if (bits & CONTACT_BIT_LATENCY)
            proxy_ping (NULL);
        }
        if (audio_status.client == AUDIO_CLIENT_TEST)
          table_add_pointer (tmp, CONTACT_KEY_AUDIO, CONTACT_AUDIO_TALKING);
      }
      if (bits & CONTACT_BIT_STATS) {
        const char *addr, *nick;
        if (proxy_get_ip_proxy (&addr, &ip, NULL)) {
          table_add (tmp, CONTACT_KEY_LOCATION, string_copy (network_convert_ip (ip)));
          proxy_get_proxy (&ip, NULL, NULL);
          nick = CONTACT_GET_NICK (addr);
          table_add (tmp, CONTACT_KEY_CONNECT, string_concat (network_convert_ip (ip), ":", nick, NULL));
        }
        for (j = 0; j <= CONTACT_STAT_COUNT; j++) {
          for (i = CONTACT_STATS_CLIENTS; i <= CONTACT_STATS_INPUT; i++)
            table_add_number (tmp, contact_stat_names[i][j], main_get_stat (i, j));
          table_add_number (tmp, contact_stat_names[CONTACT_STATS_MINE][j], audio_status.stats[j]);
        }
      }
    } else if (id == CONTACT_ID_PROXY && bits & CONTACT_BIT_STATS) {
      simnumber rcvd = proxy_get_stat (PROXY_STATS_THIS, CONTACT_STAT_RECEIVED);
      simnumber sent = proxy_get_stat (PROXY_STATS_THIS, CONTACT_STAT_SENT);
      simnumber duration = proxy_get_stat (PROXY_STATS_THIS, CONTACT_STAT_DURATION);
      tmp = table_new (9);
      for (j = 0; j <= CONTACT_STAT_COUNT; j++)
        table_add_number (tmp, contact_stat_names[CONTACT_STATS_INPUTS][j], proxy_get_stat (PROXY_STATS_ALL, j));
      table_add_number (tmp, CONTACT_KEY_THIS_INPUT_COUNT, proxy_get_stat (PROXY_STATS_THIS, CONTACT_STAT_COUNT));
      if ((rcvd += proxy_get_stat (PROXY_STATS_NOW, CONTACT_STAT_RECEIVED)) < api_proxy_rcvbytes)
        api_proxy_rcvbytes = rcvd;
      table_add_number (tmp, CONTACT_KEY_THIS_INPUT_RECEIVED, rcvd);
      if ((sent += proxy_get_stat (PROXY_STATS_NOW, CONTACT_STAT_SENT)) < api_proxy_sndbytes)
        api_proxy_sndbytes = sent;
      table_add_number (tmp, CONTACT_KEY_THIS_INPUT_SENT, sent);
      duration += proxy_get_stat (PROXY_STATS_NOW, CONTACT_STAT_DURATION);
      table_add_number (tmp, CONTACT_KEY_THIS_INPUT_TIME, duration);
      duration = system_get_tick ();
      if (api_proxy_tick) {
        table_add_number (tmp, CONTACT_KEY_THIS_CLIENT_RECEIVED, rcvd - api_proxy_rcvbytes);
        table_add_number (tmp, CONTACT_KEY_THIS_CLIENT_SENT, sent - api_proxy_sndbytes);
        table_add_number (tmp, CONTACT_KEY_THIS_CLIENT_TIME, duration - api_proxy_tick);
      }
      api_proxy_tick = duration, api_proxy_rcvbytes = rcvd, api_proxy_sndbytes = sent;
      table_add_number (tmp, CONTACT_KEY_THIS_CLIENT_COUNT, proxy_get_stat (PROXY_STATS_NOW, CONTACT_STAT_COUNT));
      table_add_number (tmp, CONTACT_KEY_PROXY_SPEED_LIMIT, limit_get_param (LIMIT_PARAM_MAX));
    } else
      err = SIM_CONTACT_UNKNOWN;
  } else if (bits & CONTACT_BIT_STATS) {
    SIM_PROTECT_ ("%u\n", bits);
    tmp = table_new (9);
    for (i = 0; i < SIM_ARRAY_SIZE (contact_stat_indexes); i++)
      for (j = 0; j <= CONTACT_STAT_COUNT; j++) {
        simnumber n = proxy_get_stat (contact_stat_deleted[i], j);
        table_add_number (tmp, contact_stat_names[contact_stat_indexes[i]][j], n);
      }
  } else {
    SIM_PROTECT_ ("\n");
    tmp = array_new_numbers ((list = contact_list.array).len + 1);
    tmp.arr[i = 1] = number_new (CONTACT_ID_TEST);
    while (i <= list.len) {
      contact = list.arr[i++].ptr;
#ifndef DONOT_DEFINE
      if (contact_list_find_id (contact->id) != contact)
        LOG_FATAL_ (SIM_OK, "contact list inconsistent ('%s' missing from table)\n", contact->nick.str);
#endif
      tmp.arr[i] = number_new (contact->id);
    }
#ifndef DONOT_DEFINE
    if (table_count (contact_list.table) != list.len)
      LOG_FATAL_ (SIM_OK, "contact list inconsistent (%u in array, %u in table)\n",
                  table_count (contact_list.table), list.len);
#endif
  }
  LOG_API_WARN_ (err);
  return SIM_UNPROTECT_VALUE_ (tmp);
}

void sim_contact_free_ (simtype contact) {
  type_free (contact);
}

int sim_contact_add_ (const char *address, simnumber *id) {
  int err = SIM_API_INIT;
  simtype addr = sim_contact_test_address (address, "S");
  SIM_PROTECT_ ("@%020llu\n", CONTACT_CONVERT_TO_ID (addr.ptr));
  if (api_init_state == API_LOGGEDIN || api_init_state == API_STATUSCHANGE)
    err = contact_accept_ (addr.ptr, id);
  string_free (addr);
  return SIM_UNPROTECT_ (err);
}

int sim_contact_set_ (simnumber id, const char *key, const simtype value) {
  int err = SIM_API_BAD_PARAM, i, port, auth;
  simcontact contact;
  sim_protect_ ();
  contact = id ? contact_list_find_id (id) : contact_list.me;
  LOG_INFO_SIMTYPE_ (value, 0, "%s '%s' %s ", __FUNCTION__,
                     contact ? (char *) contact->nick.str : contact_get_nick (id, NULL), key);
  if (! key)
    return SIM_UNPROTECT_ (SIM_API_BAD_PARAM);
  if (! contact) {
    if (id != CONTACT_ID_TEST) {
      err = SIM_CONTACT_UNKNOWN;
    } else if (! strcmp (key, CONTACT_KEY_AUTH)) {
      if (value.typ == SIMNUMBER) {
        simnumber num = value.num;
        if (num == CONTACT_AUTH_ACCEPTED || num == CONTACT_AUTH_BLOCKED || num == CONTACT_AUTH_DELETED) {
          simtype ptr = pointer_new (contact_auth_names[num - CONTACT_AUTH_REVOKED]);
          if ((err = param_set_string ("contact.test", ptr, SIM_PARAM_PERMANENT)) == SIM_OK)
            if ((err = param_save ()) == SIM_OK)
              err = SIM_KEY_NO_SAVE;
        }
      } else
        err = SIM_API_BAD_TYPE;
    }
  } else if (! strcmp (key, CONTACT_KEY_AUTH)) {
    if (value.typ != SIMNUMBER)
      return SIM_UNPROTECT_ (SIM_API_BAD_TYPE);
    if (value.num == CONTACT_AUTH_ACCEPTED) {
      err = id ? contact_accept_ (contact->addr, NULL) : SIM_CONTACT_UNKNOWN;
    } else if (value.num >= CONTACT_AUTH_DROP && value.num < CONTACT_AUTH_NEW && value.num != CONTACT_AUTH_REVOKED) {
      if (id && value.num <= CONTACT_AUTH_BLOCKED && value.num != CONTACT_AUTH_DROP)
        proxy_cancel_proxy (contact->addr, SIM_CONTACT_BLOCKED);
      if (value.num == CONTACT_AUTH_DROP) {
        simclient client = client_find (id ? contact : NULL, CLIENT_FLAG_ACCEPTED | CLIENT_FLAG_CONNECTED);
        if (! client && id)
          client = client_find_connecting (contact, false);
        if (client || ! id)
          client_cancel (client, SIM_CONTACT_DROPPED);
        err = SIM_OK;
      } else if ((auth = contact_reject (id ? contact : NULL, (int) value.num)) >= CONTACT_AUTH_DELETED) {
        if (auth == value.num) {
          err = SIM_OK;
        } else if (contact != contact_list.me) {
          contact_cancel (contact, SIM_CONTACT_BLOCKED);
          if ((err = contact_list_save_txt (NULL, CONTACT_TXT_DEFAULT)) == SIM_OK && value.num == CONTACT_AUTH_FORGET)
            xfer_save ();
        } else
          err = contact_list_save ();
      } else /* deauthorizing forgotten contact will succeed even though it returns SIM_CONTACT_UNKNOWN */
        err = auth == CONTACT_AUTH_REVOKED ? SIM_CONTACT_REVOKED : SIM_CONTACT_UNKNOWN;
    }
    return SIM_UNPROTECT_ (err);
  } else if (! strcmp (key, CONTACT_KEY_NICK)) {
    if (value.typ != SIMPOINTER && value.typ != SIMSTRING)
      return SIM_UNPROTECT_ (SIM_API_BAD_TYPE);
    if ((err = contact_rename (contact, value.len ? value : contact->own, ! value.len)) == SIM_OK) {
      if (contact != contact_list.me) {
        contact->flags &= ~CONTACT_FLAG_RENAMED;
        contact->flags |= (value.len && contact->own.len) * CONTACT_FLAG_RENAMED;
      } else
        client_send_status (NULL, CLIENT_SEND_STATUS_ON);
      err = contact_list_save_txt (NULL, CONTACT_TXT_DEFAULT);
    }
    return SIM_UNPROTECT_ (err);
  } else if (! strcmp (key, CONTACT_KEY_LINE) && contact == contact_list.me) {
    if (value.typ != SIMPOINTER && value.typ != SIMSTRING) {
      err = SIM_API_BAD_TYPE;
    } else if ((err = contact_set_line (contact, value)) == SIM_NO_ERROR) {
      client_send_status (NULL, CLIENT_SEND_STATUS_ON);
      err = contact_list_save ();
    }
    return SIM_UNPROTECT_ (err);
  } else if (! strcmp (key, CONTACT_KEY_VERIFY)) {
    simtype verify = client_convert_keys (contact->client, contact->ec, contact->rsa);
    err = contact_set_verified (contact, value, verify);
    string_free (verify);
  } else if (! strcmp (key, CONTACT_KEY_IP)) {
    if (value.typ != SIMPOINTER && value.typ != SIMSTRING)
      return SIM_UNPROTECT_ (SIM_API_BAD_TYPE);
    err = SIM_OK;
    if (value.len) {
      unsigned ip = sim_network_parse_ip_port (value.ptr, &port);
      if (ip && port > 0 && port < 0x10000) {
        contact_set_ip (contact, ip, port, CONTACT_IP_ADD);
      } else
        err = SIM_SOCKET_BAD_PORT;
    } else
      contact_add_host (contact, NULL, contact->ips = 0);
  } else if (! strcmp (key, CONTACT_KEY_FLAGS)) {
    err = contact_set_rights (contact, value);
  } else if (value.typ == SIMNUMBER) {
    for (i = 0; i <= CONTACT_STAT_COUNT; i++)
      if (! strcmp (key, contact_stat_names[CONTACT_STATS_MINE][i])) {
        contact->stats[CONTACT_STATS_MINE][i] = value.num;
        err = SIM_OK;
      }
  }
  if (err == SIM_NO_ERROR || err == SIM_KEY_NO_SAVE) {
    if (contact && contact->client)
      client_send_status (contact->client, CLIENT_SEND_STATUS_ON);
    err = err == SIM_NO_ERROR ? contact_list_save () : SIM_OK;
  } else if (err == SIM_OK)
    err = contact_list_save ();
  return SIM_UNPROTECT_ (err);
}

int sim_msg_send_utf_ (simnumber id, simtype text, simunsigned *idx) {
  int err = SIM_CONTACT_UNKNOWN;
  simcontact contact;
  SIM_PROTECT_ ("'%s'", contact_get_nick (id, NULL));
  if (idx)
    *idx = 0;
  if (text.typ != SIMPOINTER && text.typ != SIMSTRING) {
    err = SIM_API_BAD_TYPE;
  } else if ((contact = contact_list_find_id (id)) != NULL) {
    struct _message msg;
    LOG_INFO_ (" %u\n", idx && contact->auth >= CONTACT_AUTH_NEW ? contact->msgs.count + 1 : 0);
    if ((err = msg_create (contact, text, SIM_MSG_TYPE_UTF, &msg)) == SIM_OK) {
      if (contact->msgs.flags & CONTACT_MSG_OVERFLOW) {
        msg_destroy (&msg);
        err = SIM_MSG_OVERFLOW;
      } else if (! idx || ! (contact->flags & CONTACT_FLAG_UTF) || ! CONTACT_CHECK_RIGHT_UTF (contact)) {
        msg_destroy (&msg);
        err = idx ? SIM_API_NO_RIGHT : SIM_OK;
      } else if ((err = msg_send (contact, &msg, 0)) != SIM_CONTACT_BLOCKED)
        *idx = contact->msgs.count;
    }
    return SIM_UNPROTECT_ (err);
  } else
    string_free (text);
  LOG_INFO_ ("\n");
  return SIM_UNPROTECT_ (err);
}

int sim_msg_edit_utf_ (simnumber id, simtype text, simunsigned idx) {
  int err = SIM_CONTACT_UNKNOWN;
  simcontact contact;
  struct _message msg;
  SIM_PROTECT_ ("'%s' %lld\n", contact_get_nick (id, NULL), idx);
  if (text.typ != SIMPOINTER && text.typ != SIMSTRING) {
    err = SIM_API_BAD_TYPE;
  } else if ((contact = contact_list_find_id (id)) == NULL) {
    string_free (text);
  } else if ((err = msg_create (contact, text, SIM_MSG_TYPE_UTF, &msg)) == SIM_OK)
    err = msg_edit (contact, &msg, idx);
  return SIM_UNPROTECT_ (err);
}

int sim_msg_remove_ (simnumber id, simunsigned idx) {
  int err;
  simcontact contact;
  SIM_PROTECT_ ("'%s' %lld\n", contact_get_nick (id, NULL), idx);
  err = (contact = contact_list_find_id (id)) == NULL ? SIM_CONTACT_UNKNOWN : msg_remove (contact, idx);
  return SIM_UNPROTECT_ (err);
}

simtype sim_msg_get_ (simnumber id, simunsigned idx) {
  int err = SIM_MSG_BAD_INDEX;
  unsigned i, j;
  simtype table;
  simcontact contact;
  struct _message *message = NULL;
  sim_protect_ ();
  if ((contact = contact_list_find_id (id)) == NULL) {
    err = SIM_CONTACT_UNKNOWN;
  } else if (idx && idx <= contact->msgs.count) {
    if ((i = contact->msgs.indexes[idx]) == 0 || i > contact->msgs.len) {
      LOG_FATAL_ (SIM_OK, "index %d out of bounds (max = %d) '%s'\n", i, contact->msgs.len, contact->nick.str);
    } else if ((message = &contact->msgs.queue[i])->index != idx) {
      LOG_FATAL_ (SIM_OK, "index %d mismatch (wanted %lld, got %d) '%s'\n", i, idx, message->index, contact->nick.str);
    } else
      err = SIM_OK;
  }
  if (err != SIM_OK) {
    LOG_API_WARN_ (err);
    return SIM_UNPROTECT_VALUE_ (nil ());
  }
  table = table_new (2);
  if (message->status)
    table_add_number (table, SIM_CMD_MSG_STATUS, message->status);
  if (message->type)
    table_add_number (table, SIM_CMD_MSG_TYPE, message->type);
  if (message->text.typ != SIMNIL)
    table_add_pointer_len (table, SIM_CMD_MSG_TEXT, message->text.str, message->text.len);
  if (! message->tonick) {
    if (contact->msgs.nick.ptr)
      for (j = i; j <= contact->msgs.len; j++)
        if (contact->msgs.queue[j].status == SIM_MSG_INCOMING)
          if ((message->tonick = contact->msgs.queue[j].nick.ptr) != NULL)
            break;
    if (! message->tonick)
      table_add (table, SIM_CMD_MSG_NICK, string_copy_string (contact->nick));
  }
  if (message->tonick)
    table_add_pointer (table, SIM_CMD_MSG_NICK, message->tonick);
  if (message->oldhandle) {
    simnumber handle = message->oldhandle;
    if (message->type != SIM_MSG_TYPE_SYSTEM) {
      table_add_number (table, SIM_CMD_MSG_EDIT, message->sndtime ? message->sndtime : 1);
      do {
        if ((j = msg_get_hash (contact, handle)) >= i || ! j)
          break;
      } while ((handle = contact->msgs.queue[i = j].oldhandle) != 0);
      message = &contact->msgs.queue[i];
    } else
      table_add_number (table, SIM_CMD_MSG_HANDLE, handle);
  }
  if (message->nick.typ != SIMNIL)
    table_add_pointer_len (table, SIM_CMD_MSG_SENDER, message->nick.str, message->nick.len);
  if (message->sndtime)
    table_add_number (table, SIM_CMD_MSG_TIME, message->sndtime);
  if (message->rcvtime)
    table_add_number (table, SIM_CMD_MSG_RECEIVED, message->rcvtime);
  return SIM_UNPROTECT_VALUE_ (table);
}

void sim_msg_free_ (simtype msg) {
  table_free (msg);
}

simunsigned sim_msg_count_ (simnumber id) {
  simcontact contact;
  unsigned count;
  PTH_PROTECT_UNPROTECT_ (count = (contact = contact_list_find_id (id)) ? contact->msgs.count : 0);
  return count;
}

int sim_msg_load_ (simnumber id, simunsigned number) {
  simcontact contact;
  int err = SIM_CONTACT_UNKNOWN;
  SIM_PROTECT_ ("'%s' %lld\n", contact_get_nick (id, NULL), number);
  if ((simnumber) number < 0) {
    err = SIM_MSG_BAD_INDEX;
  } else if ((contact = contact_list_find_id (id)) != NULL)
    err = file_load_msg_ (contact, number);
  return SIM_UNPROTECT_ (err);
}

int sim_console_load_ (simnumber number) {
  int err;
  simunsigned cpu;
  SIM_PROTECT_ ("%lld\n", number);
  cpu = sim_system_cpu_get (SYSTEM_CPU_TIME_THREAD, NULL);
  err = log_load_ (number, contact_list.me ? param_get_number ("msg.maxmb") << 20 : 0);
  limit_set_cpu (sim_system_cpu_get (SYSTEM_CPU_TIME_THREAD, NULL) - cpu, 0);
  if (err == SIM_OK || err == SIM_FILE_END || err == SIM_FILE_START)
    if (number != LOG_NO_EVENT && number != LOG_NO_BUFFER) {
      log_xtra_ (NULL, "%s\n", sim_get_version (NULL, NULL));
      if (err == SIM_FILE_END) {
        log_xtra_ (NULL, "type 'help' to list available console commands\n");
        err = SIM_OK;
      }
    }
  return SIM_UNPROTECT_ (err);
}

int sim_xfer_send_file_ (simnumber id, const simtype pathname, simnumber *handle) {
  int err = SIM_CONTACT_UNKNOWN;
  simnumber num = 0;
  if (id != CONTACT_ID_XFER_RECEIVED && id != CONTACT_ID_XFER_SENT) {
    simcontact contact;
    SIM_PROTECT_ ("'%s' %s\n", contact_get_nick (id, NULL), pathname.str);
    if ((pathname.typ != SIMPOINTER && pathname.typ != SIMSTRING) || pathname.str[pathname.len]) {
      err = SIM_API_BAD_TYPE;
    } else if ((contact = contact_list_find_id (id)) != NULL) {
      err = contact->auth >= CONTACT_AUTH_NEW ? SIM_API_NO_RIGHT : SIM_CONTACT_BLOCKED;
      if (err == SIM_API_NO_RIGHT && contact->flags & CONTACT_FLAG_XFER && CONTACT_CHECK_RIGHT_XFER (contact))
        err = contact->msgs.flags & CONTACT_MSG_OVERFLOW ? SIM_MSG_OVERFLOW : xfer_send_file (contact, pathname, &num);
    }
    LOG_API_WARN_ (err);
    sim_unprotect_ ();
  } else
    err = id == CONTACT_ID_XFER_SENT ? sim_file_get_time (pathname.ptr, &num) : xfer_send_file (NULL, pathname, &num);
  if (handle)
    *handle = num;
  return err;
}

static simtype xfer_get_name (const struct _xfer *xfer) {
  if (xfer->name.str[xfer->name.len] >= 255)
    return string_copy_string (xfer->name);
  xfer->name.str[xfer->name.len]++;
  return pointer_new_len (xfer->name.str, xfer->name.len);
}

simtype sim_xfer_get_ (simnumber id, simnumber handle, unsigned bits) {
  int err;
  struct _xfer *xfer;
  simcontact contact = NULL;
  simtype tmp = nil (), name;
  sim_protect_ ();
  if (id ? (contact = contact_list_find_id (id)) == NULL : handle != SIM_XFER_GET_INFO) {
    LOG_API_WARN_ (SIM_CONTACT_UNKNOWN);
  } else if ((handle == SIM_XFER_GET_RECEIVED || handle == SIM_XFER_GET_SENT) && bits == SIM_XFER_BIT_DEFAULT) {
    static const char *api_xfer_keys[] = {
      SIM_CMD_XFER_SEND_TYPE, SIM_CMD_XFER_SEND_NAME, SIM_CMD_XFER_SEND_SIZE, SIM_CMD_XFER_SEND_HANDLE,
      SIM_CMD_XFER_SEND_TIME, SIM_CMD_XFER_OFFSET, SIM_CMD_XFER_CLOSE_ERROR
    };
    unsigned i = 0, j, n = 0;
    simtype arrays[SIM_ARRAY_SIZE (api_xfer_keys)];
    LOG_API_INFO_ ("'%s'\n", contact_get_nick (id, NULL));
    for (xfer = contact->xfers; xfer; xfer = xfer->next)
      n += handle != SIM_XFER_GET_RECEIVED || xfer->type >= 0;
    tmp = table_new (2);
    for (j = 0; j < SIM_ARRAY_SIZE (arrays); j++)
      table_add (tmp, api_xfer_keys[j], arrays[j] = j < 2 ? array_new_strings (n) : array_new_numbers (n));
    for (xfer = contact->xfers; xfer; xfer = xfer->next)
      if (handle != SIM_XFER_GET_RECEIVED || xfer->type >= 0) {
        simnumber pos = xfer->rcvdsize > xfer->rcvdpos && xfer->rcvdpos >= 0 ? xfer->rcvdsize : xfer->rcvdpos;
        arrays[0].arr[++i] = pointer_new (xfer_get_type_name (contact, xfer, &err));
        arrays[1].arr[i] = xfer_get_name (xfer);
        arrays[2].arr[i] = number_new (xfer->filesize);
        arrays[3].arr[i] = number_new (xfer->handle);
        arrays[4].arr[i] = number_new (xfer->type < 0 ? xfer->sndtime : xfer->rcvtime);
        arrays[5].arr[i] = number_new (pos);
        arrays[6].arr[i] = number_new (err);
      }
  } else if (handle == SIM_XFER_GET_INFO) {
    table_add_number (tmp = table_new (2), SIM_XFER_GET_INFO_PAUSE, contact ? contact->xfer.pause : 0);
    if (contact && contact->client && contact->client->xfer.current) {
      simnumber tick = contact->client->xfer.tick[1] - contact->client->xfer.tick[0];
      simnumber pos = contact->client->xfer.tickpos[1];
      table_add_number (tmp, SIM_XFER_GET_INFO_CURRENT, contact->client->xfer.current->handle);
      if (tick > 0 && (pos -= contact->client->xfer.tickpos[0]) > 0) {
        table_add_number (tmp, SIM_XFER_GET_INFO_DURATION, tick);
        table_add_number (tmp, SIM_XFER_GET_INFO_TRANSFERRED, pos);
      } else if (contact->client->xfer.tick[1])
        table_add_number (tmp, SIM_XFER_GET_INFO_DURATION, -1);
    }
    if (bits & SIM_XFER_BIT_EXTENDED) {
      if ((name = xfer_new_dir_name (contact, false)).typ != SIMNIL)
        table_add (tmp, SIM_XFER_GET_INFO_RECEIVED, name);
      if ((name = xfer_new_dir_name (contact, true)).typ != SIMNIL)
        table_add (tmp, SIM_XFER_GET_INFO_SENT, name);
    }
  } else if (bits == SIM_XFER_BIT_DEFAULT && (xfer = xfer_find (contact, handle)) != NULL) {
    simnumber pos = xfer->rcvdsize > xfer->rcvdpos && xfer->rcvdpos >= 0 ? xfer->rcvdsize : xfer->rcvdpos;
    table_add_pointer (tmp = table_new (2), SIM_CMD_XFER_SEND_TYPE, xfer_get_type_name (contact, xfer, &err));
    table_add (tmp, SIM_CMD_XFER_SEND_NAME, xfer_get_name (xfer));
    table_add_number (tmp, SIM_CMD_XFER_SEND_SIZE, xfer->filesize);
    table_add_number (tmp, SIM_CMD_XFER_SEND_TIME, xfer->type < 0 ? xfer->sndtime : xfer->rcvtime);
    table_add_number (tmp, SIM_CMD_XFER_OFFSET, pos);
    table_add_number (tmp, SIM_CMD_XFER_CLOSE_ERROR, err);
  } else if (bits == SIM_XFER_BIT_EXTENDED && handle > 0 && handle <= contact->msgs.count) {
    char *s, *p;
    unsigned i = contact->msgs.indexes[handle], j = i;
    struct _message *message = &contact->msgs.queue[i];
    if (message->type == SIM_MSG_TYPE_SYSTEM && (handle = message->oldhandle) != 0) {
      for (tmp = table_new (2); i <= contact->msgs.len; i++)
        if ((message = &contact->msgs.queue[i])->type == SIM_MSG_TYPE_SYSTEM && message->oldhandle == handle) {
          if (message->text.typ != SIMNIL && ! SIM_STRING_CHECK_DIFF_CONST (message->text.ptr, "FILE "))
            if ((s = strchr ((char *) message->text.str + SIM_STRING_GET_LENGTH_CONST ("FILE "), ' ')) != 0) {
              if (! SIM_STRING_CHECK_DIFF_CONST (message->text.ptr, "FILE SEND ")) {
                if (i == j)
                  continue;
              } else if ((s = strchr (p = s + 1, ' ')) != 0) {
                if ((err = atoi (p)) == SIM_OK && SIM_STRING_CHECK_DIFF_CONST (message->text.ptr, "FILE RECV "))
                  err = SIM_XFER_CANCELLED;
                table_add_number (tmp, SIM_CMD_XFER_CLOSE_ERROR, err);
                table_add (tmp, SIM_CMD_XFER_SEND_NAME, string_copy_len (s + 1, strlen (s)));
              }
            }
          break;
        }
    } else
      LOG_API_WARN_ (SIM_MSG_BAD_INDEX);
  } else
    LOG_API_WARN_ (SIM_XFER_BAD_HANDLE);
  return SIM_UNPROTECT_VALUE_ (tmp);
}

void sim_xfer_free_ (simtype xfer) {
  simtype names, name;
  sim_protect_ ();
  if (xfer.typ == SIMTABLE) {
    if ((names = table_get (xfer, SIM_CMD_XFER_SEND_NAME)).typ == SIMARRAY_STRING) {
      unsigned i;
      for (i = 1; i <= names.len; i++)
        if ((name = names.arr[i]).typ == SIMPOINTER)
          xfer_free_name (string_new_pointer (name.str, name.len));
    } else if (names.typ == SIMPOINTER)
      xfer_free_name (string_new_pointer (names.str, names.len));
  }
  table_free (xfer);
  sim_unprotect_ ();
}

int sim_xfer_set_ (simnumber id, simnumber handle, const char *type) {
  int err = SIM_API_BAD_TYPE;
  SIM_PROTECT_ ("'%s' #%lld %s\n", contact_get_nick (id, NULL), handle, type);
  if (type) {
    simcontact contact = contact_list_find_id (id);
    if (! contact) {
      err = SIM_CONTACT_UNKNOWN;
      if (! id && ! strcmp (type, SIM_XFER_TYPE_FREE) && ! handle)
        xfer_periodic (false);
    } else if (! strcmp (type, SIM_XFER_TYPE_FREE) && ! handle && ! contact->client) {
      table_free (contact->xfer.hash), contact->xfer.hash = nil ();
      err = SIM_OK;
    } else if (! strcmp (type, SIM_XFER_TYPE_PAUSE)) {
      err = xfer_send_pause (contact, handle ? handle : SIM_CMD_XFER_INIT_PAUSE_ALL);
    } else if (! strcmp (type, SIM_XFER_TYPE_CANCEL) || *type == '+' || *type == '-') {
      err = xfer_send_close (contact, handle, *type == '+' || *type == '-' ? atoi (type) : SIM_XFER_CANCELLED);
    } else if (! strcmp (type, SIM_XFER_TYPE_KILL))
      err = xfer_send_close (contact, handle, SIM_LIMIT_NO_ERROR);
  }
  return SIM_UNPROTECT_ (err);
}

int sim_audio_call_ (simnumber id) {
  int err;
  SIM_PROTECT_ ("'%s'\n", contact_get_nick (id, NULL));
  err = audio_call_ (id);
  return SIM_UNPROTECT_ (err);
}

int sim_audio_hangup_ (simnumber id) {
  int err = SIM_OK;
  simcontact contact;
  simnumber start;
  SIM_PROTECT_ ("'%s'\n", contact_get_nick (id, NULL));
  if (! id || ((id == CONTACT_ID_TEST && audio_device_count ()) || id == CONTACT_ID_KEYGEN)) {
    if (audio_status.client == AUDIO_CLIENT_TEST) {
      err = audio_close_ (false);
    } else if (! id)
      err = audio_hangup_ (NULL, SIM_OK);
  } else {
    if ((contact = contact_list_find_id (id)) == NULL) {
      err = SIM_CONTACT_UNKNOWN;
    } else if (contact->client) {
      contact->seen[CONTACT_SEEN_ACTIVITY] = time (NULL);
      err = audio_hangup_ (contact->client, SIM_OK);
    } else if ((start = MSG_CHECK_CALL (contact)) != 0) {
      MSG_UNSET_CALL (contact);
      event_send_audio (contact, CONTACT_AUDIO_OUTGOING, CONTACT_AUDIO_HANGUP);
      msg_send_system (contact, "CALL ", "FAILED", start, SIM_OK);
    }
  }
  return SIM_UNPROTECT_ (err);
}

simnumber sim_audio_check_talking_ (void) {
  simnumber id;
  PTH_PROTECT_UNPROTECT_ (id = audio_status.id);
  return id;
}

int sim_sound_start_ (const char *sound, int count) {
  int err;
  SIM_PROTECT_ ("%s * %d\n", sound, count);
  err = audio_start_sound (sound, count);
  return SIM_UNPROTECT_ (err);
}

int sim_sound_stop_ (const char *sound) {
  int err;
  SIM_PROTECT_ ("%s\n", sound);
  err = audio_stop_sound_ (sound);
  return SIM_UNPROTECT_ (err);
}

int sim_param_get_limits_ (const char *param, simtype *def, int *min, int *max, const char **description) {
  int err;
  SIM_PROTECT_ ("%s\n", param);
  err = param ? param_get_limits (param, def, min, max, description) : SIM_API_BAD_PARAM;
  return SIM_UNPROTECT_ (err);
}

simtype sim_param_get_ (const char *param) {
  simtype key, val, login;
  sim_protect_ (); /*SIM_PROTECT_ ("%s\n", param);*/
  if (! param || ! strcmp (param, SIM_PARAM_GET_WARNINGS)) {
    LOG_API_INFO_ ("\n");
    if (! contact_list.me || param) {
      simwalker ctx;
      val = table_new (param_login_table.len);
      for (table_walk_first (&ctx, param_login_table); (login = table_walk_next (&ctx, &key)).typ != SIMNIL;)
        if (! param) {
          if (login.num > 0)
            table_add (val, key.ptr, type_copy (param_get_any (key.ptr)));
        } else if ((contact_list.me && login.num < 0) || login.num == 2) {
          simtype tmp = param_get_saved (key.ptr);
          table_add (val, key.ptr, tmp.typ == SIMNIL ? param_get_default (key.ptr, tmp) : tmp);
        }
      return SIM_UNPROTECT_VALUE_ (val);
    }
    val = param_table;
  } else
    val = param_get_any (param);
#ifndef DONOT_DEFINE /* HAVE_LIBPTH */
  val = type_copy (val);
#endif
  return SIM_UNPROTECT_VALUE_ (val);
}

void sim_param_free_ (simtype value) {
#ifdef DONOT_DEFINE /* HAVE_LIBPTH */
  if (! contact_list.me && value.typ == SIMTABLE)
#endif
    type_free (value);
}

static int param_set_any (const char *param, const simtype value, int permanent) {
  switch (value.typ) {
    case SIMNUMBER:
      if (value.num != (int) value.num)
        return sim_param_set_error (param, value, SIM_PARAM_BAD_VALUE);
      return param_set_number (param, (int) value.num, permanent);
    case SIMARRAY_STRING:
      return param_set_strings (param, value, permanent);
    case SIMPOINTER:
    case SIMSTRING:
      return param_set_string (param, value, permanent);
    case SIMNIL:
      if (permanent == SIM_PARAM_USER)
        return param_unset (param, permanent);
  }
  return sim_param_set_error (param, value, SIM_API_BAD_TYPE);
}

int sim_param_set_ (const char *param, const simtype value, int permanent) {
  int err = SIM_OK, err2;
  simbool save = false;
  simtype key, val;
  simwalker ctx;
  sim_protect_ ();
  LOG_INFO_SIMTYPE_ (value, 0, "%s %d %s ", __FUNCTION__, permanent, param);
  sim_error_reset_text ();
  if (permanent < SIM_PARAM_UNAPPLIED || permanent > SIM_PARAM_USER) {
    err = SIM_API_BAD_PARAM;
  } else if (param) {
    save = (err = param_set_any (param, value, permanent)) == SIM_OK;
  } else if (table_walk_first (&ctx, value)) {
    while ((val = table_walk_next (&ctx, &key)).typ != SIMNIL)
      if ((err2 = param_set_any (key.ptr, val, permanent)) == SIM_OK) {
        save = true;
      } else
        err = err2;
  } else
    err = SIM_API_BAD_PARAM;
  if (save && permanent != SIM_PARAM_TEMPORARY && permanent != SIM_PARAM_UNAPPLIED) {
    char *info = sim_error_get_text (err);
    if ((err2 = param_save ()) == SIM_OK) {
      sim_error_set_text (info, NULL, NULL, err);
    } else if (simself.status != SIM_STATUS_OFF || err != SIM_OK) {
      event_test_error (NULL, SIM_EVENT_ERROR_FILE_SAVE, err2);
      sim_error_set_text (info, NULL, NULL, err);
    } else
      err = err2;
    sim_free_string (info);
  }
  return SIM_UNPROTECT_ (err);
}

simbool sim_check_version (unsigned version) {
  return api_init_version >= (int) version;
}

char *sim_get_version (char *version, const char **ui) {
  static char api_version_buffer[128];
  if (! version)
    version = api_version_buffer;
#if SIM_CORE_REVISION
  sprintf (version, PACKAGE_NAME " %d-%d %d/%d build %d.0x%06x", SIM_PROTO_VERSION_MAX, SIM_PROTO_PROXY_VERSION_MAX,
           api_init_version, SIM_CORE_VERSION, SIM_CORE_REVISION, (unsigned) (SIM_CORE_BUILD >> 24));
#else
  sprintf (version, PACKAGE_NAME " %d-%d %d/%d version " PACKAGE_VERSION,
           SIM_PROTO_VERSION_MAX, SIM_PROTO_PROXY_VERSION_MAX, api_init_version, SIM_CORE_VERSION);
#endif
  sprintf (version + strlen (version), " x%d", sizeof (struct _simelement) > 36 ? 64 : 32);
#if HAVE_LIBPTH
  sprintf (version + strlen (version), " pth-%ld.%ld%c%ld", pth_version () >> 20,
           pth_version () >> 12 & 255, 'a' + (int) (pth_version () >> 8 & 15), pth_version () & 255);
#elif defined(HAVE_LIBPTH)
  strcat (version, " npth");
#endif
#ifdef SIM_MEMORY_CHECK
  strcat (version, " memchk");
#endif
#ifdef SIM_BOUNDS_CHECK
  strcat (version, " bgcc");
#endif
#if HAVE_LIBEFENCE
  strcat (version, " efence");
#endif
  if (ui)
    *ui = api_init_ui_string.ptr;
  return version;
}

#ifdef __unix__
static simtype sim_get_desktop (void) {
  static const char *api_desktop_names[] = {
    "XDG_CURRENT_DESKTOP", "XDG_SESSION_DESKTOP", "DESKTOP_SESSION", "DESKTOP_SESSION_WM", "GDMSESSION"
  };
  unsigned i, j, n = 0;
  char *desktops[SIM_ARRAY_SIZE (api_desktop_names) + 1];
  for (i = 0; i < SIM_ARRAY_SIZE (api_desktop_names); i++) {
    char *s = getenv (api_desktop_names[i]);
    if (s && *s) {
      desktops[n++] = s;
      for (j = 0; j < n - 1; j++)
        if (! strcmp (desktops[j], s)) {
          n--;
          break;
        }
    }
  }
  desktops[n] = NULL;
  return string_concat ("", desktops[0], " ", desktops[1], " ", desktops[2], " ", desktops[3], " ", desktops[4], NULL);
}
#else
#define sim_get_desktop() pointer_new ("")
#endif

static simtype crypt_cipher_list (void) {
  const char *cipher;
  int count = -1;
  simtype ciphers = table_new (0);
  while ((cipher = sim_crypt_cipher_get (++count, NULL, NULL)) != NULL) {
    simbool preferred = false;
    crypt_cipher_find (cipher, 0, &preferred);
    if (table_add_number (ciphers, cipher, preferred).typ != SIMNIL) {
      table_free (ciphers);
      return nil ();
    }
  }
  return ciphers;
}

static simtype audio_device_list (simbool recording) {
  int i = -1, inchans, outchans, types;
  unsigned j = 1;
  simtype list = array_new_strings (audio_device_count () + 1), names = table_new (67), device;
  while ((device = audio_device_get (++i, &inchans, &outchans, &types)).typ != SIMNIL) {
    if (! (types & AUDIO_TYPE_VIRTUAL) || param_get_number ("audio.alsa.devices") & 2) {
      if ((recording ? inchans : outchans) && ++j <= list.len) {
        int count = (int) table_get_number (names, device.ptr) + 1;
        char idx[13];
        sprintf (idx, "%d:", count);
        table_set_key_number (names, string_copy (device.ptr), count);
        list.arr[j] = string_cat (idx, device.ptr);
        if (types & (recording ? AUDIO_TYPE_INPUT : AUDIO_TYPE_OUTPUT))
          list.arr[1] = list.arr[j--];
      } else if (j > list.len)
        LOG_FATAL_ (SIM_OK, "audio device list overflow %d > %u\n", j--, list.len);
    }
    string_free (device);
  }
  list.len = j;
  if (list.arr[1].typ == SIMNIL)
    array_delete (&list, 1, 1);
  table_free (names);
  return list;
}

simtype sim_list_info_ (unsigned bits) {
  simtype info = table_new (2);
  sim_protect_ ();
  if (bits & SIM_INFO_BIT_NET) {
    unsigned ip;
    int good, dubious, incoming, blocked, blacklisted;
    int status = main_get_nodes (&good, &dubious, &incoming, &blocked, &blacklisted);
    table_add_number (info, SIM_INFO_NET_DHT_GOOD, good);
    table_add_number (info, SIM_INFO_NET_DHT_DUBIOUS, dubious);
    table_add_number (info, SIM_INFO_NET_DHT_BLOCKED, blocked);
    table_add_number (info, SIM_INFO_NET_DHT_BAD, blacklisted);
    table_add_number (info, SIM_INFO_NET_DHT_INCOMING, incoming);
    table_add_number (info, SIM_INFO_NET_DHT_STATUS, status - MAIN_DHT_DISCONNECT);
    if (! network_get_port (&ip))
      ip = 0;
    table_add (info, SIM_INFO_NET_IP, string_copy (network_convert_ip (ip ? ip : sim_network_get_default ())));
  }
  if (bits & SIM_INFO_BIT_AUDIO) {
    LOG_API_INFO_ ("%u\n", bits);
    if (! (api_init_mask & SIM_INIT_BIT_NOAUDIO))
      audio_init_ (SIM_STATUS_IDLE);
    table_add (info, SIM_INFO_AUDIO_INPUT, audio_device_list (true));
    table_add (info, SIM_INFO_AUDIO_OUTPUT, audio_device_list (false));
    LOG_INFO_SIMTYPE_ (info, 0, "%s ", __FUNCTION__);
  }
  if (bits & SIM_INFO_BIT_CRYPT) {
    simtype ciphers = crypt_cipher_list ();
    LOG_INFO_SIMTYPE_ (ciphers, 0, "%s ", __FUNCTION__);
    table_add (info, SIM_INFO_CRYPT_CIPHERS, ciphers);
  }
  return SIM_UNPROTECT_VALUE_ (info);
}

#if HAVE_LIBMINIUPNPC
#include <miniupnpc/miniupnpc.h>
#elif defined(HAVE_LIBMINIUPNPC)
#include <miniupnpc/include/miniupnpc.h>
#endif

#if HAVE_LIBSPEEX
#include <speex/speex.h>
#include <portaudio.h>
#endif

#if HAVE_LIBCARES
#include <ares.h>
#endif

#if HAVE_LIBEXPAT
#include <expat.h>
#endif

simtype sim_list_versions (void) {
  simtype version = table_new (0), exe = sim_system_get_exe (NULL);
  char *s, ver[1024], release[1024];
  int v = sim_crypt_get_version ();
  simnumber size = sim_system_get_memory (true);
#if HAVE_LIBSPEEX
  const PaHostApiInfo *portaudio = Pa_GetHostApiInfo (Pa_GetDefaultHostApi ());
#endif
#if HAVE_LIBEXPAT
  XML_Expat_Version expat = XML_ExpatVersionInfo ();
#endif
  if (api_init_ui_string.ptr) {
    if (api_init_ui_autostarted) {
      table_add (version, "_ui", string_cat (api_init_ui_string.ptr, " auto"));
    } else
      table_add_pointer (version, "_ui", api_init_ui_string.ptr);
  }
  table_add (version, "_core", string_copy (sim_get_version (ver, NULL)));
  if (exe.typ == SIMNUMBER) {
    sprintf (ver, "%" SIM_FORMAT_64 "d", exe.num);
  } else
    sprintf (ver, "%s", exe.typ == SIMSTRING ? (char *) exe.str : "");
  if (size > 0)
    sprintf (ver + strlen (ver), " %" SIM_FORMAT_64 "dMB", size >> 20);
  table_add (version, "_exe", string_copy (ver));
#ifdef HAVE_LIBMINIUPNPC
#if MINIUPNPC_API_VERSION
  table_add_pointer (version, "miniupnpc", MINIUPNPC_VERSION);
#else
  table_add_pointer (version, "miniupnpc", "1.6");
#endif
#endif
  sprintf (ver, "%d.%d.%d", v / 100, v / 10 % 10, v % 10);
  table_add (version, "crypto++", string_copy (ver));
  table_add_pointer (version, "_ssl", sim_ssl_get_version ());
#if HAVE_LIBSPEEX
  strcpy (ver, Pa_GetVersionText ());
  if ((s = strstr (ver, ", revision ")) != NULL) {
    s[0] = ' ';
    s[1] = '#';
    memcpy (s + 2, s + SIM_STRING_GET_LENGTH_CONST (", revision "), 6);
    s[8] = 0;
  }
  if (portaudio)
    sprintf (ver + strlen (ver), " (%s)", portaudio->name);
#if defined(_WIN32) && ! HAVE_LIBPTH
  strcat (ver, " win32");
#elif defined(__APPLE__) && ! HAVE_LIBPTH
  strcat (ver, " CF");
#elif ! defined(_WIN32) && HAVE_UDEV
  strcat (ver, " udev");
#endif
  table_add (version, "_audio", string_copy (ver));
  speex_lib_ctl (SPEEX_LIB_GET_VERSION_STRING, &s);
  table_add_pointer (version, "speex", s);
#endif /* HAVE_LIBSPEEX */
#if HAVE_LIBCARES
  table_add_pointer (version, "c-ares", ares_version (NULL));
#endif
#if HAVE_LIBEXPAT
  sprintf (ver, "%d.%d.%d", expat.major, expat.minor, expat.micro);
  table_add (version, "expat", string_copy (ver));
#endif
  s = sim_system_get_version (ver, release);
  table_add (version, s ? SIM_VERSION_BAD : "_os", string_copy (ver));
  if (*release)
    table_add (version, "_osr", string_copy (release));
  table_add (version, "_dm", sim_get_desktop ());
  return version;
}

void sim_list_free (simtype list) {
  table_free (list);
}
