// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package git

import (
	"crypto/sha1"
	"crypto/sha256"
	"hash"
	"regexp"
	"strconv"
)

// sha1Pattern can be used to determine if a string is an valid sha
var sha1Pattern = regexp.MustCompile(`^[0-9a-f]{4,40}$`)

// sha256Pattern can be used to determine if a string is an valid sha
var sha256Pattern = regexp.MustCompile(`^[0-9a-f]{4,64}$`)

type ObjectFormat interface {
	// Name returns the name of the object format
	Name() string
	// EmptyObjectID creates a new empty ObjectID from an object format hash name
	EmptyObjectID() ObjectID
	// EmptyTree is the hash of an empty tree
	EmptyTree() ObjectID
	// FullLength is the length of the hash's hex string
	FullLength() int
	// IsValid returns true if the input is a valid hash
	IsValid(input string) bool
	// MustID creates a new ObjectID from a byte slice
	MustID(b []byte) ObjectID
	// ComputeHash compute the hash for a given ObjectType and content
	ComputeHash(t ObjectType, content []byte) ObjectID
}

func computeHash(dst []byte, hasher hash.Hash, t ObjectType, content []byte) {
	_, _ = hasher.Write(t.Bytes())
	_, _ = hasher.Write([]byte(" "))
	_, _ = hasher.Write([]byte(strconv.Itoa(len(content))))
	_, _ = hasher.Write([]byte{0})
	_, _ = hasher.Write(content)
	hasher.Sum(dst)
}

/* SHA1 Type */
type Sha1ObjectFormatImpl struct{}

var (
	emptySha1ObjectID = &Sha1Hash{}
	emptySha1Tree     = &Sha1Hash{
		0x4b, 0x82, 0x5d, 0xc6, 0x42, 0xcb, 0x6e, 0xb9, 0xa0, 0x60,
		0xe5, 0x4b, 0xf8, 0xd6, 0x92, 0x88, 0xfb, 0xee, 0x49, 0x04,
	}
)

func (Sha1ObjectFormatImpl) Name() string { return "sha1" }
func (Sha1ObjectFormatImpl) EmptyObjectID() ObjectID {
	return emptySha1ObjectID
}

func (Sha1ObjectFormatImpl) EmptyTree() ObjectID {
	return emptySha1Tree
}
func (Sha1ObjectFormatImpl) FullLength() int { return 40 }
func (Sha1ObjectFormatImpl) IsValid(input string) bool {
	return sha1Pattern.MatchString(input)
}

func (Sha1ObjectFormatImpl) MustID(b []byte) ObjectID {
	var id Sha1Hash
	copy(id[0:20], b)
	return &id
}

// ComputeHash compute the hash for a given ObjectType and content
func (h Sha1ObjectFormatImpl) ComputeHash(t ObjectType, content []byte) ObjectID {
	var obj Sha1Hash
	computeHash(obj[:0], sha1.New(), t, content)
	return &obj
}

/* SHA256 Type */
type Sha256ObjectFormatImpl struct{}

var (
	emptySha256ObjectID = &Sha256Hash{}
	emptySha256Tree     = &Sha256Hash{
		0x6e, 0xf1, 0x9b, 0x41, 0x22, 0x5c, 0x53, 0x69, 0xf1, 0xc1,
		0x04, 0xd4, 0x5d, 0x8d, 0x85, 0xef, 0xa9, 0xb0, 0x57, 0xb5,
		0x3b, 0x14, 0xb4, 0xb9, 0xb9, 0x39, 0xdd, 0x74, 0xde, 0xcc,
		0x53, 0x21,
	}
)

func (Sha256ObjectFormatImpl) Name() string { return "sha256" }
func (Sha256ObjectFormatImpl) EmptyObjectID() ObjectID {
	return emptySha256ObjectID
}

func (Sha256ObjectFormatImpl) EmptyTree() ObjectID {
	return emptySha256Tree
}
func (Sha256ObjectFormatImpl) FullLength() int { return 64 }
func (Sha256ObjectFormatImpl) IsValid(input string) bool {
	return sha256Pattern.MatchString(input)
}

func (Sha256ObjectFormatImpl) MustID(b []byte) ObjectID {
	var id Sha256Hash
	copy(id[0:32], b)
	return &id
}

// ComputeHash compute the hash for a given ObjectType and content
func (h Sha256ObjectFormatImpl) ComputeHash(t ObjectType, content []byte) ObjectID {
	var obj Sha256Hash
	computeHash(obj[:0], sha256.New(), t, content)
	return &obj
}

var (
	Sha1ObjectFormat   ObjectFormat = Sha1ObjectFormatImpl{}
	Sha256ObjectFormat ObjectFormat = Sha256ObjectFormatImpl{}
	// any addition must be reflected in IsEmptyCommitID
)

var SupportedObjectFormats = []ObjectFormat{
	Sha1ObjectFormat,
}

func ObjectFormatFromName(name string) ObjectFormat {
	for _, objectFormat := range SupportedObjectFormats {
		if name == objectFormat.Name() {
			return objectFormat
		}
	}
	return nil
}

func IsValidObjectFormat(name string) bool {
	return ObjectFormatFromName(name) != nil
}
