//
// Copyright Coinbase, Inc. All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

package curves

import (
	"crypto/elliptic"
	"encoding/json"
	"fmt"
	"math/big"

	"github.com/btcsuite/btcd/btcec"

	"source.quilibrium.com/quilibrium/monorepo/nekryptology/internal"
	"source.quilibrium.com/quilibrium/monorepo/nekryptology/pkg/core"
)

var curveNameToId = map[string]byte{
	"secp256k1": 0,
	"P-224":     1,
	"P-256":     2,
	"P-384":     3,
	"P-521":     4,
}

var curveIdToName = map[byte]func() elliptic.Curve{
	0: func() elliptic.Curve { return btcec.S256() },
	1: elliptic.P224,
	2: elliptic.P256,
	3: elliptic.P384,
	4: elliptic.P521,
}

var curveMapper = map[string]func() elliptic.Curve{
	"secp256k1": func() elliptic.Curve { return btcec.S256() },
	"P-224":     elliptic.P224,
	"P-256":     elliptic.P256,
	"P-384":     elliptic.P384,
	"P-521":     elliptic.P521,
}

// EcPoint represents an elliptic curve Point
type EcPoint struct {
	Curve elliptic.Curve
	X, Y  *big.Int
}

// EcPointJson encapsulates the data that is serialized to JSON
// used internally and not for external use. Public so other pieces
// can use for serialization
type EcPointJson struct {
	CurveName string
	X, Y      *big.Int
}

// MarshalJSON serializes
func (a EcPoint) MarshalJSON() ([]byte, error) {
	return json.Marshal(EcPointJson{
		CurveName: a.Curve.Params().Name,
		X:         a.X,
		Y:         a.Y,
	})
}

func (a *EcPoint) UnmarshalJSON(bytes []byte) error {
	data := new(EcPointJson)
	err := json.Unmarshal(bytes, data)
	if err != nil {
		return err
	}
	if mapper, ok := curveMapper[data.CurveName]; ok {
		a.Curve = mapper()
		a.X = data.X
		a.Y = data.Y
		return nil
	}
	return fmt.Errorf("unknown curve deserialized")
}

func (a *EcPoint) MarshalBinary() ([]byte, error) {
	result := [65]byte{}
	if code, ok := curveNameToId[a.Curve.Params().Name]; ok {
		result[0] = code
		a.X.FillBytes(result[1:33])
		a.Y.FillBytes(result[33:65])
		return result[:], nil
	}
	return nil, fmt.Errorf("unknown curve serialized")
}

func (a *EcPoint) UnmarshalBinary(data []byte) error {
	if mapper, ok := curveIdToName[data[0]]; ok {
		a.Curve = mapper()
		a.X = new(big.Int).SetBytes(data[1:33])
		a.Y = new(big.Int).SetBytes(data[33:65])
		return nil
	}
	return fmt.Errorf("unknown curve deserialized")
}

func (a EcPoint) IsValid() bool {
	return a.IsOnCurve() || a.IsIdentity()
}

func (a EcPoint) IsOnCurve() bool {
	return a.Curve.IsOnCurve(a.X, a.Y)
}

// IsIdentity returns true if this Point is the Point at infinity
func (a EcPoint) IsIdentity() bool {
	x := core.ConstantTimeEqByte(a.X, core.Zero)
	y := core.ConstantTimeEqByte(a.Y, core.Zero)
	return (x & y) == 1
}

// Equals return true if a + b have the same x,y coordinates
func (a EcPoint) Equals(b *EcPoint) bool {
	if !sameCurve(&a, b) {
		return false
	}
	// Next, compare coords to determine equality
	x := core.ConstantTimeEqByte(a.X, b.X)
	y := core.ConstantTimeEqByte(a.Y, b.Y)
	return (x & y) == 1
}

// IsBasePoint returns true if this Point is curve's base Point
func (a EcPoint) IsBasePoint() bool {
	p := a.Curve.Params()
	x := core.ConstantTimeEqByte(a.X, p.Gx)
	y := core.ConstantTimeEqByte(a.Y, p.Gy)
	return (x & y) == 1
}

// Normalizes the Scalar to a positive element smaller than the base Point order.
func reduceModN(curve elliptic.Curve, k *big.Int) *big.Int {
	return new(big.Int).Mod(k, curve.Params().N)
}

// Add performs elliptic curve addition on two points
func (a *EcPoint) Add(b *EcPoint) (*EcPoint, error) {
	// Validate parameters
	if a == nil || b == nil {
		return nil, internal.ErrNilArguments
	}
	// Only add points from the same curve
	if !sameCurve(a, b) {
		return nil, internal.ErrPointsDistinctCurves
	}
	p := &EcPoint{Curve: a.Curve}
	p.X, p.Y = a.Curve.Add(a.X, a.Y, b.X, b.Y)
	if !p.IsValid() {
		return nil, internal.ErrNotOnCurve
	}
	return p, nil
}

// Neg returns the negation of a Weierstrass Point.
func (a *EcPoint) Neg() (*EcPoint, error) {
	// Validate parameters
	if a == nil {
		return nil, internal.ErrNilArguments
	}
	// Only add points from the same curve
	p := &EcPoint{Curve: a.Curve, X: a.X, Y: new(big.Int).Sub(a.Curve.Params().P, a.Y)}
	if !p.IsValid() {
		return nil, internal.ErrNotOnCurve
	}
	return p, nil
}

// ScalarMult multiplies this Point by a Scalar
func (a *EcPoint) ScalarMult(k *big.Int) (*EcPoint, error) {
	if a == nil || k == nil {
		return nil, fmt.Errorf("cannot multiply nil Point or element")
	}
	n := reduceModN(a.Curve, k)
	p := new(EcPoint)
	p.Curve = a.Curve
	p.X, p.Y = a.Curve.ScalarMult(a.X, a.Y, n.Bytes())
	if !p.IsValid() {
		return nil, fmt.Errorf("result not on the curve")
	}
	return p, nil
}

// NewScalarBaseMult creates a Point from the base Point multiplied by a field element
func NewScalarBaseMult(curve elliptic.Curve, k *big.Int) (*EcPoint, error) {
	if curve == nil || k == nil {
		return nil, fmt.Errorf("nil parameters are not supported")
	}
	n := reduceModN(curve, k)
	p := new(EcPoint)
	p.Curve = curve
	p.X, p.Y = curve.ScalarBaseMult(n.Bytes())
	if !p.IsValid() {
		return nil, fmt.Errorf("result not on the curve")
	}
	return p, nil
}

// Bytes returns the bytes represented by this Point with x || y
func (a EcPoint) Bytes() []byte {
	fieldSize := internal.CalcFieldSize(a.Curve)
	out := make([]byte, fieldSize*2)

	a.X.FillBytes(out[0:fieldSize])
	a.Y.FillBytes(out[fieldSize : fieldSize*2])
	return out
}

// PointFromBytesUncompressed outputs uncompressed X || Y similar to
// https://www.secg.org/sec1-v1.99.dif.pdf section 2.2 and 2.3
func PointFromBytesUncompressed(curve elliptic.Curve, b []byte) (*EcPoint, error) {
	fieldSize := internal.CalcFieldSize(curve)
	if len(b) != fieldSize*2 {
		return nil, fmt.Errorf("invalid number of bytes")
	}
	p := &EcPoint{
		Curve: curve,
		X:     new(big.Int).SetBytes(b[:fieldSize]),
		Y:     new(big.Int).SetBytes(b[fieldSize:]),
	}
	if !p.IsValid() {
		return nil, fmt.Errorf("invalid Point")
	}
	return p, nil
}

// sameCurve determines if points a,b appear to be from the same curve
func sameCurve(a, b *EcPoint) bool {
	// Handle identical pointers and double-nil
	if a == b {
		return true
	}

	// Handle one nil pointer
	if a == nil || b == nil {
		return false
	}

	aParams := a.Curve.Params()
	bParams := b.Curve.Params()

	// Use curve order and name
	return aParams.P == bParams.P &&
		aParams.N == bParams.N &&
		aParams.B == bParams.B &&
		aParams.BitSize == bParams.BitSize &&
		aParams.Gx == bParams.Gx &&
		aParams.Gy == bParams.Gy &&
		aParams.Name == bParams.Name
}
