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

import os
import hashlib
import secrets

from basicswap.contrib.ellipticcurve import CurveFp, Point, INFINITY, jacobi_symbol
from . import i2b


class ECCParameters():
    def __init__(self, p, a, b, Gx, Gy, o):
        self.p = p
        self.a = a
        self.b = b
        self.Gx = Gx
        self.Gy = Gy
        self.o = o


ep = ECCParameters(
    p=0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f,
    a=0x0,
    b=0x7,
    Gx=0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,
    Gy=0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8,
    o=0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141)


curve_secp256k1 = CurveFp(ep.p, ep.a, ep.b)
G = Point(curve_secp256k1, ep.Gx, ep.Gy, ep.o)
SECP256K1_ORDER_HALF = ep.o // 2


def ToDER(P) -> bytes:
    return bytes((4, )) + int(P.x()).to_bytes(32, byteorder='big') + int(P.y()).to_bytes(32, byteorder='big')


def getSecretBytes() -> bytes:
    i = 1 + secrets.randbelow(ep.o - 1)
    return i2b(i)


def getSecretInt() -> int:
    return 1 + secrets.randbelow(ep.o - 1)


def getInsecureBytes() -> bytes:
    while True:
        s = os.urandom(32)

        s_test = int.from_bytes(s, byteorder='big')
        if s_test > 1 and s_test < ep.o:
            return s


def getInsecureInt() -> int:
    while True:
        s = os.urandom(32)

        s_test = int.from_bytes(s, byteorder='big')
        if s_test > 1 and s_test < ep.o:
            return s_test


def powMod(x, y, z) -> int:
    # Calculate (x ** y) % z efficiently.
    number = 1
    while y:
        if y & 1:
            number = number * x % z
        y >>= 1  # y //= 2

        x = x * x % z
    return number


def ExpandPoint(xb, sign):
    x = int.from_bytes(xb, byteorder='big')
    a = (powMod(x, 3, ep.p) + 7) % ep.p
    y = powMod(a, (ep.p + 1) // 4, ep.p)

    if sign:
        y = ep.p - y
    return Point(curve_secp256k1, x, y, ep.o)


def CPKToPoint(cpk):
    y_parity = cpk[0] - 2

    x = int.from_bytes(cpk[1:], byteorder='big')
    a = (powMod(x, 3, ep.p) + 7) % ep.p
    y = powMod(a, (ep.p + 1) // 4, ep.p)

    if y % 2 != y_parity:
        y = ep.p - y

    return Point(curve_secp256k1, x, y, ep.o)


def pointToCPK2(point, ind=0x09):
    # The function is_square(x), where x is an integer, returns whether or not x is a quadratic residue modulo p. Since p is prime, it is equivalent to the Legendre symbol (x / p) = x(p-1)/2 mod p being equal to 1[8].
    ind = bytes((ind ^ (1 if jacobi_symbol(point.y(), ep.p) == 1 else 0),))
    return ind + point.x().to_bytes(32, byteorder='big')


def pointToCPK(point):

    y = point.y().to_bytes(32, byteorder='big')
    ind = bytes((0x03,)) if y[31] % 2 else bytes((0x02,))

    cpk = ind + point.x().to_bytes(32, byteorder='big')
    return cpk


def secretToCPK(secret):
    secretInt = secret if isinstance(secret, int) \
        else int.from_bytes(secret, byteorder='big')

    R = G * secretInt

    Y = R.y().to_bytes(32, byteorder='big')
    ind = bytes((0x03,)) if Y[31] % 2 else bytes((0x02,))

    pubkey = ind + R.x().to_bytes(32, byteorder='big')

    return pubkey


def getKeypair():
    secretBytes = getSecretBytes()
    return secretBytes, secretToCPK(secretBytes)


def hashToCurve(pubkey):

    xBytes = hashlib.sha256(pubkey).digest()
    x = int.from_bytes(xBytes, byteorder='big')

    for k in range(0, 100):
        # get matching y element for point
        y_parity = 0  # always pick 0,
        a = (powMod(x, 3, ep.p) + 7) % ep.p
        y = powMod(a, (ep.p + 1) // 4, ep.p)

        # print("before parity %x" % (y))
        if y % 2 != y_parity:
            y = ep.p - y

        # If x is always mod P, can R ever not be on the curve?
        try:
            R = Point(curve_secp256k1, x, y, ep.o)
        except Exception:
            x = (x + 1) % ep.p  # % P?
            continue

        if R == INFINITY or R * ep.o != INFINITY:  # is R * O != INFINITY check necessary?  Validation of Elliptic Curve Public Keys says no if cofactor = 1
            x = (x + 1) % ep.p  # % P?
            continue
        return R

    raise ValueError('hashToCurve failed for 100 tries')


def hash256(inb):
    return hashlib.sha256(inb).digest()


def testEccUtils():
    print('testEccUtils()')

    G_enc = ToDER(G)
    assert (G_enc.hex() == '0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8')

    G_enc = pointToCPK(G)
    assert (G_enc.hex() == '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798')
    G_dec = CPKToPoint(G_enc)
    assert (G_dec == G)

    G_enc = pointToCPK2(G)
    assert (G_enc.hex() == '0879be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798')

    H = hashToCurve(ToDER(G))
    assert (pointToCPK(H).hex() == '0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0')

    print('Passed.')


if __name__ == "__main__":
    testEccUtils()
