/*
* Copyright 2016 Nu-book Inc.
* Copyright 2016 ZXing authors
*/
// SPDX-License-Identifier: Apache-2.0

#include "PDFDecoder.h"

#include "CharacterSet.h"
#include "DecoderResult.h"
#include "PDFCustomData.h"
#include "ZXAlgorithms.h"
#include "ZXBigInteger.h"
#include "ZXTestSupport.h"

#include <array>
#include <cassert>
#include <charconv>
#include <sstream>
#include <utility>

namespace ZXing::Pdf417 {

enum class Mode
{
	ALPHA,
	LOWER,
	MIXED,
	PUNCT,
	ALPHA_SHIFT,
	PUNCT_SHIFT
};

constexpr int TEXT_COMPACTION_MODE_LATCH = 900;
constexpr int BYTE_COMPACTION_MODE_LATCH = 901;
constexpr int NUMERIC_COMPACTION_MODE_LATCH = 902;
// 903-912 reserved
constexpr int MODE_SHIFT_TO_BYTE_COMPACTION_MODE = 913;
// 914-917 reserved
constexpr int LINKAGE_OTHER = 918;
// 919 reserved
constexpr int LINKAGE_EANUCC = 920; // GS1 Composite
constexpr int READER_INIT = 921; // Reader Initialisation/Programming
constexpr int MACRO_PDF417_TERMINATOR = 922;
constexpr int BEGIN_MACRO_PDF417_OPTIONAL_FIELD = 923;
constexpr int BYTE_COMPACTION_MODE_LATCH_6 = 924;
constexpr int ECI_USER_DEFINED = 925; // 810900-811799 (1 codeword)
constexpr int ECI_GENERAL_PURPOSE = 926; // 900-810899 (2 codewords)
constexpr int ECI_CHARSET = 927; // 0-899 (1 codeword)
constexpr int BEGIN_MACRO_PDF417_CONTROL_BLOCK = 928;

constexpr int MAX_NUMERIC_CODEWORDS = 15;

constexpr int MACRO_PDF417_OPTIONAL_FIELD_FILE_NAME = 0;
constexpr int MACRO_PDF417_OPTIONAL_FIELD_SEGMENT_COUNT = 1;
constexpr int MACRO_PDF417_OPTIONAL_FIELD_TIME_STAMP = 2;
constexpr int MACRO_PDF417_OPTIONAL_FIELD_SENDER = 3;
constexpr int MACRO_PDF417_OPTIONAL_FIELD_ADDRESSEE = 4;
constexpr int MACRO_PDF417_OPTIONAL_FIELD_FILE_SIZE = 5;
constexpr int MACRO_PDF417_OPTIONAL_FIELD_CHECKSUM = 6;

static const char* PUNCT_CHARS = ";<>@[\\]_`~!\r\t,:\n-.$/\"|*()?{}'";
static const char* MIXED_CHARS = "0123456789&\r\t,:#-.$/+%*=^";

constexpr int NUMBER_OF_SEQUENCE_CODEWORDS = 2;

inline bool IsECI(int code)
{
	return code >= ECI_USER_DEFINED && code <= ECI_CHARSET;
}

/**
* Whether a codeword terminates a Compaction mode.
*
* See ISO/IEC 15438:2015 5.4.2.5 (Text), 5.4.3.4 (Byte), 5.4.4.3 (Numeric)
*/
static bool TerminatesCompaction(int code)
{
	switch (code) {
	case TEXT_COMPACTION_MODE_LATCH:
	case BYTE_COMPACTION_MODE_LATCH:
	case NUMERIC_COMPACTION_MODE_LATCH:
	case BYTE_COMPACTION_MODE_LATCH_6:
	case BEGIN_MACRO_PDF417_CONTROL_BLOCK:
	case BEGIN_MACRO_PDF417_OPTIONAL_FIELD:
	case MACRO_PDF417_TERMINATOR: return true;
	}
	return false;
}

/**
* Helper to process ECIs.
**/
static int ProcessECI(const std::vector<int>& codewords, int codeIndex, const int length, const int code, Content& result)
{
	if (!IsECI(code))
		return codeIndex;

	if (codeIndex >= length)
		return codeIndex; // throw FormatError(); TODO: check why there are unit tests that expect this to be silently ignored

	if (code == ECI_CHARSET) {
		result.switchEncoding(ECI(codewords[codeIndex++]));
	} else {
		int paramCount = code == ECI_GENERAL_PURPOSE ? 2 : 1;
		if (codeIndex + paramCount > length)
			return codeIndex; // throw FormatError(); TODO: check why there are unit tests that expect this to be silently ignored
		codeIndex += paramCount; // Don't currently handle non-character set ECIs so just ignore
	}

	return codeIndex;
}

/**
* The Text Compaction mode includes all the printable ASCII characters
* (i.e. values from 32 to 126) and three ASCII control characters: HT or tab
* (ASCII value 9), LF or line feed (ASCII value 10), and CR or carriage
* return (ASCII value 13). The Text Compaction mode also includes various latch
* and shift characters which are used exclusively within the mode. The Text
* Compaction mode encodes up to 2 characters per codeword. The compaction rules
* for converting data into PDF417 codewords are defined in 5.4.2.2. The sub-mode
* switches are defined in 5.4.2.3.
*
* @param textCompactionData The text compaction data.
* @param length             The size of the text compaction data.
* @param result             The data in the character set encoding.
*/
static void DecodeTextCompaction(const std::vector<int>& textCompactionData, int length, Content& result)
{
	// Beginning from an initial state of the Alpha sub-mode
	// The default compaction mode for PDF417 in effect at the start of each symbol shall always be Text
	// Compaction mode Alpha sub-mode (uppercase alphabetic). A latch codeword from another mode to the Text
	// Compaction mode shall always switch to the Text Compaction Alpha sub-mode.
	Mode subMode = Mode::ALPHA;
	Mode priorToShiftMode = Mode::ALPHA;
	int i = 0;
	while (i < length) {
		int subModeCh = textCompactionData[i];

		// Note only have ECI and MODE_SHIFT_TO_BYTE_COMPACTION_MODE function codewords in text compaction array
		if (IsECI(subModeCh)) {
			i = ProcessECI(textCompactionData, i + 1, length, subModeCh, result);
			continue;
		}
		if (subModeCh == MODE_SHIFT_TO_BYTE_COMPACTION_MODE) {
			i++;
			while (i < length && IsECI(textCompactionData[i]))
				i = ProcessECI(textCompactionData, i + 1, length, textCompactionData[i], result);

			if (i < length)
				result.push_back((uint8_t)textCompactionData[i++]);

			continue;
		}

		char ch = 0;
		switch (subMode) {
		case Mode::ALPHA:
		case Mode::LOWER:
			// Alpha (uppercase alphabetic) or Lower (lowercase alphabetic)
			if (subModeCh < 26) {
				// Upper/lowercase character
				ch = (char)((subMode == Mode::ALPHA ? 'A' : 'a') + subModeCh);
			} else if (subModeCh == 26) { // Space
				ch = ' ';
			} else if (subModeCh == 27 && subMode == Mode::ALPHA) { // LL
				subMode = Mode::LOWER;
			} else if (subModeCh == 27 && subMode == Mode::LOWER) { // AS
				// Shift to alpha
				priorToShiftMode = subMode;
				subMode = Mode::ALPHA_SHIFT;
			} else if (subModeCh == 28) { // ML
				subMode = Mode::MIXED;
			}
			// 29 PS - ignore if last or followed by Shift to Byte, 5.4.2.4 (b) (1)
			else if (i + 1 < length && textCompactionData[i + 1] != MODE_SHIFT_TO_BYTE_COMPACTION_MODE) {
				// Shift to punctuation
				priorToShiftMode = subMode;
				subMode = Mode::PUNCT_SHIFT;
			}
			break;

		case Mode::MIXED:
			// Mixed (numeric and some punctuation)
			if (subModeCh < 25) {
				ch = MIXED_CHARS[subModeCh];
			} else if (subModeCh == 25) { // PL
				subMode = Mode::PUNCT;
			} else if (subModeCh == 26) { // Space
				ch = ' ';
			} else if (subModeCh == 27) { // LL
				subMode = Mode::LOWER;
			} else if (subModeCh == 28) { // AL
				subMode = Mode::ALPHA;
			}
			// 29 PS - ignore if last or followed by Shift to Byte, 5.4.2.4 (b) (1)
			else if (i + 1 < length && textCompactionData[i + 1] != MODE_SHIFT_TO_BYTE_COMPACTION_MODE) {
				// Shift to punctuation
				priorToShiftMode = subMode;
				subMode = Mode::PUNCT_SHIFT;
			}
			break;

		case Mode::PUNCT:
			// Punctuation
			if (subModeCh < 29)
				ch = PUNCT_CHARS[subModeCh];
			else // 29 AL - note not ignored if followed by Shift to Byte, 5.4.2.4 (b) (2)
				subMode = Mode::ALPHA;
			break;

		case Mode::ALPHA_SHIFT:
			// Restore sub-mode
			subMode = priorToShiftMode;
			if (subModeCh < 26)
				ch = (char)('A' + subModeCh);
			else if (subModeCh == 26) // Space
				ch = ' ';
			// 27 LL, 28 ML, 29 PS used as padding
			break;

		case Mode::PUNCT_SHIFT:
			// Restore sub-mode
			subMode = priorToShiftMode;
			if (subModeCh < 29)
				ch = PUNCT_CHARS[subModeCh];
			else // 29 AL
				subMode = Mode::ALPHA;
			break;
		}
		if (ch != 0)
			result.push_back(ch); // Append decoded character to result
		i++;
	}
}

/*
* Helper to put ECI codewords into Text Compaction array.
*/
static int ProcessTextECI(std::vector<int>& textCompactionData, int& index, const std::vector<int>& codewords, int codeIndex,
						  const int code)
{
	textCompactionData[index++] = code;
	if (codeIndex < codewords[0]) {
		textCompactionData[index++] = codewords[codeIndex++];
		if (codeIndex < codewords[0] && code == ECI_GENERAL_PURPOSE) {
			textCompactionData[index++] = codewords[codeIndex++];
		}
	}

	return codeIndex;
}

/**
* Text Compaction mode (see 5.4.1.5) permits all printable ASCII characters to be
* encoded, i.e. values 32 - 126 inclusive in accordance with ISO/IEC 646 (IRV), as
* well as selected control characters.
*
* @param codewords     The array of codewords (data + error)
* @param codeIndex     The current index into the codeword array.
* @param result        The data in the character set encoding.
* @return The next index into the codeword array.
*/
static int TextCompaction(const std::vector<int>& codewords, int codeIndex, Content& result)
{
	// 2 characters per codeword
	std::vector<int> textCompactionData((codewords[0] - codeIndex) * 2, 0);

	int index = 0;
	bool end = false;

	while ((codeIndex < codewords[0]) && !end) {
		int code = codewords[codeIndex++];
		if (code < TEXT_COMPACTION_MODE_LATCH) {
			textCompactionData[index] = code / 30;
			textCompactionData[index + 1] = code % 30;
			index += 2;
		} else {
			switch (code) {
			case MODE_SHIFT_TO_BYTE_COMPACTION_MODE:
				// The Mode Shift codeword 913 shall cause a temporary
				// switch from Text Compaction mode to Byte Compaction mode.
				// This switch shall be in effect for only the next codeword,
				// after which the mode shall revert to the prevailing sub-mode
				// of the Text Compaction mode. Codeword 913 is only available
				// in Text Compaction mode; its use is described in 5.4.2.4.
				textCompactionData[index++] = MODE_SHIFT_TO_BYTE_COMPACTION_MODE;
				// 5.5.3.1 allows ECIs anywhere in Text Compaction, including after a Shift to Byte
				while (codeIndex < codewords[0] && IsECI(codewords[codeIndex])) {
					codeIndex = ProcessTextECI(textCompactionData, index, codewords, codeIndex + 1, codewords[codeIndex]);
				}
				if (codeIndex < codewords[0])
					textCompactionData[index++] = codewords[codeIndex++]; // Byte to shift
				break;
			case ECI_CHARSET:
			case ECI_GENERAL_PURPOSE:
			case ECI_USER_DEFINED:
				codeIndex = ProcessTextECI(textCompactionData, index, codewords, codeIndex, code);
				break;
			default:
				if (!TerminatesCompaction(code))
					throw FormatError();

				codeIndex--;
				end = true;
				break;
			}
		}
	}
	DecodeTextCompaction(textCompactionData, index, result);
	return codeIndex;
}

/*
* Helper for Byte Compaction to look ahead and count 5-codeword batches and trailing bytes, with some checking of
* format errors.
*/
static int CountByteBatches(int mode, const std::vector<int>& codewords, int codeIndex, int& trailingCount)
{
	int count = 0;
	trailingCount = 0;

	while (codeIndex < codewords[0]) {
		int code = codewords[codeIndex++];
		if (code >= TEXT_COMPACTION_MODE_LATCH) {
			if (mode == BYTE_COMPACTION_MODE_LATCH_6 && count && count % 5)
				throw FormatError();

			if (IsECI(code)) {
				codeIndex += code == ECI_GENERAL_PURPOSE ? 2 : 1;
				continue;
			}
			if (!TerminatesCompaction(code))
				throw FormatError();
			break;
		}
		count++;
	}
	if (codeIndex > codewords[0])
		throw FormatError();

	if (count == 0)
		return 0;

	if (mode == BYTE_COMPACTION_MODE_LATCH) {
		trailingCount = count % 5;
		if (trailingCount == 0) {
			trailingCount = 5;
			count -= 5;
		}
	} else { // BYTE_COMPACTION_MODE_LATCH_6
		if (count % 5 != 0)
			throw FormatError();
	}

	return count / 5;
}

/*
* Helper to handle Byte Compaction ECIs.
*/
static int ProcessByteECIs(const std::vector<int>& codewords, int codeIndex, Content& result)
{
	while (codeIndex < codewords[0] && codewords[codeIndex] >= TEXT_COMPACTION_MODE_LATCH
			&& !TerminatesCompaction(codewords[codeIndex])) {
		int code = codewords[codeIndex++];
		if (!IsECI(code))
			throw FormatError();
		codeIndex = ProcessECI(codewords, codeIndex, codewords[0], code, result);
	}

	return codeIndex;
}

/**
* Byte Compaction mode (see 5.4.3) permits all 256 possible 8-bit byte values to be encoded.
* This includes all ASCII characters value 0 to 127 inclusive and provides for international
* character set support.
*
* @param mode          The byte compaction mode i.e. 901 or 924
* @param codewords     The array of codewords (data + error)
* @param codeIndex     The current index into the codeword array.
* @param result        The data in the character set encoding.
* @return The next index into the codeword array.
*/
static int ByteCompaction(int mode, const std::vector<int>& codewords, int codeIndex, Content& result)
{
	// Count number of 5-codeword batches and trailing bytes
	int trailingCount;
	int batches = CountByteBatches(mode, codewords, codeIndex, trailingCount);

	// Deal with initial ECIs
	codeIndex = ProcessByteECIs(codewords, codeIndex, result);

	for (int batch = 0; batch < batches; batch++) {
		int64_t value = 0;
		for (int count = 0; count < 5; count++) {
			// Per ISO/IEC 15438:2015 5.5.3.2, ECI shall not appear within a 5-codeword data group
			if (codeIndex >= codewords[0] || codewords[codeIndex] >= TEXT_COMPACTION_MODE_LATCH)
				throw FormatError();
			value = 900 * value + codewords[codeIndex++];
		}

		for (int j = 0; j < 6; ++j)
			result.push_back((uint8_t)(value >> (8 * (5 - j))));

		// Deal with inter-batch ECIs
		codeIndex = ProcessByteECIs(codewords, codeIndex, result);
	}

	for (int i = 0; i < trailingCount && codeIndex < codewords[0]; i++) {
		result.push_back((uint8_t)codewords[codeIndex++]);
		// Deal with inter-byte ECIs
		codeIndex = ProcessByteECIs(codewords, codeIndex, result);
	}

	return codeIndex;
}


/**
* Convert a list of Numeric Compacted codewords from Base 900 to Base 10.
*
* @param codewords The array of codewords
* @param count     The number of codewords
* @return The decoded string representing the Numeric data.
*/
/*
EXAMPLE
Encode the fifteen digit numeric string 000213298174000
Prefix the numeric string with a 1 and set the initial value of
t = 1 000 213 298 174 000
Calculate codeword 0
d0 = 1 000 213 298 174 000 mod 900 = 200

t = 1 000 213 298 174 000 div 900 = 1 111 348 109 082
Calculate codeword 1
d1 = 1 111 348 109 082 mod 900 = 282

t = 1 111 348 109 082 div 900 = 1 234 831 232
Calculate codeword 2
d2 = 1 234 831 232 mod 900 = 632

t = 1 234 831 232 div 900 = 1 372 034
Calculate codeword 3
d3 = 1 372 034 mod 900 = 434

t = 1 372 034 div 900 = 1 524
Calculate codeword 4
d4 = 1 524 mod 900 = 624

t = 1 524 div 900 = 1
Calculate codeword 5
d5 = 1 mod 900 = 1
t = 1 div 900 = 0
Codeword sequence is: 1, 624, 434, 632, 282, 200

Decode the above codewords involves
1 x 900 power of 5 + 624 x 900 power of 4 + 434 x 900 power of 3 +
632 x 900 power of 2 + 282 x 900 power of 1 + 200 x 900 power of 0 = 1000213298174000

Remove leading 1 =>  Result is 000213298174000
*/
static std::string DecodeBase900toBase10(const std::vector<int>& codewords, int endIndex, int count)
{
	// Table containing values for the exponent of 900.
	static const auto EXP900 = []() {
		std::array<BigInteger, 16> table = {1, 900};
		for (size_t i = 2; i < table.size(); ++i)
			table[i] = table[i - 1] * 900;
		return table;
	}();

	assert(count <= 16);

	BigInteger result;
	for (int i = 0; i < count; i++)
		result += EXP900[count - i - 1] * codewords[endIndex - count + i];

	std::string resultString = result.toString();
	if (!resultString.empty() && resultString.front() == '1')
		return resultString.substr(1);

	throw FormatError();
}


/**
* Numeric Compaction mode (see 5.4.4) permits efficient encoding of numeric data strings.
*
* @param codewords The array of codewords (data + error)
* @param codeIndex The current index into the codeword array.
* @param result    The decoded data is appended to the result.
* @return The next index into the codeword array.
*/
static int NumericCompaction(const std::vector<int>& codewords, int codeIndex, Content& result)
{
	int count = 0;

	while (codeIndex < codewords[0]) {
		int code = codewords[codeIndex];
		if (code < TEXT_COMPACTION_MODE_LATCH) {
			count++;
			codeIndex++;
		}
		if (count > 0 && (count == MAX_NUMERIC_CODEWORDS || codeIndex == codewords[0] || code >= TEXT_COMPACTION_MODE_LATCH)) {
			result.append(DecodeBase900toBase10(codewords, codeIndex, count));
			count = 0;
		}

		if (code >= TEXT_COMPACTION_MODE_LATCH) {
			if (IsECI(code)) {
				// As operating in Basic Channel Mode (i.e. not embedding backslashed ECIs and doubling backslashes)
				// allow ECIs anywhere in Numeric Compaction (i.e. ISO/IEC 15438:2015 5.5.3.4 doesn't apply).
				codeIndex = ProcessECI(codewords, codeIndex + 1, codewords[0], code, result);
			} else if (TerminatesCompaction(code)) {
				break;
			} else {
				throw FormatError();
			}
		}
	}
	return codeIndex;
}

/*
* Helper to deal with optional text fields in Macros.
*/
static int DecodeMacroOptionalTextField(const std::vector<int>& codewords, int codeIndex, std::string& field)
{
	Content result;
	// Each optional field begins with an implied reset to ECI 2 (Annex H.2.3). ECI 2 is ASCII for 0-127, and Cp437
	// for non-ASCII (128-255). Text optional fields can contain ECIs.
	result.defaultCharset = CharacterSet::Cp437;

	codeIndex = TextCompaction(codewords, codeIndex, result);

	// Converting to UTF-8 (backward-incompatible change for non-ASCII chars)
	field = result.utf8();

	return codeIndex;
}

/*
* Helper to deal with optional numeric fields in Macros.
*/
template<typename T>
int DecodeMacroOptionalNumericField(const std::vector<int>& codewords, int codeIndex, T& field)
{
	Content result;
	// Each optional field begins with an implied reset to ECI 2 (Annex H.2.3). ECI 2 is ASCII for 0-127, and Cp437
	// for non-ASCII (128-255). Text optional fields can contain ECIs.
	result.defaultCharset = CharacterSet::Cp437;

	codeIndex = NumericCompaction(codewords, codeIndex, result);

	auto txt = result.utf8();
	if (std::from_chars(txt.data(), txt.data()+txt.size(), field).ec != std::errc())
		throw FormatError();

	return codeIndex;
}

ZXING_EXPORT_TEST_ONLY
int DecodeMacroBlock(const std::vector<int>& codewords, int codeIndex, PDF417CustomData& customData)
{
	// we must have at least two codewords left for the segment index
	if (codeIndex + NUMBER_OF_SEQUENCE_CODEWORDS > codewords[0])
		throw FormatError();

	std::string strBuf = DecodeBase900toBase10(codewords, codeIndex += NUMBER_OF_SEQUENCE_CODEWORDS, NUMBER_OF_SEQUENCE_CODEWORDS);

	customData.segmentIndex = std::stoi(strBuf);

	// Decoding the fileId codewords as 0-899 numbers, each 0-filled to width 3. This follows the spec
	// (See ISO/IEC 15438:2015 Annex H.6) and preserves all info, but some generators (e.g. TEC-IT) write
	// the fileId using text compaction, so in those cases the fileId will appear mangled.
	std::ostringstream fileId;
	for (; codeIndex < codewords[0] && codewords[codeIndex] != MACRO_PDF417_TERMINATOR
		   && codewords[codeIndex] != BEGIN_MACRO_PDF417_OPTIONAL_FIELD;
		 codeIndex++) {
		fileId << ToString(codewords[codeIndex], 3);
	}
	customData.fileId = fileId.str();

	int optionalFieldsStart = -1;
	if (codeIndex < codewords[0] && codewords[codeIndex] == BEGIN_MACRO_PDF417_OPTIONAL_FIELD)
		optionalFieldsStart = codeIndex + 1;

	while (codeIndex < codewords[0]) {
		switch (codewords[codeIndex]) {
		case BEGIN_MACRO_PDF417_OPTIONAL_FIELD: {
			codeIndex++;
			if (codeIndex >= codewords[0])
				break;
			switch (codewords[codeIndex]) {
			case MACRO_PDF417_OPTIONAL_FIELD_FILE_NAME:
				codeIndex = DecodeMacroOptionalTextField(codewords, codeIndex + 1, customData.fileName);
				break;
			case MACRO_PDF417_OPTIONAL_FIELD_SENDER:
				codeIndex = DecodeMacroOptionalTextField(codewords, codeIndex + 1, customData.sender);
				break;
			case MACRO_PDF417_OPTIONAL_FIELD_ADDRESSEE:
				codeIndex = DecodeMacroOptionalTextField(codewords, codeIndex + 1, customData.addressee);
				break;
			case MACRO_PDF417_OPTIONAL_FIELD_SEGMENT_COUNT:
				codeIndex = DecodeMacroOptionalNumericField(codewords, codeIndex + 1, customData.segmentCount);
				break;
			case MACRO_PDF417_OPTIONAL_FIELD_TIME_STAMP:
				codeIndex = DecodeMacroOptionalNumericField(codewords, codeIndex + 1, customData.timestamp);
				break;
			case MACRO_PDF417_OPTIONAL_FIELD_CHECKSUM:
				codeIndex = DecodeMacroOptionalNumericField(codewords, codeIndex + 1, customData.checksum);
				break;
			case MACRO_PDF417_OPTIONAL_FIELD_FILE_SIZE:
				codeIndex = DecodeMacroOptionalNumericField(codewords, codeIndex + 1, customData.fileSize);
				break;
			default: throw FormatError();
			}
			break;
		}
		case MACRO_PDF417_TERMINATOR: {
			codeIndex++;
			customData.isLastSegment = true;
			break;
		}
		default: throw FormatError();
		}
	}

	// copy optional fields to additional options
	if (optionalFieldsStart != -1) {
		int optionalFieldsLength = codeIndex - optionalFieldsStart;
		if (customData.isLastSegment)
			optionalFieldsLength--; // do not include terminator

		customData.optionalData =
			std::vector<int>(codewords.begin() + optionalFieldsStart, codewords.begin() + optionalFieldsStart + optionalFieldsLength);
	}

	return codeIndex;
}

DecoderResult Decode(const std::vector<int>& codewords)
{
	if (codewords.empty() || codewords[0] < 1 || codewords[0] > Size(codewords))
		return FormatError();

	Content result;
	result.symbology = {'L', '2', -1};

	bool readerInit = false;
	auto customData = std::make_shared<PDF417CustomData>();

	try {
		for (int codeIndex = 1; codeIndex < codewords[0];) {
			int code = codewords[codeIndex++];
			switch (code) {
			case TEXT_COMPACTION_MODE_LATCH: codeIndex = TextCompaction(codewords, codeIndex, result); break;
				// This should only be encountered once in this loop, when default Text Compaction mode applies
				// (see default case below)
			case MODE_SHIFT_TO_BYTE_COMPACTION_MODE: codeIndex = TextCompaction(codewords, codeIndex - 1, result); break;
			case BYTE_COMPACTION_MODE_LATCH:
			case BYTE_COMPACTION_MODE_LATCH_6: codeIndex = ByteCompaction(code, codewords, codeIndex, result); break;
			case NUMERIC_COMPACTION_MODE_LATCH: codeIndex = NumericCompaction(codewords, codeIndex, result); break;
			case ECI_CHARSET:
			case ECI_GENERAL_PURPOSE:
			case ECI_USER_DEFINED: codeIndex = ProcessECI(codewords, codeIndex, codewords[0], code, result); break;
			case BEGIN_MACRO_PDF417_CONTROL_BLOCK: codeIndex = DecodeMacroBlock(codewords, codeIndex, *customData); break;
			case BEGIN_MACRO_PDF417_OPTIONAL_FIELD:
			case MACRO_PDF417_TERMINATOR:
				// Should not see these outside a macro block
				throw FormatError();
				break;
			case READER_INIT:
				if (codeIndex != 2) // Must be first codeword after symbol length (ISO/IEC 15438:2015 5.4.1.4)
					throw FormatError();
				else
					readerInit = true;
				break;
			case LINKAGE_EANUCC:
				if (codeIndex != 2) // Must be first codeword after symbol length (GS1 Composite ISO/IEC 24723:2010 4.3)
					throw FormatError();
				// TODO: handle else case
				break;
			case LINKAGE_OTHER:
				// Allowed to treat as invalid by ISO/IEC 24723:2010 5.4.1.5 and 5.4.6.1 when in Basic Channel Mode
				throw UnsupportedError("LINKAGE_OTHER, see ISO/IEC 15438:2015 5.4.1.5");
				break;
			default:
				if (code >= TEXT_COMPACTION_MODE_LATCH) { // Reserved codewords (all others in switch)
					// Allowed to treat as invalid by ISO/IEC 24723:2010 5.4.6.1 when in Basic Channel Mode
					throw UnsupportedError("Reserved codeword, see ISO/IEC 15438:2015 5.4.6.1");
				} else {
					// Default mode is Text Compaction mode Alpha sub-mode (ISO/IEC 15438:2015 5.4.2.1)
					codeIndex = TextCompaction(codewords, codeIndex - 1, result);
				}
				break;
			}
		}
	} catch (std::exception& e) {
		return FormatError(e.what());
	} catch (Error e) {
		return e;
	}

	if (result.empty() && customData->segmentIndex == -1)
		return FormatError();

	StructuredAppendInfo sai;
	if (customData->segmentIndex > -1) {
		sai.count = customData->segmentCount != -1
						? customData->segmentCount
						: (customData->isLastSegment ? customData->segmentIndex + 1 : 0);
		sai.index = customData->segmentIndex;
		sai.id    = customData->fileId;
	}

	return DecoderResult(std::move(result))
		.setStructuredAppend(sai)
		.setReaderInit(readerInit)
		.setCustomData(customData);
}

} // namespace ZXing::Pdf417
