// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package legacyx509

import (
	"bytes"
	"encoding/asn1"
	"errors"
	"math"
	"math/big"
	"math/bits"
	"strconv"
	"strings"
)

var (
	errInvalidOID = errors.New("invalid oid")
)

// An OID represents an ASN.1 OBJECT IDENTIFIER.
type OID struct {
	der []byte
}

// ParseOID parses a Object Identifier string, represented by ASCII numbers separated by dots.
func ParseOID(oid string) (OID, error) {
	var o OID
	return o, o.unmarshalOIDText(oid)
}

func newOIDFromDER(der []byte) (OID, bool) {
	if len(der) == 0 || der[len(der)-1]&0x80 != 0 {
		return OID{}, false
	}

	start := 0
	for i, v := range der {
		// ITU-T X.690, section 8.19.2:
		// The subidentifier shall be encoded in the fewest possible octets,
		// that is, the leading octet of the subidentifier shall not have the value 0x80.
		if i == start && v == 0x80 {
			return OID{}, false
		}
		if v&0x80 == 0 {
			start = i + 1
		}
	}

	return OID{der}, true
}

// OIDFromInts creates a new OID using ints, each integer is a separate component.
func OIDFromInts(oid []uint64) (OID, error) {
	if len(oid) < 2 || oid[0] > 2 || (oid[0] < 2 && oid[1] >= 40) {
		return OID{}, errInvalidOID
	}

	length := base128IntLength(oid[0]*40 + oid[1])
	for _, v := range oid[2:] {
		length += base128IntLength(v)
	}

	der := make([]byte, 0, length)
	der = appendBase128Int(der, oid[0]*40+oid[1])
	for _, v := range oid[2:] {
		der = appendBase128Int(der, v)
	}
	return OID{der}, nil
}

func base128IntLength(n uint64) int {
	if n == 0 {
		return 1
	}
	return (bits.Len64(n) + 6) / 7
}

func appendBase128Int(dst []byte, n uint64) []byte {
	for i := base128IntLength(n) - 1; i >= 0; i-- {
		o := byte(n >> uint(i*7))
		o &= 0x7f
		if i != 0 {
			o |= 0x80
		}
		dst = append(dst, o)
	}
	return dst
}

func base128BigIntLength(n *big.Int) int {
	if n.Cmp(big.NewInt(0)) == 0 {
		return 1
	}
	return (n.BitLen() + 6) / 7
}

func appendBase128BigInt(dst []byte, n *big.Int) []byte {
	if n.Cmp(big.NewInt(0)) == 0 {
		return append(dst, 0)
	}

	for i := base128BigIntLength(n) - 1; i >= 0; i-- {
		o := byte(big.NewInt(0).Rsh(n, uint(i)*7).Bits()[0])
		o &= 0x7f
		if i != 0 {
			o |= 0x80
		}
		dst = append(dst, o)
	}
	return dst
}

// AppendText implements [encoding.TextAppender]
func (o OID) AppendText(b []byte) ([]byte, error) {
	return append(b, o.String()...), nil
}

// MarshalText implements [encoding.TextMarshaler]
func (o OID) MarshalText() ([]byte, error) {
	return o.AppendText(nil)
}

// UnmarshalText implements [encoding.TextUnmarshaler]
func (o *OID) UnmarshalText(text []byte) error {
	return o.unmarshalOIDText(string(text))
}

// cutString slices s around the first instance of sep,
// returning the text before and after sep.
// The found result reports whether sep appears in s.
// If sep does not appear in s, cut returns s, "", false.
func cutString(s, sep string) (before, after string, found bool) {
	if i := strings.Index(s, sep); i >= 0 {
		return s[:i], s[i+len(sep):], true
	}
	return s, "", false
}

func (o *OID) unmarshalOIDText(oid string) error {
	// (*big.Int).SetString allows +/- signs, but we don't want
	// to allow them in the string representation of Object Identifier, so
	// reject such encodings.
	for _, c := range oid {
		isDigit := c >= '0' && c <= '9'
		if !isDigit && c != '.' {
			return errInvalidOID
		}
	}

	var (
		firstNum  string
		secondNum string
	)

	var nextComponentExists bool
	firstNum, oid, nextComponentExists = cutString(oid, ".")
	if !nextComponentExists {
		return errInvalidOID
	}
	secondNum, oid, nextComponentExists = cutString(oid, ".")

	var (
		first  = big.NewInt(0)
		second = big.NewInt(0)
	)

	if _, ok := first.SetString(firstNum, 10); !ok {
		return errInvalidOID
	}
	if _, ok := second.SetString(secondNum, 10); !ok {
		return errInvalidOID
	}

	if first.Cmp(big.NewInt(2)) > 0 || (first.Cmp(big.NewInt(2)) < 0 && second.Cmp(big.NewInt(40)) >= 0) {
		return errInvalidOID
	}

	firstComponent := first.Mul(first, big.NewInt(40))
	firstComponent.Add(firstComponent, second)

	der := appendBase128BigInt(make([]byte, 0, 32), firstComponent)

	for nextComponentExists {
		var strNum string
		strNum, oid, nextComponentExists = cutString(oid, ".")
		b, ok := big.NewInt(0).SetString(strNum, 10)
		if !ok {
			return errInvalidOID
		}
		der = appendBase128BigInt(der, b)
	}

	o.der = der
	return nil
}

// AppendBinary implements [encoding.BinaryAppender]
func (o OID) AppendBinary(b []byte) ([]byte, error) {
	return append(b, o.der...), nil
}

// MarshalBinary implements [encoding.BinaryMarshaler]
func (o OID) MarshalBinary() ([]byte, error) {
	return o.AppendBinary(nil)
}

// cloneBytes returns a copy of b[:len(b)].
// The result may have additional unused capacity.
// Clone(nil) returns nil.
func cloneBytes(b []byte) []byte {
	if b == nil {
		return nil
	}
	return append([]byte{}, b...)
}

// UnmarshalBinary implements [encoding.BinaryUnmarshaler]
func (o *OID) UnmarshalBinary(b []byte) error {
	oid, ok := newOIDFromDER(cloneBytes(b))
	if !ok {
		return errInvalidOID
	}
	*o = oid
	return nil
}

// Equal returns true when oid and other represents the same Object Identifier.
func (oid OID) Equal(other OID) bool {
	// There is only one possible DER encoding of
	// each unique Object Identifier.
	return bytes.Equal(oid.der, other.der)
}

func parseBase128Int(bytes []byte, initOffset int) (ret, offset int, failed bool) {
	offset = initOffset
	var ret64 int64
	for shifted := 0; offset < len(bytes); shifted++ {
		// 5 * 7 bits per byte == 35 bits of data
		// Thus the representation is either non-minimal or too large for an int32
		if shifted == 5 {
			failed = true
			return
		}
		ret64 <<= 7
		b := bytes[offset]
		// integers should be minimally encoded, so the leading octet should
		// never be 0x80
		if shifted == 0 && b == 0x80 {
			failed = true
			return
		}
		ret64 |= int64(b & 0x7f)
		offset++
		if b&0x80 == 0 {
			ret = int(ret64)
			// Ensure that the returned value fits in an int on all platforms
			if ret64 > math.MaxInt32 {
				failed = true
			}
			return
		}
	}
	failed = true
	return
}

// EqualASN1OID returns whether an OID equals an asn1.ObjectIdentifier. If
// asn1.ObjectIdentifier cannot represent the OID specified by oid, because
// a component of OID requires more than 31 bits, it returns false.
func (oid OID) EqualASN1OID(other asn1.ObjectIdentifier) bool {
	if len(other) < 2 {
		return false
	}
	v, offset, failed := parseBase128Int(oid.der, 0)
	if failed {
		// This should never happen, since we've already parsed the OID,
		// but just in case.
		return false
	}
	if v < 80 {
		a, b := v/40, v%40
		if other[0] != a || other[1] != b {
			return false
		}
	} else {
		a, b := 2, v-80
		if other[0] != a || other[1] != b {
			return false
		}
	}

	i := 2
	for ; offset < len(oid.der); i++ {
		v, offset, failed = parseBase128Int(oid.der, offset)
		if failed {
			// Again, shouldn't happen, since we've already parsed
			// the OID, but better safe than sorry.
			return false
		}
		if i >= len(other) || v != other[i] {
			return false
		}
	}

	return i == len(other)
}

// Strings returns the string representation of the Object Identifier.
func (oid OID) String() string {
	var b strings.Builder
	b.Grow(32)
	const (
		valSize         = 64 // size in bits of val.
		bitsPerByte     = 7
		maxValSafeShift = (1 << (valSize - bitsPerByte)) - 1
	)
	var (
		start    = 0
		val      = uint64(0)
		numBuf   = make([]byte, 0, 21)
		bigVal   *big.Int
		overflow bool
	)
	for i, v := range oid.der {
		curVal := v & 0x7F
		valEnd := v&0x80 == 0
		if valEnd {
			if start != 0 {
				b.WriteByte('.')
			}
		}
		if !overflow && val > maxValSafeShift {
			if bigVal == nil {
				bigVal = new(big.Int)
			}
			bigVal = bigVal.SetUint64(val)
			overflow = true
		}
		if overflow {
			bigVal = bigVal.Lsh(bigVal, bitsPerByte).Or(bigVal, big.NewInt(int64(curVal)))
			if valEnd {
				if start == 0 {
					b.WriteString("2.")
					bigVal = bigVal.Sub(bigVal, big.NewInt(80))
				}
				numBuf = bigVal.Append(numBuf, 10)
				b.Write(numBuf)
				numBuf = numBuf[:0]
				val = 0
				start = i + 1
				overflow = false
			}
			continue
		}
		val <<= bitsPerByte
		val |= uint64(curVal)
		if valEnd {
			if start == 0 {
				if val < 80 {
					b.Write(strconv.AppendUint(numBuf, val/40, 10))
					b.WriteByte('.')
					b.Write(strconv.AppendUint(numBuf, val%40, 10))
				} else {
					b.WriteString("2.")
					b.Write(strconv.AppendUint(numBuf, val-80, 10))
				}
			} else {
				b.Write(strconv.AppendUint(numBuf, val, 10))
			}
			val = 0
			start = i + 1
		}
	}
	return b.String()
}
