//
// Document.cpp
//
// Library: MongoDB
// Package: MongoDB
// Module:  Document
//
// Copyright (c) 2012-2025, Applied Informatics Software Engineering GmbH.
// and Contributors.
//
// SPDX-License-Identifier:	BSL-1.0
//


#include "Poco/MongoDB/Document.h"
#include "Poco/MongoDB/Binary.h"
#include "Poco/MongoDB/ObjectId.h"
#include "Poco/MongoDB/Array.h"
#include "Poco/MongoDB/RegularExpression.h"
#include "Poco/MongoDB/JavaScriptCode.h"
#include <algorithm>
#include <sstream>


namespace Poco {
namespace MongoDB {


Document::Document()
{
}


Document::~Document()
{
}


Array& Document::addNewArray(const std::string& name)
{
	Array::Ptr newArray = new Array();
	add(name, newArray);
	return *newArray;
}


Element::Ptr Document::get(const std::string& name) const
{
	// O(1) hash map lookup instead of O(n) linear search
	auto it = _elementMap.find(name);
	if (it != _elementMap.end())
	{
		return it->second;
	}

	return Element::Ptr();  // Return empty pointer if not found
}


Int64 Document::getInteger(const std::string& name) const
{
	Element::Ptr element = get(name);
	if (element.isNull()) throw Poco::NotFoundException(name);

	if (ElementTraits<double>::TypeId == element->type())
	{
		// Type is already verified, static_cast is safe and ~10x faster than dynamic_cast
		auto* concrete = static_cast<ConcreteElement<double>*>(element.get());
		return static_cast<Int64>(concrete->value());
	}
	else if (ElementTraits<Int32>::TypeId == element->type())
	{
		auto* concrete = static_cast<ConcreteElement<Int32>*>(element.get());
		return concrete->value();
	}
	else if (ElementTraits<Int64>::TypeId == element->type())
	{
		auto* concrete = static_cast<ConcreteElement<Int64>*>(element.get());
		return concrete->value();
	}
	throw Poco::BadCastException("Invalid type mismatch!");
}


void Document::read(BinaryReader& reader)
{
	int size;
	reader >> size;

	unsigned char type;
	reader >> type;

	while (type != '\0')
	{
		Element::Ptr element;

		std::string name = BSONReader(reader).readCString();

		switch (type)
		{
		case ElementTraits<double>::TypeId:
			element = new ConcreteElement<double>(name, 0);
			break;
		case ElementTraits<Int32>::TypeId:
			element = new ConcreteElement<Int32>(name, 0);
			break;
		case ElementTraits<std::string>::TypeId:
			element = new ConcreteElement<std::string>(name, "");
			break;
		case ElementTraits<Document::Ptr>::TypeId:
			element = new ConcreteElement<Document::Ptr>(name, new Document);
			break;
		case ElementTraits<Array::Ptr>::TypeId:
			element = new ConcreteElement<Array::Ptr>(name, new Array);
			break;
		case ElementTraits<Binary::Ptr>::TypeId:
			element = new ConcreteElement<Binary::Ptr>(name, new Binary);
			break;
		case ElementTraits<ObjectId::Ptr>::TypeId:
			element = new ConcreteElement<ObjectId::Ptr>(name, new ObjectId);
			break;
		case ElementTraits<bool>::TypeId:
			element = new ConcreteElement<bool>(name, false);
			break;
		case ElementTraits<Poco::Timestamp>::TypeId:
			element = new ConcreteElement<Poco::Timestamp>(name, Poco::Timestamp());
			break;
		case ElementTraits<BSONTimestamp>::TypeId:
			element = new ConcreteElement<BSONTimestamp>(name, BSONTimestamp());
			break;
		case ElementTraits<NullValue>::TypeId:
			element = new ConcreteElement<NullValue>(name, NullValue(0));
			break;
		case ElementTraits<RegularExpression::Ptr>::TypeId:
			element = new ConcreteElement<RegularExpression::Ptr>(name, new RegularExpression());
			break;
		case ElementTraits<JavaScriptCode::Ptr>::TypeId:
			element = new ConcreteElement<JavaScriptCode::Ptr>(name, new JavaScriptCode());
			break;
		case ElementTraits<Int64>::TypeId:
			element = new ConcreteElement<Int64>(name, 0);
			break;
		default:
			{
				std::stringstream ss;
				ss << "Element " << name << " contains an unsupported type 0x" << std::hex << static_cast<int>(type);
				throw Poco::NotImplementedException(ss.str());
			}
		//TODO: x0F -> JavaScript code with scope
		//		xFF -> Min Key
		//		x7F -> Max Key
		}

		element->read(reader);
		_elementNames.push_back(element->name());
		_elementMap[element->name()] = element;  // Populate hash map for O(1) lookups

		reader >> type;
	}
}


std::string Document::toString(int indent) const
{
	std::ostringstream oss;
	// Pre-reserve reasonable capacity for small-medium documents to reduce reallocations
	oss.str().reserve(256);

	oss << '{';

	if (indent > 0) oss << std::endl;

	for (auto it = _elementNames.begin(), total = _elementNames.end(); it != total; ++it)
	{
		if (it != _elementNames.begin())
		{
			oss << ',';
			if (indent > 0) oss << std::endl;
		}

		if (indent > 0)
		{
			const std::string indentStr(indent, ' ');
			oss << indentStr;
		}

		const auto& element = _elementMap.at(*it);
		oss << '"' << *it << '"';
		oss << (indent > 0  ? " : " : ":");

		oss << element->toString(indent > 0 ? indent + 2 : 0);
	}

	if (indent > 0)
	{
		oss << std::endl;
		if (indent >= 2) indent -= 2;

		const std::string indentStr(indent, ' ');
		oss << indentStr;
	}

	oss << '}';

	return oss.str();
}


void Document::write(BinaryWriter& writer) const
{
	if (_elementNames.empty())
	{
		writer << 5;
	}
	else
	{
		std::stringstream sstream;
		Poco::BinaryWriter tempWriter(sstream, BinaryWriter::LITTLE_ENDIAN_BYTE_ORDER);
		for (const auto& name : _elementNames)
		{
			const auto& element = _elementMap.at(name);
			tempWriter << static_cast<unsigned char>(element->type());
			BSONWriter(tempWriter).writeCString(element->name());
			element->write(tempWriter);
		}
		tempWriter.flush();

		Poco::Int32 len = static_cast<Poco::Int32>(5 + sstream.tellp()); /* 5 = sizeof(len) + 0-byte */
		writer << len;
		writer.writeRaw(sstream.str());
	}
	writer << '\0';
}


void Document::reserve(std::size_t size)
{
	_elementNames.reserve(size);
	_elementMap.reserve(size);
}


Document& Document::addElement(Element::Ptr element)
{
	// Check if element with this name already exists
	auto it = _elementMap.find(element->name());
	if (it == _elementMap.end())
	{
		// New element - add name to vector and element to map
		_elementNames.push_back(element->name());
		_elementMap[element->name()] = element;
	}
	else
	{
		// Element exists - only update the map (replace existing)
		_elementMap[element->name()] = element;
	}
	return *this;
}


bool Document::remove(const std::string& name)
{
	// Remove from hash map first (O(1))
	auto mapIt = _elementMap.find(name);
	if (mapIt == _elementMap.end())
		return false;

	_elementMap.erase(mapIt);

	// Then remove from vector (O(n) but unavoidable for order preservation)
	auto it = std::find(_elementNames.begin(), _elementNames.end(), name);
	if (it != _elementNames.end())
		_elementNames.erase(it);

	return true;
}


} } // namespace Poco::MongoDB
