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

package curves

import (
	"crypto/elliptic"
	crand "crypto/rand"
	"crypto/subtle"
	"errors"
	"fmt"
	"io"
	"math/big"
	"sync"

	"golang.org/x/crypto/blake2b"

	"source.quilibrium.com/quilibrium/monorepo/nekryptology/pkg/core/curves/native/pasta/fp"
	"source.quilibrium.com/quilibrium/monorepo/nekryptology/pkg/core/curves/native/pasta/fq"
)

var b = new(fp.Fp).SetUint64(5)
var three = &fp.Fp{0x6b0ee5d0fffffff5, 0x86f76d2b99b14bd0, 0xfffffffffffffffe, 0x3fffffffffffffff}
var eight = &fp.Fp{0x7387134cffffffe1, 0xd973797adfadd5a8, 0xfffffffffffffffb, 0x3fffffffffffffff}
var bool2int = map[bool]int{
	true:  1,
	false: 0,
}

var isomapper = [13]*fp.Fp{
	new(fp.Fp).SetRaw(&[4]uint64{0x775f6034aaaaaaab, 0x4081775473d8375b, 0xe38e38e38e38e38e, 0x0e38e38e38e38e38}),
	new(fp.Fp).SetRaw(&[4]uint64{0x8cf863b02814fb76, 0x0f93b82ee4b99495, 0x267c7ffa51cf412a, 0x3509afd51872d88e}),
	new(fp.Fp).SetRaw(&[4]uint64{0x0eb64faef37ea4f7, 0x380af066cfeb6d69, 0x98c7d7ac3d98fd13, 0x17329b9ec5253753}),
	new(fp.Fp).SetRaw(&[4]uint64{0xeebec06955555580, 0x8102eea8e7b06eb6, 0xc71c71c71c71c71c, 0x1c71c71c71c71c71}),
	new(fp.Fp).SetRaw(&[4]uint64{0xc47f2ab668bcd71f, 0x9c434ac1c96b6980, 0x5a607fcce0494a79, 0x1d572e7ddc099cff}),
	new(fp.Fp).SetRaw(&[4]uint64{0x2aa3af1eae5b6604, 0xb4abf9fb9a1fc81c, 0x1d13bf2a7f22b105, 0x325669becaecd5d1}),
	new(fp.Fp).SetRaw(&[4]uint64{0x5ad985b5e38e38e4, 0x7642b01ad461bad2, 0x4bda12f684bda12f, 0x1a12f684bda12f68}),
	new(fp.Fp).SetRaw(&[4]uint64{0xc67c31d8140a7dbb, 0x07c9dc17725cca4a, 0x133e3ffd28e7a095, 0x1a84d7ea8c396c47}),
	new(fp.Fp).SetRaw(&[4]uint64{0x02e2be87d225b234, 0x1765e924f7459378, 0x303216cce1db9ff1, 0x3fb98ff0d2ddcadd}),
	new(fp.Fp).SetRaw(&[4]uint64{0x93e53ab371c71c4f, 0x0ac03e8e134eb3e4, 0x7b425ed097b425ed, 0x025ed097b425ed09}),
	new(fp.Fp).SetRaw(&[4]uint64{0x5a28279b1d1b42ae, 0x5941a3a4a97aa1b3, 0x0790bfb3506defb6, 0x0c02c5bcca0e6b7f}),
	new(fp.Fp).SetRaw(&[4]uint64{0x4d90ab820b12320a, 0xd976bbfabbc5661d, 0x573b3d7f7d681310, 0x17033d3c60c68173}),
	new(fp.Fp).SetRaw(&[4]uint64{0x992d30ecfffffde5, 0x224698fc094cf91b, 0x0000000000000000, 0x4000000000000000}),
}
var isoa = new(fp.Fp).SetRaw(&[4]uint64{0x92bb4b0b657a014b, 0xb74134581a27a59f, 0x49be2d7258370742, 0x18354a2eb0ea8c9c})
var isob = new(fp.Fp).SetRaw(&[4]uint64{1265, 0, 0, 0})
var z = new(fp.Fp).SetRaw(&[4]uint64{0x992d30ecfffffff4, 0x224698fc094cf91b, 0x0000000000000000, 0x4000000000000000})

var oldPallasInitonce sync.Once
var oldPallas PallasCurve

type PallasCurve struct {
	*elliptic.CurveParams
}

func oldPallasInitAll() {
	oldPallas.CurveParams = new(elliptic.CurveParams)
	oldPallas.P = new(big.Int).SetBytes([]byte{
		0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x22, 0x46, 0x98, 0xfc, 0x09, 0x4c, 0xf9, 0x1b,
		0x99, 0x2d, 0x30, 0xed, 0x00, 0x00, 0x00, 0x01,
	})
	oldPallas.N = new(big.Int).SetBytes([]byte{
		0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x22, 0x46, 0x98, 0xfc, 0x09, 0x94, 0xa8, 0xdd,
		0x8c, 0x46, 0xeb, 0x21, 0x00, 0x00, 0x00, 0x01,
	})
	g := new(Ep).Generator()
	oldPallas.Gx = g.x.BigInt()
	oldPallas.Gy = g.y.BigInt()
	oldPallas.B = big.NewInt(5)
	oldPallas.BitSize = 255
	pallas.Name = PallasName
}

func Pallas() *PallasCurve {
	oldPallasInitonce.Do(oldPallasInitAll)
	return &oldPallas
}

func (curve *PallasCurve) Params() *elliptic.CurveParams {
	return curve.CurveParams
}

func (curve *PallasCurve) IsOnCurve(x, y *big.Int) bool {
	p := new(Ep)
	p.x = new(fp.Fp).SetBigInt(x)
	p.y = new(fp.Fp).SetBigInt(y)
	p.z = new(fp.Fp).SetOne()

	return p.IsOnCurve()
}

func (curve *PallasCurve) Add(x1, y1, x2, y2 *big.Int) (*big.Int, *big.Int) {
	p := new(Ep)
	p.x = new(fp.Fp).SetBigInt(x1)
	p.y = new(fp.Fp).SetBigInt(y1)
	p.z = new(fp.Fp).SetOne()
	if p.x.IsZero() && p.y.IsZero() {
		p.z.SetZero()
	}

	q := new(Ep)
	q.x = new(fp.Fp).SetBigInt(x2)
	q.y = new(fp.Fp).SetBigInt(y2)
	q.z = new(fp.Fp).SetOne()
	if q.x.IsZero() && q.y.IsZero() {
		q.z.SetZero()
	}
	p.Add(p, q)
	p.toAffine()
	return p.x.BigInt(), p.y.BigInt()
}

func (curve *PallasCurve) Double(x1, y1 *big.Int) (*big.Int, *big.Int) {
	p := new(Ep)
	p.x = new(fp.Fp).SetBigInt(x1)
	p.y = new(fp.Fp).SetBigInt(y1)
	p.z = new(fp.Fp).SetOne()
	if p.x.IsZero() && p.y.IsZero() {
		p.z.SetZero()
	}
	p.Double(p)
	p.toAffine()
	return p.x.BigInt(), p.y.BigInt()
}

func (curve *PallasCurve) ScalarMult(Bx, By *big.Int, k []byte) (*big.Int, *big.Int) {
	p := new(Ep)
	p.x = new(fp.Fp).SetBigInt(Bx)
	p.y = new(fp.Fp).SetBigInt(By)
	p.z = new(fp.Fp).SetOne()
	if p.x.IsZero() && p.y.IsZero() {
		p.z.SetZero()
	}
	var t [32]byte
	copy(t[:], k)
	ss := new(big.Int).SetBytes(k)
	sc := new(fq.Fq).SetBigInt(ss)
	p.Mul(p, sc)
	p.toAffine()
	return p.x.BigInt(), p.y.BigInt()
}

func (curve *PallasCurve) ScalarBaseMult(k []byte) (*big.Int, *big.Int) {
	p := new(Ep).Generator()
	var t [32]byte
	copy(t[:], k)
	ss := new(big.Int).SetBytes(k)
	sc := new(fq.Fq).SetBigInt(ss)
	p.Mul(p, sc)
	p.toAffine()
	return p.x.BigInt(), p.y.BigInt()
}

// PallasScalar - Old interface
type PallasScalar struct{}

func NewPallasScalar() *PallasScalar {
	return &PallasScalar{}
}

func (k PallasScalar) Add(x, y *big.Int) *big.Int {
	r := new(big.Int).Add(x, y)
	return r.Mod(r, fq.BiModulus)
}

func (k PallasScalar) Sub(x, y *big.Int) *big.Int {
	r := new(big.Int).Sub(x, y)
	return r.Mod(r, fq.BiModulus)
}

func (k PallasScalar) Neg(x *big.Int) *big.Int {
	r := new(big.Int).Neg(x)
	return r.Mod(r, fq.BiModulus)
}

func (k PallasScalar) Mul(x, y *big.Int) *big.Int {
	r := new(big.Int).Mul(x, y)
	return r.Mod(r, fq.BiModulus)
}

func (k PallasScalar) Div(x, y *big.Int) *big.Int {
	r := new(big.Int).ModInverse(y, fq.BiModulus)
	r.Mul(r, x)
	return r.Mod(r, fq.BiModulus)
}

func (k PallasScalar) Hash(input []byte) *big.Int {
	return new(ScalarPallas).Hash(input).(*ScalarPallas).value.BigInt()
}

func (k PallasScalar) Bytes(x *big.Int) []byte {
	return x.Bytes()
}

func (k PallasScalar) Random() (*big.Int, error) {
	s, ok := new(ScalarPallas).Random(crand.Reader).(*ScalarPallas)
	if !ok {
		return nil, errors.New("incorrect type conversion")
	}
	return s.value.BigInt(), nil
}

func (k PallasScalar) IsValid(x *big.Int) bool {
	return x.Cmp(fq.BiModulus) == -1
}

// ScalarPallas - New interface
type ScalarPallas struct {
	value *fq.Fq
}

func (s *ScalarPallas) Random(reader io.Reader) Scalar {
	if reader == nil {
		return nil
	}
	var seed [64]byte
	_, _ = reader.Read(seed[:])
	return s.Hash(seed[:])
}

func (s *ScalarPallas) Hash(bytes []byte) Scalar {
	h, _ := blake2b.New(64, []byte{})
	xmd, err := expandMsgXmd(h, bytes, []byte("pallas_XMD:BLAKE2b_SSWU_RO_"), 64)
	if err != nil {
		return nil
	}
	var t [64]byte
	copy(t[:], xmd)
	return &ScalarPallas{
		value: new(fq.Fq).SetBytesWide(&t),
	}
}

func (s *ScalarPallas) Zero() Scalar {
	return &ScalarPallas{
		value: new(fq.Fq).SetZero(),
	}
}

func (s *ScalarPallas) One() Scalar {
	return &ScalarPallas{
		value: new(fq.Fq).SetOne(),
	}
}

func (s *ScalarPallas) IsZero() bool {
	return s.value.IsZero()
}

func (s *ScalarPallas) IsOne() bool {
	return s.value.IsOne()
}

func (s *ScalarPallas) IsOdd() bool {
	return (s.value[0] & 1) == 1
}

func (s *ScalarPallas) IsEven() bool {
	return (s.value[0] & 1) == 0
}

func (s *ScalarPallas) New(value int) Scalar {
	v := big.NewInt(int64(value))
	return &ScalarPallas{
		value: new(fq.Fq).SetBigInt(v),
	}
}

func (s *ScalarPallas) Cmp(rhs Scalar) int {
	r, ok := rhs.(*ScalarPallas)
	if ok {
		return s.value.Cmp(r.value)
	} else {
		return -2
	}
}

func (s *ScalarPallas) Square() Scalar {
	return &ScalarPallas{
		value: new(fq.Fq).Square(s.value),
	}
}

func (s *ScalarPallas) Double() Scalar {
	return &ScalarPallas{
		value: new(fq.Fq).Double(s.value),
	}
}

func (s *ScalarPallas) Invert() (Scalar, error) {
	value, wasInverted := new(fq.Fq).Invert(s.value)
	if !wasInverted {
		return nil, fmt.Errorf("inverse doesn't exist")
	}
	return &ScalarPallas{
		value,
	}, nil
}

func (s *ScalarPallas) Sqrt() (Scalar, error) {
	value, wasSquare := new(fq.Fq).Sqrt(s.value)
	if !wasSquare {
		return nil, fmt.Errorf("not a square")
	}
	return &ScalarPallas{
		value,
	}, nil
}

func (s *ScalarPallas) Cube() Scalar {
	value := new(fq.Fq).Mul(s.value, s.value)
	value.Mul(value, s.value)
	return &ScalarPallas{
		value,
	}
}

func (s *ScalarPallas) Add(rhs Scalar) Scalar {
	r, ok := rhs.(*ScalarPallas)
	if ok {
		return &ScalarPallas{
			value: new(fq.Fq).Add(s.value, r.value),
		}
	} else {
		return nil
	}
}

func (s *ScalarPallas) Sub(rhs Scalar) Scalar {
	r, ok := rhs.(*ScalarPallas)
	if ok {
		return &ScalarPallas{
			value: new(fq.Fq).Sub(s.value, r.value),
		}
	} else {
		return nil
	}
}

func (s *ScalarPallas) Mul(rhs Scalar) Scalar {
	r, ok := rhs.(*ScalarPallas)
	if ok {
		return &ScalarPallas{
			value: new(fq.Fq).Mul(s.value, r.value),
		}
	} else {
		return nil
	}
}

func (s *ScalarPallas) MulAdd(y, z Scalar) Scalar {
	return s.Mul(y).Add(z)
}

func (s *ScalarPallas) Div(rhs Scalar) Scalar {
	r, ok := rhs.(*ScalarPallas)
	if ok {
		v, wasInverted := new(fq.Fq).Invert(r.value)
		if !wasInverted {
			return nil
		}
		v.Mul(v, s.value)
		return &ScalarPallas{value: v}
	} else {
		return nil
	}
}

func (s *ScalarPallas) Neg() Scalar {
	return &ScalarPallas{
		value: new(fq.Fq).Neg(s.value),
	}
}

func (s *ScalarPallas) SetBigInt(v *big.Int) (Scalar, error) {
	return &ScalarPallas{
		value: new(fq.Fq).SetBigInt(v),
	}, nil
}

func (s *ScalarPallas) BigInt() *big.Int {
	return s.value.BigInt()
}

func (s *ScalarPallas) Bytes() []byte {
	t := s.value.Bytes()
	return t[:]
}

func (s *ScalarPallas) SetBytes(bytes []byte) (Scalar, error) {
	if len(bytes) != 32 {
		return nil, fmt.Errorf("invalid length")
	}
	var seq [32]byte
	copy(seq[:], bytes)
	value, err := new(fq.Fq).SetBytes(&seq)
	if err != nil {
		return nil, err
	}
	return &ScalarPallas{
		value,
	}, nil
}

func (s *ScalarPallas) SetBytesWide(bytes []byte) (Scalar, error) {
	if len(bytes) != 64 {
		return nil, fmt.Errorf("invalid length")
	}
	var seq [64]byte
	copy(seq[:], bytes)
	return &ScalarPallas{
		value: new(fq.Fq).SetBytesWide(&seq),
	}, nil
}

func (s *ScalarPallas) Point() Point {
	return new(PointPallas).Identity()
}

func (s *ScalarPallas) Clone() Scalar {
	return &ScalarPallas{
		value: new(fq.Fq).Set(s.value),
	}
}

func (s *ScalarPallas) GetFq() *fq.Fq {
	return new(fq.Fq).Set(s.value)
}

func (s *ScalarPallas) SetFq(fq *fq.Fq) *ScalarPallas {
	s.value = fq
	return s
}

func (s *ScalarPallas) MarshalBinary() ([]byte, error) {
	return scalarMarshalBinary(s)
}

func (s *ScalarPallas) UnmarshalBinary(input []byte) error {
	sc, err := scalarUnmarshalBinary(input)
	if err != nil {
		return err
	}
	ss, ok := sc.(*ScalarPallas)
	if !ok {
		return fmt.Errorf("invalid scalar")
	}
	s.value = ss.value
	return nil
}

func (s *ScalarPallas) MarshalText() ([]byte, error) {
	return scalarMarshalText(s)
}

func (s *ScalarPallas) UnmarshalText(input []byte) error {
	sc, err := scalarUnmarshalText(input)
	if err != nil {
		return err
	}
	ss, ok := sc.(*ScalarPallas)
	if !ok {
		return fmt.Errorf("invalid scalar")
	}
	s.value = ss.value
	return nil
}

func (s *ScalarPallas) MarshalJSON() ([]byte, error) {
	return scalarMarshalJson(s)
}

func (s *ScalarPallas) UnmarshalJSON(input []byte) error {
	sc, err := scalarUnmarshalJson(input)
	if err != nil {
		return err
	}
	S, ok := sc.(*ScalarPallas)
	if !ok {
		return fmt.Errorf("invalid type")
	}
	s.value = S.value
	return nil
}

type PointPallas struct {
	value *Ep
}

func (p *PointPallas) Random(reader io.Reader) Point {
	return &PointPallas{new(Ep).Random(reader)}
}

func (p *PointPallas) Hash(bytes []byte) Point {
	return &PointPallas{new(Ep).Hash(bytes)}
}

func (p *PointPallas) Identity() Point {
	return &PointPallas{new(Ep).Identity()}
}

func (p *PointPallas) Generator() Point {
	return &PointPallas{new(Ep).Generator()}
}

func (p *PointPallas) IsIdentity() bool {
	return p.value.IsIdentity()
}

func (p *PointPallas) IsNegative() bool {
	return p.value.Y().IsOdd()
}

func (p *PointPallas) IsOnCurve() bool {
	return p.value.IsOnCurve()
}

func (p *PointPallas) Double() Point {
	return &PointPallas{new(Ep).Double(p.value)}
}

func (p *PointPallas) Scalar() Scalar {
	return &ScalarPallas{new(fq.Fq).SetZero()}
}

func (p *PointPallas) Neg() Point {
	return &PointPallas{new(Ep).Neg(p.value)}
}

func (p *PointPallas) Add(rhs Point) Point {
	r, ok := rhs.(*PointPallas)
	if !ok {
		return nil
	}
	return &PointPallas{new(Ep).Add(p.value, r.value)}
}

func (p *PointPallas) Sub(rhs Point) Point {
	r, ok := rhs.(*PointPallas)
	if !ok {
		return nil
	}
	return &PointPallas{new(Ep).Sub(p.value, r.value)}
}

func (p *PointPallas) Mul(rhs Scalar) Point {
	s, ok := rhs.(*ScalarPallas)
	if !ok {
		return nil
	}
	return &PointPallas{new(Ep).Mul(p.value, s.value)}
}

func (p *PointPallas) Equal(rhs Point) bool {
	r, ok := rhs.(*PointPallas)
	if !ok {
		return false
	}
	return p.value.Equal(r.value)
}

func (p *PointPallas) Set(x, y *big.Int) (Point, error) {
	xx := subtle.ConstantTimeCompare(x.Bytes(), []byte{})
	yy := subtle.ConstantTimeCompare(y.Bytes(), []byte{})
	xElem := new(fp.Fp).SetBigInt(x)
	var data [32]byte
	if yy == 1 {
		if xx == 1 {
			return &PointPallas{new(Ep).Identity()}, nil
		}
		data = xElem.Bytes()
		return p.FromAffineCompressed(data[:])
	}
	yElem := new(fp.Fp).SetBigInt(y)
	value := &Ep{xElem, yElem, new(fp.Fp).SetOne()}
	if !value.IsOnCurve() {
		return nil, fmt.Errorf("point is not on the curve")
	}
	return &PointPallas{value}, nil
}

func (p *PointPallas) ToAffineCompressed() []byte {
	return p.value.ToAffineCompressed()
}

func (p *PointPallas) ToAffineUncompressed() []byte {
	return p.value.ToAffineUncompressed()
}

func (p *PointPallas) FromAffineCompressed(bytes []byte) (Point, error) {
	value, err := new(Ep).FromAffineCompressed(bytes)
	if err != nil {
		return nil, err
	}
	return &PointPallas{value}, nil
}

func (p *PointPallas) FromAffineUncompressed(bytes []byte) (Point, error) {
	value, err := new(Ep).FromAffineUncompressed(bytes)
	if err != nil {
		return nil, err
	}
	return &PointPallas{value}, nil
}

func (p *PointPallas) CurveName() string {
	return PallasName
}

func (p *PointPallas) SumOfProducts(points []Point, scalars []Scalar) Point {
	eps := make([]*Ep, len(points))
	for i, pt := range points {
		ps, ok := pt.(*PointPallas)
		if !ok {
			return nil
		}
		eps[i] = ps.value
	}
	value := p.value.SumOfProducts(eps, scalars)
	return &PointPallas{value}
}

func (p *PointPallas) MarshalBinary() ([]byte, error) {
	return pointMarshalBinary(p)
}

func (p *PointPallas) UnmarshalBinary(input []byte) error {
	pt, err := pointUnmarshalBinary(input)
	if err != nil {
		return err
	}
	ppt, ok := pt.(*PointPallas)
	if !ok {
		return fmt.Errorf("invalid point")
	}
	p.value = ppt.value
	return nil
}

func (p *PointPallas) MarshalText() ([]byte, error) {
	return pointMarshalText(p)
}

func (p *PointPallas) UnmarshalText(input []byte) error {
	pt, err := pointUnmarshalText(input)
	if err != nil {
		return err
	}
	ppt, ok := pt.(*PointPallas)
	if !ok {
		return fmt.Errorf("invalid point")
	}
	p.value = ppt.value
	return nil
}

func (p *PointPallas) MarshalJSON() ([]byte, error) {
	return pointMarshalJson(p)
}

func (p *PointPallas) UnmarshalJSON(input []byte) error {
	pt, err := pointUnmarshalJson(input)
	if err != nil {
		return err
	}
	P, ok := pt.(*PointPallas)
	if !ok {
		return fmt.Errorf("invalid type")
	}
	p.value = P.value
	return nil
}

func (p *PointPallas) X() *fp.Fp {
	return p.value.X()
}

func (p *PointPallas) Y() *fp.Fp {
	return p.value.Y()
}

func (p *PointPallas) GetEp() *Ep {
	return new(Ep).Set(p.value)
}

type Ep struct {
	x *fp.Fp
	y *fp.Fp
	z *fp.Fp
}

func (p *Ep) Random(reader io.Reader) *Ep {
	var seed [64]byte
	_, _ = reader.Read(seed[:])
	return p.Hash(seed[:])
}

func (p *Ep) Hash(bytes []byte) *Ep {
	if bytes == nil {
		bytes = []byte{}
	}
	h, _ := blake2b.New(64, []byte{})
	u, _ := expandMsgXmd(h, bytes, []byte("pallas_XMD:BLAKE2b_SSWU_RO_"), 128)
	var buf [64]byte
	copy(buf[:], u[:64])
	u0 := new(fp.Fp).SetBytesWide(&buf)
	copy(buf[:], u[64:])
	u1 := new(fp.Fp).SetBytesWide(&buf)

	q0 := mapSswu(u0)
	q1 := mapSswu(u1)
	r1 := isoMap(q0)
	r2 := isoMap(q1)
	return p.Identity().Add(r1, r2)
}

func (p *Ep) Identity() *Ep {
	p.x = new(fp.Fp).SetZero()
	p.y = new(fp.Fp).SetZero()
	p.z = new(fp.Fp).SetZero()
	return p
}

func (p *Ep) Generator() *Ep {
	p.x = new(fp.Fp).SetOne()
	p.y = &fp.Fp{0x2f474795455d409d, 0xb443b9b74b8255d9, 0x270c412f2c9a5d66, 0x8e00f71ba43dd6b}
	p.z = new(fp.Fp).SetOne()
	return p
}

func (p *Ep) IsIdentity() bool {
	return p.z.IsZero()
}

func (p *Ep) Double(other *Ep) *Ep {
	if other.IsIdentity() {
		p.Set(other)
		return p
	}
	r := new(Ep)
	// essentially paraphrased https://github.com/MinaProtocol/c-reference-signer/blob/master/crypto.c#L306-L337
	a := new(fp.Fp).Square(other.x)
	b := new(fp.Fp).Square(other.y)
	c := new(fp.Fp).Square(b)
	r.x = new(fp.Fp).Add(other.x, b)
	r.y = new(fp.Fp).Square(r.x)
	r.z = new(fp.Fp).Sub(r.y, a)
	r.x.Sub(r.z, c)
	d := new(fp.Fp).Double(r.x)
	e := new(fp.Fp).Mul(three, a)
	f := new(fp.Fp).Square(e)
	r.y.Double(d)
	r.x.Sub(f, r.y)
	r.y.Sub(d, r.x)
	f.Mul(eight, c)
	r.z.Mul(e, r.y)
	r.y.Sub(r.z, f)
	f.Mul(other.y, other.z)
	r.z.Double(f)
	p.Set(r)
	return p
}

func (p *Ep) Neg(other *Ep) *Ep {
	p.x = new(fp.Fp).Set(other.x)
	p.y = new(fp.Fp).Neg(other.y)
	p.z = new(fp.Fp).Set(other.z)
	return p
}

func (p *Ep) Add(lhs *Ep, rhs *Ep) *Ep {
	if lhs.IsIdentity() {
		return p.Set(rhs)
	}
	if rhs.IsIdentity() {
		return p.Set(lhs)
	}
	z1z1 := new(fp.Fp).Square(lhs.z)
	z2z2 := new(fp.Fp).Square(rhs.z)
	u1 := new(fp.Fp).Mul(lhs.x, z2z2)
	u2 := new(fp.Fp).Mul(rhs.x, z1z1)
	s1 := new(fp.Fp).Mul(lhs.y, z2z2)
	s1.Mul(s1, rhs.z)
	s2 := new(fp.Fp).Mul(rhs.y, z1z1)
	s2.Mul(s2, lhs.z)

	if u1.Equal(u2) {
		if s1.Equal(s2) {
			return p.Double(lhs)
		} else {
			return p.Identity()
		}
	} else {
		h := new(fp.Fp).Sub(u2, u1)
		i := new(fp.Fp).Double(h)
		i.Square(i)
		j := new(fp.Fp).Mul(i, h)
		r := new(fp.Fp).Sub(s2, s1)
		r.Double(r)
		v := new(fp.Fp).Mul(u1, i)
		x3 := new(fp.Fp).Square(r)
		x3.Sub(x3, j)
		x3.Sub(x3, new(fp.Fp).Double(v))
		s1.Mul(s1, j)
		s1.Double(s1)
		y3 := new(fp.Fp).Mul(r, new(fp.Fp).Sub(v, x3))
		y3.Sub(y3, s1)
		z3 := new(fp.Fp).Add(lhs.z, rhs.z)
		z3.Square(z3)
		z3.Sub(z3, z1z1)
		z3.Sub(z3, z2z2)
		z3.Mul(z3, h)
		p.x = new(fp.Fp).Set(x3)
		p.y = new(fp.Fp).Set(y3)
		p.z = new(fp.Fp).Set(z3)

		return p
	}
}

func (p *Ep) Sub(lhs, rhs *Ep) *Ep {
	return p.Add(lhs, new(Ep).Neg(rhs))
}

func (p *Ep) Mul(point *Ep, scalar *fq.Fq) *Ep {
	bytes := scalar.Bytes()
	precomputed := [16]*Ep{}
	precomputed[0] = new(Ep).Identity()
	precomputed[1] = new(Ep).Set(point)
	for i := 2; i < 16; i += 2 {
		precomputed[i] = new(Ep).Double(precomputed[i>>1])
		precomputed[i+1] = new(Ep).Add(precomputed[i], point)
	}
	p.Identity()
	for i := 0; i < 256; i += 4 {
		// Brouwer / windowing method. window size of 4.
		for j := 0; j < 4; j++ {
			p.Double(p)
		}
		window := bytes[32-1-i>>3] >> (4 - i&0x04) & 0x0F
		p.Add(p, precomputed[window])
	}
	return p
}

func (p *Ep) Equal(other *Ep) bool {
	// warning: requires converting both to affine
	// could save slightly by modifying one so that its z-value equals the other
	// this would save one inversion and a handful of multiplications
	// but this is more subtle and error-prone, so going to just convert both to affine.
	lhs := new(Ep).Set(p)
	rhs := new(Ep).Set(other)
	lhs.toAffine()
	rhs.toAffine()
	return lhs.x.Equal(rhs.x) && lhs.y.Equal(rhs.y)
}

func (p *Ep) Set(other *Ep) *Ep {
	// check is identity or on curve
	p.x = new(fp.Fp).Set(other.x)
	p.y = new(fp.Fp).Set(other.y)
	p.z = new(fp.Fp).Set(other.z)
	return p
}

func (p *Ep) toAffine() *Ep {
	// mutates `p` in-place to convert it to "affine" form.
	if p.IsIdentity() {
		// warning: control flow / not constant-time
		p.x.SetZero()
		p.y.SetZero()
		p.z.SetOne()
		return p
	}
	zInv3, _ := new(fp.Fp).Invert(p.z) // z is necessarily nonzero
	zInv2 := new(fp.Fp).Square(zInv3)
	zInv3.Mul(zInv3, zInv2)
	p.x.Mul(p.x, zInv2)
	p.y.Mul(p.y, zInv3)
	p.z.SetOne()
	return p
}

func (p *Ep) ToAffineCompressed() []byte {
	// Use ZCash encoding where infinity is all zeros
	// and the top bit represents the sign of y and the
	// remainder represent the x-coordinate
	var inf [32]byte
	p1 := new(Ep).Set(p)
	p1.toAffine()
	x := p1.x.Bytes()
	x[31] |= (p1.y.Bytes()[0] & 1) << 7
	subtle.ConstantTimeCopy(bool2int[p1.IsIdentity()], x[:], inf[:])
	return x[:]
}

func (p *Ep) ToAffineUncompressed() []byte {
	p1 := new(Ep).Set(p)
	p1.toAffine()
	x := p1.x.Bytes()
	y := p1.y.Bytes()
	return append(x[:], y[:]...)
}

func (p *Ep) FromAffineCompressed(bytes []byte) (*Ep, error) {
	if len(bytes) != 32 {
		return nil, fmt.Errorf("invalid byte sequence")
	}

	var input [32]byte
	copy(input[:], bytes)
	sign := (input[31] >> 7) & 1
	input[31] &= 0x7F

	x := new(fp.Fp)
	if _, err := x.SetBytes(&input); err != nil {
		return nil, err
	}
	rhs := rhsPallas(x)
	if _, square := rhs.Sqrt(rhs); !square {
		return nil, fmt.Errorf("rhs of given x-coordinate is not a square")
	}
	if rhs.Bytes()[0]&1 != sign {
		rhs.Neg(rhs)
	}
	p.x = x
	p.y = rhs
	p.z = new(fp.Fp).SetOne()
	if !p.IsOnCurve() {
		return nil, fmt.Errorf("invalid point")
	}
	return p, nil
}

func (p *Ep) FromAffineUncompressed(bytes []byte) (*Ep, error) {
	if len(bytes) != 64 {
		return nil, fmt.Errorf("invalid length")
	}
	p.z = new(fp.Fp).SetOne()
	p.x = new(fp.Fp)
	p.y = new(fp.Fp)
	var x, y [32]byte
	copy(x[:], bytes[:32])
	copy(y[:], bytes[32:])
	if _, err := p.x.SetBytes(&x); err != nil {
		return nil, err
	}
	if _, err := p.y.SetBytes(&y); err != nil {
		return nil, err
	}
	if !p.IsOnCurve() {
		return nil, fmt.Errorf("invalid point")
	}
	return p, nil
}

// rhs of the curve equation
func rhsPallas(x *fp.Fp) *fp.Fp {
	x2 := new(fp.Fp).Square(x)
	x3 := new(fp.Fp).Mul(x, x2)
	return new(fp.Fp).Add(x3, b)
}

func (p Ep) CurveName() string {
	return "pallas"
}

func (p Ep) SumOfProducts(points []*Ep, scalars []Scalar) *Ep {
	nScalars := make([]*big.Int, len(scalars))
	for i, s := range scalars {
		sc, ok := s.(*ScalarPallas)
		if !ok {
			return nil
		}
		nScalars[i] = sc.value.BigInt()
	}
	return sumOfProductsPippengerPallas(points, nScalars)
}

func (p *Ep) X() *fp.Fp {
	t := new(Ep).Set(p)
	t.toAffine()
	return new(fp.Fp).Set(t.x)
}

func (p *Ep) Y() *fp.Fp {
	t := new(Ep).Set(p)
	t.toAffine()
	return new(fp.Fp).Set(t.y)
}

func (p *Ep) IsOnCurve() bool {
	// y^2 = x^3 + axz^4 + bz^6
	// a = 0
	// b = 5
	z2 := new(fp.Fp).Square(p.z)
	z4 := new(fp.Fp).Square(z2)
	z6 := new(fp.Fp).Mul(z2, z4)
	x2 := new(fp.Fp).Square(p.x)
	x3 := new(fp.Fp).Mul(x2, p.x)

	lhs := new(fp.Fp).Square(p.y)
	rhs := new(fp.Fp).SetUint64(5)
	rhs.Mul(rhs, z6)
	rhs.Add(rhs, x3)
	return p.z.IsZero() || lhs.Equal(rhs)
}

func (p *Ep) CMove(lhs, rhs *Ep, condition int) *Ep {
	p.x = new(fp.Fp).CMove(lhs.x, rhs.x, condition)
	p.y = new(fp.Fp).CMove(lhs.y, rhs.y, condition)
	p.z = new(fp.Fp).CMove(lhs.z, rhs.z, condition)
	return p
}

func sumOfProductsPippengerPallas(points []*Ep, scalars []*big.Int) *Ep {
	if len(points) != len(scalars) {
		return nil
	}

	const w = 6

	bucketSize := (1 << w) - 1
	windows := make([]*Ep, 255/w+1)
	for i := range windows {
		windows[i] = new(Ep).Identity()
	}
	bucket := make([]*Ep, bucketSize)

	for j := 0; j < len(windows); j++ {
		for i := 0; i < bucketSize; i++ {
			bucket[i] = new(Ep).Identity()
		}

		for i := 0; i < len(scalars); i++ {
			index := bucketSize & int(new(big.Int).Rsh(scalars[i], uint(w*j)).Int64())
			if index != 0 {
				bucket[index-1].Add(bucket[index-1], points[i])
			}
		}

		acc, sum := new(Ep).Identity(), new(Ep).Identity()

		for i := bucketSize - 1; i >= 0; i-- {
			sum.Add(sum, bucket[i])
			acc.Add(acc, sum)
		}
		windows[j] = acc
	}

	acc := new(Ep).Identity()
	for i := len(windows) - 1; i >= 0; i-- {
		for j := 0; j < w; j++ {
			acc.Double(acc)
		}
		acc.Add(acc, windows[i])
	}
	return acc
}

// Implements a degree 3 isogeny map.
// The input and output are in Jacobian coordinates, using the method
// in "Avoiding inversions" [WB2019, section 4.3].
func isoMap(p *Ep) *Ep {
	var z [4]*fp.Fp
	z[0] = new(fp.Fp).Square(p.z)    //z^2
	z[1] = new(fp.Fp).Mul(z[0], p.z) // z^3
	z[2] = new(fp.Fp).Square(z[0])   // z^4
	z[3] = new(fp.Fp).Square(z[1])   // z^6

	// ((iso[0] * x + iso[1] * z^2) * x + iso[2] * z^4) * x + iso[3] * z^6
	numX := new(fp.Fp).Set(isomapper[0])
	numX.Mul(numX, p.x)
	numX.Add(numX, new(fp.Fp).Mul(isomapper[1], z[0]))
	numX.Mul(numX, p.x)
	numX.Add(numX, new(fp.Fp).Mul(isomapper[2], z[2]))
	numX.Mul(numX, p.x)
	numX.Add(numX, new(fp.Fp).Mul(isomapper[3], z[3]))

	// (z^2 * x + iso[4] * z^4) * x + iso[5] * z^6
	divX := new(fp.Fp).Set(z[0])
	divX.Mul(divX, p.x)
	divX.Add(divX, new(fp.Fp).Mul(isomapper[4], z[2]))
	divX.Mul(divX, p.x)
	divX.Add(divX, new(fp.Fp).Mul(isomapper[5], z[3]))

	// (((iso[6] * x + iso[7] * z2) * x + iso[8] * z4) * x + iso[9] * z6) * y
	numY := new(fp.Fp).Set(isomapper[6])
	numY.Mul(numY, p.x)
	numY.Add(numY, new(fp.Fp).Mul(isomapper[7], z[0]))
	numY.Mul(numY, p.x)
	numY.Add(numY, new(fp.Fp).Mul(isomapper[8], z[2]))
	numY.Mul(numY, p.x)
	numY.Add(numY, new(fp.Fp).Mul(isomapper[9], z[3]))
	numY.Mul(numY, p.y)

	// (((x + iso[10] * z2) * x + iso[11] * z4) * x + iso[12] * z6) * z3
	divY := new(fp.Fp).Set(p.x)
	divY.Add(divY, new(fp.Fp).Mul(isomapper[10], z[0]))
	divY.Mul(divY, p.x)
	divY.Add(divY, new(fp.Fp).Mul(isomapper[11], z[2]))
	divY.Mul(divY, p.x)
	divY.Add(divY, new(fp.Fp).Mul(isomapper[12], z[3]))
	divY.Mul(divY, z[1])

	z0 := new(fp.Fp).Mul(divX, divY)
	x := new(fp.Fp).Mul(numX, divY)
	x.Mul(x, z0)
	y := new(fp.Fp).Mul(numY, divX)
	y.Mul(y, new(fp.Fp).Square(z0))

	return &Ep{
		x, y, z0,
	}
}

func mapSswu(u *fp.Fp) *Ep {
	//c1 := new(fp.Fp).Neg(isoa)
	//c1.Invert(c1)
	//c1.Mul(isob, c1)
	c1 := &fp.Fp{
		0x1ee770ce078456ec,
		0x48cfd64c2ce76be0,
		0x43d5774c0ab79e2f,
		0x23368d2bdce28cf3,
	}
	//c2 := new(fp.Fp).Neg(z)
	//c2.Invert(c2)
	c2 := &fp.Fp{
		0x03df915f89d89d8a,
		0x8f1e8db09ef82653,
		0xd89d89d89d89d89d,
		0x1d89d89d89d89d89,
	}

	u2 := new(fp.Fp).Square(u)
	tv1 := new(fp.Fp).Mul(z, u2)
	tv2 := new(fp.Fp).Square(tv1)
	x1 := new(fp.Fp).Add(tv1, tv2)
	x1.Invert(x1)
	e1 := bool2int[x1.IsZero()]
	x1.Add(x1, new(fp.Fp).SetOne())
	x1.CMove(x1, c2, e1)
	x1.Mul(x1, c1)
	gx1 := new(fp.Fp).Square(x1)
	gx1.Add(gx1, isoa)
	gx1.Mul(gx1, x1)
	gx1.Add(gx1, isob)
	x2 := new(fp.Fp).Mul(tv1, x1)
	tv2.Mul(tv1, tv2)
	gx2 := new(fp.Fp).Mul(gx1, tv2)
	gx1Sqrt, e2 := new(fp.Fp).Sqrt(gx1)
	x := new(fp.Fp).CMove(x2, x1, bool2int[e2])
	gx2Sqrt, _ := new(fp.Fp).Sqrt(gx2)
	y := new(fp.Fp).CMove(gx2Sqrt, gx1Sqrt, bool2int[e2])
	e3 := u.IsOdd() == y.IsOdd()
	y.CMove(new(fp.Fp).Neg(y), y, bool2int[e3])

	return &Ep{
		x: x, y: y, z: new(fp.Fp).SetOne(),
	}
}
