package checksum

import (
	
	
	
	
	
	
	
	
	
	
	
)

// Algorithm represents the checksum algorithms supported
type Algorithm string

// Enumeration values for supported checksum Algorithms.
const (
	// AlgorithmCRC32C represents CRC32C hash algorithm
	AlgorithmCRC32C Algorithm = "CRC32C"

	// AlgorithmCRC32 represents CRC32 hash algorithm
	AlgorithmCRC32 Algorithm = "CRC32"

	// AlgorithmSHA1 represents SHA1 hash algorithm
	AlgorithmSHA1 Algorithm = "SHA1"

	// AlgorithmSHA256 represents SHA256 hash algorithm
	AlgorithmSHA256 Algorithm = "SHA256"
)

var supportedAlgorithms = []Algorithm{
	AlgorithmCRC32C,
	AlgorithmCRC32,
	AlgorithmSHA1,
	AlgorithmSHA256,
}

func ( Algorithm) () string { return string() }

// ParseAlgorithm attempts to parse the provided value into a checksum
// algorithm, matching without case. Returns the algorithm matched, or an error
// if the algorithm wasn't matched.
func ( string) (Algorithm, error) {
	for ,  := range supportedAlgorithms {
		if strings.EqualFold(string(), ) {
			return , nil
		}
	}
	return "", fmt.Errorf("unknown checksum algorithm, %v", )
}

// FilterSupportedAlgorithms filters the set of algorithms, returning a slice
// of algorithms that are supported.
func ( []string) []Algorithm {
	 := map[Algorithm]struct{}{}

	 := make([]Algorithm, 0, len(supportedAlgorithms))
	for ,  := range  {
		for ,  := range supportedAlgorithms {
			// Only consider algorithms that are supported
			if !strings.EqualFold(, string()) {
				continue
			}
			// Ignore duplicate algorithms in list.
			if ,  := [];  {
				continue
			}

			 = append(, )
			[] = struct{}{}
		}
	}
	return 
}

// NewAlgorithmHash returns a hash.Hash for the checksum algorithm. Error is
// returned if the algorithm is unknown.
func ( Algorithm) (hash.Hash, error) {
	switch  {
	case AlgorithmSHA1:
		return sha1.New(), nil
	case AlgorithmSHA256:
		return sha256.New(), nil
	case AlgorithmCRC32:
		return crc32.NewIEEE(), nil
	case AlgorithmCRC32C:
		return crc32.New(crc32.MakeTable(crc32.Castagnoli)), nil
	default:
		return nil, fmt.Errorf("unknown checksum algorithm, %v", )
	}
}

// AlgorithmChecksumLength returns the length of the algorithm's checksum in
// bytes. If the algorithm is not known, an error is returned.
func ( Algorithm) (int, error) {
	switch  {
	case AlgorithmSHA1:
		return sha1.Size, nil
	case AlgorithmSHA256:
		return sha256.Size, nil
	case AlgorithmCRC32:
		return crc32.Size, nil
	case AlgorithmCRC32C:
		return crc32.Size, nil
	default:
		return 0, fmt.Errorf("unknown checksum algorithm, %v", )
	}
}

const awsChecksumHeaderPrefix = "x-amz-checksum-"

// AlgorithmHTTPHeader returns the HTTP header for the algorithm's hash.
func ( Algorithm) string {
	return awsChecksumHeaderPrefix + strings.ToLower(string())
}

// base64EncodeHashSum computes base64 encoded checksum of a given running
// hash. The running hash must already have content written to it. Returns the
// byte slice of checksum and an error
func ( hash.Hash) []byte {
	 := .Sum(nil)
	 := make([]byte, base64.StdEncoding.EncodedLen(len()))
	base64.StdEncoding.Encode(, )
	return 
}

// hexEncodeHashSum computes hex encoded checksum of a given running hash. The
// running hash must already have content written to it. Returns the byte slice
// of checksum and an error
func ( hash.Hash) []byte {
	 := .Sum(nil)
	 := make([]byte, hex.EncodedLen(len()))
	hex.Encode(, )
	return 
}

// computeMD5Checksum computes base64 MD5 checksum of an io.Reader's contents.
// Returns the byte slice of MD5 checksum and an error.
func ( io.Reader) ([]byte, error) {
	 := md5.New()

	// Copy errors may be assumed to be from the body.
	if ,  := io.Copy(, );  != nil {
		return nil, fmt.Errorf("failed compute MD5 hash of reader, %w", )
	}

	// Encode the MD5 checksum in base64.
	return base64EncodeHashSum(), nil
}

// computeChecksumReader provides a reader wrapping an underlying io.Reader to
// compute the checksum of the stream's bytes.
type computeChecksumReader struct {
	stream            io.Reader
	algorithm         Algorithm
	hasher            hash.Hash
	base64ChecksumLen int

	mux            sync.RWMutex
	lockedChecksum string
	lockedErr      error
}

// newComputeChecksumReader returns a computeChecksumReader for the stream and
// algorithm specified. Returns error if unable to create the reader, or
// algorithm is unknown.
func ( io.Reader,  Algorithm) (*computeChecksumReader, error) {
	,  := NewAlgorithmHash()
	if  != nil {
		return nil, 
	}

	,  := AlgorithmChecksumLength()
	if  != nil {
		return nil, 
	}

	return &computeChecksumReader{
		stream:            io.TeeReader(, ),
		algorithm:         ,
		hasher:            ,
		base64ChecksumLen: base64.StdEncoding.EncodedLen(),
	}, nil
}

// Read wraps the underlying reader. When the underlying reader returns EOF,
// the checksum of the reader will be computed, and can be retrieved with
// ChecksumBase64String.
func ( *computeChecksumReader) ( []byte) (int, error) {
	,  := .stream.Read()
	if  == nil {
		return , nil
	} else if  != io.EOF {
		.mux.Lock()
		defer .mux.Unlock()

		.lockedErr = 
		return , 
	}

	 := base64EncodeHashSum(.hasher)

	.mux.Lock()
	defer .mux.Unlock()

	.lockedChecksum = string()

	return , 
}

func ( *computeChecksumReader) () Algorithm {
	return .algorithm
}

// Base64ChecksumLength returns the base64 encoded length of the checksum for
// algorithm.
func ( *computeChecksumReader) () int {
	return .base64ChecksumLen
}

// Base64Checksum returns the base64 checksum for the algorithm, or error if
// the underlying reader returned a non-EOF error.
//
// Safe to be called concurrently, but will return an error until after the
// underlying reader is returns EOF.
func ( *computeChecksumReader) () (string, error) {
	.mux.RLock()
	defer .mux.RUnlock()

	if .lockedErr != nil {
		return "", .lockedErr
	}

	if .lockedChecksum == "" {
		return "", fmt.Errorf(
			"checksum not available yet, called before reader returns EOF",
		)
	}

	return .lockedChecksum, nil
}

// validateChecksumReader implements io.ReadCloser interface. The wrapper
// performs checksum validation when the underlying reader has been fully read.
type validateChecksumReader struct {
	originalBody   io.ReadCloser
	body           io.Reader
	hasher         hash.Hash
	algorithm      Algorithm
	expectChecksum string
}

// newValidateChecksumReader returns a configured io.ReadCloser that performs
// checksum validation when the underlying reader has been fully read.
func (
	 io.ReadCloser,
	 Algorithm,
	 string,
) (*validateChecksumReader, error) {
	,  := NewAlgorithmHash()
	if  != nil {
		return nil, 
	}

	return &validateChecksumReader{
		originalBody:   ,
		body:           io.TeeReader(, ),
		hasher:         ,
		algorithm:      ,
		expectChecksum: ,
	}, nil
}

// Read attempts to read from the underlying stream while also updating the
// running hash. If the underlying stream returns with an EOF error, the
// checksum of the stream will be collected, and compared against the expected
// checksum. If the checksums do not match, an error will be returned.
//
// If a non-EOF error occurs when reading the underlying stream, that error
// will be returned and the checksum for the stream will be discarded.
func ( *validateChecksumReader) ( []byte) ( int,  error) {
	,  = .body.Read()
	if  == io.EOF {
		if  := .validateChecksum();  != nil {
			return , 
		}
	}

	return , 
}

// Close closes the underlying reader, returning any error that occurred in the
// underlying reader.
func ( *validateChecksumReader) () ( error) {
	return .originalBody.Close()
}

func ( *validateChecksumReader) () error {
	// Compute base64 encoded checksum hash of the payload's read bytes.
	 := base64EncodeHashSum(.hasher)
	if ,  := .expectChecksum, string(); !strings.EqualFold(, ) {
		return validationError{
			Algorithm: .algorithm, Expect: , Actual: ,
		}
	}

	return nil
}

type validationError struct {
	Algorithm Algorithm
	Expect    string
	Actual    string
}

func ( validationError) () string {
	return fmt.Sprintf("checksum did not match: algorithm %v, expect %v, actual %v",
		.Algorithm, .Expect, .Actual)
}