#!/usr/bin/env python3
#
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
# Copyright (C)2013-2024 The MMGen Project <mmgen@tuta.io>
# Licensed under the GNU General Public License, Version 3:
#   https://www.gnu.org/licenses
# Public project repositories:
#   https://github.com/mmgen/mmgen-wallet
#   https://gitlab.com/mmgen/mmgen-wallet

"""
altcointest.py - Test constants for Bitcoin-derived altcoins
"""

import sys

try:
	from include import test_init
except ImportError:
	from test.include import test_init

from mmgen.cfg import gc,Config
from mmgen.util import msg
from mmgen.altcoin.params import CoinInfo

def test_equal(desc,a,b,*cdata):
	if type(a) is int:
		a = hex(a)
		b = hex(b)
	(network,coin,_,b_desc,verbose) = cdata
	if verbose:
		msg(f'  {desc:20}: {a!r}')
	if a != b:
		raise ValueError(
			f'{desc.capitalize()}s for {coin.upper()} {network} do not match:\n  CoinInfo: {a}\n  {b_desc}: {b}' )

class TestCoinInfo(CoinInfo):

	# Sources (see CoinInfo) that are in agreement for these coins
	# No check for segwit, p2sh check skipped if source doesn't support it
	cross_checks = {
		'2GIVE':  ['wn'],
		'42':     ['vg','wn'],
		'611':    ['wn'],
		'AC':     ['lb','vg'],
		'ACOIN':  ['wn'],
		'ALF':    ['wn'],
		'ANC':    ['vg','wn'],
		'APEX':   ['wn'],
		'ARCO':   ['wn'],
		'ARG':    ['pc'],
		'AUR':    ['vg','wn'],
		'BCH':    ['wn'],
		'BLK':    ['lb','vg','wn'],
		'BQC':    ['vg','wn'],
		'BSTY':   ['wn'],
		'BTC':    ['lb','vg','wn'],
		'BTCD':   ['lb','vg','wn'],
		'BUCKS':  ['wn'],
		'CASH':   ['wn'],
		'CBX':    ['wn'],
		'CCN':    ['lb','vg','wn'],
		'CDN':    ['lb','vg','wn'],
		'CHC':    ['wn'],
		'CLAM':   ['lb','vg'],
		'CON':    ['vg','wn'],
		'CPC':    ['wn'],
		'DASH':   ['lb','pc','vg','wn'],
		'DCR':    ['pc'],
		'DFC':    ['pc'],
		'DGB':    ['lb','vg'],
		'DGC':    ['lb','vg','wn'],
		'DOGE':   ['lb','pc','vg','wn'],
		'DOGED':  ['lb','vg','wn'],
		'DOPE':   ['lb','vg'],
		'DVC':    ['vg','wn'],
		'EFL':    ['lb','vg','wn'],
		'EMC':    ['vg'],
		'EMD':    ['wn'],
		'ESP':    ['wn'],
		'FAI':    ['pc'],
		'FC2':    ['wn'],
		'FIBRE':  ['wn'],
		'FJC':    ['wn'],
		'FLO':    ['wn'],
		'FLT':    ['wn'],
		'FST':    ['wn'],
		'FTC':    ['lb','pc','vg','wn'],
		'GCR':    ['lb','vg'],
		'GOOD':   ['wn'],
		'GRC':    ['vg','wn'],
		'GUN':    ['vg','wn'],
		'HAM':    ['vg','wn'],
		'HTML5':  ['wn'],
		'HYP':    ['wn'],
		'ICASH':  ['wn'],
		'INFX':   ['wn'],
		'IPC':    ['wn'],
		'JBS':    ['lb','pc','vg','wn'],
		'JUDGE':  ['wn'],
		'LANA':   ['wn'],
		'LAT':    ['wn'],
		'LDOGE':  ['wn'],
		'LMC':    ['wn'],
		'LTC':    ['lb','vg','wn'],
		'MARS':   ['wn'],
		'MEC':    ['pc','wn'],
		'MINT':   ['wn'],
		'MOBI':   ['wn'],
		'MONA':   ['lb','vg'],
		'MOON':   ['wn'],
		'MUE':    ['lb','vg'],
		'MXT':    ['wn'],
		'MYR':    ['pc'],
		'MYRIAD': ['vg','wn'],
		'MZC':    ['lb','pc','vg','wn'],
		'NEOS':   ['lb','vg'],
		'NEVA':   ['wn'],
		'NKA':    ['wn'],
		'NLG':    ['vg','wn'],
		'NMC':    ['lb','vg'],
		'NVC':    ['lb','vg','wn'],
		'OK':     ['lb','vg'],
		'OMC':    ['vg','wn'],
		'ONION':  ['vg','wn'],
		'PART':   ['wn'],
		'PINK':   ['vg','wn'],
		'PIVX':   ['wn'],
		'PKB':    ['lb','vg','wn'],
		'PND':    ['lb','vg','wn'],
		'POT':    ['lb','vg','wn'],
		'PPC':    ['lb','vg','wn'],
		'PTC':    ['vg','wn'],
		'PXC':    ['wn'],
		'QRK':    ['wn'],
		'RAIN':   ['wn'],
		'RBT':    ['wn'],
		'RBY':    ['lb','vg'],
		'RDD':    ['vg','wn'],
		'RIC':    ['pc','vg','wn'],
		'SDC':    ['lb','vg'],
		'SIB':    ['wn'],
		'SMLY':   ['wn'],
		'SONG':   ['wn'],
		'SPR':    ['vg','wn'],
		'START':  ['lb','vg'],
		'SYS':    ['wn'],
		'TAJ':    ['wn'],
		'TIT':    ['wn'],
		'TPC':    ['lb','vg'],
		'TRC':    ['wn'],
		'TTC':    ['wn'],
		'TX':     ['wn'],
		'UNO':    ['pc','vg','wn'],
		'VIA':    ['lb','pc','vg','wn'],
		'VPN':    ['lb','vg'],
		'VTC':    ['lb','vg','wn'],
		'WDC':    ['vg','wn'],
		'WISC':   ['wn'],
		'WKC':    ['vg','wn'],
		'WSX':    ['wn'],
		'XCN':    ['wn'],
		'XGB':    ['wn'],
		'XPM':    ['lb','vg','wn'],
		'XST':    ['wn'],
		'XVC':    ['wn'],
		'ZET':    ['wn'],
		'ZOOM':   ['lb','vg'],
		'ZRC':    ['lb','vg']
	}

	@classmethod
	def verify_leading_symbols(cls,quiet=False,verbose=False):

		for network in ('mainnet','testnet'):
			for coin in [e.symbol for e in cls.coin_constants[network]]:
				e = cls.get_entry(coin,network)
				cdata = (network,coin,e,'Computed value',verbose)

				if not quiet:
					msg(f'{coin} {network}')

				vn_info = e.p2pkh_info
				ret = cls.find_addr_leading_symbol(vn_info[0])
				test_equal('P2PKH leading symbol',vn_info[1],ret,*cdata)

				vn_info = e.p2sh_info
				if vn_info:
					ret = cls.find_addr_leading_symbol(vn_info[0])
					test_equal('P2SH leading symbol',vn_info[1],ret,*cdata)

	@classmethod
	def verify_core_coin_data(cls,cfg,quiet=False,verbose=False):
		from mmgen.protocol import CoinProtocol,init_proto

		for network in ('mainnet','testnet'):
			for coin in gc.core_coins:
				e = cls.get_entry(coin,network)
				if e:
					proto = init_proto( cfg, coin, network=network )
					cdata = (network,coin,e,type(proto).__name__,verbose)
					if not quiet:
						msg(f'Verifying {coin.upper()} {network}')

					if coin != 'bch': # TODO
						test_equal('coin name',e.name,proto.name,*cdata)

					if e.trust_level != -1:
						test_equal('Trust level',e.trust_level,CoinProtocol.coins[coin].trust_level,*cdata)

					test_equal(
						'WIF version number',
						e.wif_ver_num,
						int.from_bytes(proto.wif_ver_bytes['std'],'big'),
						*cdata )

					test_equal(
						'P2PKH version number',
						e.p2pkh_info[0],
						int.from_bytes(proto.addr_fmt_to_ver_bytes['p2pkh'],'big'),
						*cdata )

					test_equal(
						'P2SH version number',
						e.p2sh_info[0],
						int.from_bytes(proto.addr_fmt_to_ver_bytes['p2sh'],'big'),
						*cdata )

	# Data is one of the coin_constants lists above.  Normalize ints to hex of correct width, add
	# missing leading letters, set trust level from external_tests.
	# Insert a coin entry from outside source, set version info leading letters to '?' and trust level
	# to 0, then run TestCoinInfo.fix_table(data).  'has_segwit' field is updated manually for now.
	@classmethod
	def fix_table(cls,data):
		import re

		def myhex(n):
			return '0x{:0{}x}'.format(n,2 if n < 256 else 4)

		def fix_ver_info(e,k):
			e[k] = list(e[k])
			e[k][0] = myhex(e[k][0])
			s1 = cls.find_addr_leading_symbol(int(e[k][0][2:],16))
			m = f'Fixing leading address letter for coin {e["symbol"]} ({e[k][1]!r} --> {s1})'
			if e[k][1] != '?':
				assert s1 == e[k][1], f'First letters do not match! {m}'
			else:
				msg(m)
				e[k][1] = s1
			e[k] = tuple(e[k])

		old_sym = None
		for sym in sorted([e.symbol for e in data]):
			if sym == old_sym:
				msg(f'{sym!r}: duplicate coin symbol in data!')
				sys.exit(2)
			old_sym = sym

		tt = cls.create_trust_table()

		name_w = max(len(e.name) for e in data)
		fs = '\t({:%s} {:10} {:7} {:17} {:17} {:6} {}),' % (name_w+3)
		for e in data:
			e = e._asdict()
			e['wif_ver_num'] = myhex(e['wif_ver_num'])
			sym,trust = e['symbol'],e['trust_level']

			fix_ver_info(e,'p2pkh_info')
			if isinstance(e['p2sh_info'],tuple):
				fix_ver_info(e,'p2sh_info')

			for k in e.keys():
				e[k] = repr(e[k])
				e[k] = re.sub(r"'0x(..)'",r'0x\1',e[k])
				e[k] = re.sub(r"'0x(....)'",r'0x\1',e[k])
				e[k] = re.sub(r' ',r'',e[k]) + ('',',')[k != 'trust_level']

			if trust != -1:
				if sym in tt:
					src = tt[sym]
					if src != trust:
						msg(f'Updating trust for coin {sym!r}: {trust} -> {src}')
						e['trust_level'] = src
				else:
					if trust != 0:
						msg(f'Downgrading trust for coin {sym!r}: {trust} -> 0')
						e['trust_level'] = 0

				if sym in cls.cross_checks:
					if int(e['trust_level']) == 0 and len(cls.cross_checks[sym]) > 1:
						msg(f'Upgrading trust for coin {sym!r}: {e["trust_level"]} -> 1')
						e['trust_level'] = 1

			print(fs.format(*e.values()))
		msg(f'Processed {len(data)} entries')

	@classmethod
	def find_addr_leading_symbol(cls,ver_num,verbose=False):

		if ver_num == 0:
			return '1'

		def phash2addr(ver_num,pk_hash):
			from mmgen.proto.btc.common import b58chk_encode
			bl = ver_num.bit_length()
			ver_bytes = int.to_bytes(ver_num,bl//8 + bool(bl%8),'big')
			return b58chk_encode(ver_bytes + pk_hash)

		low = phash2addr(ver_num,b'\x00'*20)
		high = phash2addr(ver_num,b'\xff'*20)

		if verbose:
			print('low address:  ' + low)
			print('high address: ' + high)

		l1,h1 = low[0],high[0]
		return (l1,h1) if l1 != h1 else l1

	@classmethod
	def print_symbols(cls,include_names=False,reverse=False):
		for e in cls.coin_constants['mainnet']:
			if reverse:
				print(f'{e.symbol:6} {e.name}')
			else:
				name_w = max(len(e.name) for e in cls.coin_constants['mainnet'])
				print((f'{e.name:{name_w}} ' if include_names else '') + e.symbol)

	@classmethod
	def create_trust_table(cls):
		tt = {}
		mn = cls.external_tests['mainnet']
		for ext_prog in mn:
			assert len(set(mn[ext_prog])) == len(mn[ext_prog]), f'Duplicate entry in {ext_prog!r}!'
			for coin in mn[ext_prog]:
				if coin in tt:
					tt[coin] += 1
				else:
					tt[coin] = 1
		for k in cls.trust_override:
			tt[k] = cls.trust_override[k]
		return tt

	trust_override = {'BTC':3,'BCH':3,'LTC':3,'DASH':1,'EMC':2}

	@classmethod
	def get_test_support(cls,coin,addr_type,network,toolname=None,verbose=False):
		"""
		If requested tool supports coin/addr_type/network triplet, return tool name.
		If 'tool' is None, return tool that supports coin/addr_type/network triplet.
		Return None on failure.
		"""
		all_tools = [toolname] if toolname else list(cls.external_tests[network].keys())
		coin = coin.upper()

		for tool in all_tools:
			if coin in cls.external_tests[network][tool]:
				break
		else:
			if verbose:
				m1 = 'Requested tool {t!r} does not support coin {c} on network {n}'
				m2 = 'No test tool found for coin {c} on network {n}'
				msg((m1 if toolname else m2).format(t=tool,c=coin,n=network))
			return None

		if addr_type == 'zcash_z':
			if toolname in (None,'zcash-mini'):
				return 'zcash-mini'
			else:
				if verbose:
					msg(f"Address type {addr_type!r} supported only by tool 'zcash-mini'")
				return None

		try:
			bl = cls.external_tests_blacklist[addr_type][tool]
		except:
			pass
		else:
			if bl is True or coin in bl:
				if verbose:
					msg(f'Tool {tool!r} blacklisted for coin {coin}, addr_type {addr_type!r}')
				return None

		if toolname: # skip whitelists
			return tool

		if addr_type in ('segwit','bech32'):
			st = cls.external_tests_segwit_whitelist
			if addr_type in st and coin in st[addr_type]:
				return tool
			else:
				if verbose:
					m1 = 'Requested tool {t!r} does not support coin {c}, addr_type {a!r}, on network {n}'
					m2 = 'No test tool found supporting coin {c}, addr_type {a!r}, on network {n}'
					msg((m1 if toolname else m2).format(t=tool,c=coin,n=network,a=addr_type))
				return None

		return tool

	external_tests = {
		'mainnet': {
			# List in order of preference.
			# If 'tool' is not specified, the first tool supporting the coin will be selected.
			'pycoin': (
				'DASH', # only compressed
				'BCH',
				'BTC','LTC','VIA','FTC','DOGE','MEC',
				'JBS','MZC','RIC','DFC','FAI','ARG','ZEC','DCR'),
			'keyconv': (
				'BCH',
				# broken: PIVX
				'42','AC','AIB','ANC','ARS','ATMOS','AUR','BLK','BQC','BTC','TEST','BTCD','CCC','CCN','CDN',
				'CLAM','CNC','CNOTE','CON','CRW','DEEPONION','DGB','DGC','DMD','DOGED','DOGE','DOPE',
				'DVC','EFL','EMC','EXCL','FAIR','FLOZ','FTC','GAME','GAP','GCR','GRC','GRS','GUN','HAM','HODL',
				'IXC','JBS','LBRY','LEAF','LTC','MMC','MONA','MUE','MYRIAD','MZC','NEOS','NLG','NMC','NVC',
				'NYAN','OK','OMC','PIGGY','PINK','PKB','PND','POT','PPC','PTC','PTS','QTUM','RBY','RDD',
				'RIC','SCA','SDC','SKC','SPR','START','SXC','TPC','UIS','UNO','VIA','VPN','VTC','WDC','WKC',
				'WUBS', 'XC', 'XPM', 'YAC', 'ZOOM', 'ZRC'),
			'ethkey': ('ETH','ETC'),
			'zcash-mini': ('ZEC',),
			'monero-python': ('XMR',),
		},
		'testnet': {
			'pycoin': {
				'DASH':'tDASH', # only compressed
				'BCH':'XTN',
				'BTC':'XTN','LTC':'XLT','VIA':'TVI','FTC':'FTX','DOGE':'XDT','DCR':'DCRT'
				},
			'ethkey': {},
			'keyconv': {}
		}
	}
	external_tests_segwit_whitelist = {
		# Whitelists apply to the *first* tool in cls.external_tests supporting the given coin/addr_type.
		# They're ignored if specific tool is requested.
		'segwit': ('BTC',), # LTC Segwit broken on pycoin: uses old fmt
		'bech32': ('BTC','LTC'),
		'compressed': (
			'BTC','LTC','VIA','FTC','DOGE','DASH','MEC','MYR','UNO',
			'JBS','MZC','RIC','DFC','FAI','ARG','ZEC','DCR','ZEC'
		),
	}
	external_tests_blacklist = {
		# Unconditionally block testing of the given coin/addr_type with given tool, or all coins if True
		'legacy': {},
		'segwit': { 'keyconv': True },
		'bech32': { 'keyconv': True },
	}

if __name__ == '__main__':

	opts_data = {
		'text': {
			'desc': 'Check altcoin data',
			'usage':'[opts]',
			'options': '-q, --quiet    Be quieter\n-v, --verbose  Be more verbose'
		}
	}

	cfg = Config( opts_data=opts_data, need_amt=False )

	msg('Checking CoinInfo WIF/P2PKH/P2SH version numbers and trust levels against protocol.py')
	TestCoinInfo.verify_core_coin_data( cfg, cfg.quiet, cfg.verbose )

	msg('Checking CoinInfo address leading symbols')
	TestCoinInfo.verify_leading_symbols( cfg.quiet, cfg.verbose )