# -*- coding: utf-8 -*-

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


import json
import time
import decimal


COIN = 100000000


decimal_ctx = decimal.Context()
decimal_ctx.prec = 20


class TemporaryError(ValueError):
    pass


class AutomationConstraint(ValueError):
    pass


class InactiveCoin(Exception):
    def __init__(self, coinid):
        self.coinid = coinid

    def __str__(self):
        return str(self.coinid)


class LockedCoinError(Exception):
    def __init__(self, coinid):
        self.coinid = coinid

    def __str__(self):
        return 'Coin must be unlocked: ' + str(self.coinid)


def ensure(v, err_string):
    if not v:
        raise ValueError(err_string)


def toBool(s) -> bool:
    if isinstance(s, bool):
        return s
    return s.lower() in ['1', 'true']


def jsonDecimal(obj):
    if isinstance(obj, decimal.Decimal):
        return str(obj)
    raise TypeError


def dumpj(jin, indent=4):
    return json.dumps(jin, indent=indent, default=jsonDecimal)


def dumpje(jin):
    return json.dumps(jin, default=jsonDecimal).replace('"', '\\"')


def SerialiseNum(n: int) -> bytes:
    if n == 0:
        return bytes((0x00,))
    if n > 0 and n <= 16:
        return bytes((0x50 + n,))
    rv = bytearray()
    neg = n < 0
    absvalue = -n if neg else n
    while (absvalue):
        rv.append(absvalue & 0xff)
        absvalue >>= 8
    if rv[-1] & 0x80:
        rv.append(0x80 if neg else 0)
    elif neg:
        rv[-1] |= 0x80
    return bytes((len(rv),)) + rv


def DeserialiseNum(b: bytes, o: int = 0) -> int:
    if b[o] == 0:
        return 0
    if b[o] > 0x50 and b[o] <= 0x50 + 16:
        return b[o] - 0x50
    v = 0
    nb = b[o]
    o += 1
    for i in range(0, nb):
        v |= b[o + i] << (8 * i)
    # If the input vector's most significant byte is 0x80, remove it from the result's msb and return a negative.
    if b[o + nb - 1] & 0x80:
        return -(v & ~(0x80 << (8 * (nb - 1))))
    return v


def float_to_str(f: float) -> str:
    # stackoverflow.com/questions/38847690
    d1 = decimal_ctx.create_decimal(repr(f))
    return format(d1, 'f')


def make_int(v, scale: int = 8, r: int = 0) -> int:  # r = 0, no rounding (fail), r > 0 round off, r < 0 floor
    if isinstance(v, float):
        v = float_to_str(v)
    elif isinstance(v, int):
        return v * 10 ** scale

    sign = 1
    if v[0] == '-':
        v = v[1:]
        sign = -1
    ep = 10 ** scale
    have_dp = False
    rv = 0
    for c in v:
        if c == '.':
            rv *= ep
            have_dp = True
            continue
        if not c.isdigit():
            raise ValueError('Invalid char: ' + c)
        if have_dp:
            ep //= 10
            if ep <= 0:
                if r == 0:
                    raise ValueError('Mantissa too long')
                if r > 0:
                    # Round off
                    if int(c) > 4:
                        rv += 1
                break
            rv += ep * int(c)
        else:
            rv = rv * 10 + int(c)
    if not have_dp:
        rv *= ep
    return rv * sign


def validate_amount(amount, scale: int = 8) -> bool:
    str_amount = float_to_str(amount) if isinstance(amount, float) else str(amount)
    has_decimal = False
    for c in str_amount:
        if c == '.' and not has_decimal:
            has_decimal = True
            continue
        if not c.isdigit():
            raise ValueError('Invalid amount')

    ar = str_amount.split('.')
    if len(ar) > 1 and len(ar[1]) > scale:
        raise ValueError('Too many decimal places in amount {}'.format(str_amount))
    return True


def format_amount(i: int, display_scale: int, scale: int = None) -> str:
    if not isinstance(i, int):
        raise ValueError('Amount must be an integer.')  # Raise error instead of converting as amounts should always be integers
    if scale is None:
        scale = display_scale
    ep = 10 ** scale
    n = abs(i)
    quotient = n // ep
    remainder = n % ep
    if display_scale != scale:
        remainder %= (10 ** display_scale)
    rv = '{}.{:0>{scale}}'.format(quotient, remainder, scale=display_scale)
    if i < 0:
        rv = '-' + rv
    return rv


def format_timestamp(value: int, with_seconds: bool = False) -> str:
    str_format = '%Y-%m-%d %H:%M'
    if with_seconds:
        str_format += ':%S'
    str_format += ' %z'
    return time.strftime(str_format, time.localtime(value))


def b2i(b: bytes) -> int:
    # bytes32ToInt
    return int.from_bytes(b, byteorder='big')


def i2b(i: int) -> bytes:
    # intToBytes32
    return i.to_bytes(32, byteorder='big')


def b2h(b: bytes) -> str:
    return b.hex()


def h2b(h: str) -> bytes:
    if h.startswith('0x'):
        h = h[2:]
    return bytes.fromhex(h)


def i2h(x: int) -> str:
    return b2h(i2b(x))


def zeroIfNone(value) -> int:
    if value is None:
        return 0
    return value
