/**
    configuration parameters

    Copyright (c) 2020-2021 The Creators of Simphone

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

#ifndef _GNU_SOURCE
#define _GNU_SOURCE /* gmtime_r */
#endif

#include "config.h"

#include "logger.h"
#include "table.h"
#include "error.h"
#include "utils.h"
#include "system.h"
#include "crypto.h"
#include "file.h"
#include "contact.h"
#include "param.h"
#include "proxy.h"
#include "server.h"
#include "client.h"
#include "api.h"

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

#define SIM_MODULE "param"

#define FILE_LOGIN "login"
#define FILE_CONFIG "config"

/* four tables keyed by parameter name: */
simtype param_table;              /* current parameter value */
simtype param_login_table;        /* value of login from const struct param_xxx or -1 if not defined there */
static simtype param_saved_table; /* currently saved parameter value (does not contain default or temporary values) */
static simtype param_limit_table; /* 1-based index into const struct param_number (zero if not a numeric parameter) */

static const struct {
  int login;
  const char *key;
  int min, max, value;
  const char *description;
} param_number[] = {
  { 1, "ui.login.showpass", 0, 1, 0, "do not show entered password in login screen (1 = do show)" },
  { 1, "ui.login.countries", 0, 3, 2, "country list in login screen\n\t0 = no country list\n\t"
                                      "1 = no country names\n\t2 = no country flags\n\t3 = country names and flags" },
  { 1, "ui.login.autostarted", 0, 1, 0, "non-zero if autostart has succeeded at least once" },
  { 1, "ui.login.autostart", 0, 60, 1, "number of seconds to delay start when -autostart specified" },
  { 1, "ui.login.wait", 1, 10, 1, "timeout waiting for single start RPC to respond" },

  { 1, "ui.call.speech", 0, 15000, 5000, "warn when no speech detected within the specified number of milliseconds\n\t"
                                         "0 = disable" },
  { 0, "ui.call.latency", 0, 3000, 400, "unacceptable network latency for audio call (milliseconds)\n\t0 = none" },
  { 0, "ui.call.filter", 1, 30, 5, "number of seconds (pings) to collect (for) latency calculation" },
  { 0, "ui.call.delay", 2, 10, 2, "initial delay for latency detection" },
  { 0, "ui.call.hangup", 0, 5000, 1000, "timeout for detecting audio errors and frame loss (milliseconds)" },
  { 1, "ui.call.errors", 0, 101, 10, "read/write error threshold (per cent) for audio calls and audio test\n\t"
                                     "0 = any\n\t101 = none" },
  { 0, "ui.call.lostframes", 0, 101, 20, "frame loss threshold (per cent) for audio calls\n\t0 = any\n\t101 = none" },

#ifdef __APPLE__
  { 0, "ui.main.menu", 0, 1, 1, "use native menu bar in main window (0 = use menu inside the window)" },
#endif
  { 0, "ui.main.accept", 0, 1, 0, "show information after adding a contact (1 = do not show)" },
#ifdef SIM_NO_TRAY
  { 0, "ui.main.tray", -60, 60, -60, "do not use system tray even if available and possible (positive = use it)" },
#else
  { 0, "ui.main.tray", -60, 60, 60, "use system tray if available and possible (non-positive = do not use it)" },
#endif
#ifdef __unix__
  { 0, "ui.main.trayhtml", -1, 2, 1, "html-escaped tray icon tooltip\n\t0 = text\n\t2 = preformatted\n\t-1 = no nick" },
#else
  { 0, "ui.main.trayhtml", -1, 2, 0, "text tray icon tooltip\n\t1 = html-escaped\n\t2 = preformatted\n\t-1 = no nick" },
#endif
  { 0, "ui.main.split", 0, 1, 0, "split window view (1 = enable)" },
  { 0, "ui.main.avatar", 0, 1, 0, "do not show avatars (1 = show them)" },
  { 0, "ui.main.notifications", 0, 1, 0, "show a single notification by default (1 = show both call and chat)" },
  { 0, "ui.main.shownostatus", 0, 1, 0, "show 'offline' when status is unknown (1 = show no status)" },
  { 0, "ui.main.away", -86400, 86400, 300, "number of seconds to auto-switch to away (non-positive = disable)" },
  { 0, "ui.main.hideshow", 0, 1, 1, "allow contacts to see your hide status (0 = do not allow)" },
  { 0, "ui.main.splitter", 10, 65000, 0, "splitter size (contact size in combined window)" },
  { 0, "ui.main.docked", 20, 65000, 0, "docked size (right part of splitter)" },
  { 0, "ui.main.taskbar", 0, 1, 0, "do not always stay in taskbar (1 = always stay in task bar)" },
  { 0, "ui.main.popup", 0, 2, 1, "popup in taskbar on received message (or audio call)\n\t"
                                 "0 = no popup\n\t1 = popup minimized\n\t2 = popup window" },
  { 0, "ui.main.popuphide", 0, 2, 0, "popup in taskbar on received message (or audio call) when hidden\n\t"
                                     "0 = no popup\n\t1 = popup minimized\n\t2 = popup window" },
#ifdef __APPLE__
  { 0, "ui.main.blink", 0, 3, 2, "bounce dock icon if window is visible but not on focus\n\t"
                                 "0 = disable\n\t1 = only when apple allows\n\t2 = always" },
#else
  { 0, "ui.main.blink", 0, 2, 2, "blink tray icon (0 = do not blink)" },
#endif
  { 0, "ui.main.sleep", -999, 999, 200, "number of milliseconds to wait between popup and blink (0 = disable)" },
  { 0, "ui.main.doubleclick", -2, 2, 0, "double click to call contact\n\t"
                                        "non-positive = disable\n\t1 = only in single window mode\n\t2 = enable" },
  { 0, "ui.main.sort", -1, 1, 0, "sort contact list\n\t-1 = none\n\t0 = by default\n\t1 = by nick" },
  { 0, "ui.main.deleted", 0, 1, 0, "do not show deleted contacts (1 = do show)" },
  { 0, "ui.main.gripper", 0, 1, 1, "show gripper in main window (0 = do not show)" },
  { 0, "ui.main.titlemax", 0, 400, 33, "maximal own info line length in window title (0 = do not show info line)" },
  { 0, "ui.main.infoline", 0, 7, 3, "show info line\n\t"
                                    "1 = in chat\n\t3 = in chat and contact list\n\t6 = only in contact list" },
  { 0, "ui.main.click", 0, 1, 0, "chat with contact on mouse press in single view (1 = chat with contact on release)" },
#ifdef __APPLE__
  { 0, "ui.main.showkey", -8191, 8191, 4609, "show hotkey (default = CTRL+SHIFT+S)\n\tnon-positive = none\n\t"
                                             "bit 8 = CMD\n\tbit 9 = SHIFT\n\tbit 11 = ALT\n\tbit 12 = CTRL" },
  { 0, "ui.main.hidekey", -8191, 8191, 4612, "hide hotkey (default = CTRL+SHIFT+H)\n\tnon-positive = none\n\t"
                                             "bit 8 = CMD\n\tbit 9 = SHIFT\n\tbit 11 = ALT\n\tbit 12 = CTRL" },
#else
  { 0, "ui.main.showkey", -4095, 4095, 1619, "show hotkey (default = CTRL+SHIFT+S)\n\tnon-positive = none\n\t"
                                             "bit 8 = ALT\n\tbit 9 = CTRL\n\tbit 10 = SHIFT\n\tbit 11 = WIN" },
  { 0, "ui.main.hidekey", -4095, 4095, 1608, "hide hotkey (default = CTRL+SHIFT+H)\n\tnon-positive = none\n\t"
                                             "bit 8 = ALT\n\tbit 9 = CTRL\n\tbit 10 = SHIFT\n\tbit 11 = WIN" },
#endif
  { 0, "ui.main.shutdown", 0, 2, 0, "commit data phase\n\t0 = automatic\n\t1 = immediate\n\t2 = deferred" },
  { 0, "ui.main.autostart", -1, 1, -1, "automatically start ui on os login\n\t"
                                       "-1 = only for default user\n\t0 = disable\n\t1 = enable" },
#ifdef HAVE_LIBPTH
  { 1, "ui.main.core", 1, 1000, 10, "execute simcore periodically (number of milliseconds)" },
#else
  { 1, "ui.main.core", 0, 0, 0, NULL },
#endif

  { 0, "ui.settings.tab", -6, 6, 0, "default settings tab (1-based)\n\tpositive = fixed" },

  { 0, "ui.chat.split", 0, 104857, 15, "message size limit in units of 1000 bytes\n\t"
                                       "0 = do not split multi-line messages" },
  { 0, "ui.chat.connection", 0, 1, 1, "do show connection status icon (0 = do not show)" },
  { 0, "ui.chat.showall", 0, 1, 0, "do not add loading of full chat to context menu (1 = add to context menu)" },
  { 0, "ui.chat.load", 0, 1000000000, 1000, "auto-load last saved chat messages (0 = disable)" },
  { 0, "ui.chat.tracking", 0, 1, 1, "enable scrollbar tracking (0 = disable for fast scroll)" },
  { 0, "ui.chat.grid", 0, 1, 0, "do not show grid in chat (1 = do show)" },
  { 0, "ui.chat.showtime", 0, 3, 1, "show message time\n\t0 = on changed time\n\t"
                                    "1 = on changed sender\n\t2 = on changed status\n\t3 = always" },
  { 0, "ui.chat.showstatus", 0, 1, 0, "do not show message status character (1 = do show)" },
  { 0, "ui.chat.duration", 0, 2, 1, "log duration of system messages\n\t0 = don't\n\t1 = only hangup\n\t2 = all" },
  { 0, "ui.chat.oldtime", 0, 3600, 125, "number of seconds to consider time as reversed (0 = disable)" },
  { 0, "ui.chat.olddate", 0, 2592000, 86400, "number of seconds to consider date as reversed (0 = disable)" },
  { 0, "ui.chat.linebold", 0, 1, 0, "do not use bold font for date line (1 = do use)" },
  { 0, "ui.chat.altarrow", 0, 1, 1, "require ALT key for edit message or find next/previous (0 = do not require)" },
  { 0, "ui.chat.ctrlenter", 0, 1, 0, "do not require CTRL key for send message (1 = do require)" },
  { 0, "ui.chat.doubleclick", 0, 1, 1, "double click to copy chat message (0 = disable)" },
  { 0, "ui.chat.escape", 0, 3, 3, "esc key in chat edit\n\t0 = do nothing\n\t"
                                  "1 = clear text\n\t2 = minimize window\n\t3 = clear and then minimize" },
  { 0, "ui.chat.copytext", 0, 1, 0, "copy chat message time and nick on CTRL+C or ALT+C (1 = copy only message text)" },
  { 0, "ui.chat.icon", -1, 1, -1, "chat window icon type\n\t0 = avatar\n\t1 = status\n\t-1 = as ui.main.avatar" },
  { 0, "ui.chat.title", 0, 1, 1, "chat window title\n\t0 = nick\n\t1 = nick and status" },
  { 0, "ui.chat.statistics", 0, 1, 1, "show traffic statistics in user contact info\n\t"
                                      "0 = as absolute statistics\n\t1 = as kilobytes per second" },

  { 1, "ui.console.split", 0, 1, 1, "allow paste of multiple console commands (0 = do not split at new line)" },
  { 1, "ui.console.connection", 0, 1, 1, "do show connection status icon (0 = do not show)" },
  { 1, "ui.console.showall", 0, 1, 0, "disable loading of full log (1 = enable)" },
  { 1, "ui.console.load", 0, 1000000000, 1000, "auto-load last saved log messages (0 = disable)" },
  { 1, "ui.console.tracking", 0, 1, 1, "enable scrollbar tracking (0 = disable for fast scroll)" },
  { 1, "ui.console.grid", 0, 1, 0, "do not show grid in console (1 = do show)" },
  { 1, "ui.console.showtime", 0, 1, 0, "do not always show message time in console (1 = always show)" },
  { 1, "ui.console.showlevel", 0, 1, 0, "do not always show message level in console (1 = always show)" },
  { 1, "ui.console.linebold", 0, 1, 0, "do not use bold font for date line (1 = do use)" },
  { 1, "ui.console.doubleclick", 0, 1, 1, "double click to copy console message (0 = disable)" },
  { 1, "ui.console.escape", 0, 3, 3, "esc key in console edit\n\t0 = do nothing\n\t"
                                     "1 = clear text\n\t2 = minimize window\n\t3 = clear and then minimize" },
  { 1, "ui.console.copytext", 0, 1, 0, "copy console message time on CTRL+C or ALT+C (1 = copy only message text)" },
  { 1, "ui.console.unload", -1, 1, -1, "unload console messages on close\n\t"
                                       "0 = disable\n\t1 = enable\n\t-1 = disable and preload on login" },
  { 1, "ui.console.commands", 0, 1, 0, "allow execution of console commands only after prompt (1 = do not prompt)" },
  { 1, "ui.console.popup", 0, 1, 1, "activate console on logged error (0 = disable)" },
  { 0, "ui.console.statistics", 0, 1, 1, "show traffic statistics in test contact info\n\t"
                                         "0 = as absolute statistics\n\t1 = as kilobytes per second" },
  { 1, "ui.00000000000000000001.bottom", -1000000, 1000000, 0, NULL }, /* SIM_PARAM_USER */
  { 1, "ui.00000000000000000001.top", -1000000, 1000000, 0, NULL },    /* SIM_PARAM_USER */

  /* play sounds on events (bit 0 = closed window, bit 1 = minimized, bit 2 = normal):" */
  { 0, "sound.play.contact", -7, 7, 7, NULL },    /* SIM_EVENT_CONTACT */
  { 0, "sound.play.msg", -7, 7, 7, NULL },        /* play on received message */
  { 0, "sound.play.msgrecv", -7, 7, 7, NULL },    /* play on received message when off-focus */
  { 0, "sound.play.msgnotsent", -7, 7, 7, NULL }, /* play on unsent message (after msg.nak timeout) */
  { 0, "sound.play.msgresent", -7, 7, 7, NULL },  /* play on sent but previously unsent message */
  { 0, "sound.play.logon", -7, 7, 4, NULL },      /* SIM_EVENT_STATUS */
  { 0, "sound.play.logoff", -7, 7, 4, NULL },     /* SIM_EVENT_STATUS: SIM_STATUS_OFF */
  { 0, "sound.play.failed", -7, 7, 7, NULL },     /* SIM_EVENT_HISTORY SIM_MSG_INCOMING: "BUSY "
                                                                       SIM_MSG_DELIVERED: "FAILED " and "ABORT" */
  { 0, "sound.play.hangup", -7, 7, 7, NULL },     /* SIM_EVENT_HISTORY: "HANGUP", "HUNGUP", "ABORT"
                                                                        SIM_MSG_DELIVERED: "FAILED" and "BUSY" */
  { 0, "sound.play.connect", -7, 7, 7, NULL },    /* SIM_EVENT_AUDIO: CONTACT_AUDIO_CALLING */
  { 0, "sound.play.disconnect", -7, 7, 7, NULL }, /* SIM_EVENT_HISTORY: "HANGUP ", "HUNGUP " */
  { 0, "sound.play.up", -7, 15, 7, NULL },        /* SIM_EVENT_NET: SIM_EVENT_NET_STATUS > 0
                                                     (> 7 = also play the first time after login) */
  { 0, "sound.play.down", -7, 7, 7, NULL },       /* SIM_EVENT_NET: SIM_EVENT_NET_STATUS <= 0 */
  { 0, "sound.play.call", -7, 7, 7, NULL },       /* SIM_EVENT_AUDIO: CONTACT_AUDIO_INCOMING */
  { 1, "sound.play.keygen", -7, 7, 7, NULL },     /* SIM_EVENT_KEYGEN */
#ifdef DONOT_DEFINE
  { 0, "sound.play.ring", -7, 7, 7, NULL },     /* SIM_EVENT_AUDIO: CONTACT_AUDIO_RINGING */
  { 0, "sound.play.busy", -7, 7, 7, NULL },     /* SIM_EVENT_HISTORY SIM_MSG_INCOMING: "BUSY" */
  { 0, "sound.play.protocol", -7, 7, 7, NULL }, /* SIM_EVENT_ERROR_PROTOCOL */
  { 0, "sound.play.tofu", 7, 7, 7, NULL },      /* SIM_EVENT_ERROR_KEY */
#endif

  { 0, "sound.delay.msg", 0, 10000, 1000, "time interval to NOT play msg sound repeatedly (0 = disable)" },
  { 0, "sound.delay.logon", 0, 60000, 5000, "time interval to NOT play logon sound repeatedly (0 = disable)" },
#ifdef _WIN32
  { 1, "sound.threads", 0, 1, 1, "mix multiple sound streams (0 = cannot mix)" },
#else
  { 1, "sound.threads", 0, 1, 0, "mix multiple sound streams (1 = can mix)" },
#endif
  { 1, "sound.latency", 0, 3000, 350, "sound playing latency in milliseconds (0 = default portaudio high)" },

  { 1, "system.install", -1, 1, 1, "perform installation\n\t-1 = disable\n\t"
                                   "0 = disable installation as administrator" },
  { 1, "system.crash", 1, 5, 4, "log crashes\n\tbit 0 = allow debugging\n\t  > 1 = log crash\n\tbit 2 = notify user" },
  { 1, "system.lock", 0, 1024, 0, "number of megabytes required to lock the whole program into memory if non-zero" },

  { 1, "crypto.entropy", -3072, 3072, -128, "entropy bits to collect during audio test" },
  { 1, "crypto.validate", -1, 3, 2, "validation level for loaded private keys on logon (1 = do not validate)" },
  { 0, "crypto.reseed", 0, 3, 3, "reseed random generator with audio input (0 = disable)" },
  { 0, "crypto.revoke", 0, 1, 1, "allow key revokation (0 = do not allow)" },
  { 0, "crypto.tagsize", 0, 128, 0, "packet tag/IV size (0 = default)" },
  { 0, "crypto.padding", 128, 4344, 1448, "pad encrypted handshake packets to that many bytes" },

  { 1, "file.log", 0, 1, 1, "keep debug/error log file (0 = disable)" },
  { 1, "file.tagsize", 0, 128, 0, "size of cipher token in file header (0 = default)" },

  { 0, "socket.listen", 0, 128, 0, "server TCP listen queue size (0 = default)" },
  { 0, "socket.qos", 0, 5, 4, "ip TOS for audio packets (for both TCP and UDP)" }, /* QOS_TRAFFIC_TYPE */
  { 0, "socket.ivs", 0, 1, 1, "use random initialization vectors (0 = use unique initialization vectors)" },
  { 0, "socket.send", 3, 3600, 30, "send timeout" },
  { 0, "socket.recv", 22, 3602, 62, "receive timeout" },
  { 0, "socket.connect", 0, 300, 5, "connect timeout (0 = use the OS default)" },
  { 0, "socket.socks", 5, 600, 0, "socks connect timeout (0 = use double the receive timeout)" },
  { 0, "socket.wait", 0, 999, 250, "number of milliseconds to wait for socket lock when sending audio data" },
  { 0, "socket.maxmb", 1, 524288, 1024, "maximal number of megabytes to send or receive with the same key" },
  { 0, "socket.mtu", 1500, 32772, 1500, "assumed MTU (for bandwidth calculation)" },
  { 0, "socket.packet", 15928, 32772, 16384, "maximal size of SSL packets to send" },
  { 0, "socket.padding", 128, 4344, 181, "pad encrypted SSL packets to that many bytes" },

  { 0, "socket.tcp.send", 0, 1048576, 0, "default TCP socket send buffer size (0 = use the OS default)" },
  { 0, "socket.tcp.recv", 0, 1048576, 0, "default TCP socket receive buffer size (0 = use the OS default)" },
  { 0, "socket.tcp.proxy", 0, 1048576, 4096, "proxy socket receive buffer default size (0 = use the default)" },
  { 0, "socket.tcp.local", 0, 1048576, 1024, "proxy local socket send buffer size (0 = use the default)" },
  { 0, "socket.tcp.server", 0, 1048576, 16384, "proxy control socket send buffer size (0 = use the default)" },
  { 0, "socket.tcp.client", 0, 1048576, 8192, "proxy client socket send buffer size (0 = use the default)" },

  { 0, "net.tor.port", -65535, 65535, -9150, "socks proxy por\n\tnon-positive = disable\n\t9050 or 9150 to use TOR" },
  { 0, "net.dns.retry", 1, 15, 2, "number of retries for a DNS query" },
  { 0, "net.dns.timeout", 1, 60000, 2000, "how long to wait for a DNS query try to complete (milliseconds)" },
  { 0, "net.dns.alt", 0, 3, 1, "use dynDNS\n\t3 = only mainstream\n\t2 = only alternative\n\t1 = both\n\t0 = none" },
  { 0, "net.dns.dht", -3, 3, -3, "DNS use for DHT boot (positive = use TOR if available)" },

  { 0, "net.upnp.time", 0, 86400, 300, "seconds between rechecking uPNP port mappings (0 = disable uPNP)" },
  { 0, "net.upnp.iptime", 0, 300, 60, "minimal number of seconds between rechecking external ip address" },
  { 0, "net.upnp.retry", 0, 100, 5, "number of times to retry allocating a different port (0 = disable)" },
  { 0, "net.upnp.step", 1, 32768, 0, "external port increment half-step (0 = random)" },
  { 0, "net.upnp.delay", 50, 60000, 2000, "timeout waiting for uPNP device discovery to complete (milliseconds)" },
  { 0, "net.upnp.ttl", 1, 255, 2, "multicast TTL value for uPNP SSDP device discovery" },
  { 0, "net.upnp.port", 0, 65535, 0, "SSDP packets local (source) port for uPNP (0 = any)" },
  { 0, "net.upnp.restart", 0, 150, 60, "restart failed restart attempt once after that many seconds (0 = do not)" },

  { 0, "main.dht.alpha", 4, 32, 8, "number of initial parallel searches (in active mode)" },
  { 0, "main.dht.beta", 4, 16, 8, "number of following parallel searches (in active mode)" },
  { 0, "main.dht.listening", 8, 32, 16, "number of incoming nodes to report LISTEN state" },

  { 0, "main.searches", 1, 16, 2, "maximal number of searches per DHT node to allow" },
  { 0, "main.research", 1, 60, 5, "number of seconds to retry searches that could not be started" },
  { 0, "main.announce", 0, 1800, 600, "seconds between announcing myself (0 = disable)" },
  { 0, "main.reannounce", 0, 1, 1, "restart announcement until successful (0 = disable)" },
  { 0, "main.timeout", 0, 3600, 132, "seconds for detection of network connection loss (0 = disable)" },
  { 0, "main.port", -65535, 65535, 0, "UDP port for mainline DHT\n\t0 = random\n\tnegative = auto-changeable" },
#ifdef _WIN32
  { 0, "main.boot.size", 6, 256, 26, "size/type of bencoded node for loading nodes from dht.dat (default = uTorrent)" },
#else
  { 0, "main.boot.size", 6, 256, 6, "size/type of bencoded node for loading nodes from dht.dat "
                                    "(default = transmission)" },
#endif
  { 0, "main.blacklist", -99, 99, 10, "rate divisor for checking of malicious nodes (non-positive = disable)" },
  { 0, "main.blacklisted", 9, 9999, 999, "maximal number of blacklisted nodes" },
  { 0, "main.bits", 1, 32, 32, "number of random bits for checking for malicious nodes" },
  { 0, "main.verify", 0, 10800, 4140, "timeout for proxy DHT announcement verification (0 = disable)" },

  { 0, "main.cpu.min", 0, 50, 1, "minimal DHT cpu load per cent allowed (0 = less than one per cent)" },
  { 0, "main.cpu.max", 1, 100, 50, "maximal DHT cpu load per cent allowed (100 = unlimited)" },
  { 0, "main.cpu.factor", 0, 2000, 200, "factor for maximal allowed DHT cpu load (0 = unlimited)" },
  { 0, "main.cpu.burst", 1, 180, 60, "maximal number of seconds allowed for cpu load burst" },
  { 0, "main.cpu.search", 5, 100, 25, "maximal acceptable cpu burst percentage to allow when starting a search" },

  { 0, "contact.ips", 1, 8, 2, "maximal number of ip addresses per contact" },
  { 0, "contact.txt", 0, 2, 0, "automatically write contacts.txt file\n\t0 = no\n\t1 = only ids\n\t2 = ids and nicks" },
  { 0, "contact.strangers", 0, 4, 2, "allow incoming connections from non-contacts\n\t0 = disable\n\t"
                                     "1 = don't send own key\n\t2 = enable\n\t3 = connect to contact\n\t"
                                     "4 = auto-accept" },
  { 0, "contact.requests", -1, 1000, 5, "maximal number of contact requests per day (-1 = infinite)" },
  { 0, "contact.max", 0, 10000, 1000, "maximal number of contacts allowed (0 = unlimited)" },
  { 0, "contact.save", 60, 86400, 900, "auto-save time for DHT node cache, contact list and proxy list" },
  { 0, "contact.offline", 0, 86400, 60, "mark all contacts as offline if i was offline for no less than this time\n\t"
                                        "0 = never\n\t1 = always" },
  { 0, "contact.probe", 0, 300, 60, "minimal number of seconds between sending probes of the same type (0 = none)" },

  { 0, "rights.audio", 0, 1, 1, "allow audio calls (0 = do not allow)" },
  { 0, "rights.utf", 0, 1, 1, "allow UTF-8 chat messages (0 = do not allow)" },
  { 0, "rights.typing", -4000, 4000, 5, "ping timeout for clearing typing notification\n\t"
                                        "0 = do not wish to receive typing notification" },
  { 0, "rights.type", -60, 60, 5, "typing notification timeout (non-positive = do not send typing notification)" },
  { 0, "rights.history", 0, 1, 1, "save chat history to files (0 = disable)" },

  { 1, "limit.maxsockets", 0, 10240, 10240, "maximal number of socket descriptors (0 = use the OS maximum)" },
  { 1, "limit.maxfds", 4, 512, 32, "number of reserved file descriptors (unix only)" },
  { 0, "limit.probes", 0, 16384, 1024, "maximal number of probe sockets (0 = maximum)" },   /* max = 1/4 of limit.maxsockets */
  { 0, "limit.clients", 0, 16384, 1024, "maximal number of client sockets (0 = maximum)" }, /* max = 1/4 of limit.maxsockets */
  { 0, "limit.newsockets", 1, 256, 32, "maximal number of new (non-contact) socket descriptors" },
  { 0, "limit.oldsocket", -1, 120, -1, "number of seconds when a new socket has become discardable\n\t"
                                       "-1 = never\n\t0 = always" },
  { 0, "limit.blacklist", 0, 3600, 60, "initial number of seconds to clearing blacklist (0 = no blacklisting)" },
  { 0, "limit.blacklisted", 9, 100000, 1000, "maximal number of blacklisted ip addresses "
                                             "(stop accepting connections when exceeded)" },
  { 0, "limit.cpuload", 0, 100, 25, "maximal acceptable cpu load per cent "
                                    "(start blacklisting incoming connections when exceeded)" },
  { 0, "limit.cputime", 1000, 10000, 1000, "minimal number of milliseconds between measuring cpu load" },
  { 0, "limit.cpuconnections", 0, 512, 16, "minimal number of incoming connections between measuring cpu load" },
  { 0, "limit.cpuevent", -1, 2, 0, "action on cpu overload\n\t-1 = do not send event\n\t"
                                   "0 = send error event when blacklist table full\n\t"
                                   "1 = also send event when blacklisting an ip address\n\t2 = also log an ERROR" },

  { 0, "limit.maxspeed", -999, 999, -128, "speed limit (kilobytes per second) for proxy customers "
                                          "(non-positive = maximum)" },
  { 0, "limit.reserved", 0, 32, 16, "speed reserved for non-proxy use (kilobytes per second)" },
  { 0, "limit.server", 8, 64, 8, "minimal speed limit (kilobytes per second) per customer (contact or non-contact)" },
  { 0, "limit.contact", 8, 999, 999, "maximal speed limit (kilobytes per second) for contacts" },
  { 0, "limit.stranger", 8, 999, 999, "maximal speed limit (kilobytes per second) for non-contacts" },
  { 0, "limit.client", 0, 8192, 200, "minimal speed limit (bytes per second) for proxy clients (0 = unlimited)" },
  { 0, "limit.udp", -8192, 8192, 612, "UDP traversal requests maximal bytes per second per customer (-612 = disable)" },
  { 0, "limit.time", 1, 20, 3, "minimal number of seconds between rechecking speed limit" },
  { 0, "limit.blackfactor", 1, 25, 10, "blacklist maximal burst speed factor (1 = no blacklisting on speed burst)" },
  { 0, "limit.local", 0, 1, 0, "do not apply speed limit to local customers (1 = do apply)" },
  { 0, "limit.burst", 0, 100, 90, "burst stop hysteresis per cent\n\t0 = disable burst\n\t100 = no hysteresis" },
  { 0, "limit.handshakes", 0, 25, 5, "percentage of speed to reserve for unauthenticated handshakes (0 = unlimited)" },
#if HAVE_LIBPTH
  { 0, "limit.sockets", 2, 128, 4, "minimal number of socket descriptors to reserve per customer" },
#else
  { 0, "limit.sockets", 2, 128, 31, "minimal number of socket descriptors to reserve per customer" },
#endif
  { 0, "limit.servers", 0, 8, 1, "maximal number of non-contact customers per ip address (0 = unlimited)" },
  { 0, "limit.connections", 0, 16384, 0, "maximal number of clients per ip address (0 = maximal)" },
  { 0, "limit.factor", 25, 100, 50, "percentage of maximal measured internet speed to use for proxy customers" },
  { 0, "limit.average", 0, 267, 16, "take an average of last internet speed measurements (0 = take the maximum)" },
  { 0, "limit.measure", -3600, 3600, -323, "minimal time between measurements of maximal internet speed\n\t"
                                           "0 = do not measure" },

  { 0, "proxy.require", -2, 2, 0, "require the use of proxy\n\t1 = always\n\t-1 = never\n\t0 = auto-detect" },
  { 0, "proxy.strangers", -1, 3600, 120, "use of proxies\n\t-1 = only non-contacts\n\t"
                                         "0 = only contacts\n\tpositive = both but prefer contacts" },
  { 0, "proxy.delay", 15, 300, 60, "wait a while before re-searching a completely exhausted proxy list" },
  { 0, "proxy.reconnect", 61, 901, 121, "minimal time before disconnect to consider proxy successfully connected" },
  { 0, "proxy.retry", 0, 86400, 3600, "re-check incoming TCP connectivity every hour by connecting to a proxy "
                                      "(0 = never)" },
  { 0, "proxy.max", -1000, 1000, 100, "maximal number of proxies to remember\n\t"
                                      "0 = unlimited\n\tnon-positive = do not save" },
  { 0, "proxy.announce", 60, 1800, 600, "seconds between announcing each of the customers to the DHT" },
  { 0, "proxy.fin", 13, 60, 15, "time to wait before sending FIN to traversers (13 = immediate)" },
  { 0, "proxy.ivs", 0, 1, 0, "use unique initialization vectors (1 = use random initialization vectors)" },
  { 0, "proxy.send", 20, 1500, 210, "send timeout for proxy sockets" },
  { 0, "proxy.recv", 63, 600, 90, "receive timeout for proxy sockets" },
  { 0, "proxy.reverse", 0, 600, 60, "minimal time between rechecking backwards connection (0 = never do this)" },
  { 0, "proxy.report", 0, 300, 30, "minimal time between sending speed limit reports (0 = never do this)" },
  { 0, "proxy.measure", -1, 86400, 5168, "minimal time between measurements of internet speed per customer\n\t"
                                         "-1 = none\n\t0 = only on init" },
  { 0, "proxy.anonymous", 0, 1, 0, "do not accept anonymous traversers (0 = do accept)" },

  { 0, "nat.local", 0, 1, 1, "allow contacts on the same local network to connect over the intranet (0 = disable)" },
  { 0, "nat.reverse", -1, 60, 15, "NAT reversal socket timeout\n\t-1 = socket default\n\t0 = disable reversal" },
  { 0, "nat.traverse", 0, 12, 10, "NAT traversal probe timeout (0 = disable traversal)" },
  { 0, "nat.udp", 0, 15000, 1000, "NAT UDP timeout milliseconds (0 = disable UDP traversal)" },
  { 0, "nat.init", -1, 1, 1, "initiate NAT traversal on reversal failure\n\t"
                             "0 = disable\n\t1 = client initiates\n\t-1 = server initiates" },
  { 0, "nat.periodic", 0, 1440, 60, "minimal number of minutes between automatic NAT traversal attempts "
                                    "(0 = disable)" },
  { 0, "nat.attempt", 0, 1, 1, "attempt traversal on audio calls (0 = disable)" },
  { 0, "nat.resyn", 0, 10000, 6000, "send new SYN after the specified number of milliseconds elapsed (0 = disable)" },
  { 0, "nat.delay", 0, 1000, 500, "NAT traversal initial delay milliseconds to request (0 = parallel traversal)" },
  { 0, "nat.probe", 0, 5000, 1200, "NAT traversal error probe milliseconds (0 = no retry on connect error)" },
  { 0, "nat.syn", 0, 200, 20, "number of milliseconds between sending SYNs to consecutive port numbers (0 = none)" },
  { 0, "nat.ping", 0, 10, 1, "number of message ACKs to send after successful traversal authentication" },
  { 0, "nat.ack", 1, 10, 3, "number of times to repeat traversal success packet" },
  { 0, "nat.lock", -1, 1, 1, "outgoing connections while NAT traversal is attempted\n\t"
                             "0 = allow\n\t1 = do not allow\n\t-1 = auto-detect" },
  { 0, "nat.retry", 15, 300, 30, "number of seconds to retry temporarily rejected request" },
  { 0, "nat.reconnect", 0, 10, 2, "number of reconnects to traverser proxy (0 = none)" },
  { 0, "nat.blacklist", 0, 302, 90, "NAT traversal disable timeout (0 = do not disable traversal)" },
  { 0, "nat.recv", 22, 3602, 27, "forced value of socket.recv on successfully traversed socket" },
  { 0, "nat.ips", 1, 5, 3, "number of ip addresses to try for NAT reversal" },
  { 0, "nat.delta", -9, 10, 10, "source port delta for NAT traversal (10 = auto-detect)" },
  { 0, "nat.zero", -9, 9, 0, "zero delta for NAT traversal (do not change this value)" },
  { 0, "nat.test", 0, 2147483647, 0, "only for testing (do not ever set to non-zero)" },

  { 0, "server.close", -1, 1, 0, "do not close server port on status off (1 = close it)" },
  { 0, "server.logon", 0, 233617, 0, "incoming logon period (0 = depending on the number of authorized contacts)" },
  { 0, "server.reverse", 0, 1, 1, "enable spontaneous connection reversal (0 = disable)" },
  { 0, "server.versions", 1, 100, 1, "maximal number of OS versions to receive (do not change)" },
  { 0, "server.sleep", 0, 500, 50, "number of milliseconds before starting the main loop (0 = none)" },
  { 0, "server.measure", -1, 86400, 997, "minimal time between measurements of internet speed per client\n\t"
                                         "-1 = none\n\t0 = do not measure" },

  { 0, "client.timeout", 0, 3720, 60, "seconds before search for a contact expires (0 = default)" },
  { 0, "client.logon", 0, 3600, 60, "seconds between searching for contacts (0 = disable)" },
  { 0, "client.logoff", -1, 3600, 60, "time to wait for logoff to finish before exiting\n\t0 = none\n\t-1 = infinite" },
  { 0, "client.reap", 0, 86400, 3600, "maximal idle time before disconnecting (0 = never)" },
  { 0, "client.udp", 0, 3, 1, "allow UDP connections (0 = disable)" },
  { 0, "client.ring", 5, 62, 5, "forced value of socket.recv while contact is ringing or calling" },
  { 0, "client.verify", 0, 62, 16, "verify proxy ip address (0 = disable)" },
  { 0, "client.verified", 15, 60, 30, "timeout for verification of proxy ip address" },
  { 0, "client.info", 0, 3, 0, "info to send on handshake\n\tbit 0 = nick\n\tbit 1 = info line" },

  { 0, "msg.ack", 1, 15, 1, "timeout for sending acknowledge of received messages" },
  { 0, "msg.nak", 2, 30, 5, "timeout for receiving acknowledge of sent messages" },
  { 0, "msg.save", 5, 60, 10, "initial time interval for saving pending messages to file" },
  { 0, "msg.drain", -5000, 5000, 500, "milliseconds to wait for pending messages to drain before reload\n\t"
                                      "0 = none\n\tnegative = no auto-recover" },
  { 0, "msg.load", 0, 1000000000, 100, "preload last saved messages (0 = disable)" },
  { 0, "msg.maxmb", 0, 2047, 0, "maximal number of megabytes of messages per contact (0 = auto-default)" },
  { 0, "msg.edit", -100, 100, 10, "allow editing the last N outgoing messages\n\t"
                                  "0 = allow only for undelivered messages" },
  { 0, "msg.remove", 0, 1, 0, "do not allow partial removal of edited messages (1 = do allow)" },
  { 0, "msg.errors", 0, 1, 1, "show an error message when history file is corrupted (0 = do not show)" },

  { 1, "audio.sample", -48000, 48000, 0, "sample rate hertz\n\t"
                                         "negative = fixed\n\t0 = default\n\tpositive = take minimum" },
  { 1, "audio.channels.input", 0, 2, 0, "number of input channels\n\t0 = default\n\t1 = mono\n\t2 = stereo" },
  { 1, "audio.channels.output", 0, 2, 0, "number of output channels\n\t0 = default\n\t1 = mono\n\t2 = stereo" },
  { 1, "audio.channels.ring", 0, 2, 0, "number of ring-output channels\n\t0 = default\n\t1 = mono\n\t2 = stereo" },
  { 1, "speex.speech.delay", 0, 5000, 2000, "initial millisecond delay for speech detection (0 = immediate)" },
  { 1, "speex.speech.start", 0, 100, 35, "speech start probability threshold" },
  { 1, "speex.speech.continue", 0, 100, 20, "speech continue probability threshold" },
  { 0, "speex.quality.min", 0, 10, 0, "desired speex bit rate\n\t1 = lowest\n\t10 = highest\n\t0 = default" },
  { 0, "speex.quality.max", 0, 10, 10, "allowed speex bit rate\n\t1 = lowest\n\t10 = highest\n\t0 = default" },
  { 0, "speex.complexity", 0, 10, -1, "encoder compression level (0 to 10)\n\t-1 = default" },
  { 0, "speex.plctuning", 2, 30, 0, "encoder packet loss concealment (2 to 30)\n\t0 = default" },
  { 0, "speex.enhance", 0, 1, 1, "enhance your decoding perception (0 = disable)" },

  { 1, "speex.denoise", 0, 1, 1, "encoder noise suppression (0 = disable)" },
  { 1, "speex.agc", 0, 1, 1, "encoder automatic gain control (0 = disable)" },
  { 0, "speex.echo", -999, 999, -100, "echo canceller tail length in milliseconds\n\t"
                                      "positive = enable by default\n\t0 = forced disable" },
  { 0, "speex.jitter.step", 1, 50, 1, "number of frames for each step when updating jitter buffering" },
  { 0, "speex.jitter.activity", -1, 100, 29, "update jitter buffer size when not above this threshold\n\t"
                                             "-1 = never\n\t100 = always" },
  { 0, "speex.jitter.update", 0, 60, 5, "minimal number of seconds before updating jitter buffer size (0 = none)" },
  { 0, "speex.jitter.latency", 0, 400, 0, "jitter buffer latency (number of frames to always keep in it) "
                                          "in milliseconds" },
  { 0, "speex.jitter.late", -100, 100, 0, "\tnegative = jitter buffer max late rate\n"
                                          "\tpositive = jitter buffer late cost" },
  { 0, "audio.frames.latency", 1, 1000, 80, "number of frames per packet (1 = no frame aggregation)" },
  { 0, "audio.frames.min", 1, 1000, 1, "minimal allowed number of frames per packet (milliseconds)" },
  { 0, "audio.frames.max", 1, 1000, 1000, "maximal allowed number of frames per packet (milliseconds)" },
  { 0, "audio.frames.mtu", 576, 16000, 1500, "maximal number of bytes to send in a single audio packet" },

  { 0, "audio.timeout", 0, 60000, 5000, "hangup timeout when no data is coming or going in milliseconds "
                                        "(0 = disable)" },
  { 0, "audio.answer", 0, 1, 0, "do not answer incoming calls on request (1 = answer automatically)" },
  { 0, "audio.reopen", 0, 1, 1, "allow reopening audio device to change the sample rate (0 = disable)" },
  { 1, "audio.test", 0, 60, 10, "audio test duration seconds (0 = disable)" },
  { 1, "audio.pnp", 0, 10000, 2000, "PnP device detection timeout in milliseconds (0 = disable)" },
#if HAVE_ALSA
  { 1, "audio.alsa.mmap", 0, 1, 1, "use MMAP if available with the audio device (0 = use read/write)" },
  { 1, "audio.alsa.devices", 0, 7, 1, "ALSA audio device selection\n\tbit 0 = plughw\n\tbit 1 = non-hw\n\tbit 2 = hw" },
  { 1, "audio.alsa.retries", -1, 1000, -1, "number of retries when trying to open an ALSA device (0 = none)" },
  { 1, "audio.alsa.periods", 0, 16, 2, "number of fragments for ALSA audio drivers (0 = portaudio default)" },
#endif
  { 1, "audio.latency", -1, 999, 0, "audio latency per frame in milliseconds (0 = default portaudio low)" },
#ifndef __APPLE__
  { 1, "audio.samples", 0, 12000, 160, "number of audio samples per buffer" } /* automatically adjusted for the sample rate */
#else
  { 1, "audio.samples", 0, 12000, 0, "number of audio samples per buffer" } /* automatically adjusted for the sample rate */
#endif
};

static const char *param_crypto_ciphers[] = {
  "serpent", "rijndael", "twofish", "rc6", "mars", "camellia", "aria", "cast", NULL
};

static const char *param_ssl_curves[] = { "prime256v1", "secp384r1", "secp521r1", NULL };
static const char *param_ssl_sigalgs[] = { NULL };

static const char *param_net_dns_servers[] = {
  "91.217.137.37", "151.80.222.79", "94.247.43.254", "51.89.88.77", "204.44.81.128", NULL /* OpenNIC servers */
};

static const char *param_main_boot_ips[] = {
  /* "router.bitcomet.net:6881", */ "router.utorrent.com:6881", "router.bittorrent.com:6881",
  "dht.transmissionbt.com:6881", "p6881.router.bitflu.org:6881", "router.bitflu.org:7088", NULL
};

#ifdef _WIN32
static const char *param_main_boot_dirs[] = { "uTorrent", "BitTorrent", NULL };
static const char *param_main_boot_files[] = { "dht.dat", "dht.dat.old", NULL };
#else
static const char *param_main_boot_dirs[] = { ".config/transmission/torrents", NULL };
static const char *param_main_boot_files[] = { "../dht.dat", NULL };
#endif

static const char *param_proxy_test[] = { NULL };

static const struct {
  int login;
  const char *key, **values;
  const char *description;
} param_strings[] = {
  { 0, "crypto.ciphers", param_crypto_ciphers, "list of preferred ciphers" },
  { 0, "ssl.curves", param_ssl_curves, "list of advertised elliptic curves" },
  { 0, "ssl.sigalgs", param_ssl_sigalgs, "list of advertised signature algorithms" },
  { 0, "net.dns.servers", param_net_dns_servers, "list of alternative DNS servers" },
  { 0, "main.boot.ips", param_main_boot_ips, "list of DHT routers" },
  { 0, "main.boot.dirs", param_main_boot_dirs, "list of .torrent file directories" },
  { 0, "main.boot.files", param_main_boot_files, "list of DHT node caches" },
  { 0, "proxy.test", param_proxy_test, "list of test proxies" } /* prepend on start */
};

static const struct {
  int login;
  const char *key, *value;
  const char *description;
} param_string[] = {
  { 1, "ui.login.language", "", "language for ui messages ('' = automatic)" },
  { 0, "ui.main.contact", "00000000000000000000", "contact identifer to open chat with on start" },

  { 0, "ui.chat.line", "default", "foreground color of date line in chat" },
  { 0, "ui.chat.date", "default", "background color of date line when date has changed in chat" },
  { 0, "ui.chat.backtime", "default", "background color of date line when time goes back" },
  { 0, "ui.chat.backdate", "black", "background color of date line when date goes back" },
  { 0, "ui.chat.editreceived", "red", "foreground color of received edited messages star" },
  { 0, "ui.chat.editsent", "default", "foreground color of sent edited messages star" },
  { 0, "ui.chat.system", "default", "foreground color of system messages dot" },
  { 0, "ui.chat.missed", "red", "foreground color of new missed call messages" },
  { 0, "ui.chat.notfound", "darkred", "background color of not found text in chat" },
  { 0, "ui.chat.button", "red", "text color of auto-scroll button in chat" },
  { 0, "ui.chat.received", "default", "background color of received messages" },
  { 0, "ui.chat.sent", "default", "background color of sent messages" },
  { 0, "ui.chat.notsent", "default", "background color of not sent messages" },

  { 1, "ui.console.line", "default", "foreground color of date line in console" },
  { 1, "ui.console.date", "default", "background color of date line in console" },
  { 1, "ui.console.time", "black", "background color of date line in console when time travel is detected" },
  { 1, "ui.console.notfound", "darkred", "background color of not found text in console" },
  { 1, "ui.console.button", "red", "text color of auto-scroll button in console" },
  { 1, "ui.00000000000000000001.utf", "", NULL }, /* SIM_PARAM_USER */

  { 0, "socket.ip", "0.0.0.0", "network interface to bind TCP connects to (0 = default)" },
  { 0, "main.ip", "0.0.0.0", "network interface to bind UDP port to (0 = default route)" },
  { 0, "proxy.local", "127.0.0.1", "local interface for proxy (0 = default route)" },
  { 0, "net.tor.ip", "127.0.0.1", "socks proxy ip address" },
  { 0, "net.upnp.ip", "", "network interface to use for uPNP\n\t'' = same as main.ip\n\t0 = miniupnpc default" },
  { 0, "net.upnp.lanaddr", "", "ip address to map port to\n\t'' = same as net.upnp.ip\n\t0 = miniupnpc default" },

  { 0, "net.upnp.url", "", "bypass discovery and always use this root URL (if non-empty string)" },
  { 0, "net.upnp.path", "", "this may bypass your local ip binding if set, so don't use it" },
  { 0, "net.upnp.description", "", "uPNP port mapping description (must be unique throughout LAN if non-empty)" },
  { 1, "crypto.rng0", "", "name of cipher for RNG0" }, /* random_public */
  { 0, "ssl.ciphers", "", "list of (colon-separated) advertised TLS ciphers" },
  { 0, "file.cipher", "", "name of cipher to use for file encryption" },
  { 1, "contact.test", "a", "authorization level of test contact\n\t\"\" = deleted\n\t'a' = accepted" }, /* CONTACT_AUTH_xxx */
  { 1, "client.status", "on", "current status" },                                                        /* SIM_STATUS_xxx */
  { 0, "client.host", ".", "dynDNS hostname\n\t'' = none\n\tdot-prefixed = disabled" },

  { 1, "audio.device.input", "", "input device name ('' = default device)" },
  { 1, "audio.device.output", "", "output device name ('' = default device)" },
  { 1, "audio.device.ring", "", "ring device name ('' = default device)" },

  /* log levels for all modules: */
  { 1, "log.table", "error", "level: xtra debug info note warn error fatal" },
  { 1, "log.utils", "error", "level: xtra debug info note warn error fatal" },
  { 1, "log.system", "error", "level: xtra debug info note warn error fatal" },
  { 1, "log.crypto", "error", "level: xtra debug info note warn error fatal" },
  { 1, "log.ssl", "error", "level: xtra debug info note warn error fatal" },
  { 1, "log.file", "error", "level: xtra debug info note warn error fatal" },
  { 1, "log.socket", "error", "level: xtra debug info note warn error fatal" },
  { 1, "log.keygen", "error", "level: xtra debug info note warn error fatal" },
  { 1, "log.network", "error", "level: xtra debug info note warn error fatal" },
  { 1, "log.main", "error", "level: xtra debug info note warn error fatal" },
  { 1, "log.contact", "error", "level: xtra debug info note warn error fatal" },
  { 1, "log.param", "error", "level: xtra debug info note warn error fatal" },
  { 1, "log.limit", "error", "level: xtra debug info note warn error fatal" },
  { 1, "log.proxy", "error", "level: xtra debug info note warn error fatal" },
  { 1, "log.proxies", "error", "level: xtra debug info note warn error fatal" },
  { 1, "log.nat", "error", "level: xtra debug info note warn error fatal" },
  { 1, "log.server", "error", "level: xtra debug info note warn error fatal" },
  { 1, "log.client", "error", "level: xtra debug info note warn error fatal" },
  { 1, "log.msg", "error", "level: xtra debug info note warn error fatal" },
  { 1, "log.audio", "error", "level: xtra debug info note warn error fatal" },
  { 1, "log.api", "error", "level: xtra debug info note warn error fatal" },
  { 1, "log.console", "error", "level: xtra debug info note warn error fatal" }, /* log_count_ / log_get_string_ and SIM_EVENT_LOG */
  { 1, "log.ui", "error", "level: xtra debug info note warn error fatal" }
};

simbool param_check_set (const char *param) {
  return table_get (param_saved_table, param).typ != SIMNIL;
}

#if defined(SIM_TYPE_CHECKS) && ! SIM_TYPE_CHECKS
#define param_check(param, file, line) true
#else
static simbool param_check (const char *param, const char *file, unsigned line) {
  if (contact_list.me || table_get_number (param_login_table, param))
    return true;
  LOG_ERROR_ ("get %s:%u login %s\n", file, line, param);
  return false;
}

simtype _param_get (const char *param, int type, const char *file, unsigned line) {
  param_check (param, file, line);
  return table_get_type (param_table, param, type);
}

simtype _param_get_any (const char *param, const char *file, unsigned line) {
  param_check (param, file, line);
  return table_get (param_table, param);
}

int _param_get_type (const char *param) {
  return contact_list.me || table_get_number (param_login_table, param) ? table_get (param_table, param).typ : SIMNIL;
}
#endif

int param_get_limits (const char *param, simtype *def, int *min, int *max, const char **description) {
  unsigned i;
  switch (table_get (param_table, param).typ) {
    case SIMNIL:
      return SIM_PARAM_BAD_KEY;
    case SIMNUMBER:
      if ((i = (unsigned) table_get_number (param_limit_table, param)) == 0)
        return SIM_PARAM_UNKNOWN_KEY;
      if (def)
        *def = number_new (param_number[i - 1].value);
      if (min)
        *min = param_number[i - 1].min;
      if (max)
        *max = param_number[i - 1].max;
      if (description)
        *description = param_number[i - 1].description;
      return SIM_OK;
    case SIMPOINTER:
    case SIMSTRING:
      for (i = 0; i < SIM_ARRAY_SIZE (param_string); i++)
        if (! strcmp (param_string[i].key, param)) {
          if (def)
            *def = pointer_new (param_string[i].value);
          if (description)
            *description = param_string[i].description;
          return SIM_OK;
        }
      break;
    case SIMARRAY_STRING:
      for (i = 0; i < SIM_ARRAY_SIZE (param_strings); i++)
        if (! strcmp (param_strings[i].key, param)) {
          if (def)
            *def = sim_param_parse_strings (param_strings[i].values);
          if (description)
            *description = param_strings[i].description;
          return SIM_OK;
        }
  }
  return SIM_PARAM_UNKNOWN_KEY;
}

simtype param_get_default (const char *param, const simtype def) {
  simtype tmp = *(simtype *) &def;
  param_get_limits (param, &tmp, NULL, NULL, NULL);
  return tmp;
}

int param_get_min (const char *param, int min) {
  param_get_limits (param, NULL, &min, NULL, NULL);
  return min;
}

int param_get_max (const char *param, int max) {
  param_get_limits (param, NULL, NULL, &max, NULL);
  return max;
}

int param_get_number_default (const char *param, int def) {
  int val = param_get_number (param);
  return val > 0 ? val : def;
}

int param_get_number_min (const char *param, int min) {
  int val = param_get_number (param);
  return val > 0 && val < min ? min : val;
}

int param_get_number_max (const char *param, int max) {
  int val = param_get_number (param);
  return val > max ? max : val;
}

simtype sim_param_parse_strings (const char **values) {
  unsigned i, len;
  simtype array;
  const char **v = values;
  for (len = 0; *v++; len++) {}
  array = array_new_strings (len);
  for (i = 1; i <= len; i++)
    array.arr[i] = pointer_new (*values++);
  return array;
}

int sim_param_set_error (const char *param, const simtype value, int error) {
  char errbuf[SIM_SIZE_ERROR];
  *errbuf = 0;
  if (param)
    strcat (strcpy (errbuf, " "), param);
  switch (value.typ) {
    case SIMNUMBER:
      sprintf (errbuf + strlen (errbuf), " = %" SIM_FORMAT_64 "d", value.num);
      break;
    case SIMPOINTER:
    case SIMSTRING:
      sprintf (errbuf + strlen (errbuf), " = %s", value.str);
  }
  sim_error_set_text (errbuf, NULL, NULL, error);
  return error;
}

static int param_set_end (const char *param, const simtype value) { /* new value has been set */
  if (! strcmp (param, "rights.utf") || ! strcmp (param, "rights.audio") ||
      ! strcmp (param, "rights.typing") || ! strcmp (param, "rights.type") || ! strcmp (param, "msg.edit")) {
    client_send_status (NULL, CLIENT_SEND_STATUS_ON);
  } else if (! strcmp (param, "client.host")) {
    client_send_status (NULL, CLIENT_SEND_STATUS_INFO);
  } else if (! strcmp (param, "contact.txt") && contact_list.me) { /* save or delete contacts.txt file */
    event_test_error (NULL, SIM_EVENT_ERROR_FILE_SAVE, contact_list_save_txt (NULL, CONTACT_TXT_DEFAULT));
  } else if (! strcmp (param, "proxy.require") && value.num <= 0 && param_get_number ("net.tor.port") <= 0) {
    proxy_cancel_proxy (NULL, SIM_PROXY_NOT_NEEDED);
  } else if (! strcmp (param, "proxy.strangers") && ! value.num)
    proxy_cancel_proxy (NULL, SIM_PROXY_NOT_PREFERRED);
  return SIM_OK;
}

static int param_set_value (const char *param, const simtype value, int permanent) { /* apply new setting immediately */
  int err = SIM_OK;
  unsigned level;
  LOG_DEBUG_SIMTYPE_ (value, 0, "set%d %s ", permanent - SIM_PARAM_UNAPPLIED, param);
  if (permanent != SIM_PARAM_UNAPPLIED && ! strcmp (param, "main.port")) {
    err = server_set_port (abs ((int) value.num));
  } else if (! SIM_STRING_CHECK_DIFF_CONST (param, "log.") && (value.typ == SIMPOINTER || value.typ == SIMSTRING)) {
    static const char *param_level_names[] = { "xtra", "debug", "info", "note", "warn", "error", "fatal" };
    for (level = 0; level < SIM_ARRAY_SIZE (param_level_names); level++)
      if (! string_check_diff (value, param_level_names[level]))
        break;
    if (level >= SIM_ARRAY_SIZE (param_level_names)) {
      sim_param_set_error (param, value, err = SIM_PARAM_BAD_VALUE);
    } else if (table_get_number (param_login_table, param) != -1)
      log_set_level_ (param + SIM_STRING_GET_LENGTH_CONST ("log."), level);
  } else if (! strcmp (param, "client.host") && string_check_diff (value, ".")) {
    if (string_check_diff_len (param_get_string (param), value.str, value.len)) {
      if (value.len && value.str[0] != '.' && permanent != SIM_PARAM_UNAPPLIED)
        client_cancel (NULL, SIM_SOCKET_BAD_PORT);
      contact_list_set_info (0);
    }
  } else if (! strcmp (param, "crypto.ciphers")) {
    if ((err = crypt_set_ciphers (value, permanent != SIM_PARAM_UNAPPLIED)) != SIM_OK)
      if (permanent == SIM_PARAM_UNAPPLIED)
        crypt_set_ciphers (param_get_strings (param), true);
  } else if (! strcmp (param, "crypto.rng0")) {
    random_set_cipher (value.ptr);
  } else if (! strcmp (param, "system.lock")) {
    err = system_init_memory ((int) value.num);
  } else if (! strcmp (param, "file.log"))
    if ((err = log_init_file_ (! value.num ? NULL : FILE_LOG_CORE, true)) != SIM_OK)
      return err;
  if (err != SIM_OK && err != SIM_CRYPT_NO_CIPHERS)
    sim_param_set_error (param, value, err);
  return err;
}

static int param_set_values (const char *params, const char *except, const simtype values, int permanent) {
  int err = SIM_OK;
  simtype param, val;
  simwalker ctx;
  for (table_walk_first (&ctx, param_table); (val = table_walk_next (&ctx, &param)).typ != SIMNIL;)
    if (! strncmp (param.ptr, params, strlen (params)) && (! except || strcmp (param.ptr, except))) {
      if (values.typ == SIMNIL) {
        param_set_value (param.ptr, val, permanent);
      } else if (values.typ == SIMNUMBER) {
        err = param_set_number (param.ptr, (int) values.num, permanent);
      } else
        err = param_set_string (param.ptr, values, permanent);
    }
  return err;
}

static int param_set_key (const simtype param, simtype value, int permanent) {
  simtype old = table_get_key (param_table, param);
  int type = old.typ == SIMPOINTER ? SIMSTRING : old.typ, err;
  if (! param_check (param.ptr, __FUNCTION__, __LINE__)) {
    sim_param_set_error (param.ptr, nil (), err = SIM_PARAM_BAD_KEY);
  } else if (value.typ != type && (permanent != SIM_PARAM_USER || type != SIMNIL)) {
    LOG_WARN_ ("set %s type %d (wanted %d)\n", param.str, value.typ, old.typ);
    sim_param_set_error (param.ptr, value, err = type == SIMNIL ? SIM_PARAM_BAD_KEY : SIM_PARAM_BAD_TYPE);
  } else if ((err = param_set_value (param.ptr, value, SIM_PARAM_UNAPPLIED)) == SIM_OK)
    table_set_key (param_table, string_copy_string (param), value);
  if (err != SIM_OK)
    type_free (value);
  return err;
}

int param_set_number (const char *param, int number, int permanent) {
  int err;
  simnumber val;
  if (! strcmp (param, "sound.play"))
    return param_set_values ("sound.play.", NULL, number_new (number), permanent);
  if (permanent != SIM_PARAM_USER) {
    unsigned i = (unsigned) table_get_number (param_limit_table, param);
    if (! i--) {
      LOG_WARN_ ("set %s no such number\n", param);
      return sim_param_set_error (param, nil (), SIM_PARAM_BAD_KEY);
    } else if (number == param_number[i].value) {
      if (permanent != SIM_PARAM_TEMPORARY)
        return param_unset (param, permanent);
    } else if (number < param_number[i].min || number > param_number[i].max) {
      LOG_WARN_ ("set %s bad value %d\n", param, number);
      return sim_param_set_error (param, number_new (number), SIM_PARAM_BAD_VALUE);
    }
  }
  if (! param_check (param, __FUNCTION__, __LINE__))
    return sim_param_set_error (param, nil (), SIM_PARAM_BAD_KEY);
  val = param_get_number (param);
  if ((err = param_set_value (param, number_new (number), permanent)) == SIM_OK) {
    table_set_key_number (param_table, string_copy (param), number);
    if (permanent != SIM_PARAM_TEMPORARY)
      table_set_key_number (param_saved_table, string_copy (param), number);
    if (number != val)
      err = param_set_end (param, number_new (number));
  }
  return err;
}

int param_set_string (const char *param, const simtype string, int permanent) {
  int err;
  simtype key = pointer_new (param), def;
  if (! strcmp (param, "log"))
    return param_set_values ("log.", "log.console", string, permanent);
  if ((err = param_get_limits (param, &def, NULL, NULL, NULL)) != SIM_OK && permanent != SIM_PARAM_USER)
    return err;
  if ((err = param_set_key (key, string_copy_string (string), permanent)) != SIM_OK)
    return err;
  if (permanent != SIM_PARAM_TEMPORARY) {
    if (permanent != SIM_PARAM_USER && ! string_check_diff_len (def, string.str, string.len)) {
      table_delete_key (param_saved_table, key);
    } else
      table_set_key (param_saved_table, string_copy (param), string_copy_string (string));
  }
  return param_set_end (param, string);
}

int param_set_strings (const char *param, const simtype array, int permanent) {
  int err;
  simtype key = pointer_new (param), def = nil ();
  if ((err = param_get_limits (param, &def, NULL, NULL, NULL)) != SIM_OK && permanent != SIM_PARAM_USER)
    return err;
  if ((err = param_set_key (key, array_copy_strings (array), permanent)) == SIM_OK) {
    if (permanent != SIM_PARAM_TEMPORARY) {
      if (permanent != SIM_PARAM_USER && ! array_check_diff (def, array)) {
        table_delete_key (param_saved_table, key);
      } else
        table_set_key (param_saved_table, string_copy (param), array_copy_strings (array));
    }
    err = param_set_end (param, array);
  }
  array_free (def);
  return err;
}

int param_unset (const char *param, int permanent) {
  simtype def;
  int err = param_get_limits (param, &def, NULL, NULL, NULL);
  if (err == SIM_PARAM_BAD_KEY)
    return err;
  if (err == SIM_OK) {
    if ((err = param_set_value (param, def, SIM_PARAM_UNAPPLIED)) != SIM_OK)
      return err;
    table_set (param_table, param, def);
    if (table_get (param_saved_table, param).typ != SIMNIL)
      param_set_end (param, def);
  }
  if (permanent != SIM_PARAM_TEMPORARY)
    table_delete (param_saved_table, param);
  return SIM_OK;
}

int param_save (void) {
  simbool login = true;
  int err[2] = { SIM_OK, SIM_OK };
  char *info = NULL;
  do {
    simtype table = table_new (table_count (param_saved_table)), param, val;
    simwalker ctx;
    for (table_walk_first (&ctx, param_saved_table); (val = table_walk_next (&ctx, &param)).typ != SIMNIL;)
      if (! table_get_key_number (param_login_table, param) == ! login) {
        LOG_XTRA_SIMTYPE_ (val, 0, "%s %s ", login ? FILE_LOGIN : FILE_CONFIG, param.str);
        table_add_key (table, pointer_new_len (param.str, param.len), type_copy (val));
      }
    err[login] = file_save (table, login ? FILE_LOGIN : FILE_CONFIG, login ? FILE_TYPE_PLAINTEXT : FILE_TYPE_ENCRYPTED);
    if (login)
      info = sim_error_get_text (err[true]);
    table_free (table);
  } while (contact_list.me && ! --login);
  if (info && err[false] == SIM_OK)
    sim_error_set_text (info, NULL, NULL, err[true]);
  sim_free_string (info);
  return err[err[false] == SIM_OK];
}

static void param_init_types (void) {
  unsigned i;
  simtype val;
  for (i = 0; i < SIM_ARRAY_SIZE (param_number); i++)
    if ((val = table_get (param_table, param_number[i].key)).typ != SIMNUMBER)
      table_set_number (param_table, param_number[i].key, param_number[i].value);
  for (i = 0; i < SIM_ARRAY_SIZE (param_string); i++)
    if ((val = table_get (param_table, param_string[i].key)).typ == SIMARRAY_STRING) {
      table_set (param_table, param_string[i].key, val.len ? string_copy_string (val.arr[1]) : pointer_new (""));
    } else if (val.typ != SIMSTRING)
      table_set_pointer (param_table, param_string[i].key, param_string[i].value);
  for (i = 0; i < SIM_ARRAY_SIZE (param_strings); i++)
    if ((val = table_get (param_table, param_strings[i].key)).typ == SIMSTRING) {
      table_set (param_table, param_strings[i].key, array_new_type (string_copy_string (val)));
    } else if (val.typ != SIMARRAY_STRING)
      table_set (param_table, param_strings[i].key, sim_param_parse_strings (param_strings[i].values));
}

int param_init (simbool init) {
  int err;
  unsigned i;
  const unsigned n = SIM_ARRAY_SIZE (param_number) + SIM_ARRAY_SIZE (param_string) + SIM_ARRAY_SIZE (param_strings);
  simtype params, param, exe = sim_system_get_exe (NULL);
  simwalker ctx;
  if (init && (err = file_lock (true)) != SIM_OK)
    return err;
  if ((err = file_load (FILE_LOGIN, FILE_TYPE_PLAINTEXT, &params)) != SIM_OK) {
    file_lock (false);
    table_free (params);
    return err;
  }
  param_saved_table = table_copy (param_table = params, table_count (params) / 8 + 1);
  param_limit_table = table_new (SIM_ARRAY_SIZE (param_number) * 2);
  param_login_table = table_new (n);
  for (table_walk_first (&ctx, params); table_walk_next (&ctx, &param).typ != SIMNIL;)
    table_add_key_number (param_login_table, string_copy_string (param), -1);
  for (i = 0; i < SIM_ARRAY_SIZE (param_number); i++) {
    table_add_number (param_table, param_number[i].key, param_number[i].value);
    table_add_number (param_limit_table, param_number[i].key, i + 1);
    if (param_number[i].login)
      table_set_number (param_login_table, param_number[i].key, param_number[i].login);
  }
  for (i = 0; i < SIM_ARRAY_SIZE (param_string); i++) {
    table_add_pointer (param_table, param_string[i].key, param_string[i].value);
    if (param_string[i].login)
      table_set_number (param_login_table, param_string[i].key, param_string[i].login);
  }
  for (i = 0; i < SIM_ARRAY_SIZE (param_strings); i++) {
    table_add (param_table, param_strings[i].key, sim_param_parse_strings (param_strings[i].values));
    if (param_strings[i].login)
      table_set_number (param_login_table, param_strings[i].key, param_strings[i].login);
  }
  param_init_types ();
  if (init) {
    err = param_set_values ("log.", NULL, nil (), SIM_PARAM_TEMPORARY); /* these must be set first */
    param_set_value ("system.lock", number_new (param_get_number ("system.lock")), SIM_PARAM_TEMPORARY);
    param_set_value ("crypto.rng0", param_get_string ("crypto.rng0"), SIM_PARAM_TEMPORARY);
    if (err == SIM_OK) /* this must be set last */
      err = param_set_value ("file.log", number_new (param_get_number ("file.log")), SIM_PARAM_TEMPORARY);
    if (err == SIM_OK && LOG_PROTECT_ (SIM_MODULE, SIM_LOG_INFO)) {
      time_t now = time (NULL), utc;
#ifndef _WIN32
      struct tm res, *tm = gmtime_r (&now, &res);
#else
      struct tm res, *tm = gmtime (&now);
      if (tm)
        res = *tm, tm = &res;
#define mktime(tm) ((unsigned) mktime (tm))
#endif
      LOG_INFO_ ("%s (%s)", sim_get_version (NULL, NULL), exe.typ == SIMSTRING ? (char *) exe.str : "");
      LOG_INFO_ (" CPU=%d", sim_system_cpu_count ());
      if (tm && now != (time_t) -1) {
        tm->tm_isdst = -1;
        if ((int) (utc = mktime (tm)) != -1)
          LOG_INFO_ (" UTC%+gh", (double) (now - utc) / 3600);
      }
      LOG_INFO_ ("\n");
      LOG_UNPROTECT_ ();
    }
    if (err == SIM_OK)
      err = param_save (); /* save the login file to make sure that user directory is writable at program start */
    if (err != SIM_OK)
      param_uninit ();
  }
  return err;
}

int param_open (void) {
  int err;
  simtype params, param, val;
  LOG_INFO_ ("open\n");
  if ((err = file_load (FILE_CONFIG, FILE_TYPE_ENCRYPTED, &params)) == SIM_OK) {
    simwalker ctx;
    for (table_walk_first (&ctx, params); (val = table_walk_next (&ctx, &param)).typ != SIMNIL;)
      if (! table_get_key_number (param_login_table, param)) {
        table_set_key (param_table, string_copy_string (param), type_copy (val));
        table_set_key (param_saved_table, string_copy_string (param), type_copy (val));
      }
    table_free (params);
    param_init_types ();
    param_table = table_copy (params = param_table, table_count (param_table) * 2);
    table_free (params);
    param_saved_table = table_copy (params = param_saved_table, table_count (param_table) / 8 + 1);
    err = param_set_value ("crypto.ciphers", param_get_strings ("crypto.ciphers"), SIM_PARAM_TEMPORARY);
    if (err != SIM_OK)
      err = param_unset ("crypto.ciphers", SIM_PARAM_TEMPORARY);
    contact_list.test = param_get_number ("nat.test");
  }
  table_free (params);
  return err;
}

void param_close (void) {
  LOG_INFO_ ("close\n");
  contact_list_free ();
  random_close (random_session);
  random_close (random_public);
  file_set_password (nil (), false);
}

int param_uninit (void) {
  int err = file_lock (false);
  table_free (param_login_table), param_login_table = nil ();
  table_free (param_limit_table), param_limit_table = nil ();
  table_free (param_saved_table), param_saved_table = nil ();
  table_free (param_table), param_table = nil ();
  return err;
}
