// Copyright 2020-2025 Buf Technologies, Inc.
//
// 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 bufcas

import (
	"bytes"
	"fmt"
	"io"
)

// Blob is content with its associated Digest.
type Blob interface {
	// Digest returns the Digest of the Blob.
	//
	// Always non-nil.
	//
	// NewDigestForContent(bytes.NewReader(blob.Content())) should always match this value.
	Digest() Digest
	// Content returns the content of the Blob.
	//
	// May be empty.
	Content() []byte

	// Protect against creation of a Blob outside of this package, as we
	// do very careful validation.
	isBlob()
}

// NewBlobForContent returns a new Blob for the content as read from the Reader.
//
// The reader is read until io.EOF.
func NewBlobForContent(reader io.Reader, options ...BlobOption) (Blob, error) {
	blobOptions := newBlobOptions()
	for _, option := range options {
		option(blobOptions)
	}
	buffer := bytes.NewBuffer(nil)
	teeReader := io.TeeReader(reader, buffer)
	digest, err := NewDigestForContent(teeReader, DigestWithDigestType(blobOptions.digestType))
	if err != nil {
		return nil, err
	}
	blob := newBlob(digest, buffer.Bytes())
	if blobOptions.knownDigest != nil && !DigestEqual(blob.Digest(), blobOptions.knownDigest) {
		return nil, fmt.Errorf("Digest %v did not match known Digest %v when creating a new Blob", blob.Digest(), blobOptions.knownDigest)
	}
	return blob, nil
}

// BlobOption is an option when constructing a new Blob
type BlobOption func(*blobOptions)

// BlobWithKnownDigest returns a new BlobOption that results in validation that the
// Digest for the new Blob matches an existing known Digest.
func BlobWithKnownDigest(knownDigest Digest) BlobOption {
	return func(blobOptions *blobOptions) {
		blobOptions.knownDigest = knownDigest
	}
}

// BlobWithDigestType returns a new BlobOption sets the DigestType to be used.
//
// The default is DigestTypeShake256.
func BlobWithDigestType(digestType DigestType) BlobOption {
	return func(blobOptions *blobOptions) {
		blobOptions.digestType = digestType
	}
}

// *** PRIVATE ***

type blob struct {
	digest  Digest
	content []byte
}

func newBlob(digest Digest, content []byte) *blob {
	return &blob{
		digest:  digest,
		content: content,
	}
}

func (b *blob) Digest() Digest {
	return b.digest
}

func (b *blob) Content() []byte {
	return b.content
}

func (*blob) isBlob() {}

type blobOptions struct {
	knownDigest Digest
	digestType  DigestType
}

func newBlobOptions() *blobOptions {
	return &blobOptions{}
}
