//
// XMLTemplate.cpp
//

#include "Poco/PDF/XMLTemplate.h"
#include "Poco/AutoPtr.h"
#include "Poco/Exception.h"
#include "Poco/FileStream.h"
#include "Poco/PDF/Table.h"
#include "Poco/Path.h"
#include "Poco/SAX/Attributes.h"
#include "Poco/SAX/DefaultHandler.h"
#include "Poco/SAX/InputSource.h"
#include "Poco/SAX/SAXParser.h"
#include "Poco/String.h"
#include "Poco/TextConverter.h"
#include "Poco/UTF8Encoding.h"
#include "Poco/UTF8String.h"
#include "Poco/Util/PropertyFileConfiguration.h"
#include <algorithm>
#include <hpdf.h>
#include <ranges>
#include <set>
#include <sstream>
#include <vector>

namespace Poco {
namespace PDF {

class StackedConfiguration : public Poco::Util::AbstractConfiguration
{
public:
	using ConfigPtr = Poco::AutoPtr<Poco::Util::AbstractConfiguration>;
	using ConfigStack = std::vector<ConfigPtr>;

	void push(ConfigPtr pConfig)
	{
		_stack.push_back(pConfig);
	}

	void pop()
	{
		_stack.pop_back();
	}

	ConfigPtr current() const
	{
		poco_assert(_stack.size() > 0);
		return _stack.back();
	}

	float getFloat(const std::string& value)
	{
		return static_cast<float>(getDouble(value));
	}

	float getFloat(const std::string& value, float deflt)
	{
		return static_cast<float>(getDouble(value, deflt));
	}

	// AbstractConfiguration
	bool getRaw(const std::string& key, std::string& value) const override
	{
		for (const auto& it : std::ranges::reverse_view(_stack))
		{
			if (it->has(key))
			{
				value = it->getRawString(key);
				return true;
			}
		}
		return false;
	}

	void setRaw(const std::string& key, const std::string& value) override
	{
		throw Poco::InvalidAccessException("not writable");
	}

	void enumerate(const std::string& key, Poco::Util::AbstractConfiguration::Keys& range) const override
	{
		std::set<std::string> keys;
		for (const auto& itc : _stack)
		{
			Poco::Util::AbstractConfiguration::Keys partRange;
			itc->keys(key, partRange);
			for (const auto& itr : partRange)
			{
				if (keys.find(itr) == keys.end())
				{
					range.push_back(itr);
					keys.insert(itr);
				}
			}
		}
	}

	void removeRaw(const std::string& key) override
	{
		throw Poco::InvalidAccessException("not writable");
	}

private:
	std::vector<ConfigPtr> _stack;
};

class Box
{
public:
	Box() :
		_x(0),
		_y(0),
		_width(0),
		_height(0)
	{
	}

	Box(float x, float y, float width, float height) :
		_x(x),
		_y(y),
		_width(width),
		_height(height)
	{

	}

	Box(const Box& box) = default;
	~Box() = default;

	Box& operator = (const Box& box)
	{
		Box tmp(box);
		tmp.swap(*this);
		return *this;
	}

	void swap(Box& box)
	{
		std::swap(_x, box._x);
		std::swap(_y, box._y);
		std::swap(_width, box._width);
		std::swap(_height, box._height);
	}

	float left() const
	{
		return _x;
	}

	float right() const
	{
		return _x + _width;
	}

	float bottom() const
	{
		return _y;
	}

	float top() const
	{
		return _y + _height;
	}

	float width() const
	{
		return _width;
	}

	float height() const
	{
		return _height;
	}

	void inset(float delta)
	{
		_x += delta;
		_y -= delta;
		_width -= 2 * delta;
		_height -= 2 * delta;
	}

	void extend(float delta)
	{
		inset(-delta);
	}

	void inset(float left, float right, float top, float bottom)
	{
		_x += left;
		_y += bottom;
		_width -= (left + right);
		_height -= (top + bottom);
	}

private:
	float _x;
	float _y;
	float _width;
	float _height;
};


class TemplateHandler : public Poco::XML::DefaultHandler
{
public:
	using StylePtr = Poco::AutoPtr<Poco::Util::AbstractConfiguration>;

	TemplateHandler(const Poco::Path& base) : _base(base)
	{
		_styles.push(parseStyle("font-family: Helvetica; font-size: 12; line-height: 1.2"));
	}

	~TemplateHandler() override
	{
		_styles.pop();
		delete _pPage;
		delete _pDocument;
	}

	Document* document()
	{
		Document* pDocument = _pDocument;
		_pDocument = nullptr;
		return pDocument;
	}

	void startDoc(const Poco::XML::Attributes& attributes)
	{
		if (_pDocument) throw Poco::IllegalStateException("only one <document> element is allowed");

		StylePtr pStyle = pushStyle(attributes);

		_size = attributes.getValue("size");
		if (_size.empty()) _size = "A4";
		_orientation = attributes.getValue("orientation");
		if (_orientation.empty()) _orientation = "portrait";

		_encoding = attributes.getValue("encoding");
		if (_encoding.empty()) _encoding = "WinAnsiEncoding";

		_pDocument = new Document(0, parsePageSize(_size), parseOrientation(_orientation));
	}

	void endDoc()
	{
		popStyle();
	}

	void startPage(const Poco::XML::Attributes& attributes)
	{
		if (!_pDocument) throw Poco::IllegalStateException("missing <document> element");
		if (_pPage) throw Poco::IllegalStateException("nested <page> elements are not allowed");

		StylePtr pStyle = pushStyle(attributes);

		std::string size = attributes.getValue("size");
		if (size.empty()) size = _size;
		std::string orientation = attributes.getValue("orientation");
		if (orientation.empty()) orientation = _orientation;

		_pPage = new Page(_pDocument->addPage(parsePageSize(_size), parseOrientation(orientation)));
		_pPage->setLineWidth(0.2f);
		RGBColor black = {0, 0, 0};
		_pPage->setRGBStroke(black);

		// read or force font, page must have default
		std::string fontFamily = _styles.getString("font-family", "helvetica");
		float fontSize = _styles.getFloat("font-size", 10.0);
		std::string fontStyle = _styles.getString("font-style", "normal");
		std::string fontWeight = _styles.getString("font-weight", "normal");

		Font font = loadFont(fontFamily, fontStyle, fontWeight);
		_pPage->setFont(font, fontSize);

		_boxes.emplace_back(0, 0, _pPage->getWidth(), _pPage->getHeight());

		float margin = _styles.getFloat("margin", 0);
		float marginLeft = _styles.getFloat("margin-left", margin);
		float marginRight = _styles.getFloat("margin-right", margin);
		float marginTop = _styles.getFloat("margin-top", margin);
		float marginBottom = _styles.getFloat("margin-bottom", margin);

		_boxes.back().inset(marginLeft, marginRight, marginTop, marginBottom);

		_y = _boxes.back().top();
	}

	void endPage()
	{
		_boxes.pop_back();
		delete _pPage;
		_pPage = nullptr;
		popStyle();
	}

	void startSpan(const Poco::XML::Attributes& attributes)
	{
		if (!_pPage) throw Poco::IllegalStateException("missing <page> element");

		StylePtr pStyle = pushStyle(attributes);

		_text.clear();
	}

	void endSpan()
	{
		std::string fontFamily = _styles.getString("font-family");
		float fontSize = _styles.getFloat("font-size");
		float lineHeight = _styles.getFloat("line-height");
		std::string textAlign = _styles.getString("text-align", "left");
		std::string fontStyle = _styles.getString("font-style", "normal");
		std::string fontWeight = _styles.getString("font-weight", "normal");
		std::string textTransform = _styles.getString("text-transform", "none");

		_text = transform(_text, textTransform);
		_text = transcode(_text);

		Font font = loadFont(fontFamily, fontStyle, fontWeight);
		_pPage->setFont(font, fontSize);

		float width = static_cast<float>(font.textWidth(_text).width*fontSize / 1000);
		float height = static_cast<float>(font.upperHeight()*fontSize / 1000)*lineHeight;

		float x = static_cast<float>(_styles.current()->getDouble("left", _styles.current()->getDouble("right", width) - width));
		float y = static_cast<float>(_styles.current()->getDouble("bottom", _styles.current()->getDouble("top", _y) - height));

		if (textAlign == "center")
			x = (box().width() - width) / 2;
		else if (textAlign == "right")
			x = box().width() - width;

		translateInBox(x, y);

		_pPage->writeOnce(x, y, _text);

		moveY(y);

		popStyle();
	}

	void startImg(const Poco::XML::Attributes& attributes)
	{
		if (!_pPage) throw Poco::IllegalStateException("missing <page> element");

		StylePtr pStyle = pushStyle(attributes);

		std::string path = attributes.getValue("src");
		Image image = loadImage(path);

		float width = static_cast<float>(pStyle->getDouble("width", image.width()));
		float height = static_cast<float>(pStyle->getDouble("height", image.height()));

		float scale = static_cast<float>(pStyle->getDouble("scale", 1.0));
		float scaleX = static_cast<float>(pStyle->getDouble("scale-x", scale));
		float scaleY = static_cast<float>(pStyle->getDouble("scale-y", scale));

		width *= scaleX;
		height *= scaleY;

		float x = static_cast<float>(_styles.current()->getDouble("left", _styles.current()->getDouble("right", width) - width));
		float y = static_cast<float>(_styles.current()->getDouble("bottom", _styles.current()->getDouble("top", _y) - height));

		translateInBox(x, y);

		_pPage->drawImage(image, x, y, width, height);

		moveY(y);
	}

	void endImg()
	{
		popStyle();
	}

	void startTable(const Poco::XML::Attributes& attributes)
	{
		if (!_pPage) throw Poco::IllegalStateException("missing <page> element");

		StylePtr pStyle = pushStyle(attributes);

		_pTable = new Table(0, 0, attributes.getValue("name"));
	}

	void endTable()
	{
		if (_pTable->rows() > 0)
		{
			std::string fontFamily = _styles.getString("font-family");
			float fontSize = _styles.getFloat("font-size");
			std::string fontStyle = _styles.getString("font-style", "normal");
			std::string fontWeight = _styles.getString("font-weight", "normal");

			Font font = loadFont(fontFamily, fontStyle, fontWeight);

			float lineHeight = static_cast<float>((font.ascent() - font.descent())*fontSize / 1000)*_styles.getFloat("line-height");

			float width = static_cast<float>(_styles.current()->getDouble("width", box().width()));
			float height = static_cast<float>(_styles.current()->getDouble("height", _pTable->rows()*lineHeight));

			float x = static_cast<float>(_styles.current()->getDouble("left", _styles.current()->getDouble("right", width) - width));
			float y = static_cast<float>(_styles.current()->getDouble("bottom", _styles.current()->getDouble("top", _y) - height) + height);
			y -= lineHeight;

			translateInBox(x, y);

			_pTable->draw(*_pPage, x, y, width, height);

			y -= height - lineHeight;

			moveY(y);
		}
		_pTable = nullptr;
		popStyle();
	}

	void startTr(const Poco::XML::Attributes& attributes)
	{
		if (!_pTable) throw Poco::IllegalStateException("missing <table> element");

		StylePtr pStyle = pushStyle(attributes);
		_row.clear();
	}

	void endTr()
	{
		_pTable->addRow(_row);
		_row.clear();
		popStyle();
	}

	void startTd(const Poco::XML::Attributes& attributes)
	{
		StylePtr pStyle = pushStyle(attributes);

		_text.clear();
	}

	void endTd()
	{
		AttributedString::Alignment align = AttributedString::ALIGN_LEFT;
		int style = AttributedString::STYLE_PLAIN;


		std::string fontFamily = _styles.getString("font-family");
		float       fontSize = _styles.getFloat("font-size");
		std::string textAlign = _styles.getString("text-align", "left");
		std::string fontStyle = _styles.getString("font-style", "normal");
		std::string fontWeight = _styles.getString("font-weight", "normal");
		std::string textTransform = _styles.getString("text-transform", "none");
		std::string widthPct = _styles.getString("width", "");
		// solid only supported at this time
		bool borderAll = _styles.getString("border-style", "") == "solid";
		bool borderLeft = _styles.getString("border-left", "") == "solid";
		bool borderTop = _styles.getString("border-top", "") == "solid";
		bool borderRight = _styles.getString("border-right", "") == "solid";
		bool borderBottom = _styles.getString("border-bottom", "") == "solid";

		_text = transform(_text, textTransform);
		_text = transcode(_text);

		if (textAlign == "right")
			align = AttributedString::ALIGN_RIGHT;
		else if (textAlign == "left")
			align = AttributedString::ALIGN_LEFT;
		else if (textAlign == "center")
			align = AttributedString::ALIGN_CENTER;

		if (fontStyle == "italic" || fontStyle == "oblique")
			style |= AttributedString::STYLE_ITALIC;

		if (fontWeight == "bold")
			style |= AttributedString::STYLE_BOLD;

		AttributedString content(_text, align, style);
		content.setAttribute(AttributedString::ATTR_SIZE, fontSize);

		Cell::FontMapPtr pFontMap = new Cell::FontMap;
		std::string normalizedFontFamily(normalizeFontName(fontFamily));
		(*pFontMap)[AttributedString::STYLE_PLAIN] = normalizedFontFamily;
		(*pFontMap)[AttributedString::STYLE_BOLD] = boldFontName(normalizedFontFamily);
		(*pFontMap)[AttributedString::STYLE_ITALIC] = italicFontName(normalizedFontFamily);
		(*pFontMap)[AttributedString::STYLE_BOLD | AttributedString::STYLE_ITALIC] = boldItalicFontName(normalizedFontFamily);

		int width = -1;
		if (!widthPct.empty())
		{
			if (*widthPct.rbegin() != '%')
				throw Poco::InvalidArgumentException("Only percentage widths supported for table cells.");
			else
			{
				widthPct.erase(widthPct.length() - 1);
				width = NumberParser::parse(widthPct);
			}
		}

		Cell cell(content, pFontMap, _encoding, false, width);
		if (borderAll) cell.borderAll(true);
		if (borderLeft) cell.borderLeft(true);
		if (borderTop) cell.borderTop(true);
		if (borderRight) cell.borderRight(true);
		if (borderBottom) cell.borderBottom(true);
		_row.push_back(cell);

		popStyle();
	}

	void startHr(const Poco::XML::Attributes& attributes)
	{
		if (!_pPage) throw Poco::IllegalStateException("missing <page> element");

		StylePtr pStyle = pushStyle(attributes);

		float width = static_cast<float>(_styles.current()->getDouble("width", box().width()));
		float height = static_cast<float>(_styles.current()->getDouble("height", 0.2));

		float x = static_cast<float>(_styles.current()->getDouble("left", _styles.current()->getDouble("right", width) - width));
		float y = static_cast<float>(_styles.current()->getDouble("bottom", _styles.current()->getDouble("top", _y) - height));

		translateInBox(x, y);

		_pPage->moveTo(x, y);
		_pPage->lineTo(x + width, y);
		_pPage->stroke();

		moveY(y);
	}

	void endHr()
	{
		popStyle();
	}

	// DocumentHandler
	void startDocument() override
	{
	}

	void endDocument() override
	{
	}

	void startElement(const Poco::XML::XMLString& uri, const Poco::XML::XMLString& localName, const Poco::XML::XMLString& qname, const Poco::XML::Attributes& attributes) override
	{
		if (localName == "document")
			startDoc(attributes);
		else if (localName == "page")
			startPage(attributes);
		else if (localName == "span")
			startSpan(attributes);
		else if (localName == "img")
			startImg(attributes);
		else if (localName == "table")
			startTable(attributes);
		else if (localName == "tr")
			startTr(attributes);
		else if (localName == "td")
			startTd(attributes);
		else if (localName == "hr")
			startHr(attributes);
	}

	void endElement(const Poco::XML::XMLString& uri, const Poco::XML::XMLString& localName, const Poco::XML::XMLString& qname) override
	{
		if (localName == "document")
			endDoc();
		else if (localName == "page")
			endPage();
		else if (localName == "span")
			endSpan();
		else if (localName == "img")
			endImg();
		else if (localName == "table")
			endTable();
		else if (localName == "tr")
			endTr();
		else if (localName == "td")
			endTd();
		else if (localName == "hr")
			endHr();
	}

	void characters(const Poco::XML::XMLChar ch[], int start, int length) override
	{
		_text.append(ch + start, length);
	}

protected:
	StylePtr pushStyle(const Poco::XML::Attributes& attributes)
	{
		StylePtr pStyle = parseStyle(attributes);
		if (_boxes.size() > 0)
		{
			pStyle->setDouble("box.width", box().width());
			pStyle->setDouble("box.height", box().height());
		}
		_styles.push(pStyle);
		return pStyle;
	}

	void popStyle()
	{
		_styles.pop();
	}

	StylePtr parseStyle(const Poco::XML::Attributes& attributes)
	{
		return parseStyle(attributes.getValue("style"));
	}

	StylePtr parseStyle(const std::string& style) const
	{
		std::string props = Poco::translate(style, ";", "\n");
		std::istringstream istr(props);
		return new Poco::Util::PropertyFileConfiguration(istr);
	}

	static Page::Size parsePageSize(const std::string& size)
	{
		using Poco::icompare;
		if (icompare(size, "letter") == 0)
			return Page::PAGE_SIZE_LETTER;
		else if (icompare(size, "legal") == 0)
			return Page::PAGE_SIZE_LEGAL;
		else if (icompare(size, "a3") == 0)
			return Page::PAGE_SIZE_A3;
		else if (icompare(size, "a4") == 0)
			return Page::PAGE_SIZE_A4;
		else if (icompare(size, "a5") == 0)
			return Page::PAGE_SIZE_A5;
		else if (icompare(size, "b4") == 0)
			return Page::PAGE_SIZE_B4;
		else if (icompare(size, "b5") == 0)
			return Page::PAGE_SIZE_B5;
		else if (icompare(size, "executive") == 0)
			return Page::PAGE_SIZE_EXECUTIVE;
		else if (icompare(size, "us4x6") == 0)
			return Page::PAGE_SIZE_US4x6;
		else if (icompare(size, "us4x8") == 0)
			return Page::PAGE_SIZE_US4x8;
		else if (icompare(size, "us5x7") == 0)
			return Page::PAGE_SIZE_US5x7;
		else if (icompare(size, "comm10") == 0)
			return Page::PAGE_SIZE_COMM10;
		else throw Poco::InvalidArgumentException("size", size);
	}

	static Page::Orientation parseOrientation(const std::string& orientation)
	{
		if (icompare(orientation, "portrait") == 0)
			return Page::ORIENTATION_PORTRAIT;
		else if (icompare(orientation, "landscape") == 0)
			return Page::ORIENTATION_LANDSCAPE;
		else throw Poco::InvalidArgumentException("orientation", orientation);
	}

	std::string normalizeFontName(const std::string& fontFamily)
	{
		poco_assert(fontFamily.size());

		std::string fontName = toLower(fontFamily);
		fontName[0] = Poco::Ascii::toUpper(fontName[0]);
		return fontName;
	}

	Font loadFont(const std::string& fontFamily, const std::string& fontStyle, const std::string& fontWeight)
	{
		poco_assert(_pDocument);

		std::string normalizedFontFamily = normalizeFontName(fontFamily);
		std::string fontName;
		if (fontStyle == "italic" || fontStyle == "oblique")
		{
			if (fontWeight == "bold")
				fontName = boldItalicFontName(normalizedFontFamily);
			else
				fontName = italicFontName(normalizedFontFamily);
		} else if (fontWeight == "bold")
		{
			fontName = boldFontName(normalizedFontFamily);
		} else
		{
			fontName = normalizedFontFamily;
		}
		return _pDocument->font(fontName, _encoding);
	}

	std::string boldFontName(const std::string& fontFamily)
	{
		try
		{
			return _pDocument->font(fontFamily + "-Bold", _encoding).name();
		} catch (...)
		{
		}
		return fontFamily;
	}

	std::string italicFontName(const std::string& fontFamily)
	{
		try
		{
			return _pDocument->font(fontFamily + "-Oblique", _encoding).name();
		} catch (...)
		{
		}
		try
		{
			return _pDocument->font(fontFamily + "-Italic", _encoding).name();
		} catch (...)
		{
		}
		return fontFamily;
	}

	std::string boldItalicFontName(const std::string& fontFamily)
	{
		try
		{
			return _pDocument->font(fontFamily + "-BoldOblique", _encoding).name();
		} catch (...)
		{
		}
		try
		{
			Font font = _pDocument->font(fontFamily + "-BoldItalic", _encoding);
		} catch (...)
		{
		}
		return fontFamily;
	}

	Image loadImage(const std::string& path)
	{
		Poco::Path p(_base);
		p.resolve(path);
		if (Poco::icompare(p.getExtension(), "jpg") == 0 || icompare(p.getExtension(), "jpeg") == 0)
			return _pDocument->loadJPEGImage(p.toString());
		else if (Poco::icompare(p.getExtension(), "png") == 0)
			return _pDocument->loadPNGImage(p.toString());
		else
			throw Poco::InvalidArgumentException("cannot determine image type", path);
	}

	void translateInBox(float& x, float& y)
	{
		if (x < 0)
			x = box().right() + x;
		else
			x += box().left();

		if (y < 0)
			y = box().top() + y;
		else
			y += box().bottom();
	}

	void moveY(float y)
	{
		float padding = _styles.getFloat("padding", 0);
		float newY = y - padding;
		newY -= box().bottom();

		_y = std::min(_y, newY);
	}

	const Box& box() const
	{
		poco_assert(_boxes.size() > 0);
		return _boxes.back();
	}

	std::string transcode(const std::string& text)
	{
		std::string result;
		Poco::UTF8Encoding inEncoding;
		Poco::TextEncoding& outEncoding = Poco::TextEncoding::byName(mapEncoding(_encoding));
		Poco::TextConverter converter(inEncoding, outEncoding);
		converter.convert(text, result);
		return result;
	}

	static std::string mapEncoding(const std::string& encoding)
	{
		if (encoding == "WinAnsiEncoding")
			return "Latin-1";
		else if (encoding == "ISO8859-2")
			return "Latin-2";
		else if (encoding == "ISO8859-15")
			return "Latin-9";
		else if (encoding == "CP1250")
			return "CP1250";
		else if (encoding == "CP1251")
			return "CP1251";
		else if (encoding == "CP1252")
			return "CP1252";
		else
			throw Poco::InvalidArgumentException("PDF Document encoding not supported", encoding);
	}

	static std::string transform(const std::string& text, const std::string& trans)
	{
		if (trans == "uppercase")
			return UTF8::toUpper(text);
		else if (trans == "lowercase")
			return UTF8::toLower(text);
		else
			return text;
	}

private:
	Poco::Path _base;
	std::string _encoding;
	Document* _pDocument{nullptr};
	Page* _pPage{nullptr};
	Table::Ptr _pTable;
	TableRow _row;
	StackedConfiguration _styles;
	std::vector<Box> _boxes;
	std::string _size;
	std::string _orientation;
	std::string _text;
	float _y{0};
};


XMLTemplate::XMLTemplate(std::istream& xmlStream, const std::string& base) :
	_base(base),
	_pDocument(nullptr)
{
	load(xmlStream);
}


XMLTemplate::XMLTemplate(const std::string& path) :
	_base(path),
	_pDocument(nullptr)
{
	Poco::FileInputStream xmlStream(path);
	load(xmlStream);
}


XMLTemplate::~XMLTemplate()
{
	delete _pDocument;
}


void XMLTemplate::load(std::istream& xmlStream)
{
	Poco::XML::InputSource xmlSource(xmlStream);
	Poco::XML::SAXParser parser;
	TemplateHandler handler(_base);
	parser.setContentHandler(&handler);
	parser.setFeature(Poco::XML::XMLReader::FEATURE_NAMESPACES, true);
	parser.setFeature(Poco::XML::XMLReader::FEATURE_NAMESPACE_PREFIXES, true);
	parser.parse(&xmlSource);

	_pDocument = handler.document();
}


void XMLTemplate::create(const std::string& path)
{
	_pDocument->save(path);
}


} } // namespace Poco::PDF
