#include "ANSLPR.h"
#include <json.hpp>
#include "ANSLibsLoader.h"

static bool ansalprLicenceValid = false;
// Global once_flag to protect license checking
static std::once_flag ansalprLicenseOnceFlag;
template <typename T>
T GetData(const boost::property_tree::ptree& pt, const std::string& key)
{
	T ret;
	if (boost::optional<T> data = pt.get_optional<T>(key))
	{
		ret = data.get();
	}
	return ret;
}
namespace ANSCENTER {
	// Checker class
	void ALPRChecker::Init(int framesToStore) {
		maxFrames = framesToStore;
	}

	int ALPRChecker::levenshteinDistance(const std::string& s1, const std::string& s2) {
		try {
			int len1 = static_cast<int>(s1.size());
			int len2 = static_cast<int>(s2.size());
			std::vector<std::vector<int>> dp(len1 + 1, std::vector<int>(len2 + 1));

			for (int i = 0; i <= len1; i++) dp[i][0] = i;
			for (int j = 0; j <= len2; j++) dp[0][j] = j;

			for (int i = 1; i <= len1; i++) {
				for (int j = 1; j <= len2; j++) {
					if (s1[i - 1] == s2[j - 1]) {
						dp[i][j] = dp[i - 1][j - 1];
					}
					else {
						dp[i][j] = std::min({ dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 1 });
					}
				}
			}
			return dp[len1][len2];
		}
		catch (const std::exception& e) {
			return -1;
		}
	}

	float ALPRChecker::computeIoU(const cv::Rect& a, const cv::Rect& b) {
		int x1 = std::max(a.x, b.x);
		int y1 = std::max(a.y, b.y);
		int x2 = std::min(a.x + a.width, b.x + b.width);
		int y2 = std::min(a.y + a.height, b.y + b.height);
		int intersection = std::max(0, x2 - x1) * std::max(0, y2 - y1);
		int unionArea = a.area() + b.area() - intersection;
		return (unionArea > 0) ? static_cast<float>(intersection) / unionArea : 0.f;
	}

	std::string ALPRChecker::majorityVote(const std::deque<std::string>& history) {
		if (history.empty()) return "";

		// Count frequency of each plate text, weighted by recency
		// (more recent entries get higher weight)
		std::map<std::string, float> weightedFreq;
		int n = static_cast<int>(history.size());
		for (int i = 0; i < n; i++) {
			// Recency weight: older=1.0, newest=2.0
			float weight = 1.0f + static_cast<float>(i) / std::max(1, n - 1);
			weightedFreq[history[i]] += weight;
		}

		// Also group similar plates (Levenshtein <= 1) under the most frequent variant
		std::string bestText;
		float bestScore = 0.f;
		for (const auto& kv : weightedFreq) {
			float totalScore = kv.second;
			// Add scores from similar plates
			for (const auto& other : weightedFreq) {
				if (other.first == kv.first) continue;
				// Use quick length check to skip obvious non-matches
				int lenDiff = std::abs(static_cast<int>(kv.first.size()) - static_cast<int>(other.first.size()));
				if (lenDiff <= 1) {
					// Only compute Levenshtein for candidates within 1 char length difference
					int dist = 0;
					// Inline fast Levenshtein for short strings
					const std::string& s1 = kv.first;
					const std::string& s2 = other.first;
					int len1 = static_cast<int>(s1.size()), len2 = static_cast<int>(s2.size());
					if (len1 <= 15 && len2 <= 15) {
						std::vector<int> prev(len2 + 1), curr(len2 + 1);
						for (int j = 0; j <= len2; j++) prev[j] = j;
						for (int i = 1; i <= len1; i++) {
							curr[0] = i;
							for (int j = 1; j <= len2; j++) {
								if (s1[i - 1] == s2[j - 1]) curr[j] = prev[j - 1];
								else curr[j] = 1 + std::min({ prev[j], curr[j - 1], prev[j - 1] });
							}
							std::swap(prev, curr);
						}
						dist = prev[len2];
					}
					if (dist == 1) {
						totalScore += other.second * 0.5f; // partial credit for 1-edit variants
					}
				}
			}
			if (totalScore > bestScore) {
				bestScore = totalScore;
				bestText = kv.first;
			}
		}
		return bestText;
	}

	// Original API — backward compatible, no spatial tracking
	std::string ALPRChecker::checkPlate(const std::string& cameraId, const std::string& detectedPlate) {
		// Delegate to spatial version with empty box (disables IoU matching)
		return checkPlate(cameraId, detectedPlate, cv::Rect());
	}

	// Enhanced API with bounding box for spatial plate tracking
	std::string ALPRChecker::checkPlate(const std::string& cameraId, const std::string& detectedPlate, const cv::Rect& plateBox) {
		std::lock_guard<std::recursive_mutex> lock(_mutex);
		try {
			if (detectedPlate.empty()) return detectedPlate;

			auto& plates = trackedPlates[cameraId];
			bool hasSpatial = (plateBox.width > 0 && plateBox.height > 0);

			// Increment framesSinceLastSeen for all plates (will be reset for matched plate)
			for (auto& p : plates) {
				p.framesSinceLastSeen++;
			}

			// Periodic pruning: remove plates not seen for a long time
			_pruneCounter++;
			if (_pruneCounter >= 30 && plates.size() > 10) {
				_pruneCounter = 0;
				int staleThreshold = maxFrames * 3; // 180 frames = ~6 seconds at 30fps
				plates.erase(
					std::remove_if(plates.begin(), plates.end(),
						[staleThreshold](const TrackedPlate& p) {
							return p.framesSinceLastSeen > staleThreshold;
						}),
					plates.end());
			}

			// Step 1: Find matching tracked plate
			// Strategy: try IoU first, then text similarity as fallback
			int bestIdx = -1;
			float bestIoU = 0.f;

			// 1a. Try spatial IoU matching
			if (hasSpatial) {
				for (int i = 0; i < static_cast<int>(plates.size()); i++) {
					float iou = computeIoU(plateBox, plates[i].lastBox);
					if (iou > iouMatchThreshold && iou > bestIoU) {
						bestIoU = iou;
						bestIdx = i;
					}
				}
			}

			// 1b. If IoU failed, try text similarity against locked texts
			if (bestIdx < 0) {
				for (int i = 0; i < static_cast<int>(plates.size()); i++) {
					if (!plates[i].lockedText.empty()) {
						int dist = levenshteinDistance(detectedPlate, plates[i].lockedText);
						if (dist <= 1) {
							bestIdx = i;
							break;
						}
					}
					// Also check against recent history even if not locked
					if (bestIdx < 0 && !plates[i].textHistory.empty()) {
						int checkCount = std::min(static_cast<int>(plates[i].textHistory.size()), 3);
						for (int j = static_cast<int>(plates[i].textHistory.size()) - 1;
							 j >= static_cast<int>(plates[i].textHistory.size()) - checkCount; j--) {
							int dist = levenshteinDistance(detectedPlate, plates[i].textHistory[j]);
							if (dist <= 1) {
								bestIdx = i;
								break;
							}
						}
					}
				}
			}

			// Step 2: Create new tracked plate if no match found
			if (bestIdx < 0) {
				// Before creating a new plate, check if this detection conflicts with
				// a nearby locked plate — if so, it's likely a bad OCR read and we should
				// return the locked text instead of creating a garbage entry
				if (hasSpatial) {
					float bestProximity = 0.f;
					int proximityIdx = -1;
					for (int i = 0; i < static_cast<int>(plates.size()); i++) {
						if (plates[i].lockedText.empty()) continue;
						if (plates[i].lastBox.width <= 0 || plates[i].lastBox.height <= 0) continue;
						float cx1 = plateBox.x + plateBox.width * 0.5f;
						float cy1 = plateBox.y + plateBox.height * 0.5f;
						float cx2 = plates[i].lastBox.x + plates[i].lastBox.width * 0.5f;
						float cy2 = plates[i].lastBox.y + plates[i].lastBox.height * 0.5f;
						float dx = std::abs(cx1 - cx2);
						float dy = std::abs(cy1 - cy2);
						float maxDim = std::max(static_cast<float>(plateBox.width), static_cast<float>(plates[i].lastBox.width));
						if (dx < maxDim * 2.0f && dy < maxDim * 1.5f) {
							float proximity = 1.0f / (1.0f + dx + dy);
							if (proximity > bestProximity) {
								bestProximity = proximity;
								proximityIdx = i;
							}
						}
					}
					if (proximityIdx >= 0) {
						auto& tp = plates[proximityIdx];
						tp.lastBox = plateBox;
						tp.framesSinceLastSeen = 0;
						tp.textHistory.push_back(detectedPlate);
						return tp.lockedText;
					}
				}

				TrackedPlate newPlate;
				newPlate.lastBox = plateBox;
				newPlate.textHistory.push_back(detectedPlate);
				plates.push_back(newPlate);
				return detectedPlate;
			}

			// Step 3: Update matched tracked plate
			auto& tp = plates[bestIdx];
			if (hasSpatial) tp.lastBox = plateBox;
			tp.framesSinceLastSeen = 0; // Reset — this plate was just seen

			// Store RAW detection (not corrected — avoids feedback loop)
			tp.textHistory.push_back(detectedPlate);
			if (static_cast<int>(tp.textHistory.size()) > maxFrames) {
				tp.textHistory.pop_front();
			}

			// Step 4: Majority vote to find stable text
			std::string voted = majorityVote(tp.textHistory);

			// Step 5: Lock logic — once we have enough consistent reads, lock the text
			if (tp.lockedText.empty()) {
				int matchCount = 0;
				int lookback = std::min(static_cast<int>(tp.textHistory.size()), maxFrames);
				for (int i = static_cast<int>(tp.textHistory.size()) - 1;
					 i >= static_cast<int>(tp.textHistory.size()) - lookback; i--) {
					if (tp.textHistory[i] == voted) matchCount++;
				}
				if (matchCount >= minVotesToStabilize) {
					tp.lockedText = voted;
					tp.lockCount = 1;
				}
				return voted;
			}
			else {
				int dist = levenshteinDistance(detectedPlate, tp.lockedText);
				if (dist == 0) {
					tp.lockCount++;
					return tp.lockedText;
				}
				else {
					int newDist = levenshteinDistance(voted, tp.lockedText);
					if (newDist > 1) {
						int newMatchCount = 0;
						int recent = std::min(static_cast<int>(tp.textHistory.size()), minVotesToStabilize * 2);
						for (int i = static_cast<int>(tp.textHistory.size()) - 1;
							 i >= static_cast<int>(tp.textHistory.size()) - recent; i--) {
							if (tp.textHistory[i] == voted) newMatchCount++;
						}
						if (newMatchCount >= minVotesToStabilize) {
							tp.lockedText = voted;
							tp.lockCount = 1;
							return tp.lockedText;
						}
					}
					return tp.lockedText;
				}
			}
		}
		catch (const std::exception& e) {
			return detectedPlate;
		}
	}

	// Hybrid trackId-based plate stabilization:
	// Primary: O(1) hash lookup by trackId
	// Fallback: Levenshtein search for lost tracks (track ID changed after occlusion)
	std::string ALPRChecker::checkPlateByTrackId(const std::string& cameraId,
	                                              const std::string& detectedPlate,
	                                              int trackId) {
		std::lock_guard<std::recursive_mutex> lock(_mutex);
		try {
			if (detectedPlate.empty()) return detectedPlate;

			auto& plates = trackedPlatesById[cameraId];

			// Age all plates for this camera
			for (auto& [id, p] : plates) {
				p.framesSinceLastSeen++;
			}

			// Periodic pruning: remove stale entries.
			// NOTE: previously used `static thread_local int pruneCounterById`.
			// LabVIEW dispatches calls across a worker-thread pool, so the
			// thread-local counter fragmented across threads and pruning
			// effectively stopped firing — `trackedPlatesById[cameraId]` then
			// grew unbounded (one entry per frame when trackIds never repeat),
			// which manifested as a slow LabVIEW-side memory leak. The counter
			// is now a plain instance member guarded by `_mutex` (taken above),
			// so every 30th call across the whole engine triggers a prune pass.
			_pruneCounterById++;
			if (_pruneCounterById >= 30 && plates.size() > 20) {
				_pruneCounterById = 0;
				int staleThreshold = maxFrames * 3;
				for (auto it = plates.begin(); it != plates.end(); ) {
					if (it->second.framesSinceLastSeen > staleThreshold) {
						it = plates.erase(it);
					} else {
						++it;
					}
				}
			}

			// --- Primary: direct trackId lookup (O(1)) ---
			auto it = plates.find(trackId);
			if (it != plates.end()) {
				auto& tp = it->second;
				tp.framesSinceLastSeen = 0;

				// Store RAW text (not corrected — avoids feedback loop)
				tp.textHistory.push_back(detectedPlate);
				if (static_cast<int>(tp.textHistory.size()) > maxFrames) {
					tp.textHistory.pop_front();
				}

				// Majority vote
				std::string voted = majorityVote(tp.textHistory);

				// Lock logic
				if (tp.lockedText.empty()) {
					int matchCount = 0;
					int lookback = std::min(static_cast<int>(tp.textHistory.size()), maxFrames);
					for (int i = static_cast<int>(tp.textHistory.size()) - 1;
					     i >= static_cast<int>(tp.textHistory.size()) - lookback; i--) {
						if (tp.textHistory[i] == voted) matchCount++;
					}
					if (matchCount >= minVotesToStabilize) {
						tp.lockedText = voted;
						tp.lockCount = 1;
						ANS_DBG("ALPR_TrackId", "cam=%s tid=%d LOCKED '%s' votes=%d",
							cameraId.c_str(), trackId, voted.c_str(), matchCount);
					}
					ANS_DBG("ALPR_TrackId", "cam=%s tid=%d hit: raw='%s' voted='%s' hist=%d lock=%s",
						cameraId.c_str(), trackId, detectedPlate.c_str(), voted.c_str(),
						static_cast<int>(tp.textHistory.size()),
						tp.lockedText.empty() ? "(none)" : tp.lockedText.c_str());
					return voted;
				} else {
					int dist = levenshteinDistance(detectedPlate, tp.lockedText);
					if (dist == 0) {
						tp.lockCount++;
						return tp.lockedText;
					} else {
						int newDist = levenshteinDistance(voted, tp.lockedText);
						if (newDist > 1) {
							int newMatchCount = 0;
							int recent = std::min(static_cast<int>(tp.textHistory.size()),
							                      minVotesToStabilize * 2);
							for (int i = static_cast<int>(tp.textHistory.size()) - 1;
							     i >= static_cast<int>(tp.textHistory.size()) - recent; i--) {
								if (tp.textHistory[i] == voted) newMatchCount++;
							}
							if (newMatchCount >= minVotesToStabilize) {
								ANS_DBG("ALPR_TrackId", "cam=%s tid=%d RE-LOCK '%s'->'%s'",
									cameraId.c_str(), trackId, tp.lockedText.c_str(), voted.c_str());
								tp.lockedText = voted;
								tp.lockCount = 1;
								return tp.lockedText;
							}
						}
						ANS_DBG("ALPR_TrackId", "cam=%s tid=%d noise: raw='%s' lock='%s' dist=%d",
							cameraId.c_str(), trackId, detectedPlate.c_str(), tp.lockedText.c_str(), dist);
						return tp.lockedText;
					}
				}
			}

			// --- Fallback: Levenshtein search for lost tracks (track ID changed after occlusion) ---
			for (auto& [id, p] : plates) {
				if (!p.lockedText.empty()) {
					int dist = levenshteinDistance(detectedPlate, p.lockedText);
					if (dist <= 1) {
						// Found a match — migrate history to new trackId
						TrackedPlateById migrated = std::move(p);
						migrated.trackId = trackId;
						migrated.framesSinceLastSeen = 0;
						migrated.textHistory.push_back(detectedPlate);
						if (static_cast<int>(migrated.textHistory.size()) > maxFrames) {
							migrated.textHistory.pop_front();
						}
						std::string result = migrated.lockedText;
						plates.erase(id);
						plates[trackId] = std::move(migrated);
						ANS_DBG("ALPR_TrackId", "cam=%s tid=%d MIGRATED from tid=%d lock='%s'",
							cameraId.c_str(), trackId, id, result.c_str());
						return result;
					}
				}
				// Check last 3 history entries if not locked
				if (p.lockedText.empty() && !p.textHistory.empty()) {
					int checkCount = std::min(static_cast<int>(p.textHistory.size()), 3);
					for (int j = static_cast<int>(p.textHistory.size()) - 1;
					     j >= static_cast<int>(p.textHistory.size()) - checkCount; j--) {
						int dist = levenshteinDistance(detectedPlate, p.textHistory[j]);
						if (dist <= 1) {
							TrackedPlateById migrated = std::move(p);
							migrated.trackId = trackId;
							migrated.framesSinceLastSeen = 0;
							migrated.textHistory.push_back(detectedPlate);
							if (static_cast<int>(migrated.textHistory.size()) > maxFrames) {
								migrated.textHistory.pop_front();
							}
							std::string voted = majorityVote(migrated.textHistory);
							plates.erase(id);
							plates[trackId] = std::move(migrated);
							ANS_DBG("ALPR_TrackId", "cam=%s tid=%d MIGRATED(unlocked) from tid=%d voted='%s'",
								cameraId.c_str(), trackId, id, voted.c_str());
							return voted;
						}
					}
				}
			}

			// --- No match at all: create new entry ---
			TrackedPlateById newPlate;
			newPlate.trackId = trackId;
			newPlate.textHistory.push_back(detectedPlate);
			plates[trackId] = std::move(newPlate);
			ANS_DBG("ALPR_TrackId", "cam=%s tid=%d NEW entry raw='%s'",
				cameraId.c_str(), trackId, detectedPlate.c_str());
			return detectedPlate;
		}
		catch (const std::exception& e) {
			return detectedPlate;
		}
	}

	// Skip-when-locked query — see header for full contract.
	//
	// Returns the locked plate text if the caller may skip OCR this frame for
	// (cameraId, trackId), or "" if the caller must run OCR.  Increments the
	// re-verify counter on skip; resets it when the counter hits reverifyEvery
	// so the next frame falls through to a real OCR run.
	std::string ALPRChecker::tryReuseLockedText(const std::string& cameraId,
	                                             int trackId,
	                                             int reverifyEvery) {
		std::lock_guard<std::recursive_mutex> lock(_mutex);
		try {
			auto camIt = trackedPlatesById.find(cameraId);
			if (camIt == trackedPlatesById.end()) return "";
			auto& plates = camIt->second;
			auto it = plates.find(trackId);
			if (it == plates.end()) return "";

			auto& tp = it->second;
			if (tp.lockedText.empty()) return "";  // not locked yet — caller must run OCR

			// Locked.  Decide whether to skip this frame's OCR or fall through
			// for a periodic re-verify.  reverifyEvery <= 0 means "always skip"
			// (no re-verify), reverifyEvery == 1 means "always run OCR".
			if (reverifyEvery > 0 && tp.framesSinceVerify >= reverifyEvery) {
				tp.framesSinceVerify = 0;
				ANS_DBG("ALPR_TrackId",
					"cam=%s tid=%d REVERIFY (every=%d) — letting OCR through",
					cameraId.c_str(), trackId, reverifyEvery);
				return "";  // signal: caller should run OCR this frame
			}

			++tp.framesSinceVerify;
			// Keep the entry alive — checkPlateByTrackId is what normally
			// resets framesSinceLastSeen, but on skipped frames it's never
			// called, and the prune pass would eventually delete this lock.
			tp.framesSinceLastSeen = 0;
			ANS_DBG("ALPR_TrackId",
				"cam=%s tid=%d SKIP-LOCKED text='%s' reverifyIn=%d",
				cameraId.c_str(), trackId, tp.lockedText.c_str(),
				reverifyEvery > 0 ? (reverifyEvery - tp.framesSinceVerify) : -1);
			return tp.lockedText;
		}
		catch (const std::exception&) {
			return "";  // any failure → fall back to running OCR
		}
	}

	//
	static void VerifyGlobalANSALPRLicense(const std::string& licenseKey) {
		try {
			ansalprLicenceValid = ANSCENTER::ANSLicenseHelper::LicenseVerification(licenseKey, 1006, "ANSALPR");//Default productId=1006
			if (!ansalprLicenceValid) { // we also support ANSTS license
				ansalprLicenceValid = ANSCENTER::ANSLicenseHelper::LicenseVerification(licenseKey, 1003, "ANSVIS");//Default productId=1003 (ANSVIS)
			}
		}
		catch (std::exception& e) {
			ansalprLicenceValid = false;
		}
	}
	void ANSALPR::CheckLicense() {
		try {
			// Check once globally
			std::call_once(ansalprLicenseOnceFlag, [this]() {
				VerifyGlobalANSALPRLicense(_licenseKey);
				});

			// Update this instance's local license flag
			_licenseValid = ansalprLicenceValid;
		}
		catch (const std::exception& e) {
			this->_logger.LogFatal("ANSODBase::CheckLicense. Error:", e.what(), __FILE__, __LINE__);
		}
	}
	bool ANSALPR::LoadEngine() {
		return true;
	}

	bool ANSALPR::Initialize(const std::string& licenseKey, const std::string& modelZipFilePath, const std::string& modelZipPassword, double detectorThreshold, double ocrThreshold, double colorThreshold) {
		try {
			ANSCENTER::ANSLibsLoader::Initialize();
			_licenseKey = licenseKey;
			_licenseValid = false;
			_detectorThreshold = detectorThreshold;
			_ocrThreshold = ocrThreshold;
			_colorThreshold = colorThreshold;
			ANSALPR::CheckLicense();
			if (!_licenseValid) {
				this->_logger.LogError("ANSALPR::Initialize.", "License is not valid.", __FILE__, __LINE__);
				return false;
			}
			// Extract model folder 
			// 0. Check if the modelZipFilePath exist?
			if (!FileExist(modelZipFilePath)) {
				this->_logger.LogFatal("ANSALPR::Initialize", "Model zip file is not exist", __FILE__, __LINE__);
			}
			// 1. Unzip model zip file to a special location with folder name as model file (and version)
			std::string outputFolder;
			std::vector<std::string> passwordArray;
			if (!modelZipPassword.empty()) passwordArray.push_back(modelZipPassword);
			passwordArray.push_back("AnsDemoModels20@!");
			passwordArray.push_back("Sh7O7nUe7vJ/417W0gWX+dSdfcP9hUqtf/fEqJGqxYL3PedvHubJag==");
			passwordArray.push_back("3LHxGrjQ7kKDJBD9MX86H96mtKLJaZcTYXrYRdQgW8BKGt7enZHYMg==");
			std::string modelName = GetFileNameWithoutExtension(modelZipFilePath);
			//this->_logger.LogDebug("ANSFDBase::Initialize. Model name", modelName);

			size_t vectorSize = passwordArray.size();
			for (size_t i = 0; i < vectorSize; i++) {
				if (ExtractPasswordProtectedZip(modelZipFilePath, passwordArray[i], modelName, _modelFolder, false))
					break; // Break the loop when the condition is met. 
			}
			// 2. Check if the outputFolder exist
			if (!FolderExist(_modelFolder)) {
				this->_logger.LogError("ANSFDBase::Initialize. Output model folder is not exist", _modelFolder, __FILE__, __LINE__);
				return false; // That means the model file is not exist or the password is not correct
			}
			return true;
		}
		catch (std::exception& e) {
			this->_logger.LogFatal("ANSALPR::Initialize", e.what(), __FILE__, __LINE__);
			return false;
		}
	}
	ANSALPR::~ANSALPR(){};
	bool ANSALPR::Destroy() { return true; };

	// Default batch implementation — fallback that loops RunInference over
	// vehicle crops one at a time. Subclasses should override this with a
	// true batched path (see ANSALPR_OD::RunInferencesBatch and
	// ANSALPR_OCR::RunInferencesBatch) to issue a single LP-detect and a
	// single OCR call per frame instead of N. The fallback preserves the
	// "transform bboxes back to frame coordinates" contract so a subclass
	// that never overrides still produces valid output.
	std::vector<Object> ANSALPR::RunInferencesBatch(
		const cv::Mat& input,
		const std::vector<cv::Rect>& vehicleBoxes,
		const std::string& cameraId)
	{
		std::vector<Object> out;
		if (input.empty() || vehicleBoxes.empty()) return out;
		const cv::Rect frameRect(0, 0, input.cols, input.rows);
		out.reserve(vehicleBoxes.size());
		for (const auto& r : vehicleBoxes) {
			cv::Rect c = r & frameRect;
			if (c.width <= 0 || c.height <= 0) continue;
			const cv::Mat crop = input(c);
			std::vector<Object> perVehicle = RunInference(crop, cameraId);
			for (auto& obj : perVehicle) {
				obj.box.x += c.x;
				obj.box.y += c.y;
				out.push_back(std::move(obj));
			}
		}
		return out;
	}

	std::vector<cv::Rect> ANSCENTER::ANSALPR::GetBoundingBoxes(const std::string& strBBoxes) {
		std::vector<cv::Rect> bBoxes;
		bBoxes.clear();
		std::stringstream ss;
		ss << strBBoxes;
		boost::property_tree::ptree pt;
		boost::property_tree::read_json(ss, pt);
		BOOST_FOREACH(const boost::property_tree::ptree::value_type & child, pt.get_child("results"))
		{
			const boost::property_tree::ptree& result = child.second;
			const auto x = GetData<float>(result, "x");
			const auto y = GetData<float>(result, "y");
			const auto width = GetData<float>(result, "width");
			const auto height = GetData<float>(result, "height");
			cv::Rect rectTemp;
			rectTemp.x = x;
			rectTemp.y = y;
			rectTemp.width = width;
			rectTemp.height = height;
			bBoxes.push_back(rectTemp);
		}
		return bBoxes;
	}
	std::string ANSCENTER::ANSALPR::PolygonToString(const std::vector<cv::Point2f>& polygon) {
		if (polygon.empty()) {
			return "";
		}
		std::string result;
		result.reserve(polygon.size() * 20);

		char buffer[64];
		for (size_t i = 0; i < polygon.size(); ++i) {
			if (i > 0) {
				snprintf(buffer, sizeof(buffer), ";%.3f;%.3f", polygon[i].x, polygon[i].y);
			}
			else {
				snprintf(buffer, sizeof(buffer), "%.3f;%.3f", polygon[i].x, polygon[i].y);
			}
			result += buffer;
		}

		return result;
	}

	static std::string DoubleEscapeUnicode(const std::string& utf8Str) {
		bool hasNonAscii = false;
		for (unsigned char c : utf8Str) {
			if (c >= 0x80) { hasNonAscii = true; break; }
		}
		if (!hasNonAscii) return utf8Str;
		std::string result;
		result.reserve(utf8Str.size() * 2);
		size_t i = 0;
		while (i < utf8Str.size()) {
			unsigned char c = static_cast<unsigned char>(utf8Str[i]);
			if (c < 0x80) { result += utf8Str[i++]; continue; }
			uint32_t cp = 0;
			if ((c & 0xE0) == 0xC0 && i + 1 < utf8Str.size()) {
				cp = ((c & 0x1F) << 6) | (static_cast<unsigned char>(utf8Str[i + 1]) & 0x3F); i += 2;
			} else if ((c & 0xF0) == 0xE0 && i + 2 < utf8Str.size()) {
				cp = ((c & 0x0F) << 12) | ((static_cast<unsigned char>(utf8Str[i + 1]) & 0x3F) << 6) | (static_cast<unsigned char>(utf8Str[i + 2]) & 0x3F); i += 3;
			} else if ((c & 0xF8) == 0xF0 && i + 3 < utf8Str.size()) {
				cp = ((c & 0x07) << 18) | ((static_cast<unsigned char>(utf8Str[i + 1]) & 0x3F) << 12) | ((static_cast<unsigned char>(utf8Str[i + 2]) & 0x3F) << 6) | (static_cast<unsigned char>(utf8Str[i + 3]) & 0x3F); i += 4;
			} else { i++; continue; }
			if (cp <= 0xFFFF) { char buf[8]; snprintf(buf, sizeof(buf), "\\u%04x", cp); result += buf; }
			else { cp -= 0x10000; char buf[16]; snprintf(buf, sizeof(buf), "\\u%04x\\u%04x", 0xD800 + (uint16_t)(cp >> 10), 0xDC00 + (uint16_t)(cp & 0x3FF)); result += buf; }
		}
		return result;
	}

	std::string ANSALPR::VectorDetectionToJsonString(const std::vector<Object>& dets) {
		if (dets.empty()) {
			return R"({"results":[]})";
		}
		try {
			nlohmann::json root;
			auto& results = root["results"] = nlohmann::json::array();

			for (const auto& det : dets) {
				results.push_back({
					{"class_id", std::to_string(det.classId)},
					//{"track_id", std::to_string(det.trackId)},
					{"track_id", std::to_string(0)},
					{"class_name", DoubleEscapeUnicode(det.className)},
					{"prob", std::to_string(det.confidence)},
					{"x", std::to_string(det.box.x)},
					{"y", std::to_string(det.box.y)},
					{"width", std::to_string(det.box.width)},
					{"height", std::to_string(det.box.height)},
					{"mask", ""},
					{"extra_info", det.extraInfo},
					{"camera_id", det.cameraId},
					{"polygon", PolygonToString(det.polygon)},
					{"kps", KeypointsToString(det.kps)}
					});
			}
			return root.dump();
		}
		catch (const std::exception& e) {
			this->_logger.LogFatal("ANSALPR::VectorDetectionToJsonString", e.what(), __FILE__, __LINE__);
			return R"({"results":[],"error":"Serialization failed"})";
		}
	}
	
	void ANSALPR::SetPlateFormats(const std::vector<std::string>& formats) {
		std::lock_guard<std::recursive_mutex> lock(_mutex);
		_plateFormats.clear();
		_plateFormats = formats;
	}
	void ANSALPR::SetPlateFormat(const std::string& format) {
		std::lock_guard<std::recursive_mutex> lock(_mutex);
		_plateFormats.clear();
		_plateFormats.push_back(format);
	}

	std::vector<std::string> ANSALPR::GetPlateFormats() const {
		return _plateFormats;
	}

	std::string ANSCENTER::ANSALPR::KeypointsToString(const std::vector<float>& kps) {
		if (kps.empty()) {
			return "";
		}
		std::string result;
		result.reserve(kps.size() * 10);

		char buffer[32];
		for (size_t i = 0; i < kps.size(); ++i) {
			if (i > 0) result += ';';
			snprintf(buffer, sizeof(buffer), "%.3f", kps[i]);
			result += buffer;
		}

		return result;
	}
	std::vector<cv::Point2f> ANSCENTER::ANSALPR::RectToNormalizedPolygon(const cv::Rect& rect, float imageWidth, float imageHeight) {
		// Ensure imageWidth and imageHeight are non-zero to avoid division by zero
		if (imageWidth <= 0 || imageHeight <= 0) {
			std::vector<cv::Point2f> emptyPolygon;
			return emptyPolygon;
		}

		// Calculate normalized points for each corner of the rectangle
		std::vector<cv::Point2f> polygon = {
			{ rect.x / imageWidth, rect.y / imageHeight },                     // Top-left
			{ (rect.x + rect.width) / imageWidth, rect.y / imageHeight },      // Top-right
			{ (rect.x + rect.width) / imageWidth, (rect.y + rect.height) / imageHeight }, // Bottom-right
			{ rect.x / imageWidth, (rect.y + rect.height) / imageHeight }      // Bottom-left
		};

		return polygon;
	}

	// Functions for screen size division
	double ANSALPR::calculateDistanceToCenter(const cv::Point& center, const cv::Rect& rect) {
		cv::Point rectCenter(rect.x + rect.width / 2, rect.y + rect.height / 2);
		return std::sqrt(std::pow(rectCenter.x - center.x, 2) + std::pow(rectCenter.y - center.y, 2));
	}
	std::vector<ANSALPR::ImageSection> ANSALPR::divideImage(const cv::Mat& image) {
		if (image.empty()) {
			std::cerr << "Error: Empty image!" << std::endl;
			return cachedSections;
		}
		cv::Size currentSize(image.cols, image.rows);

		// Check if the image size has changed
		if (currentSize == previousImageSize) {
			return cachedSections; // Return cached sections if size is the same
		}

		// Update previous size
		previousImageSize = currentSize;
		cachedSections.clear();

		int width = image.cols;
		int height = image.rows;
		int maxDimension = std::max(width, height);
		int numSections = 10;// std::max(1, numSections); // Ensure at least 1 section
		if (maxDimension <= 2560)numSections = 8;
		if (maxDimension <= 1280)numSections = 6;
		if (maxDimension <= 960)numSections = 4;
		if (maxDimension <= 640)numSections = 2;
		if (maxDimension <= 320)numSections = 1;

		int gridRows = std::sqrt(numSections);
		int gridCols = (numSections + gridRows - 1) / gridRows; // Ensure all sections are covered

		int sectionWidth = width / gridCols;
		int sectionHeight = height / gridRows;

		cv::Point imageCenter(width / 2, height / 2);
		std::vector<std::pair<double, ImageSection>> distancePriorityList;

		// Create sections and store their distance from the center
		for (int r = 0; r < gridRows; ++r) {
			for (int c = 0; c < gridCols; ++c) {
				int x = c * sectionWidth;
				int y = r * sectionHeight;
				int w = (c == gridCols - 1) ? width - x : sectionWidth;
				int h = (r == gridRows - 1) ? height - y : sectionHeight;

				ImageSection section(cv::Rect(x, y, w, h));
				double distance = calculateDistanceToCenter(imageCenter, section.region);
				distancePriorityList.emplace_back(distance, section);
			}
		}

		// Sort sections based on distance from center, then top-to-bottom, then left-to-right
		std::sort(distancePriorityList.begin(), distancePriorityList.end(),
			[](const std::pair<double, ImageSection>& a, const std::pair<double, ImageSection>& b) {
				if (std::abs(a.first - b.first) > 1e-5) {
					return a.first < b.first; // Sort by closest distance to center
				}
				// If distance is the same, prioritize top to bottom, then left to right
				return a.second.region.y == b.second.region.y
					? a.second.region.x < b.second.region.x
					: a.second.region.y < b.second.region.y;
			});

		// Assign priority
		int priority = 1;
		for (auto& entry : distancePriorityList) {
			entry.second.priority = priority++;
			cachedSections.push_back(entry.second);
		}

		return cachedSections;
	}
	
	
	std::vector<ANSALPR::ImageSection> ANSALPR::createSlideScreens(const cv::Mat& image) {
		if (image.empty()) {
			std::cerr << "Error: Empty image!" << std::endl;
			return cachedSections;
		}

		cv::Size currentSize(image.cols, image.rows);
		if (currentSize == previousImageSize) {
			return cachedSections;
		}
		previousImageSize = currentSize;
		cachedSections.clear();

		int maxSize = std::max(image.cols, image.rows);
		const int minCellSize = 320;
		int maxSections = 10;
		const float minAspectRatio = 0.8f;
		const float maxAspectRatio = 1.2f;

		if (maxSize <= 640)	maxSections = 1;
		else if (maxSize <= 960) maxSections = 2;
		else if (maxSize <= 1280) maxSections = 4;
		else if (maxSize <= 2560) maxSections = 6;
		else if (maxSize <= 3840) maxSections = 8;
		else maxSections = 10;

		int width = image.cols;
		int height = image.rows;
		int bestRows = 1, bestCols = 1;
		int bestTileSize = std::numeric_limits<int>::max();

		for (int rows = 1; rows <= maxSections; ++rows) {
			for (int cols = 1; cols <= maxSections; ++cols) {
				if (rows * cols > maxSections) continue;

				int tileWidth = (width + cols - 1) / cols;
				int tileHeight = (height + rows - 1) / rows;

				if (tileWidth < minCellSize || tileHeight < minCellSize)
					continue;

				float aspectRatio = static_cast<float>(tileWidth) / static_cast<float>(tileHeight);
				if (aspectRatio < minAspectRatio || aspectRatio > maxAspectRatio)
					continue;

				int maxTileDim = std::max(tileWidth, tileHeight);
				if (maxTileDim < bestTileSize) {
					bestTileSize = maxTileDim;
					bestRows = rows;
					bestCols = cols;
				}
			}
		}

		// Generate tiles using bestRows, bestCols
		int tileWidth = (width + bestCols - 1) / bestCols;
		int tileHeight = (height + bestRows - 1) / bestRows;

		int priority = 1;
		for (int r = bestRows - 1; r >= 0; --r) {
			for (int c = 0; c < bestCols; ++c) {
				int x = c * tileWidth;
				int y = r * tileHeight;

				int w = std::min(tileWidth, width - x);
				int h = std::min(tileHeight, height - y);

				if (w <= 0 || h <= 0) continue;

				cv::Rect region(x, y, w, h);
				ImageSection section(region);
				section.priority = priority++;
				cachedSections.push_back(section);
			}
		}

		return cachedSections;
	}
	int ANSALPR::getHighestPriorityRegion() {
		if (!cachedSections.empty()) {
			return cachedSections.front().priority; // First element has the highest priority
		}
		return 0; // Return empty rect if no sections exist
	}
	int ANSALPR::getLowestPriorityRegion() {
		if (!cachedSections.empty()) {
			return cachedSections.back().priority; // Last element has the lowest priority
		}
		return 0; // Return empty rect if no sections exist
	}
	cv::Rect ANSALPR::getRegionByPriority(int priority) {
		for (const auto& section : cachedSections) {
			if (section.priority == priority) {
				return section.region;
			}
		}
		return cv::Rect(); // Return empty rect if priority not found
	}

	std::vector<Object> ANSALPR::AdjustLicensePlateBoundingBoxes(
		const std::vector<Object>& detectionsInROI,
		const cv::Rect& roi,
		const cv::Size& fullImageSize,
		float aspectRatio,
		int padding
	) {
		std::vector<Object> adjustedDetections;

		try {
			// Basic input validation
			if (detectionsInROI.empty()) {
				return adjustedDetections;
			}

			if (roi.width <= 0 || roi.height <= 0 ||
				fullImageSize.width <= 0 || fullImageSize.height <= 0) {
				return adjustedDetections;
			}

			for (const auto& detInROI : detectionsInROI) {
				try {
					if (detInROI.box.width <= 0 || detInROI.box.height <= 0)
						continue; // Skip invalid box

					// Convert ROI-relative box to full-image coordinates
					cv::Rect detInFullImg;
					try {
						detInFullImg = detInROI.box + roi.tl();
						detInFullImg &= cv::Rect(0, 0, fullImageSize.width, fullImageSize.height);
					}
					catch (const std::exception& e) {
						std::cerr << "[AdjustBBox] Failed to calculate full image box: " << e.what() << std::endl;
						continue;
					}

					// Check if it touches ROI border
					bool touchesLeft = detInROI.box.x <= 0;
					bool touchesRight = detInROI.box.x + detInROI.box.width >= roi.width - 1;
					bool touchesTop = detInROI.box.y <= 0;
					bool touchesBottom = detInROI.box.y + detInROI.box.height >= roi.height - 1;
					bool touchesBorder = touchesLeft || touchesRight || touchesTop || touchesBottom;

					// Compute target width based on aspect ratio
					int targetWidth = 0, expandWidth = 0;
					try {
						targetWidth = std::max(detInFullImg.width, static_cast<int>(detInFullImg.height * aspectRatio));
						expandWidth = std::max(0, targetWidth - detInFullImg.width);
					}
					catch (const std::exception& e) {
						std::cerr << "[AdjustBBox] Aspect ratio adjustment failed: " << e.what() << std::endl;
						continue;
					}

					int expandLeft = expandWidth / 2;
					int expandRight = expandWidth - expandLeft;
					int padX = touchesBorder ? padding : 0;
					int padY = touchesBorder ? (padding / 2) : 0;

					// Apply padded and expanded box
					int newX = std::max(0, detInFullImg.x - expandLeft - padX);
					int newY = std::max(0, detInFullImg.y - padY);
					int newWidth = detInFullImg.width + expandWidth + 2 * padX;
					int newHeight = detInFullImg.height + 2 * padY;

					// Clamp to image boundaries
					if (newX + newWidth > fullImageSize.width) {
						newWidth = fullImageSize.width - newX;
					}
					if (newY + newHeight > fullImageSize.height) {
						newHeight = fullImageSize.height - newY;
					}

					if (newWidth <= 0 || newHeight <= 0)
						continue;

					// Construct adjusted object
					Object adjustedDet = detInROI;
					adjustedDet.box = cv::Rect(newX, newY, newWidth, newHeight);
					adjustedDetections.push_back(adjustedDet);
				}
				catch (const std::exception& e) {
					std::cerr << "[AdjustBBox] Exception per detection: " << e.what() << std::endl;
					continue;
				}
				catch (...) {
					std::cerr << "[AdjustBBox] Unknown exception per detection." << std::endl;
					continue;
				}
			}
		}
		catch (const std::exception& e) {
			std::cerr << "[AdjustBBox] Fatal error: " << e.what() << std::endl;
		}
		catch (...) {
			std::cerr << "[AdjustBBox] Unknown fatal error occurred." << std::endl;
		}
		return adjustedDetections;
	}

}
