// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package subtle

import (
	"bytes"
	"crypto/elliptic"
	"crypto/rand"
	"errors"
	"fmt"
	"math/big"
)

// ECPublicKey represents a elliptic curve public key.
type ECPublicKey struct {
	elliptic.Curve
	Point ECPoint
}

// ECPrivateKey represents a elliptic curve private key.
type ECPrivateKey struct {
	PublicKey ECPublicKey
	D         *big.Int
}

// GetECPrivateKey converts a stored private key to ECPrivateKey.
func GetECPrivateKey(c elliptic.Curve, b []byte) *ECPrivateKey {
	d := new(big.Int)
	d.SetBytes(b)

	x, y := c.Params().ScalarBaseMult(b)
	pub := ECPublicKey{
		Curve: c,
		Point: ECPoint{
			X: x,
			Y: y,
		},
	}
	return &ECPrivateKey{
		PublicKey: pub,
		D:         d,
	}

}

// ECPoint represents a point on the elliptic curve.
type ECPoint struct {
	X, Y *big.Int
}

func (p *ECPrivateKey) getParams() *elliptic.CurveParams {
	return p.PublicKey.Curve.Params()
}

func getModulus(c elliptic.Curve) *big.Int {
	return c.Params().P
}

func fieldSizeInBits(c elliptic.Curve) int {
	t := big.NewInt(1)
	r := t.Sub(getModulus(c), t)
	return r.BitLen()
}

func fieldSizeInBytes(c elliptic.Curve) int {
	return (fieldSizeInBits(c) + 7) / 8
}

func encodingSizeInBytes(c elliptic.Curve, p string) (int, error) {
	cSize := fieldSizeInBytes(c)
	switch p {
	case "UNCOMPRESSED":
		return 2*cSize + 1, nil
	case "DO_NOT_USE_CRUNCHY_UNCOMPRESSED":
		return 2 * cSize, nil
	case "COMPRESSED":
		return cSize + 1, nil
	}
	return 0, fmt.Errorf("invalid point format :%s", p)

}

// PointEncode encodes a point into the format specified.
func PointEncode(c elliptic.Curve, pFormat string, pt ECPoint) ([]byte, error) {
	if !c.IsOnCurve(pt.X, pt.Y) {
		return nil, errors.New("curve check failed")
	}
	cSize := fieldSizeInBytes(c)
	y := pt.Y.Bytes()
	x := pt.X.Bytes()
	switch pFormat {
	case "UNCOMPRESSED":
		encoded := make([]byte, 2*cSize+1)
		copy(encoded[1+2*cSize-len(y):], y)
		copy(encoded[1+cSize-len(x):], x)
		encoded[0] = 4
		return encoded, nil
	case "DO_NOT_USE_CRUNCHY_UNCOMPRESSED":
		encoded := make([]byte, 2*cSize)
		if len(x) > cSize {
			x = bytes.Replace(x, []byte("\x00"), []byte{}, -1)
		}
		if len(y) > cSize {
			y = bytes.Replace(y, []byte("\x00"), []byte{}, -1)
		}
		copy(encoded[2*cSize-len(y):], y)
		copy(encoded[cSize-len(x):], x)
		return encoded, nil
	case "COMPRESSED":
		encoded := make([]byte, cSize+1)
		copy(encoded[1+cSize-len(x):], x)
		encoded[0] = 2
		if pt.Y.Bit(0) > 0 {
			encoded[0] = 3
		}
		return encoded, nil
	}
	return nil, errors.New("invalid point format")

}

// PointDecode decodes a encoded point to return an ECPoint
func PointDecode(c elliptic.Curve, pFormat string, e []byte) (*ECPoint, error) {
	cSize := fieldSizeInBytes(c)
	x, y := new(big.Int), new(big.Int)
	switch pFormat {
	case "UNCOMPRESSED":
		if len(e) != (2*cSize + 1) {
			return nil, errors.New("invalid point size")
		}
		if e[0] != 4 {
			return nil, errors.New("invalid point format")
		}
		x.SetBytes(e[1 : cSize+1])
		y.SetBytes(e[cSize+1:])
		if !c.IsOnCurve(x, y) {
			return nil, errors.New("invalid point")
		}
		return &ECPoint{
			X: x,
			Y: y,
		}, nil
	case "DO_NOT_USE_CRUNCHY_UNCOMPRESSED":
		if len(e) != 2*cSize {
			return nil, errors.New("invalid point size")
		}
		x.SetBytes(e[:cSize])
		y.SetBytes(e[cSize:])
		if !c.IsOnCurve(x, y) {
			return nil, errors.New("invalid point")
		}
		return &ECPoint{
			X: x,
			Y: y,
		}, nil
	case "COMPRESSED":
		if len(e) != cSize+1 {
			return nil, errors.New("compressed point has wrong length")
		}
		lsb := false
		if e[0] == 2 {
			lsb = false
		} else if e[0] == 3 {
			lsb = true
		} else {
			return nil, errors.New("invalid format")
		}
		x := new(big.Int)
		x.SetBytes(e[1:])
		if (x.Sign() == -1) || (x.Cmp(c.Params().P) != -1) {
			return nil, errors.New("x is out of range")
		}
		y := getY(x, lsb, c)
		return &ECPoint{
			X: x,
			Y: y,
		}, nil
	}
	return nil, fmt.Errorf("invalid format: %s", pFormat)
}

func getY(x *big.Int, lsb bool, c elliptic.Curve) *big.Int {
	// y² = x³ - 3x + b
	x3 := new(big.Int).Mul(x, x)
	x3.Mul(x3, x)

	threeX := new(big.Int).Lsh(x, 1)
	threeX.Add(threeX, x)
	b := c.Params().B
	p := c.Params().P

	x3.Sub(x3, threeX)
	x3.Add(x3, b)
	x3.ModSqrt(x3, p)
	e := uint(1)
	if lsb {
		e = 0
	}
	if e == x3.Bit(0) {
		x3 := x3.Sub(p, x3)
		x3.Mod(x3, p)
	}
	return x3
}

func validatePublicPoint(pub *ECPoint, priv *ECPrivateKey) error {
	if priv.PublicKey.Curve.IsOnCurve(pub.X, pub.Y) {
		return nil
	}
	return errors.New("invalid public key")
}

// ComputeSharedSecret is used to compute a shared secret using given private key and peer public key.
func ComputeSharedSecret(pub *ECPoint, priv *ECPrivateKey) ([]byte, error) {
	if err := validatePublicPoint(pub, priv); err != nil {
		return nil, err
	}

	x, y := priv.PublicKey.Curve.ScalarMult(pub.X, pub.Y, priv.D.Bytes())

	if x == nil {
		return nil, errors.New("shared key compute error")
	}
	// check if x,y are on the curve
	if err := validatePublicPoint(&ECPoint{X: x, Y: y}, priv); err != nil {
		return nil, errors.New("invalid shared key")
	}

	sharedSecret := make([]byte, maxSharedKeyLength(priv.PublicKey))
	return x.FillBytes(sharedSecret), nil
}

func maxSharedKeyLength(pub ECPublicKey) int {
	return (pub.Curve.Params().BitSize + 7) / 8
}

// GenerateECDHKeyPair will create a new private key for a given curve.
func GenerateECDHKeyPair(c elliptic.Curve) (*ECPrivateKey, error) {
	p, x, y, err := elliptic.GenerateKey(c, rand.Reader)
	if err != nil {
		return nil, err
	}
	return &ECPrivateKey{
		PublicKey: ECPublicKey{
			Curve: c,
			Point: ECPoint{
				X: x,
				Y: y,
			},
		},
		D: new(big.Int).SetBytes(p),
	}, nil

}

// GetCurve returns the elliptic.Curve for a given standard curve name.
func GetCurve(c string) (elliptic.Curve, error) {
	switch c {
	case "secp224r1", "NIST_P224", "P-224":
		return elliptic.P224(), nil
	case "secp256r1", "NIST_P256", "P-256", "EllipticCurveType_NIST_P256":
		return elliptic.P256(), nil
	case "secp384r1", "NIST_P384", "P-384", "EllipticCurveType_NIST_P384":
		return elliptic.P384(), nil
	case "secp521r1", "NIST_P521", "P-521", "EllipticCurveType_NIST_P521":
		return elliptic.P521(), nil
	default:
		return nil, errors.New("unsupported curve")
	}
}
