package checksum

import (
	
	
	
	
	
)

const (
	crlf = "\r\n"

	// https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html
	defaultChunkLength = 1024 * 64

	awsTrailerHeaderName           = "x-amz-trailer"
	decodedContentLengthHeaderName = "x-amz-decoded-content-length"

	contentEncodingHeaderName            = "content-encoding"
	awsChunkedContentEncodingHeaderValue = "aws-chunked"

	trailerKeyValueSeparator = ":"
)

var (
	crlfBytes       = []byte(crlf)
	finalChunkBytes = []byte("0" + crlf)
)

type awsChunkedEncodingOptions struct {
	// The total size of the stream. For unsigned encoding this implies that
	// there will only be a single chunk containing the underlying payload,
	// unless ChunkLength is also specified.
	StreamLength int64

	// Set of trailer key:value pairs that will be appended to the end of the
	// payload after the end chunk has been written.
	Trailers map[string]awsChunkedTrailerValue

	// The maximum size of each chunk to be sent. Default value of -1, signals
	// that optimal chunk length will be used automatically. ChunkSize must be
	// at least 8KB.
	//
	// If ChunkLength and StreamLength are both specified, the stream will be
	// broken up into ChunkLength chunks. The encoded length of the aws-chunked
	// encoding can still be determined as long as all trailers, if any, have a
	// fixed length.
	ChunkLength int
}

type awsChunkedTrailerValue struct {
	// Function to retrieve the value of the trailer. Will only be called after
	// the underlying stream returns EOF error.
	Get func() (string, error)

	// If the length of the value can be pre-determined, and is constant
	// specify the length. A value of -1 means the length is unknown, or
	// cannot be pre-determined.
	Length int
}

// awsChunkedEncoding provides a reader that wraps the payload such that
// payload is read as a single aws-chunk payload. This reader can only be used
// if the content length of payload is known. Content-Length is used as size of
// the single payload chunk. The final chunk and trailing checksum is appended
// at the end.
//
// https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html#sigv4-chunked-body-definition
//
// Here is the aws-chunked payload stream as read from the awsChunkedEncoding
// if original request stream is "Hello world", and checksum hash used is SHA256
//
//	<b>\r\n
//	Hello world\r\n
//	0\r\n
//	x-amz-checksum-sha256:ZOyIygCyaOW6GjVnihtTFtIS9PNmskdyMlNKiuyjfzw=\r\n
//	\r\n
type awsChunkedEncoding struct {
	options awsChunkedEncodingOptions

	encodedStream        io.Reader
	trailerEncodedLength int
}

// newUnsignedAWSChunkedEncoding returns a new awsChunkedEncoding configured
// for unsigned aws-chunked content encoding. Any additional trailers that need
// to be appended after the end chunk must be included as via Trailer
// callbacks.
func (
	 io.Reader,
	 ...func(*awsChunkedEncodingOptions),
) *awsChunkedEncoding {
	 := awsChunkedEncodingOptions{
		Trailers:     map[string]awsChunkedTrailerValue{},
		StreamLength: -1,
		ChunkLength:  -1,
	}
	for ,  := range  {
		(&)
	}

	var  io.Reader
	if .ChunkLength != -1 || .StreamLength == -1 {
		if .ChunkLength == -1 {
			.ChunkLength = defaultChunkLength
		}
		 = newBufferedAWSChunkReader(, .ChunkLength)
	} else {
		 = newUnsignedChunkReader(, .StreamLength)
	}

	 := newAWSChunkedTrailerReader(.Trailers)

	return &awsChunkedEncoding{
		options: ,
		encodedStream: io.MultiReader(,
			,
			bytes.NewBuffer(crlfBytes),
		),
		trailerEncodedLength: .EncodedLength(),
	}
}

// EncodedLength returns the final length of the aws-chunked content encoded
// stream if it can be determined without reading the underlying stream or lazy
// header values, otherwise -1 is returned.
func ( *awsChunkedEncoding) () int64 {
	var  int64
	if .options.StreamLength == -1 || .trailerEncodedLength == -1 {
		return -1
	}

	if .options.StreamLength != 0 {
		// If the stream length is known, and there is no chunk length specified,
		// only a single chunk will be used. Otherwise the stream length needs to
		// include the multiple chunk padding content.
		if .options.ChunkLength == -1 {
			 += getUnsignedChunkBytesLength(.options.StreamLength)

		} else {
			// Compute chunk header and payload length
			 := .options.StreamLength / int64(.options.ChunkLength)
			 +=  * getUnsignedChunkBytesLength(int64(.options.ChunkLength))
			if  := .options.StreamLength % int64(.options.ChunkLength);  != 0 {
				 += getUnsignedChunkBytesLength()
			}
		}
	}

	// End chunk
	 += int64(len(finalChunkBytes))

	// Trailers
	 += int64(.trailerEncodedLength)

	// Encoding terminator
	 += int64(len(crlf))

	return 
}

func ( int64) int64 {
	 := strconv.FormatInt(, 16)
	return int64(len()) + int64(len(crlf)) +  + int64(len(crlf))
}

// HTTPHeaders returns the set of headers that must be included the request for
// aws-chunked to work. This includes the content-encoding: aws-chunked header.
//
// If there are multiple layered content encoding, the aws-chunked encoding
// must be appended to the previous layers the stream's encoding. The best way
// to do this is to append all header values returned to the HTTP request's set
// of headers.
func ( *awsChunkedEncoding) () map[string][]string {
	 := map[string][]string{
		contentEncodingHeaderName: {
			awsChunkedContentEncodingHeaderValue,
		},
	}

	if len(.options.Trailers) != 0 {
		 := make([]string, 0, len(.options.Trailers))
		for  := range .options.Trailers {
			 = append(, strings.ToLower())
		}
		[awsTrailerHeaderName] = 
	}

	return 
}

func ( *awsChunkedEncoding) ( []byte) ( int,  error) {
	return .encodedStream.Read()
}

// awsChunkedTrailerReader provides a lazy reader for reading of aws-chunked
// content encoded trailers. The trailer values will not be retrieved until the
// reader is read from.
type awsChunkedTrailerReader struct {
	reader               *bytes.Buffer
	trailers             map[string]awsChunkedTrailerValue
	trailerEncodedLength int
}

// newAWSChunkedTrailerReader returns an initialized awsChunkedTrailerReader to
// lazy reading aws-chunk content encoded trailers.
func ( map[string]awsChunkedTrailerValue) *awsChunkedTrailerReader {
	return &awsChunkedTrailerReader{
		trailers:             ,
		trailerEncodedLength: trailerEncodedLength(),
	}
}

func ( map[string]awsChunkedTrailerValue) ( int) {
	for ,  := range  {
		 += len() + len(trailerKeyValueSeparator)
		 := .Length
		if  == -1 {
			return -1
		}
		 +=  + len(crlf)
	}

	return 
}

// EncodedLength returns the length of the encoded trailers if the length could
// be determined without retrieving the header values. Returns -1 if length is
// unknown.
func ( *awsChunkedTrailerReader) () ( int) {
	return .trailerEncodedLength
}

// Read populates the passed in byte slice with bytes from the encoded
// trailers. Will lazy read header values first time Read is called.
func ( *awsChunkedTrailerReader) ( []byte) (int, error) {
	if .trailerEncodedLength == 0 {
		return 0, io.EOF
	}

	if .reader == nil {
		 := .trailerEncodedLength
		if .trailerEncodedLength == -1 {
			 = 0
		}
		.reader = bytes.NewBuffer(make([]byte, 0, ))
		for ,  := range .trailers {
			.reader.WriteString()
			.reader.WriteString(trailerKeyValueSeparator)
			,  := .Get()
			if  != nil {
				return 0, fmt.Errorf("failed to get trailer value, %w", )
			}
			.reader.WriteString()
			.reader.WriteString(crlf)
		}
	}

	return .reader.Read()
}

// newUnsignedChunkReader returns an io.Reader encoding the underlying reader
// as unsigned aws-chunked chunks. The returned reader will also include the
// end chunk, but not the aws-chunked final `crlf` segment so trailers can be
// added.
//
// If the payload size is -1 for unknown length the content will be buffered in
// defaultChunkLength chunks before wrapped in aws-chunked chunk encoding.
func ( io.Reader,  int64) io.Reader {
	if  == -1 {
		return newBufferedAWSChunkReader(, defaultChunkLength)
	}

	var  bytes.Buffer
	if  == 0 {
		.Write(finalChunkBytes)
		return &
	}

	.WriteString(crlf)
	.Write(finalChunkBytes)

	var  bytes.Buffer
	.WriteString(strconv.FormatInt(, 16))
	.WriteString(crlf)
	return io.MultiReader(
		&,
		,
		&,
	)
}

// Provides a buffered aws-chunked chunk encoder of an underlying io.Reader.
// Will include end chunk, but not the aws-chunked final `crlf` segment so
// trailers can be added.
//
// Note does not implement support for chunk extensions, e.g. chunk signing.
type bufferedAWSChunkReader struct {
	reader       io.Reader
	chunkSize    int
	chunkSizeStr string

	headerBuffer *bytes.Buffer
	chunkBuffer  *bytes.Buffer

	multiReader    io.Reader
	multiReaderLen int
	endChunkDone   bool
}

// newBufferedAWSChunkReader returns an bufferedAWSChunkReader for reading
// aws-chunked encoded chunks.
func ( io.Reader,  int) *bufferedAWSChunkReader {
	return &bufferedAWSChunkReader{
		reader:       ,
		chunkSize:    ,
		chunkSizeStr: strconv.FormatInt(int64(), 16),

		headerBuffer: bytes.NewBuffer(make([]byte, 0, 64)),
		chunkBuffer:  bytes.NewBuffer(make([]byte, 0, +len(crlf))),
	}
}

// Read attempts to read from the underlying io.Reader writing aws-chunked
// chunk encoded bytes to p. When the underlying io.Reader has been completed
// read the end chunk will be available. Once the end chunk is read, the reader
// will return EOF.
func ( *bufferedAWSChunkReader) ( []byte) ( int,  error) {
	if .multiReaderLen == 0 && .endChunkDone {
		return 0, io.EOF
	}
	if .multiReader == nil || .multiReaderLen == 0 {
		.multiReader, .multiReaderLen,  = .newMultiReader()
		if  != nil {
			return 0, 
		}
	}

	,  = .multiReader.Read()
	.multiReaderLen -= 

	if  == io.EOF && !.endChunkDone {
		// Edge case handling when the multi-reader has been completely read,
		// and returned an EOF, make sure that EOF only gets returned if the
		// end chunk was included in the multi-reader. Otherwise, the next call
		// to read will initialize the next chunk's multi-reader.
		 = nil
	}
	return , 
}

// newMultiReader returns a new io.Reader for wrapping the next chunk. Will
// return an error if the underlying reader can not be read from. Will never
// return io.EOF.
func ( *bufferedAWSChunkReader) () (io.Reader, int, error) {
	// io.Copy eats the io.EOF returned by io.LimitReader. Any error that
	// occurs here is due to an actual read error.
	,  := io.Copy(.chunkBuffer, io.LimitReader(.reader, int64(.chunkSize)))
	if  != nil {
		return nil, 0, 
	}
	if  == 0 {
		// Early exit writing out only the end chunk. This does not include
		// aws-chunk's final `crlf` so that trailers can still be added by
		// upstream reader.
		.headerBuffer.Reset()
		.headerBuffer.WriteString("0")
		.headerBuffer.WriteString(crlf)
		.endChunkDone = true

		return .headerBuffer, .headerBuffer.Len(), nil
	}
	.chunkBuffer.WriteString(crlf)

	 := .chunkSizeStr
	if int() != .chunkSize {
		 = strconv.FormatInt(, 16)
	}

	.headerBuffer.Reset()
	.headerBuffer.WriteString()
	.headerBuffer.WriteString(crlf)

	return io.MultiReader(
		.headerBuffer,
		.chunkBuffer,
	), .headerBuffer.Len() + .chunkBuffer.Len(), nil
}