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

#include "ODCode128Reader.h"

#include "ODCode128Patterns.h"
#include "BarcodeData.h"
#include "ByteArray.h"
#include "JSON.h"
#include "SymbologyIdentifier.h"
#include "ZXAlgorithms.h"

#include <array>
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>

namespace ZXing::OneD {

using namespace Code128;

static const float MAX_AVG_VARIANCE = 0.25f;
static const float MAX_INDIVIDUAL_VARIANCE = 0.7f;

static const int CODE_SHIFT = 98;

static const int CODE_CODE_C = 99;
static const int CODE_CODE_B = 100;
static const int CODE_CODE_A = 101;

static const int CODE_FNC_1 = 102;
static const int CODE_FNC_2 = 97;
static const int CODE_FNC_3 = 96;

static const int CODE_START_A = 103;
static const int CODE_START_C = 105;
static const int CODE_STOP = 106;

class Raw2TxtDecoder
{
	int codeSet = 0;
	SymbologyIdentifier _symbologyIdentifier = {'C', '0'}; // ISO/IEC 15417:2007 Annex C Table C.1
	bool _readerInit = false;
	bool _havePositionFNC1 = false;
	std::string txt;
	size_t lastTxtSize = 0;

	bool fnc4All = false;
	bool fnc4Next = false;
	bool shift = false;

	void fnc1(const bool isCodeSetC)
	{
		if (!_havePositionFNC1 && txt.empty()) {
			// ISO/IEC 15417:2007 Annex B.1 and GS1 General Specifications 21.0.1 Section 5.4.3.7
			// If the first char after the start code is FNC1 then this is GS1-128.
			_symbologyIdentifier.modifier = '1';
			// GS1 General Specifications Section 5.4.6.4
			// "Transmitted data ... is prefixed by the symbology identifier ]C1, if used."
			// Choosing not to use symbology identifier, i.e. to not prefix to data.
			_symbologyIdentifier.aiFlag = AIFlag::GS1;
			_havePositionFNC1 = true;
		}
		else if (!_havePositionFNC1
					&& ((isCodeSetC && txt.size() == 2 && IsDigit(txt[0]) && IsDigit(txt[1]))
						|| (!isCodeSetC && txt.size() == 1 && IsAlpha(txt[0])))) {
			// ISO/IEC 15417:2007 Annex B.2
			// FNC1 in second position following Code Set C "00-99" or Code Set A/B "A-Za-z" - AIM
			_symbologyIdentifier.modifier = '2';
			_symbologyIdentifier.aiFlag = AIFlag::AIM;
			_havePositionFNC1 = true;
		}
		else {
			// ISO/IEC 15417:2007 Annex B.3. Otherwise FNC1 is returned as ASCII 29 (GS)
			txt.push_back((char)29);
		}
	};

public:
	Raw2TxtDecoder(int startCode) : codeSet(204 - startCode)
	{
		txt.reserve(20);
	}

	bool decode(int code)
	{
		lastTxtSize = txt.size();

		if (codeSet == CODE_CODE_C) {
			if (code < 100) {
				txt.append(ToString(code, 2));
			} else if (code == CODE_FNC_1) {
				fnc1(true /*isCodeSetC*/);
			} else {
				codeSet = code; // CODE_A / CODE_B
			}
		} else { // codeSet A or B
			bool unshift = shift;

			switch (code) {
			case CODE_FNC_1: fnc1(false /*isCodeSetC*/); break;
			case CODE_FNC_2:
				// Message Append - do nothing?
				break;
			case CODE_FNC_3:
				_readerInit = true; // Can occur anywhere in the symbol (ISO/IEC 15417:2007 4.3.4.2 (c))
				break;
			case CODE_SHIFT:
				if (shift)
					return false; // two shifts in a row make no sense
				shift = true;
				codeSet = codeSet == CODE_CODE_A ? CODE_CODE_B : CODE_CODE_A;
				break;
			case CODE_CODE_A:
			case CODE_CODE_B:
				if (codeSet == code) {
					// FNC4
					if (fnc4Next)
						fnc4All = !fnc4All;
					fnc4Next = !fnc4Next;
				} else {
					codeSet = code;
				}
				break;
			case CODE_CODE_C: codeSet = CODE_CODE_C; break;

			default: {
				// code < 96 at this point
				int offset;
				if (codeSet == CODE_CODE_A && code >= 64)
					offset = fnc4All == fnc4Next ? -64 : +64;
				else
					offset = fnc4All == fnc4Next ? ' ' : ' ' + 128;
				txt.push_back((char)(code + offset));
				fnc4Next = false;
				break;
			}
			}

			// Unshift back to another code set if we were shifted
			if (unshift) {
				codeSet = codeSet == CODE_CODE_A ? CODE_CODE_B : CODE_CODE_A;
				shift = false;
			}
		}

		return true;
	}

	std::string text() const
	{
		// Need to pull out the check digit(s) from string (if the checksum code happened to
		// be a printable character).
		return txt.substr(0, lastTxtSize);
	}

	SymbologyIdentifier symbologyIdentifier() const { return _symbologyIdentifier; }
	bool readerInit() const { return _readerInit; }
};

// all 3 start patterns share the same 2-1-1 prefix
constexpr auto START_PATTERN_PREFIX = FixedPattern<3, 4>{2, 1, 1};
constexpr float QUIET_ZONE = 5;	// quiet zone spec is 10 modules, real world examples ignore that, see #138

BarcodeData Code128Reader::decodePattern(int rowNumber, PatternView& next, std::unique_ptr<DecodingState>&) const
{
	int minCharCount = 4; // start + payload + checksum + stop
	auto decodePattern = [](const PatternView& view, bool start = false) {
		// This is basically the reference algorithm from the specification
		int code = IndexOf(E2E_PATTERNS, ToInt(NormalizedE2EPattern<CHAR_LEN, CHAR_MODS>(view)));
		if (code == -1 && !start) // if the reference algo fails, give the original upstream version a try (required to decode a few samples)
			code = DecodeDigit(view, CODE_PATTERNS, MAX_AVG_VARIANCE, MAX_INDIVIDUAL_VARIANCE);
		return code;
	};

	next = FindLeftGuard(next, minCharCount * CHAR_LEN, START_PATTERN_PREFIX, QUIET_ZONE);
	if (!next.isValid())
		return {};

	next = next.subView(0, CHAR_LEN);
	int startCode = decodePattern(next, true);
	if (!(CODE_START_A <= startCode && startCode <= CODE_START_C))
		return {};

	int xStart = next.pixelsInFront();
	ByteArray rawCodes;
	rawCodes.reserve(20);
	rawCodes.push_back(narrow_cast<uint8_t>(startCode));

	Raw2TxtDecoder raw2txt(startCode);

	while (true) {
		if (!next.skipSymbol())
			return {};

		// Decode another code from image
		int code = decodePattern(next);
		if (code == -1)
			return {};
		if (code == CODE_STOP)
			break;
		if (code >= CODE_START_A)
			return {};
		if (!raw2txt.decode(code))
			return {};

		rawCodes.push_back(narrow_cast<uint8_t>(code));
	}

	if (Size(rawCodes) < minCharCount - 1) // stop code is missing in rawCodes
		return {};

	// check termination bar (is present and not wider than about 2 modules) and quiet zone (next is now 13 modules
	// wide, require at least 8)
	next = next.subView(0, CHAR_LEN + 1);
	if (!next.isValid() || next[CHAR_LEN] > next.sum(CHAR_LEN) / 4 || !next.hasQuietZoneAfter(QUIET_ZONE/13))
		return {};

	Error error;
	int checksum = rawCodes.front();
	for (int i = 1; i < Size(rawCodes) - 1; ++i)
		checksum += i * rawCodes[i];
	// the last code is the checksum:
	if (checksum % 103 != rawCodes.back())
		error = ChecksumError();

	int xStop = next.pixelsTillEnd();
	return LinearBarcode(BarcodeFormat::Code128, raw2txt.text(), rowNumber, xStart, xStop, raw2txt.symbologyIdentifier(), error,
				   JsonProp(BarcodeExtra::ReaderInit, raw2txt.readerInit()));
}

} // namespace ZXing::OneD
