/*
 * Copyright 2026 CodingMarkus
 *
 * SPDX-License-Identifier: AGPL-3.0-or-later OR Apache-2.0
 */

#include "compress.h"

#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

static const size_t MINIMUM_WINDOW_SIZE = 16;
static const size_t MAXIMUM_WINDOW_SIZE = 65536;
static const size_t NO_POSITION         = (size_t)-1;
static const size_t MAXIMUM_RLE_LENGTH  = 65790;

#define COPY_CLASS_COUNT 4

static const uint_least64_t MAXIMUM_UNCOMPRESSED_SIZE =
	((uint_least64_t)1 << 40) - 1;

static const unsigned char MAXIMUM_BYTE_VALUE = 0xFF;

static const unsigned char RLE_INSTRUCTION           = 0xF5;
static const unsigned char COPY_3_INSTRUCTION        = 0xF6;
static const unsigned char COPY_VARIABLE_INSTRUCTION = 0xFB;
static const unsigned char EXTENDED_RLE_LENGTH       = 0x01;


struct Ring {
	unsigned char * bytes;
	size_t first;
	size_t length;
	size_t size;
};

struct Input {
	unsigned char * bytes;
	size_t first;
	size_t length;
	size_t size;
};

struct Output {
	unsigned char * bytes;
	size_t length;
	size_t size;
};

struct Matcher {
	size_t * head;
	size_t * previous;
	size_t bucketCount;
	size_t position;
	size_t size;
};


static
size_t ringIndex( const struct Ring * ring, size_t offset )
{
	return ((ring->first + offset) % ring->size);
}


static
unsigned char ringByte( const struct Ring * ring, size_t offset )
{
	return (ring->bytes[ringIndex(ring, offset)]);
}


static
size_t inputIndex( const struct Input * input, size_t offset )
{
	size_t endLength = input->size - input->first;
	return (offset < endLength ? input->first + offset : offset - endLength);
}


static
unsigned char inputByte( const struct Input * input, size_t offset )
{
	return (input->bytes[inputIndex(input, offset)]);
}


static
size_t inputFill(
	struct Input * input,
	const unsigned char * bytes,
	size_t length )
{
	size_t space = input->size - input->length;
	if (length > space) length = space;
	if (length == 0) return 0;
	size_t offset = inputIndex(input, input->length);
	size_t firstLength = input->size - offset;
	if (firstLength > length) firstLength = length;
	memcpy(input->bytes + offset, bytes, firstLength);
	memcpy(input->bytes, bytes + firstLength, length - firstLength);
	input->length += length;
	return length;
}


static
void ringAppend( struct Ring * ring, unsigned char byte )
{
	if (ring->length < ring->size) {
		ring->bytes[ringIndex(ring, ring->length)] = byte;
		ring->length += 1;
		return;
	}
	ring->bytes[ring->first] = byte;
	ring->first = (ring->first + 1) % ring->size;
}


static
size_t hashBytes(
	const unsigned char * bytes,
	size_t bucketCount )
{
	return (
		(
			((size_t)bytes[0] * 251U)
			^ ((size_t)bytes[1] * 67U)
			^ (size_t)bytes[2]
		) % bucketCount
	);
}


static
size_t hashInput(
	const struct Input * input,
	size_t bucketCount )
{
	unsigned char bytes[3] = { 0, 0, 0 };

	bytes[0] = inputByte(input, 0);
	bytes[1] = inputByte(input, 1);
	bytes[2] = inputByte(input, 2);
	return (hashBytes(bytes, bucketCount));
}


static
size_t hashRingEnd(
	const struct Ring * ring,
	size_t bucketCount )
{
	unsigned char bytes[3] = { 0, 0, 0 };

	bytes[0] = ringByte(ring, ring->length - 3);
	bytes[1] = ringByte(ring, ring->length - 2);
	bytes[2] = ringByte(ring, ring->length - 1);
	return (hashBytes(bytes, bucketCount));
}


static
int matcherInitialize(
	struct Matcher * matcher,
	size_t size,
	size_t bucketCount )
{
	size_t index = 0;

	if (bucketCount > SIZE_MAX / sizeof(*matcher->head)) return -1;
	matcher->head = malloc(bucketCount * sizeof(*matcher->head));
	matcher->previous = malloc(size * sizeof(*matcher->previous));
	if ((matcher->head == NULL) || (matcher->previous == NULL)) {
		free(matcher->head);
		free(matcher->previous);
		matcher->head = NULL;
		matcher->previous = NULL;
		return -1;
	}
	while (index < bucketCount) {
		matcher->head[index] = NO_POSITION;
		index += 1;
	}
	index = 0;
	while (index < size) {
		matcher->previous[index] = NO_POSITION;
		index += 1;
	}
	matcher->bucketCount = bucketCount;
	matcher->position = 0;
	matcher->size = size;
	return 0;
}


static
void matcherAppend(
	struct Matcher * matcher, const struct Ring * ring )
{
	if (matcher->position >= 2) {
		size_t position = matcher->position - 2;
		size_t hash = hashRingEnd(ring, matcher->bucketCount);
		matcher->previous[position % matcher->size] = matcher->head[hash];
		matcher->head[hash] = position;
	}
	matcher->position += 1;
}


static
void historyAppend(
	struct Ring * ring,
	struct Matcher * matcher,
	unsigned char byte )
{
	ringAppend(ring, byte);
	matcherAppend(matcher, ring);
}


static
size_t copyLength(
	const struct Ring * ring,
	const struct Input * input,
	size_t offset,
	size_t maximum );


static
void saveMatch(
	size_t length,
	size_t offset,
	size_t * savedLength,
	size_t * savedOffset )
{
	if (length <= *savedLength) return;
	*savedLength = length;
	*savedOffset = offset;
}


static
void matcherFind(
	const struct Matcher * matcher,
	const struct Ring * ring,
	const struct Input * input,
	size_t matchLengths[COPY_CLASS_COUNT],
	size_t copyOffsets[COPY_CLASS_COUNT],
	bool * mayContinue,
	size_t matchLimit )
{
	if ((input->length < 3) || (ring->length < 3)) return;
	size_t candidate = matcher->head[
		hashInput(input, matcher->bucketCount)
	];
	size_t checked = 0;
	size_t oldest = matcher->position - ring->length;
	while ((candidate != NO_POSITION) && (checked < matchLimit)) {
		if (candidate < oldest) break;
		size_t offset = candidate - oldest;
		size_t copyOffset = ring->length - offset - 1;
		size_t length = copyLength(
			ring, input, offset, MAXIMUM_RLE_LENGTH
		);
		if ((length == input->length) && (length < MAXIMUM_RLE_LENGTH))
		{
			*mayContinue = true;
		}
		if ((length >= 3) && (copyOffset >= 1) && (copyOffset <= 249)) {
			saveMatch(3, copyOffset, &matchLengths[0], &copyOffsets[0]);
		}
		if ((length >= 4) && (copyOffset >= 1) && (copyOffset <= 256)) {
			size_t fixedLength = length > 7 ? 7 : length;

			saveMatch(fixedLength, copyOffset,
				&matchLengths[0], &copyOffsets[0]);
		}
		if ((length >= 8) && (copyOffset >= 1)
			&& (copyOffset <= 256))
		{
			size_t recentLength = length > MAXIMUM_BYTE_VALUE
				? MAXIMUM_BYTE_VALUE : length;

			saveMatch(recentLength, copyOffset,
				&matchLengths[1], &copyOffsets[1]);
		}
		if ((copyOffset >= 1) && (copyOffset <= 4096)) {
			size_t mediumLength = length > 4095 ? 4095 : length;

			saveMatch(mediumLength, copyOffset,
				&matchLengths[2], &copyOffsets[2]);
		}
		if ((copyOffset >= 1) && (length <= MAXIMUM_BYTE_VALUE)) {
			saveMatch(length, copyOffset,
				&matchLengths[2], &copyOffsets[2]);
		} else if (copyOffset >= 1) {
			saveMatch(length, copyOffset,
				&matchLengths[3], &copyOffsets[3]);
		}
		candidate = matcher->previous[candidate % matcher->size];
		checked += 1;
	}
}


static
void inputConsume( struct Input * input, size_t length )
{
	input->first = inputIndex(input, length);
	input->length -= length;
}


static
size_t copyLength(
	const struct Ring * ring,
	const struct Input * input,
	size_t offset,
	size_t maximum )
{
	if ((offset >= ring->length) || (maximum < 3)) return 0;
	size_t distance = ring->length - offset;
	if (maximum > input->length) maximum = input->length;
	if ((maximum < 3)
		|| (inputByte(input, 0) != ringByte(ring, offset))
		|| (inputByte(input, 1) != ringByte(ring,
			offset + 1))
		|| (inputByte(input, 2) != ringByte(ring,
			offset + 2)))
	{
		return 0;
	}
	size_t length = 3;
	while (length < maximum) {
		unsigned char sourceByte = length < distance
			? ringByte(ring, offset + length)
			: inputByte(input, length - distance);

		if (inputByte(input, length) != sourceByte) break;
		length += 1;
	}
	return length;
}


static
int writeByte( struct Output * output, unsigned char byte )
{
	if (output->length == output->size) return -1;
	output->bytes[output->length] = byte;
	output->length += 1;
	return 0;
}


static
int writeLiteral(
	struct Output * output,
	unsigned char byte )
{
	if ((byte >= RLE_INSTRUCTION) && (byte <= COPY_VARIABLE_INSTRUCTION)) {
		return (
			((writeByte(output, COPY_3_INSTRUCTION) != 0)
			|| (writeByte(output, (unsigned char)(byte - RLE_INSTRUCTION
				+ 249)) != 0)) ? -1 : 0
		);
	}
	return (writeByte(output, byte));
}


static
int writeCopy(
	struct Output * output, size_t offset, size_t length )
{
	if (offset == 0) return -1;
	if ((length == 3) && (offset >= 1) && (offset <= 249)) {
		return (
			((writeByte(output, COPY_3_INSTRUCTION) != 0)
			|| (writeByte(output, (unsigned char)(offset - 1)) != 0)) ? -1 : 0
		);
	}
	if ((length >= 4) && (length <= 7)
		&& (offset >= 1) && (offset <= 256))
	{
		return (
			((writeByte(
				output,
				(unsigned char)(COPY_3_INSTRUCTION + length - 3)
			) != 0)
			|| (writeByte(output, (unsigned char)(offset - 1)) != 0)) ? -1 : 0
		);
	}
	if ((length >= 8) && (length <= MAXIMUM_BYTE_VALUE)
		&& (offset >= 1) && (offset <= 256))
	{
		return (
			((writeByte(output, COPY_VARIABLE_INSTRUCTION) != 0)
			|| (writeByte(output, (unsigned char)length) != 0)
			|| (writeByte(output, (unsigned char)(offset - 1)) != 0)) ? -1 : 0
		);
	}
	if ((length <= 4095) && (offset >= 1) && (offset <= 4096)) {
		uint_fast32_t value = (uint_fast32_t)(
			(length << 12) | (offset - 1)
		);

		return (
			((writeByte(output, COPY_VARIABLE_INSTRUCTION) != 0)
			|| (writeByte(output, 1) != 0)
			|| (writeByte(output, (unsigned char)(value >> 16)) != 0)
			|| (writeByte(output, (unsigned char)(value >> 8)) != 0)
			|| (writeByte(output, (unsigned char)value) != 0)) ? -1 : 0
		);
	}
	if (length <= MAXIMUM_BYTE_VALUE) {
		offset -= 1;
		return (
			((writeByte(output, COPY_VARIABLE_INSTRUCTION) != 0)
			|| (writeByte(output, 2) != 0)
			|| (writeByte(output, (unsigned char)length) != 0)
			|| (writeByte(output, (unsigned char)(offset >> 8)) != 0)
			|| (writeByte(output, (unsigned char)offset) != 0)) ? -1 : 0
		);
	}
	offset -= 1;
	return (
		((writeByte(output, COPY_VARIABLE_INSTRUCTION) != 0)
		|| (writeByte(output, 3) != 0)
		|| (writeByte(output, (unsigned char)((length - 255) >> 8)) != 0)
		|| (writeByte(output, (unsigned char)(length - 255)) != 0)
		|| (writeByte(output, (unsigned char)(offset >> 8)) != 0)
		|| (writeByte(output, (unsigned char)offset) != 0)) ? -1 : 0
	);
}


static
size_t literalCost(
	const struct Input * input,
	size_t length )
{
	size_t cost = 0;
	size_t index = 0;
	while (index < length) {
		unsigned char byte = inputByte(input, index);
		cost += ((byte >= RLE_INSTRUCTION)
			&& (byte <= COPY_VARIABLE_INSTRUCTION)) ? 2 : 1;
		index += 1;
	}
	return cost;
}


static
int compressAction(
	struct Ring * ring,
	struct Matcher * matcher,
	struct Input * input,
	struct Output * output,
	size_t matchLimit,
	unsigned char * lastByte,
	bool * hasLastByte,
	bool mayRefill )
{
	if (input->length != 0) {
		static const size_t copyCosts[COPY_CLASS_COUNT] = { 2, 3, 5, 6 };
		size_t copyLengths[COPY_CLASS_COUNT] = { 0, 0, 0, 0 };
		size_t copyOffsets[COPY_CLASS_COUNT] = { 0, 0, 0, 0 };
		size_t copyOffset = 0;
		size_t length = 0;
		size_t actionLength = 1;
		size_t bestSavings = 0;
		unsigned char action = 0;
		bool mayContinue = false;

		if (*hasLastByte && (inputByte(input, 0) == *lastByte)) {
			length = 0;
			while ((length < input->length)
				&& (length < MAXIMUM_RLE_LENGTH)
				&& (inputByte(input, length) == *lastByte))
			{
				length += 1;
			}
			if ((length == input->length)
				&& (length < MAXIMUM_RLE_LENGTH))
			{
				mayContinue = true;
			}
			if (length >= 3) {
				action = RLE_INSTRUCTION;
				actionLength = length;
			}
		}
		if (action != RLE_INSTRUCTION) {
			matcherFind(matcher, ring, input, copyLengths, copyOffsets,
				&mayContinue, matchLimit);
		}
		if (mayRefill && mayContinue) return 1;
		{
			size_t copyClass = 0;

			while (copyClass < COPY_CLASS_COUNT) {
				if (copyLengths[copyClass] >= 3) {
					size_t cost = copyCosts[copyClass];
					size_t literals = literalCost(
						input, copyLengths[copyClass]
					);
					size_t savings = literals > cost
						? literals - cost : 0;

					if (savings > bestSavings) {
						action = COPY_3_INSTRUCTION;
						actionLength = copyLengths[copyClass];
						copyOffset = copyOffsets[copyClass];
						bestSavings = savings;
					}
				}
				copyClass += 1;
			}
		}
		if (action == 0) {
			unsigned char byte = inputByte(input, 0);
			if (writeLiteral(output, byte) != 0) return -1;
			historyAppend(ring, matcher, byte);
			*lastByte = byte;
			*hasLastByte = true;
		} else if (action == RLE_INSTRUCTION) {
			if ((writeByte(output, RLE_INSTRUCTION) != 0)
				|| ((actionLength <= MAXIMUM_BYTE_VALUE)
					? writeByte(output, (unsigned char)actionLength)
					: ((writeByte(output, EXTENDED_RLE_LENGTH) != 0)
						|| (writeByte(
							output, (unsigned char)((actionLength - 255) >> 8)
						) != 0)
						|| (writeByte(
							output, (unsigned char)(actionLength - 255)
						) != 0))))
			{
				return -1;
			}
		} else if (action == COPY_3_INSTRUCTION) {
			if (writeCopy(output, copyOffset, actionLength) != 0) return -1;
		}
		if ((action != 0) && (action != RLE_INSTRUCTION)) {
			size_t index = 0;

			*lastByte = inputByte(input, actionLength - 1);
			*hasLastByte = true;
			while (index < actionLength) {
				historyAppend(ring, matcher, inputByte(input, index));
				index += 1;
			}
		}
		inputConsume(input, actionLength);
	}
	return 0;
}


enum SquinchResult SquinchCompress(
	const void * inputBytes,
	size_t inputSize,
	void * outputBytes,
	size_t outputSize,
	size_t * outBytesWritten,
	size_t windowSize,
	size_t matchLimit,
	size_t bucketCount )
{
	struct Ring ring = { NULL, 0, 0, 0 };
	struct Input input = { NULL, 0, 0, 0 };
	struct Matcher matcher = { NULL, NULL, 0, 0, 0 };
	struct Output output = { NULL, 0, 0 };
	unsigned char lastByte = 0;
	bool hasLastByte = false;
	uint_least64_t uncompressedSize = inputSize;
	enum SquinchResult result = OutputBufferIsNULL_SquinchResult;

	if ((inputBytes == NULL) && (inputSize != 0))
		return InputBufferIsNULL_SquinchResult;
	if ((outputBytes == NULL) && (outputSize != 0))
		return OutputBufferIsNULL_SquinchResult;
	if (outBytesWritten == NULL)
		return OutBytesWrittenIsNULL_SquinchResult;
	if ((windowSize < MINIMUM_WINDOW_SIZE)
		|| (windowSize > MAXIMUM_WINDOW_SIZE))
	{
		return WindowSizeOutOfRange_SquinchResult;
	}
	if (matchLimit == 0) matchLimit = SQUINCH_DEFAULT_MATCH_LIMIT;
	if (matchLimit > SQUINCH_MAXIMUM_MATCH_LIMIT)
		return MatchLimitOutOfRange_SquinchResult;
	if (bucketCount == 0) bucketCount = SQUINCH_DEFAULT_BUCKET_COUNT;
	if (bucketCount < SQUINCH_MINIMUM_BUCKET_COUNT)
		return BucketCountOutOfRange_SquinchResult;
	*outBytesWritten = 0;
	ring.bytes = malloc(windowSize);
	if (ring.bytes == NULL) return OutOfMemory_SquinchResult;
	ring.first = 0;
	ring.length = 0;
	ring.size = windowSize;
	input.bytes = (unsigned char *)inputBytes;
	input.first = 0;
	input.length = inputSize;
	input.size = inputSize;
	output.bytes = outputBytes;
	output.length = 0;
	output.size = outputSize;
	if (matcherInitialize(&matcher, windowSize, bucketCount) != 0) {
		result = OutOfMemory_SquinchResult;
		goto out;
	}
	if (uncompressedSize > MAXIMUM_UNCOMPRESSED_SIZE) uncompressedSize = 0;
	if ((writeByte(&output, 'S') != 0)
		|| (writeByte(&output, 'Q') != 0)
		|| (writeByte(&output, 'U') != 0)
		|| (writeByte(&output, 'I') != 0)
		|| (writeByte(&output, 'N') != 0)
		|| (writeByte(&output, 'C') != 0)
		|| (writeByte(&output, 'H') != 0)
		|| (writeByte(&output, 1) != 0)
		|| (writeByte(
			&output, (unsigned char)(windowSize >> 8)
		) != 0)
		|| (writeByte(&output, (unsigned char)windowSize) != 0)
		|| (writeByte(
			&output, (unsigned char)(uncompressedSize >> 40)
		) != 0)
		|| (writeByte(
			&output, (unsigned char)(uncompressedSize >> 32)
		) != 0)
		|| (writeByte(
			&output, (unsigned char)(uncompressedSize >> 24)
		) != 0)
		|| (writeByte(
			&output, (unsigned char)(uncompressedSize >> 16)
		) != 0)
		|| (writeByte(
			&output, (unsigned char)(uncompressedSize >> 8)
		) != 0)
		|| (writeByte(&output, (unsigned char)uncompressedSize) != 0))
	{
		goto out;
	}
	while (input.length != 0) {
		if (compressAction(&ring, &matcher, &input, &output, matchLimit,
			&lastByte, &hasLastByte, false) != 0)
		{
			goto out;
		}
	}
	result = Success_SquinchResult;
out:
	*outBytesWritten = output.length;
	free(matcher.head);
	free(matcher.previous);
	free(ring.bytes);
	return result;
}


struct SquinchCompressor {
	struct Ring ring;
	struct Input lookAhead;
	struct Matcher matcher;
	size_t windowSize;
	uint_least64_t uncompressedSize;
	size_t matchLimit;
	unsigned char lastByte;
	unsigned char pendingOutput[6];
	size_t pendingOutputLength;
	size_t pendingOutputOffset;
	size_t headerOffset;
	bool hasLastByte;
	bool headerWritten;
	bool ending;
	bool finished;
	bool drained;
};


enum SquinchResult SquinchCompressorCreate(
	struct SquinchCompressor ** outCompressor,
	uint_least64_t uncompressedSize,
	size_t windowSize,
	size_t lookAheadSize,
	size_t bucketCount,
	size_t matchLimit )
{
	if (outCompressor == NULL) return CompressorIsNULL_SquinchResult;
	if ((windowSize < MINIMUM_WINDOW_SIZE)
		|| (windowSize > MAXIMUM_WINDOW_SIZE))
	{
		return WindowSizeOutOfRange_SquinchResult;
	}
	if (lookAheadSize == 0) {
		lookAheadSize = windowSize < SQUINCH_DEFAULT_LOOK_AHEAD_SIZE
			? windowSize : SQUINCH_DEFAULT_LOOK_AHEAD_SIZE;
	}
	if (matchLimit == 0) matchLimit = SQUINCH_DEFAULT_MATCH_LIMIT;
	if (matchLimit > SQUINCH_MAXIMUM_MATCH_LIMIT)
		return MatchLimitOutOfRange_SquinchResult;
	if (bucketCount == 0) bucketCount = SQUINCH_DEFAULT_BUCKET_COUNT;
	if (bucketCount < SQUINCH_MINIMUM_BUCKET_COUNT)
		return BucketCountOutOfRange_SquinchResult;
	struct SquinchCompressor * result = calloc(1, sizeof(*result));
	if (result == NULL) return OutOfMemory_SquinchResult;
	result->ring.bytes = malloc(windowSize);
	result->lookAhead.bytes = malloc(lookAheadSize);
	if ((result->ring.bytes == NULL)
		|| (result->lookAhead.bytes == NULL)
		|| (matcherInitialize(&result->matcher, windowSize, bucketCount) != 0))
	{
		free(result->ring.bytes);
		free(result->lookAhead.bytes);
		free(result);
		return OutOfMemory_SquinchResult;
	}
	result->ring.size = windowSize;
	result->lookAhead.size = lookAheadSize;
	result->windowSize = windowSize;
	result->uncompressedSize = uncompressedSize;
	result->matchLimit = matchLimit;
	*outCompressor = result;
	return Success_SquinchResult;
}


static
void compressorWriteHeader(
	struct SquinchCompressor * compressor,
	struct Output * output )
{
	unsigned char header[16] = { 0 };

	if (compressor->headerWritten) return;
	uint_least64_t size = compressor->uncompressedSize;
	if (size > MAXIMUM_UNCOMPRESSED_SIZE) size = 0;
	header[0] = 'S';
	header[1] = 'Q';
	header[2] = 'U';
	header[3] = 'I';
	header[4] = 'N';
	header[5] = 'C';
	header[6] = 'H';
	header[7] = 1;
	header[8] = (unsigned char)(compressor->windowSize >> 8);
	header[9] = (unsigned char)compressor->windowSize;
	header[10] = (unsigned char)(size >> 40);
	header[11] = (unsigned char)(size >> 32);
	header[12] = (unsigned char)(size >> 24);
	header[13] = (unsigned char)(size >> 16);
	header[14] = (unsigned char)(size >> 8);
	header[15] = (unsigned char)size;
	size_t length = 16 - compressor->headerOffset;
	if (length > output->size - output->length) {
		length = output->size - output->length;
	}
	if (length != 0) {
		memcpy(output->bytes + output->length,
			header + compressor->headerOffset, length);
		output->length += length;
		compressor->headerOffset += length;
	}
	if (compressor->headerOffset == 16) compressor->headerWritten = true;
}


static
void compressorWritePending(
	struct SquinchCompressor * compressor,
	struct Output * output )
{
	size_t length = compressor->pendingOutputLength
		- compressor->pendingOutputOffset;
	if (length > output->size - output->length) {
		length = output->size - output->length;
	}
	if (length != 0) {
		memcpy(output->bytes + output->length,
			compressor->pendingOutput + compressor->pendingOutputOffset,
			length);
		output->length += length;
		compressor->pendingOutputOffset += length;
	}
	if (compressor->pendingOutputOffset
		== compressor->pendingOutputLength)
	{
		compressor->pendingOutputOffset = 0;
		compressor->pendingOutputLength = 0;
	}
}


static
int compressorEncodeAction( struct SquinchCompressor * compressor )
{
	struct Output output = {
		compressor->pendingOutput, 0, sizeof(compressor->pendingOutput)
	};
	int result = compressAction(&compressor->ring, &compressor->matcher,
		&compressor->lookAhead, &output, compressor->matchLimit,
		&compressor->lastByte,
		&compressor->hasLastByte,
		!compressor->ending
			&& (compressor->lookAhead.length < compressor->lookAhead.size)
	);
	compressor->pendingOutputLength = output.length;
	return result;
}


enum SquinchResult SquinchCompressorStep(
	struct SquinchCompressor * compressor,
	const void * input,
	size_t inputSize,
	size_t * outInputBytesConsumed,
	void * outputBytes,
	size_t outputSize,
	size_t * outOutputBytesWritten )
{
	const unsigned char * inputBytes = input;
	size_t inputOffset = 0;

	if (compressor == NULL) return CompressorIsNULL_SquinchResult;
	if (outInputBytesConsumed == NULL)
		return InputBytesConsumedIsNULL_SquinchResult;
	if (outOutputBytesWritten == NULL)
		return OutputBytesWrittenIsNULL_SquinchResult;
	if ((input == NULL) && (inputSize != 0))
		return InputBufferIsNULL_SquinchResult;
	if ((outputBytes == NULL) && (outputSize != 0))
		return OutputBufferIsNULL_SquinchResult;
	if ((compressor->ending || compressor->finished) && (inputSize != 0))
		return InputAfterEndOfData_SquinchResult;
	if (compressor->drained) return NoMoreOutputAvailable_SquinchResult;
	*outInputBytesConsumed = 0;
	*outOutputBytesWritten = 0;
	if (compressor->finished) {
		compressor->drained = true;
		return Success_SquinchResult;
	}
	if (inputSize == 0) compressor->ending = true;
	struct Output output = { outputBytes, 0, outputSize };
	for (;;) {
		size_t length = 0;

		compressorWriteHeader(compressor, &output);
		if (compressor->headerWritten) {
			compressorWritePending(compressor, &output);
		}
		if (compressor->pendingOutputLength != 0) break;
		if (inputOffset < inputSize) {
			length = inputFill(&compressor->lookAhead,
				inputBytes + inputOffset, inputSize - inputOffset);
		}
		inputOffset += length;
		if (compressor->lookAhead.length == 0) {
			if (compressor->ending && compressor->headerWritten) {
				compressor->finished = true;
			}
			break;
		}
		if (!compressor->ending && (compressor->lookAhead.length < 3)
			&& (compressor->lookAhead.length < compressor->lookAhead.size))
		{
			break;
		}
		if (compressorEncodeAction(compressor) == 1) {
			if (inputOffset == inputSize) break;
			continue;
		}
	}
	*outInputBytesConsumed = inputOffset;
	*outOutputBytesWritten = output.length;
	return Success_SquinchResult;
}


void SquinchCompressorDestroy( struct SquinchCompressor * compressor )
{
	if (compressor == NULL) return;
	free(compressor->ring.bytes);
	free(compressor->lookAhead.bytes);
	free(compressor->matcher.head);
	free(compressor->matcher.previous);
	free(compressor);
}
