#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# Copyright (c) 2019-2021 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.

import os
import sys
import json
import mmap
import stat
import gnupg
import signal
import socket
import hashlib
import tarfile
import zipfile
import logging
import platform
import urllib.parse
from urllib.request import urlretrieve

import basicswap.config as cfg
from basicswap.rpc import (
    callrpc_cli,
    waitForRPC,
)
from basicswap.basicswap import BasicSwap
from basicswap.chainparams import Coins
from basicswap.contrib.rpcauth import generate_salt, password_to_hmac
from bin.basicswap_run import startDaemon, startXmrWalletDaemon


if platform.system() == 'Darwin':
    BIN_ARCH = 'osx64'
    FILE_EXT = 'tar.gz'
elif platform.system() == 'Windows':
    BIN_ARCH = 'win64'
    FILE_EXT = 'zip'
else:
    BIN_ARCH = 'x86_64-linux-gnu'
    FILE_EXT = 'tar.gz'

known_coins = {
    'particl': ('0.21.2.6', ''),
    'litecoin': ('0.18.1', ''),
    'bitcoin': ('0.21.1', ''),
    'namecoin': ('0.18.0', ''),
    'monero': ('0.17.3.0', ''),
}

logger = logging.getLogger()
logger.level = logging.DEBUG
if not len(logger.handlers):
    logger.addHandler(logging.StreamHandler(sys.stdout))

XMR_RPC_HOST = os.getenv('XMR_RPC_HOST', '127.0.0.1')
BASE_XMR_RPC_PORT = int(os.getenv('BASE_XMR_RPC_PORT', 29798))
BASE_XMR_ZMQ_PORT = int(os.getenv('BASE_XMR_ZMQ_PORT', 30898))
BASE_XMR_WALLET_PORT = int(os.getenv('BASE_XMR_WALLET_PORT', 29998))
XMR_WALLET_RPC_HOST = os.getenv('XMR_WALLET_RPC_HOST', '127.0.0.1')
XMR_WALLET_RPC_USER = os.getenv('XMR_WALLET_RPC_USER', 'xmr_wallet_user')
XMR_WALLET_RPC_PWD = os.getenv('XMR_WALLET_RPC_PWD', 'xmr_wallet_pwd')
XMR_SITE_COMMIT = 'be137696c3a1d93bfaddf619da0d05a3e6cee4f2'  # Lock hashes.txt to monero version

DEFAULT_XMR_RESTORE_HEIGHT = 2245107

UI_HTML_PORT = int(os.getenv('UI_HTML_PORT', 12700))
PART_ZMQ_PORT = int(os.getenv('PART_ZMQ_PORT', 20792))

PART_RPC_HOST = os.getenv('PART_RPC_HOST', '127.0.0.1')
LTC_RPC_HOST = os.getenv('LTC_RPC_HOST', '127.0.0.1')
BTC_RPC_HOST = os.getenv('BTC_RPC_HOST', '127.0.0.1')
NMC_RPC_HOST = os.getenv('NMC_RPC_HOST', '127.0.0.1')

PART_RPC_PORT = int(os.getenv('PART_RPC_PORT', 19792))
LTC_RPC_PORT = int(os.getenv('LTC_RPC_PORT', 19795))
BTC_RPC_PORT = int(os.getenv('BTC_RPC_PORT', 19796))
NMC_RPC_PORT = int(os.getenv('NMC_RPC_PORT', 19798))

PART_RPC_USER = os.getenv('PART_RPC_USER', '')
PART_RPC_PWD = os.getenv('PART_RPC_PWD', '')
BTC_RPC_USER = os.getenv('BTC_RPC_USER', '')
BTC_RPC_PWD = os.getenv('BTC_RPC_PWD', '')
LTC_RPC_USER = os.getenv('LTC_RPC_USER', '')
LTC_RPC_PWD = os.getenv('LTC_RPC_PWD', '')

COINS_BIND_IP = os.getenv('COINS_BIND_IP', '127.0.0.1')

extract_core_overwrite = True


def make_reporthook():
    read = 0  # Number of bytes read so far
    last_percent_str = ''

    def reporthook(blocknum, blocksize, totalsize):
        nonlocal read
        nonlocal last_percent_str
        read += blocksize
        if totalsize > 0:
            percent_str = '%5.0f%%' % (read * 1e2 / totalsize)
            if percent_str != last_percent_str:
                logger.info(percent_str)
                last_percent_str = percent_str
        else:
            logger.info('read %d' % (read,))
    return reporthook


def downloadFile(url, path):
    logger.info('Downloading file %s', url)
    logger.info('To %s', path)
    opener = urllib.request.build_opener()
    opener.addheaders = [('User-agent', 'Mozilla/5.0')]
    urllib.request.install_opener(opener)

    # Set one second timeout for urlretrieve connections
    old_timeout = socket.getdefaulttimeout()
    socket.setdefaulttimeout(1)
    urlretrieve(url, path, make_reporthook())
    socket.setdefaulttimeout(old_timeout)


def extractCore(coin, version_pair, settings, bin_dir, release_path):
    version, version_tag = version_pair
    logger.info('extractCore %s v%s%s', coin, version, version_tag)

    if coin == 'monero':
        bins = ['monerod', 'monero-wallet-rpc']
        num_exist = 0
        for b in bins:
            out_path = os.path.join(bin_dir, b)
            if os.path.exists(out_path):
                num_exist += 1
        if not extract_core_overwrite and num_exist == len(bins):
            logger.info('Skipping extract, files exist.')
            return

        with tarfile.open(release_path) as ft:
            for member in ft.getmembers():
                if member.isdir():
                    continue
                bin_name = os.path.basename(member.name)
                if bin_name not in bins:
                    continue
                out_path = os.path.join(bin_dir, bin_name)
                if (not os.path.exists(out_path)) or extract_core_overwrite:
                    with open(out_path, 'wb') as fout, ft.extractfile(member) as fi:
                        fout.write(fi.read())
                    try:
                        os.chmod(out_path, stat.S_IRWXU | stat.S_IXGRP | stat.S_IXOTH)
                    except Exception as e:
                        logging.warning('Unable to set file permissions: %s, for %s', str(e), out_path)
        return

    bins = [coin + 'd', coin + '-cli', coin + '-tx']
    versions = version.split('.')
    if int(versions[1]) >= 19:
        bins.append(coin + '-wallet')
    if 'win32' in BIN_ARCH or 'win64' in BIN_ARCH:
        with zipfile.ZipFile(release_path) as fz:
            for b in bins:
                b += '.exe'
                out_path = os.path.join(bin_dir, b)
                if (not os.path.exists(out_path)) or extract_core_overwrite:
                    with open(out_path, 'wb') as fout:
                        fout.write(fz.read('{}-{}/bin/{}'.format(coin, version, b)))
                    try:
                        os.chmod(out_path, stat.S_IRWXU | stat.S_IXGRP | stat.S_IXOTH)
                    except Exception as e:
                        logging.warning('Unable to set file permissions: %s, for %s', str(e), out_path)
    else:
        with tarfile.open(release_path) as ft:
            for b in bins:
                out_path = os.path.join(bin_dir, b)
                if not os.path.exists(out_path) or extract_core_overwrite:
                    with open(out_path, 'wb') as fout, ft.extractfile('{}-{}/bin/{}'.format(coin, version + version_tag, b)) as fi:
                        fout.write(fi.read())
                    try:
                        os.chmod(out_path, stat.S_IRWXU | stat.S_IXGRP | stat.S_IXOTH)
                    except Exception as e:
                        logging.warning('Unable to set file permissions: %s, for %s', str(e), out_path)


def prepareCore(coin, version_pair, settings, data_dir):
    version, version_tag = version_pair
    logger.info('prepareCore %s v%s%s', coin, version, version_tag)

    bin_dir = os.path.expanduser(settings['chainclients'][coin]['bindir'])
    if not os.path.exists(bin_dir):
        os.makedirs(bin_dir)

    if 'osx' in BIN_ARCH:
        os_dir_name = 'osx-unsigned'
        os_name = 'osx'
    elif 'win32' in BIN_ARCH or 'win64' in BIN_ARCH:
        os_dir_name = 'win-unsigned'
        os_name = 'win'
    else:
        os_dir_name = 'linux'
        os_name = 'linux'

    if coin == 'monero':
        use_file_ext = 'tar.bz2' if FILE_EXT == 'tar.gz' else FILE_EXT
        release_filename = '{}-{}-{}.{}'.format(coin, version, BIN_ARCH, use_file_ext)
        if os_name == 'osx':
            os_name = 'mac'
        release_url = 'https://downloads.getmonero.org/cli/monero-{}-x64-v{}.{}'.format(os_name, version, use_file_ext)
        release_path = os.path.join(bin_dir, release_filename)
        if not os.path.exists(release_path):
            downloadFile(release_url, release_path)

        assert_filename = 'monero-{}-hashes.txt'.format(version)
        # assert_url = 'https://www.getmonero.org/downloads/hashes.txt'
        assert_url = 'https://raw.githubusercontent.com/monero-project/monero-site/{}/downloads/hashes.txt'.format(XMR_SITE_COMMIT)
        assert_path = os.path.join(bin_dir, assert_filename)
        if not os.path.exists(assert_path):
            downloadFile(assert_url, assert_path)
    else:
        release_filename = '{}-{}-{}.{}'.format(coin, version + version_tag, BIN_ARCH, FILE_EXT)
        if coin == 'particl':
            signing_key_name = 'tecnovert'
            release_url = 'https://github.com/particl/particl-core/releases/download/v{}/{}'.format(version + version_tag, release_filename)
            assert_filename = '{}-{}-{}-build.assert'.format(coin, os_name, version)
            assert_url = 'https://raw.githubusercontent.com/particl/gitian.sigs/master/%s-%s/%s/%s' % (version + version_tag, os_dir_name, signing_key_name, assert_filename)
        elif coin == 'litecoin':
            signing_key_name = 'thrasher'
            release_url = 'https://download.litecoin.org/litecoin-{}/{}/{}'.format(version, os_name, release_filename)
            assert_filename = '{}-{}-{}-build.assert'.format(coin, os_name, version.rsplit('.', 1)[0])
            assert_url = 'https://raw.githubusercontent.com/litecoin-project/gitian.sigs.ltc/master/%s-%s/%s/%s' % (version, os_dir_name, signing_key_name, assert_filename)
        elif coin == 'bitcoin':
            signing_key_name = 'laanwj'
            release_url = 'https://bitcoincore.org/bin/bitcoin-core-{}/{}'.format(version, release_filename)
            assert_filename = '{}-core-{}-{}-build.assert'.format(coin, os_name, '.'.join(version.split('.')[:2]))
            assert_url = 'https://raw.githubusercontent.com/bitcoin-core/gitian.sigs/master/%s-%s/%s/%s' % (version, os_dir_name, signing_key_name, assert_filename)
        elif coin == 'namecoin':
            signing_key_name = 'JeremyRand'
            release_url = 'https://beta.namecoin.org/files/namecoin-core/namecoin-core-{}/{}'.format(version, release_filename)
            assert_filename = '{}-{}-{}-build.assert'.format(coin, os_name, version.rsplit('.', 1)[0])
            assert_url = 'https://raw.githubusercontent.com/namecoin/gitian.sigs/master/%s-%s/%s/%s' % (version, os_dir_name, signing_key_name, assert_filename)
        else:
            raise ValueError('Unknown coin')

        assert_sig_filename = assert_filename + '.sig'
        assert_sig_url = assert_url + '.sig'

        release_path = os.path.join(bin_dir, release_filename)
        if not os.path.exists(release_path):
            downloadFile(release_url, release_path)

        # Rename assert files with full version
        assert_filename = '{}-{}-{}-build.assert'.format(coin, os_name, version)
        assert_path = os.path.join(bin_dir, assert_filename)
        if not os.path.exists(assert_path):
            downloadFile(assert_url, assert_path)

        assert_sig_filename = '{}-{}-{}-build.assert.sig'.format(coin, os_name, version)
        assert_sig_path = os.path.join(bin_dir, assert_sig_filename)
        if not os.path.exists(assert_sig_path):
            downloadFile(assert_sig_url, assert_sig_path)

    hasher = hashlib.sha256()
    with open(release_path, 'rb') as fp:
        hasher.update(fp.read())
    release_hash = hasher.digest()

    logger.info('%s hash: %s', release_filename, release_hash.hex())
    with open(assert_path, 'rb', 0) as fp, mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ) as s:
        if s.find(bytes(release_hash.hex(), 'utf-8')) == -1:
            raise ValueError('Error: release hash %s not found in assert file.' % (release_hash.hex()))
        else:
            logger.info('Found release hash in assert file.')

    """
    gnupghome = os.path.join(data_dir, 'gpg')
    if not os.path.exists(gnupghome):
        os.makedirs(gnupghome)
    """
    gpg = gnupg.GPG()

    if coin == 'monero':
        with open(assert_path, 'rb') as fp:
            verified = gpg.verify_file(fp)

        if verified.username is None:
            logger.warning('Signature not verified.')

            pubkeyurl = 'https://raw.githubusercontent.com/monero-project/monero/master/utils/gpg_keys/binaryfate.asc'
            logger.info('Importing public key from url: ' + pubkeyurl)
            rv = gpg.import_keys(urllib.request.urlopen(pubkeyurl).read())
            print('import_keys', rv)
            assert('F0AF4D462A0BDF92' in rv.fingerprints[0])
            gpg.trust_keys(rv.fingerprints[0], 'TRUST_FULLY')
            with open(assert_path, 'rb') as fp:
                verified = gpg.verify_file(fp)
    else:
        with open(assert_sig_path, 'rb') as fp:
            verified = gpg.verify_file(fp, assert_path)

        if verified.username is None:
            logger.warning('Signature not verified.')

            pubkeyurl = 'https://raw.githubusercontent.com/tecnovert/basicswap/master/gitianpubkeys/{}_{}.pgp'.format(coin, signing_key_name)
            logger.info('Importing public key from url: ' + pubkeyurl)
            rv = gpg.import_keys(urllib.request.urlopen(pubkeyurl).read())

            for key in rv.fingerprints:
                gpg.trust_keys(key, 'TRUST_FULLY')

            with open(assert_sig_path, 'rb') as fp:
                verified = gpg.verify_file(fp, assert_path)

    if verified.valid is False \
       and not (verified.status == 'signature valid' and verified.key_status == 'signing key has expired'):
        raise ValueError('Signature verification failed.')

    extractCore(coin, version_pair, settings, bin_dir, release_path)


def prepareDataDir(coin, settings, chain, particl_mnemonic, use_containers=False):
    core_settings = settings['chainclients'][coin]
    bin_dir = core_settings['bindir']
    data_dir = core_settings['datadir']

    if not os.path.exists(data_dir):
        os.makedirs(data_dir)

    if coin == 'monero':
        core_conf_path = os.path.join(data_dir, coin + 'd.conf')
        if os.path.exists(core_conf_path):
            exitWithError('{} exists'.format(core_conf_path))
        with open(core_conf_path, 'w') as fp:
            if chain == 'regtest':
                fp.write('regtest=1\n')
                fp.write('keep-fakechain=1\n')
                fp.write('fixed-difficulty=1\n')
            else:
                fp.write('bootstrap-daemon-address=auto\n')
                fp.write('restricted-rpc=1\n')
            if chain == 'testnet':
                fp.write('testnet=1\n')
            fp.write('data-dir={}\n'.format(data_dir))
            fp.write('rpc-bind-port={}\n'.format(core_settings['rpcport']))
            fp.write('rpc-bind-ip={}\n'.format(COINS_BIND_IP))
            fp.write('zmq-rpc-bind-port={}\n'.format(core_settings['zmqport']))
            fp.write('zmq-rpc-bind-ip={}\n'.format(COINS_BIND_IP))
            fp.write('prune-blockchain=1\n')

        wallets_dir = core_settings.get('walletsdir', data_dir)
        if not os.path.exists(wallets_dir):
            os.makedirs(wallets_dir)

        wallet_conf_path = os.path.join(wallets_dir, coin + '_wallet.conf')
        if os.path.exists(wallet_conf_path):
            exitWithError('{} exists'.format(wallet_conf_path))
        with open(wallet_conf_path, 'w') as fp:
            if use_containers:
                fp.write('daemon-address={}:{}\n'.format(core_settings['rpchost'], core_settings['rpcport']))
            fp.write('untrusted-daemon=1\n')
            fp.write('no-dns=1\n')
            fp.write('rpc-bind-port={}\n'.format(core_settings['walletrpcport']))
            fp.write('rpc-bind-ip={}\n'.format(COINS_BIND_IP))
            fp.write('wallet-dir={}\n'.format(os.path.join(data_dir, 'wallets')))
            fp.write('log-file={}\n'.format(os.path.join(data_dir, 'wallet.log')))
            fp.write('shared-ringdb-dir={}\n'.format(os.path.join(data_dir, 'shared-ringdb')))
            fp.write('rpc-login={}:{}\n'.format(core_settings['walletrpcuser'], core_settings['walletrpcpassword']))
        return
    core_conf_path = os.path.join(data_dir, coin + '.conf')
    if os.path.exists(core_conf_path):
        exitWithError('{} exists'.format(core_conf_path))
    with open(core_conf_path, 'w') as fp:
        if chain != 'mainnet':
            fp.write(chain + '=1\n')
            if chain == 'testnet':
                fp.write('[test]\n\n')
            if chain == 'regtest':
                fp.write('[regtest]\n\n')
            else:
                logger.warning('Unknown chain %s', chain)

        if COINS_BIND_IP != '127.0.0.1':
            fp.write('rpcallowip=127.0.0.1\n')
            fp.write('rpcallowip=172.0.0.0/8\n')  # Allow 172.x.x.x, range used by docker
            fp.write('rpcbind={}\n'.format(COINS_BIND_IP))

        fp.write('rpcport={}\n'.format(core_settings['rpcport']))
        fp.write('printtoconsole=0\n')
        fp.write('daemon=0\n')
        fp.write('wallet=wallet.dat\n')

        salt = generate_salt(16)
        if coin == 'particl':
            fp.write('debugexclude=libevent\n')
            fp.write('zmqpubsmsg=tcp://{}:{}\n'.format(COINS_BIND_IP, settings['zmqport']))
            fp.write('spentindex=1\n')
            fp.write('txindex=1\n')
            fp.write('staking=0\n')
            if PART_RPC_USER != '':
                fp.write('rpcauth={}:{}${}\n'.format(PART_RPC_USER, salt, password_to_hmac(salt, PART_RPC_PWD)))
            if particl_mnemonic == 'none':
                fp.write('createdefaultmasterkey=1')
        elif coin == 'litecoin':
            fp.write('prune=2000\n')
            if LTC_RPC_USER != '':
                fp.write('rpcauth={}:{}${}\n'.format(LTC_RPC_USER, salt, password_to_hmac(salt, LTC_RPC_PWD)))
        elif coin == 'bitcoin':
            fp.write('prune=2000\n')
            fp.write('fallbackfee=0.0002\n')
            if BTC_RPC_USER != '':
                fp.write('rpcauth={}:{}${}\n'.format(BTC_RPC_USER, salt, password_to_hmac(salt, BTC_RPC_PWD)))
        elif coin == 'namecoin':
            fp.write('prune=2000\n')
        else:
            logger.warning('Unknown coin %s', coin)

    wallet_util = coin + '-wallet'
    if os.path.exists(os.path.join(bin_dir, wallet_util)):
        logger.info('Creating wallet.dat for {}.'.format(wallet_util.capitalize()))
        callrpc_cli(bin_dir, data_dir, chain, '-wallet=wallet.dat create', wallet_util)


def printVersion():
    from basicswap import __version__
    logger.info('Basicswap version: %s', __version__)

    logger.info('Core versions:')
    for coin, version in known_coins.items():
        logger.info('\t%s: %s%s', coin, version[0], version[1])


def printHelp():
    logger.info('Usage: basicswap-prepare ')
    logger.info('\n--help, -h               Print help.')
    logger.info('--version, -v            Print version.')
    logger.info('--datadir=PATH           Path to basicswap data directory, default:{}.'.format(cfg.DEFAULT_DATADIR))
    logger.info('--bindir=PATH            Path to cores directory, default:datadir/bin.')
    logger.info('--mainnet                Run in mainnet mode.')
    logger.info('--testnet                Run in testnet mode.')
    logger.info('--regtest                Run in regtest mode.')
    logger.info('--particl_mnemonic=      Recovery phrase to use for the Particl wallet, default is randomly generated,\n'
                + '                         "none" to set autogenerate account mode.')
    logger.info('--withcoin=              Prepare system to run daemon for coin.')
    logger.info('--withoutcoin=           Do not prepare system to run daemon for coin.')
    logger.info('--addcoin=               Add coin to existing setup.')
    logger.info('--disablecoin=           Make coin inactive.')
    logger.info('--preparebinonly         Don\'t prepare settings or datadirs.')
    logger.info('--nocores                Don\'t download and extract any coin clients.')
    logger.info('--usecontainers          Expect each core to run in a unique container.')
    logger.info('--portoffset=n           Raise all ports by n.')
    logger.info('--htmlhost=              Interface to host on, default:127.0.0.1.')
    logger.info('--xmrrestoreheight=n     Block height to restore Monero wallet from, default:{}.'.format(DEFAULT_XMR_RESTORE_HEIGHT))
    logger.info('--noextractover          Prevent extracting cores if files exist.  Speeds up tests')

    logger.info('\n' + 'Known coins: %s', ', '.join(known_coins.keys()))


def make_rpc_func(bin_dir, data_dir, chain):
    bin_dir = bin_dir
    data_dir = data_dir
    chain = chain

    def rpc_func(cmd):
        nonlocal bin_dir
        nonlocal data_dir
        nonlocal chain

        return callrpc_cli(bin_dir, data_dir, chain, cmd, cfg.PARTICL_CLI)
    return rpc_func


def exitWithError(error_msg):
    sys.stderr.write('Error: {}, exiting.\n'.format(error_msg))
    sys.exit(1)


def main():
    global extract_core_overwrite
    data_dir = None
    bin_dir = None
    port_offset = None
    chain = 'mainnet'
    particl_wallet_mnemonic = None
    prepare_bin_only = False
    no_cores = False
    use_containers = False
    with_coins = {'particl', }
    add_coin = ''
    disable_coin = ''
    htmlhost = '127.0.0.1'
    xmr_restore_height = DEFAULT_XMR_RESTORE_HEIGHT

    for v in sys.argv[1:]:
        if len(v) < 2 or v[0] != '-':
            exitWithError('Unknown argument {}'.format(v))

        s = v.split('=')
        name = s[0].strip()

        for i in range(2):
            if name[0] == '-':
                name = name[1:]

        if name == 'v' or name == 'version':
            printVersion()
            return 0
        if name == 'h' or name == 'help':
            printHelp()
            return 0
        if name == 'mainnet':
            continue
        if name == 'testnet':
            chain = 'testnet'
            continue
        if name == 'regtest':
            chain = 'regtest'
            continue
        if name == 'preparebinonly':
            prepare_bin_only = True
            continue
        if name == 'nocores':
            no_cores = True
            continue
        if name == 'usecontainers':
            use_containers = True
            continue
        if name == 'noextractover':
            extract_core_overwrite = False
            continue
        if len(s) == 2:
            if name == 'datadir':
                data_dir = os.path.expanduser(s[1].strip('"'))
                continue
            if name == 'bindir':
                bin_dir = os.path.expanduser(s[1].strip('"'))
                continue
            if name == 'portoffset':
                port_offset = int(s[1])
                continue
            if name == 'particl_mnemonic':
                particl_wallet_mnemonic = s[1].strip('"')
                continue
            if name == 'withcoin' or name == 'withcoins':
                coins = s[1].split(',')
                for coin in coins:
                    if coin not in known_coins:
                        exitWithError('Unknown coin {}'.format(coin))
                    with_coins.add(coin)
                continue
            if name == 'withoutcoin' or name == 'withoutcoins':
                coins = s[1].split(',')
                for coin in coins:
                    if coin not in known_coins:
                        exitWithError('Unknown coin {}'.format(coin))
                    with_coins.discard(coin)
                continue
            if name == 'addcoin':
                if s[1] not in known_coins:
                    exitWithError('Unknown coin {}'.format(s[1]))
                add_coin = s[1]
                with_coins = [add_coin, ]
                continue
            if name == 'disablecoin':
                if s[1] not in known_coins:
                    exitWithError('Unknown coin {}'.format(s[1]))
                disable_coin = s[1]
                continue
            if name == 'htmlhost':
                htmlhost = s[1].strip('"')
                continue
            if name == 'xmrrestoreheight':
                xmr_restore_height = int(s[1])
                continue

        exitWithError('Unknown argument {}'.format(v))

    if data_dir is None:
        data_dir = os.path.join(os.path.expanduser(cfg.DEFAULT_DATADIR))
    if bin_dir is None:
        bin_dir = os.path.join(data_dir, 'bin')

    logger.info('datadir: %s', data_dir)
    logger.info('bindir:  %s', bin_dir)
    logger.info('Chain: %s', chain)

    if port_offset is None:
        port_offset = 300 if chain == 'testnet' else 0

    if not os.path.exists(data_dir):
        os.makedirs(data_dir)
    config_path = os.path.join(data_dir, cfg.CONFIG_FILENAME)

    withchainclients = {}
    chainclients = {
        'particl': {
            'connection_type': 'rpc',
            'manage_daemon': True if ('particl' in with_coins and PART_RPC_HOST == '127.0.0.1') else False,
            'rpchost': PART_RPC_HOST,
            'rpcport': PART_RPC_PORT + port_offset,
            'datadir': os.getenv('PART_DATA_DIR', os.path.join(data_dir, 'particl')),
            'bindir': os.path.join(bin_dir, 'particl'),
            'blocks_confirmed': 2,
            'override_feerate': 0.002,
            'conf_target': 2,
            'core_version_group': 18,
            'chain_lookups': 'local',
        },
        'litecoin': {
            'connection_type': 'rpc' if 'litecoin' in with_coins else 'none',
            'manage_daemon': True if ('litecoin' in with_coins and LTC_RPC_HOST == '127.0.0.1') else False,
            'rpchost': LTC_RPC_HOST,
            'rpcport': LTC_RPC_PORT + port_offset,
            'datadir': os.getenv('LTC_DATA_DIR', os.path.join(data_dir, 'litecoin')),
            'bindir': os.path.join(bin_dir, 'litecoin'),
            'use_segwit': True,
            'blocks_confirmed': 2,
            'conf_target': 2,
            'core_version_group': 18,
            'chain_lookups': 'local',
        },
        'bitcoin': {
            'connection_type': 'rpc' if 'bitcoin' in with_coins else 'none',
            'manage_daemon': True if ('bitcoin' in with_coins and BTC_RPC_HOST == '127.0.0.1') else False,
            'rpchost': BTC_RPC_HOST,
            'rpcport': BTC_RPC_PORT + port_offset,
            'datadir': os.getenv('BTC_DATA_DIR', os.path.join(data_dir, 'bitcoin')),
            'bindir': os.path.join(bin_dir, 'bitcoin'),
            'use_segwit': True,
            'blocks_confirmed': 1,
            'conf_target': 2,
            'core_version_group': 18,
            'chain_lookups': 'local',
        },
        'namecoin': {
            'connection_type': 'rpc' if 'namecoin' in with_coins else 'none',
            'manage_daemon': True if ('namecoin' in with_coins and NMC_RPC_HOST == '127.0.0.1') else False,
            'rpchost': NMC_RPC_HOST,
            'rpcport': NMC_RPC_PORT + port_offset,
            'datadir': os.getenv('NMC_DATA_DIR', os.path.join(data_dir, 'namecoin')),
            'bindir': os.path.join(bin_dir, 'namecoin'),
            'use_segwit': False,
            'use_csv': False,
            'blocks_confirmed': 1,
            'conf_target': 2,
            'core_version_group': 18,
            'chain_lookups': 'local',
        },
        'monero': {
            'connection_type': 'rpc' if 'monero' in with_coins else 'none',
            'manage_daemon': True if ('monero' in with_coins and XMR_RPC_HOST == '127.0.0.1') else False,
            'manage_wallet_daemon': True if ('monero' in with_coins and XMR_WALLET_RPC_HOST == '127.0.0.1') else False,
            'rpcport': BASE_XMR_RPC_PORT + port_offset,
            'zmqport': BASE_XMR_ZMQ_PORT + port_offset,
            'walletrpcport': BASE_XMR_WALLET_PORT + port_offset,
            'rpchost': XMR_RPC_HOST,
            'walletrpchost': XMR_WALLET_RPC_HOST,
            'walletrpcuser': XMR_WALLET_RPC_USER,
            'walletrpcpassword': XMR_WALLET_RPC_PWD,
            'walletfile': 'swap_wallet',
            'datadir': os.getenv('XMR_DATA_DIR', os.path.join(data_dir, 'monero')),
            'bindir': os.path.join(bin_dir, 'monero'),
            'restore_height': xmr_restore_height,
            'blocks_confirmed': 7,  # TODO: 10?
        }
    }

    if PART_RPC_USER != '':
        chainclients['particl']['rpcuser'] = PART_RPC_USER
        chainclients['particl']['rpcpassword'] = PART_RPC_PWD
    if LTC_RPC_USER != '':
        chainclients['litecoin']['rpcuser'] = LTC_RPC_USER
        chainclients['litecoin']['rpcpassword'] = LTC_RPC_PWD
    if BTC_RPC_USER != '':
        chainclients['bitcoin']['rpcuser'] = BTC_RPC_USER
        chainclients['bitcoin']['rpcpassword'] = BTC_RPC_PWD

    chainclients['monero']['walletsdir'] = os.getenv('XMR_WALLETS_DIR', chainclients['monero']['datadir'])

    if disable_coin != '':
        logger.info('Disabling coin: %s', disable_coin)
        if not os.path.exists(config_path):
            exitWithError('{} does not exist'.format(config_path))
        with open(config_path) as fs:
            settings = json.load(fs)

        if disable_coin not in settings['chainclients']:
            exitWithError('{} has not been prepared'.format(disable_coin))
        settings['chainclients'][disable_coin]['connection_type'] = 'none'
        settings['chainclients'][disable_coin]['manage_daemon'] = False

        with open(config_path, 'w') as fp:
            json.dump(settings, fp, indent=4)

        logger.info('Done.')
        return 0

    if add_coin != '':
        logger.info('Adding coin: %s', add_coin)
        if not os.path.exists(config_path):
            exitWithError('{} does not exist'.format(config_path))
        with open(config_path) as fs:
            settings = json.load(fs)

        if add_coin in settings['chainclients']:
            coin_settings = settings['chainclients'][add_coin]
            if coin_settings['connection_type'] == 'none' and coin_settings['manage_daemon'] is False:
                logger.info('Enabling coin: %s', add_coin)
                coin_settings['connection_type'] = 'rpc'
                coin_settings['manage_daemon'] = True
                with open(config_path, 'w') as fp:
                    json.dump(settings, fp, indent=4)
                logger.info('Done.')
                return 0
            exitWithError('{} is already in the settings file'.format(add_coin))

        settings['chainclients'][add_coin] = chainclients[add_coin]

        if not no_cores:
            prepareCore(add_coin, known_coins[add_coin], settings, data_dir)

        if not prepare_bin_only:
            prepareDataDir(add_coin, settings, chain, particl_wallet_mnemonic, use_containers=use_containers)
            with open(config_path, 'w') as fp:
                json.dump(settings, fp, indent=4)

        logger.info('Done.')
        return 0

    logger.info('With coins: %s', ', '.join(with_coins))
    if os.path.exists(config_path):
        if not prepare_bin_only:
            exitWithError('{} exists'.format(config_path))
        else:
            with open(config_path) as fs:
                settings = json.load(fs)
    else:
        for c in with_coins:
            withchainclients[c] = chainclients[c]

        settings = {
            'debug': True,
            'zmqhost': 'tcp://127.0.0.1',
            'zmqport': PART_ZMQ_PORT + port_offset,
            'htmlhost': htmlhost,
            'htmlport': UI_HTML_PORT + port_offset,
            'network_key': '7sW2UEcHXvuqEjkpE5mD584zRaQYs6WXYohue4jLFZPTvMSxwvgs',
            'network_pubkey': '035758c4a22d7dd59165db02a56156e790224361eb3191f02197addcb3bde903d2',
            'chainclients': withchainclients,
            'min_delay_event': 5,  # Min delay in seconds before reacting to an event
            'max_delay_event': 50,  # Max delay in seconds before reacting to an event
            'check_progress_seconds': 60,
            'check_watched_seconds': 60,
            'check_expired_seconds': 60
        }

    if not no_cores:
        for c in with_coins:
            prepareCore(c, known_coins[c], settings, data_dir)

    if prepare_bin_only:
        logger.info('Done.')
        return 0

    for c in with_coins:
        prepareDataDir(c, settings, chain, particl_wallet_mnemonic, use_containers=use_containers)

    with open(config_path, 'w') as fp:
        json.dump(settings, fp, indent=4)

    if particl_wallet_mnemonic == 'none':
        logger.info('Done.')
        return 0

    logger.info('Loading Particl mnemonic')

    particl_settings = settings['chainclients']['particl']
    partRpc = make_rpc_func(particl_settings['bindir'], particl_settings['datadir'], chain)

    daemons = []
    daemons.append(startDaemon(particl_settings['datadir'], particl_settings['bindir'], cfg.PARTICLD, ['-noconnect', '-nofindpeers', '-nostaking', '-nodnsseed', '-nolisten']))
    try:
        waitForRPC(partRpc)

        if particl_wallet_mnemonic is None:
            particl_wallet_mnemonic = partRpc('mnemonic new')['mnemonic']
        partRpc('extkeyimportmaster "{}"'.format(particl_wallet_mnemonic))

        # Initialise wallets
        with open(os.path.join(data_dir, 'basicswap.log'), 'a') as fp:
            swap_client = BasicSwap(fp, data_dir, settings, chain)

            swap_client.setCoinConnectParams(Coins.PART)
            swap_client.setDaemonPID(Coins.PART, daemons[-1].pid)
            swap_client.setCoinRunParams(Coins.PART)
            swap_client.createCoinInterface(Coins.PART)

            for coin_name in with_coins:
                coin_settings = settings['chainclients'][coin_name]
                c = swap_client.getCoinIdFromName(coin_name)
                if c == Coins.PART:
                    continue

                swap_client.setCoinConnectParams(c)

                if c == Coins.XMR:
                    if not coin_settings['manage_wallet_daemon']:
                        continue
                    daemons.append(startXmrWalletDaemon(coin_settings['datadir'], coin_settings['bindir'], 'monero-wallet-rpc'))
                else:
                    if not coin_settings['manage_daemon']:
                        continue
                    filename = coin_name + 'd' + ('.exe' if os.name == 'nt' else '')
                    daemons.append(startDaemon(coin_settings['datadir'], coin_settings['bindir'], filename, ['-noconnect', '-nodnsseed', '-nolisten']))
                swap_client.setDaemonPID(c, daemons[-1].pid)
                swap_client.setCoinRunParams(c)
                swap_client.createCoinInterface(c)
                swap_client.waitForDaemonRPC(c)
                swap_client.initialiseWallet(c)
            swap_client.finalise()
            del swap_client
    finally:
        for d in daemons:
            logging.info('Interrupting {}'.format(d.pid))
            d.send_signal(signal.SIGINT)
            d.wait(timeout=120)
            for fp in (d.stdout, d.stderr, d.stdin):
                if fp:
                    fp.close()

    logger.info('IMPORTANT - Save your particl wallet recovery phrase:\n{}\n'.format(particl_wallet_mnemonic))
    logger.info('Done.')


if __name__ == '__main__':
    main()