#!/usr/bin/env python3

from Cryptotools.Groups.elliptic import Point
import numpy as np
from math import sqrt


class Curve:
    # Curve
    WEIERSTRASS = 0
    MONTGOMERY = 1


    def __init__(self, a, b, t):
        self._a = a
        self._b = b

        if t not in (Curve.WEIERSTRASS, Curve.MONTGOMERY):
            raise Exception(f"The type of the curve is not recognized")

        self._type = t
        self._xtmp = np.linspace(-5, 5, 500).tolist()
        self._x = list()
        self._y = list()
        self._yn = list()
        self._points = list()
        self._pointsSym = list()

    def f(self, x):
        if self._type == Curve.WEIERSTRASS:
            y = pow(x, 3) + (self._a * x) + self._b
        if self._type == Curve.MONTGOMERY:
            y = pow(x, 3) + (3 * pow(x, 2)) + x
        if y > 0:
            return sqrt(y)
        return None

    def generatePoints(self):
        for x in self._xtmp:
            y = self.f(x)
            if y is None:
                continue

            self._x.append(x)
            self._y.append(y)
            self._yn.append(-y)
            self._points.append(Point(
                x,
                y
            ))
            self._pointsSym.append(Point(
                x,
                -y 
            ))

    @property
    def x(self):
        return self._x

    @property
    def y(self):
        return self._y

    @property
    def yn(self):
        return self._yn

    def getPoints(self):
        return self._points

    def getPointsSym(self):
        return self._pointsSym

    def add(self, P, Q) -> Point:
        """
            This function operathe addition operation on two points P and Q

            Args:
                P (Object): The first Point on the curve
                Q (Object): The second Point on the curve

            Returns:
                Return the Point object R
        """

        ## Check if P or Q are infinity
        if (P.x, P.y) == (0, 0) and (Q.x, Q.y) == (0, 0):
            return Point(0, 0)
        elif (P.x, P.y) == (0, 0):
            return Point(Q.x, Q.y)
        elif (Q.x, Q.y) == (0, 0):
            return Point(P.x, P.y)

        # point doubling
        if P.x == Q.x:
            # Infinity
            if P.y != Q.y or Q.y == 0:
                return Point(0, 0)

            # Point doubling
            try:
                inv = pow(2 * P.y, -1); # It's working with the inverse modular, WHY ???
                m = ((3 * pow(P.x, 2)) + self._a) * inv 
            except ValueError:
                return Point(0, 0)

        else:
            try:
                inv = pow(Q.x - P.x, -1)
                m = (Q.y - P.y) * inv
            except ValueError:
                # May call this Exception: base is not invertible for the given modulus
                # I return an Infinity point until I fixed that
                return Point(0, 0)

        xr = (pow(m, 2) - P.x - Q.x)
        
        yr = (m * (P.x - xr)) - P.y
        return Point(xr, yr)

    def scalar(self, P, n) -> Point:
        """
            This function compute a Scalar Multiplication of P, n time. This algorithm is also known as Double and Add.

            Args:
                P (point): the Point to multiplication
                n (Integer): multiplicate n time P

            Returns:
                Return the result of the Scalar multiplication
        """
        binary = bin(n)[2:]
        binary = binary[::-1] # We need to reverse the binary
        
        nP = Point(0, 0)
        Rtmp = P

        for b in binary:
            if b == '1':
                nP = self.add(nP, Rtmp)
            Rtmp = self.add(Rtmp, Rtmp)  # Double P
             
        return nP

    def find_reverse(self, P):
        """
            This function return the reverse of the Point P
            Args:
                P (Point): Point object to find 

            Returns:
                Return the object Pr, which is the reverse point of P
        """
        Pr = None
        for p in self._pointsSym:
             if P.x == p.x and -P.y == p.y:
                 Pr = Point(p.x, p.y)
                 break
        return Pr
