module ToyECDSA

using SHA

import Base.in, Base.==, Base.+, Base.*

export ECDSA_Key, ECDSA_Public_Key, genkey, ECDSA_sign, isverifiedECDSA

# T will be BigInt in most applications
struct CurveFP{T}
    p::T
    a::T
    b::T
    CurveFP(p, a::T, b::T) where T <: Number = new{T}(p, a, b)
end

struct PointEC{T}
    curve::CurveFP{T}
    x::T
    y::T
    order::Union{Number, Nothing}
    function PointEC(curve, x::T, y::T, order=nothing) where T <: Number
        @assert((x, y) in curve)
        new{T}(curve, x, y, order)
    end
end

struct PointINF end
const INFINITY = PointINF()

function ==(point_a::PointEC, point_b::PointEC)
    if point_a.curve == point_b.curve && point_a.x == point_b.x && point_a.y == point_b.y
        return true
    end
    return false
end

function ==(curve_a::CurveFP, curve_b::CurveFP)
    if curve_a.a == curve_b.a && curve_a.b == curve_b.b && curve_a.p == curve_b.p
        return true
    end
    return false
end

+(point_a::PointINF, point_b::PointINF) = point_b
+(point_a::PointINF, point_b::PointEC) = point_b
+(point_a::PointEC, point_b::PointINF) = point_a

function +(point_a::PointEC, point_b::PointEC)
     @assert(point_a.curve == point_b.curve)
     if point_a.x == point_b.x
         if (point_a.y + point_b.y) % point_a.curve.p == 0
             return INFINITY
         else
             return double(point_a)
        end
    end
    p = point_a.curve.p
    λ = (point_a.y - point_b.y) * invmod(point_a.x - point_b.x, p)
    xr = mod(λ * λ - point_a.x - point_b.x, p)
    yr = mod(λ * (point_a.x - xr) - point_a.y, p)
    return PointEC(point_a.curve, xr, yr, point_a.order)
end

*(point_a::PointINF, int_b::Number) = INFINITY
*(int_b::Number, point_a::PointINF) = INFINITY
*(int_b::Number, point_a::PointEC) = point_a * int_b

function *(point_a::PointEC, int_b::Number)
    leftmost_bit(x) = big"2"^Int(trunc(log(2, x)))
    if point_a.order != nothing
        int_b %= point_a.order
    end
    if int_b == 0
        return INFINITY
    end
    int_3b = 3 * int_b
    negative_a = PointEC(point_a.curve, point_a.x, -point_a.y, point_a.order)
    i = BigInt(leftmost_bit(int_3b) ÷ 2)
    result = point_a
    while i > 1
        result = double(result)
        if (int_3b & i) != 0 && (int_b & i) == 0
            result += point_a
        end
        if (int_3b & i) == 0 && (int_b & i) != 0
            result += negative_a
        end
        i ÷= 2
    end
    return result
end

in(z::Tuple, curve::CurveFP) = (z[2]^2 - (z[1]^3 + curve.a*z[1] + curve.b)) % curve.p == 0
in(x::Number,y::Number, curve::CurveFP) = (y^2 -(x^3 + curve.a*x + curve.b)) % curve.p == 0
in(p::PointEC, curve::CurveFP) = (p.y^2 - (p.x^3 + curve.a * p.x + curve.b)) % curve.p == 0
in(point::PointINF, curve::CurveFP) = true

double(point_a::PointINF) = INFINITY

function double(point_a::PointEC)
  a, p = point_a.curve.a, point_a.curve.p
  l = mod((3 * point_a.x^2 + a) * invmod(2 * point_a.y, p), p)
  x3 = mod(l^2 - 2 * point_a.x, p)
  y3 = mod(l * (point_a.x - x3) - point_a.y, p)
  return PointEC(point_a.curve, x3, y3, point_a.order)
end

const secp256k1 = (  # use the Bitcoin ECDSA curve
    p = big"0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F",
    a = big"0x0",
    b = big"0x7",
    r = big"0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141",
    Gx = big"0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798",
    Gy = big"0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8",
)
@assert((secp256k1.Gy^2 - secp256k1.Gx^3 - 7) % secp256k1.p == 0)

function generatemultiplier(stdcurve)
    return foldl((x, y) -> 16 * BigInt(x) + y, rand(0:16, ndigits(stdcurve.r - 1, base=16)))
end

struct ECDSA_Key
    E::CurveFP
    secret::BigInt
    G::PointEC
    r::BigInt
    W::PointEC
end

struct ECDSA_Public_Key
    E::CurveFP
    G::PointEC
    r::BigInt
    W::PointEC
end

function genkey(curve=secp256k1) # default: use Bitcoin standard EC curve secp256k1
    E = CurveFP(curve.p, curve.a, curve.b)
    s = generatemultiplier(curve)
    G = PointEC(E, curve.Gx, curve.Gy, curve.r)
    W = s * G
    return ECDSA_Key(E, s, G, curve.r, W)
end

aspublickey(k::ECDSA_Key) = ECDSA_Public_Key(k.E, k.G, k.r, k.W)
privatekey(k::ECDSA_Key) = k.secret

function ECDSA_sign(m::String, key::ECDSA_Key, digestfunction=sha256)
    r, f = key.r, digestfunction(codeunits(m)) # f = H(m)
    # order of curve points length must be >= sha digest length (in bytes)
    @assert(ndigits(r, base=16) >= length(f))
    c, d, bindigest = BigInt(0), BigInt(0), foldl((x, y) -> 16 * BigInt(x) + y, f)
    while c == 0 || d == 0
        u = generatemultiplier(secp256k1)
        V = u * key.G
        c = mod(V.x, r)
        d = mod((invmod(u, r) * (bindigest + key.secret * c)), r)
    end
    return aspublickey(key), c, d
end

function isverifiedECDSA(m::String, publickey, c, d, digestfunction=sha256)
    if 1 <= c < publickey.r && 1 <= d < publickey.r
        r, f = publickey.r, digestfunction(codeunits(m))
        h, bindigest = invmod(d, r), foldl((x, y) -> 16 * BigInt(x) + y, f)
        h1, h2 = mod(bindigest * h, r), mod(c * h, r)
        verifierpoint = h1 * publickey.G + h2 * publickey.W
        return mod(verifierpoint.x, r) == c
    end
    return false
end

end  # module
