

// chaPolyShrink provides functions to encrypt or decrypt bytes using ChaPolyShrink
// ChaPolyShrink first compresses bytes using zlib, adds padding to obscure size of encrypted bytes, and ciphers data using chaPoly

package chaPolyShrink

import "encoding/binary"
import "golang.org/x/crypto/chacha20poly1305"
import "compress/zlib"
import "crypto/rand"
import "errors"
import "bytes"
import "io"
import "slices"

// A paddingSize is specified, which adds padding so that resulting encrypted result size is divisible by paddingSize

//Outputs:
//	-[]byte: ChaPoly Encrypted bytes
//	-error
func EncryptChaPolyShrink(input []byte, key [32]byte, nonce [24]byte, compressContent bool, paddingSize int, includeAdditionalData bool, additionalData [32]byte)([]byte, error){

	if (paddingSize > 10000000 || paddingSize < 0) {
		return nil, errors.New("EncryptChaPolyShrink called with invalid paddingSize.")
	}

	getCompressionStrength := func()int{
		if (compressContent == false){
			return 0
		}
		return 9
	}

	compressionStrength := getCompressionStrength()

	var compressedBuffer bytes.Buffer

	compressorWriter, err := zlib.NewWriterLevel(&compressedBuffer, compressionStrength)
	if (err != nil) { return nil, err }
	compressorWriter.Write(input)
	compressorWriter.Close()

	compressedBytes := compressedBuffer.Bytes()
	 
	compressedBytesLength := len(compressedBytes)

	getNeededPaddingLength := func()int{

		if (paddingSize == 0){
			return 0
		}

		if (compressedBytesLength < paddingSize){
			paddingLength := paddingSize - compressedBytesLength
			return paddingLength
		}

		remainder := compressedBytesLength % paddingSize

		if (remainder == 0){
			return 0
		}

		paddingLengthInBytes := paddingSize - remainder

		return paddingLengthInBytes	
	}

	neededPaddingLength := getNeededPaddingLength()

	paddingLengthUint32 := uint32(neededPaddingLength)

	paddingLengthHeader := make([]byte, 4)

	binary.LittleEndian.PutUint32(paddingLengthHeader, paddingLengthUint32)

	paddingBytes := make([]byte, neededPaddingLength)
	_, err = rand.Read(paddingBytes[:])
	if (err != nil){ return nil, err }

	compressedBytesWithPaddingAndHeader := slices.Concat(paddingLengthHeader, paddingBytes, compressedBytes)

	getAdditionalDataParameter := func()[]byte{
		if (includeAdditionalData == false){
			return nil
		}

		result := additionalData[:]

		return result
	}

	additionalDataParameter := getAdditionalDataParameter()

	cipherObject, err := chacha20poly1305.NewX(key[:])
	if (err != nil) { return nil, err }

	encryptedMessage := cipherObject.Seal(nil, nonce[:], compressedBytesWithPaddingAndHeader, additionalDataParameter)

	return encryptedMessage, nil
}


//Outputs:
//	-bool: Able to decrypt
//	-[]byte: Decrypted bytes
//	-error (will return error if inputs are invalid)
func DecryptChaPolyShrink(inputBytes []byte, key [32]byte, nonce [24]byte, additionalDataExists bool, additionalData [32]byte)(bool, []byte, error){

	getAdditionalDataParameter := func()[]byte{
		if (additionalDataExists == false){
			return nil
		}

		result := additionalData[:]

		return result
	}

	additionalDataParameter := getAdditionalDataParameter()

	cipherObject, err := chacha20poly1305.NewX(key[:])
	if (err != nil) { return false, nil, err }

	decryptedBytes, err := cipherObject.Open(nil, nonce[:], inputBytes, additionalDataParameter)
	if (err != nil) {
		return false, nil, nil
	}

	paddingHeader := decryptedBytes[:4]

	paddingBytesUint32 := binary.LittleEndian.Uint32(paddingHeader[:])

	endOfPaddingIndex := 4 + paddingBytesUint32

	compressedBytes := decryptedBytes[endOfPaddingIndex:]
	compressedBytesReader := bytes.NewReader(compressedBytes)

	decompressedReader, err := zlib.NewReader(compressedBytesReader)
	if (err != nil) {
		return false, nil, nil
	}

	decompressedBytes, err := io.ReadAll(decompressedReader)
	if (err != nil) {
		return false, nil, nil
	}

	return true, decompressedBytes, nil
}


