#!/bin/sh

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

set -eu

SQUINCH_VERSION=0.1


printHelp( )
{
	cat <<'HELP'

USAGE

    unsquinch <input-file|-> <output-file|->


OPTIONS

    -h, -help, --help    Show this help page.

    --version            Print the version.

    Use '--' to mark the end of options.


NOTES

    '-' reads standard input or writes standard output.
HELP
}

addPositional( )
{
	if [ "$positionalCount" -eq 0 ]
	then
		inputPath=$1
	elif [ "$positionalCount" -eq 1 ]
	then
		outputPath=$1
	else
		unexpectedArgument=$1
	fi
	positionalCount=$((positionalCount + 1))
}

if [ "$#" -eq 1 ] && [ "$1" = "--version" ]
then
	printf '%s\n' "$SQUINCH_VERSION"
	exit 0
fi

positionalCount=0
inputPath=
outputPath=
unexpectedArgument=
while [ "$#" -gt 0 ]
do
	case "$1" in
	-h|-help|--help)
		printHelp
		exit 0
		;;
	--)
		shift
		while [ "$#" -gt 0 ]
		do
			addPositional "$1"
			shift
		done
		;;
	*)
		addPositional "$1"
		shift
		;;
	esac
done

if [ "$positionalCount" -eq 0 ]
then
	printf '%s: missing input file\n' "$0" >&2
	exit 1
fi
if [ "$positionalCount" -eq 1 ]
then
	printf '%s: missing output file\n' "$0" >&2
	exit 1
fi
if [ "$positionalCount" -gt 2 ]
then
	printf '%s: unexpected argument: %s\n' "$0" "$unexpectedArgument" >&2
	exit 1
fi

if [ "$inputPath" != "-" ] && [ ! -f "$inputPath" ]
then
	printf '%s: input is not a regular file: %s\n' "$0" "$inputPath" >&2
	exit 1
fi

dumpInput( )
{
	if [ "$inputPath" = "-" ]
	then
		od -An -tu1 -v
	else
		od -An -tu1 -v -- "$inputPath"
	fi
}

awkProgram=$(cat <<'AWK'
function fail( message )
{
	error = 1
	exit 1
}

function append( byte )
{
	ring[ringWritten % windowSize] = byte
	ringWritten += 1
	if (ringLength < windowSize) ringLength += 1
}

function writeByte( byte, addToRing )
{
	printf "%c", byte
	lastByte = byte
	hasLastByte = 1
	if (addToRing) append(byte)
}

function copyBytes( offset, copyLength, a, b, c )
{
	if (offset >= ringLength) fail("invalid copy offset")
	for (b = 0; b < copyLength; b += 1) {
		a = ringWritten - offset - 1
		writeByte(ring[a % windowSize], 1)
	}
}

function finishInstruction( )
{
	if (instruction == 245) {
		if (!hasLastByte) fail("RLE without a previous byte")
		if (parameter[1] == 1) {
			outputLength = parameter[2] * 256 + parameter[3]
			if (outputLength == 0) fail("invalid extended RLE length")
			outputLength += 255
		} else {
			outputLength = parameter[1]
			if (outputLength < 3) fail("invalid RLE length")
		}
		for (copyIndex = 0; copyIndex < outputLength; copyIndex += 1) {
			writeByte(lastByte, 0)
		}
	} else if (instruction == 246) {
		if (parameter[1] >= 249) {
			writeByte(parameter[1] - 249 + 245, 1)
		} else {
			copyBytes(parameter[1] + 1, 3)
		}
	} else if ((instruction >= 247) && (instruction <= 250)) {
		copyBytes(parameter[1] + 1, instruction - 243)
	} else if (selector >= 8) {
		copyBytes(parameter[2] + 1, selector)
	} else if (selector == 1) {
		value = parameter[2] * 65536 + parameter[3] * 256 + parameter[4]
		outputLength = int(value / 4096)
		if (outputLength == 0) fail("invalid medium copy length")
		copyBytes(value % 4096 + 1, outputLength)
	} else if (selector == 2) {
		if (parameter[2] == 0) fail("invalid full copy length")
		copyBytes(parameter[3] * 256 + parameter[4] + 1, parameter[2])
	} else if (selector == 3) {
		outputLength = parameter[2] * 256 + parameter[3]
		if (outputLength == 0) fail("invalid extended full copy length")
		copyBytes(parameter[4] * 256 + parameter[5] + 1,
			outputLength + 255)
	} else {
		fail("invalid copy selector")
	}
	state = "instruction"
}

function readInstruction( byte )
{
	if ((byte <= 244) || (byte >= 252)) {
		writeByte(byte, 1)
	} else if (byte == 245) {
		instruction = byte
		parameterLength = 0
		state = "rle"
	} else if (byte == 246) {
		instruction = byte
		parameterCount = 1
		parameterLength = 0
		state = "parameters"
	} else if (byte <= 250) {
		instruction = byte
		parameterCount = 1
		parameterLength = 0
		state = "parameters"
	} else {
		instruction = byte
		state = "selector"
	}
}

function readByte( byte )
{
	if (state == "header") {
		headerLength += 1
		header[headerLength] = byte
		if (headerLength != 16) return
		if ((header[1] != 83) || (header[2] != 81) || (header[3] != 85) \
			|| (header[4] != 73) || (header[5] != 78) || (header[6] != 67) \
			|| (header[7] != 72) || (header[8] != 1) || (header[11] != 0)) {
			fail("invalid Squinch header")
		}
		windowSize = header[9] * 256 + header[10]
		if (windowSize == 0) windowSize = 65536
		if (windowSize < 16) fail("invalid history window")
		state = "instruction"
	} else if (state == "instruction") {
		readInstruction(byte)
	} else if (state == "rle") {
		parameter[1] = byte
		if (byte == 1) {
			parameterCount = 3
			parameterLength = 1
			state = "parameters"
		} else {
			finishInstruction()
		}
	} else if (state == "selector") {
		selector = byte
		if (selector >= 8) parameterCount = 2
		else if (selector == 1) parameterCount = 4
		else if (selector == 2) parameterCount = 4
		else if (selector == 3) parameterCount = 5
		else fail("invalid copy selector")
		parameter[1] = selector
		parameterLength = 1
		state = "parameters"
	} else {
		parameterLength += 1
		parameter[parameterLength] = byte
		if (parameterLength == parameterCount) finishInstruction()
	}
}

BEGIN {
	state = "header"
}

{
	for (field = 1; field <= NF; field += 1) readByte($field)
}

END {
	if (!error && (state != "instruction")) fail("truncated stream")
}
AWK
)

if [ "$outputPath" = "-" ]
then
	if ! dumpInput | awk "$awkProgram"
	then
		printf '%s: invalid Squinch stream\n' "$0" >&2
		exit 1
	fi
elif ! dumpInput | awk "$awkProgram" \
	> "$outputPath"
then
	printf '%s: invalid Squinch stream\n' "$0" >&2
	exit 1
fi
