/*************************************************************************
** TransformSimplifier.cpp                                              **
**                                                                      **
** This file is part of dvisvgm -- a fast DVI to SVG converter          **
** Copyright (C) 2005-2024 Martin Gieseking <martin.gieseking@uos.de>   **
**                                                                      **
** This program is free software; you can redistribute it and/or        **
** modify it under the terms of the GNU General Public License as       **
** published by the Free Software Foundation; either version 3 of       **
** the License, or (at your option) any later version.                  **
**                                                                      **
** This program is distributed in the hope that it will be useful, but  **
** WITHOUT ANY WARRANTY; without even the implied warranty of           **
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the         **
** GNU General Public License for more details.                         **
**                                                                      **
** You should have received a copy of the GNU General Public License    **
** along with this program; if not, see <http://www.gnu.org/licenses/>. **
*************************************************************************/

#include <cmath>
#include <cstdlib>
#include "TransformSimplifier.hpp"
#include "../Matrix.hpp"
#include "../utility.hpp"
#include "../XMLNode.hpp"
#include "../XMLString.hpp"

using namespace std;

const char* TransformSimplifier::info () const {
	return "try to simplify and shorten the values of 'transform' attributes";
}


/** Tries to simplify the transform attributes of the context node and all its descendants. */
void TransformSimplifier::execute (XMLElement *context) {
	if (!context)
		return;
	if (const char *transform = context->getAttributeValue("transform")) {
		Matrix matrix = Matrix::parseSVGTransform(transform);
		if (!incorporateTransform(context, matrix)) {
			string decomp = decompose(matrix);
			if (decomp.length() > matrix.toSVG().length())
				context->addAttribute("transform", matrix.toSVG());
			else {
				if (decomp.empty())
					context->removeAttribute("transform");
				else
					context->addAttribute("transform", decomp);
			}
		}
	}
	// continue with child elements
	for (XMLNode *child : *context) {
		if (XMLElement *elem = child->toElement())
			execute(elem);
	}
}


/** Tries to incorporate the translation and scaling components of the 'transform' attribute
 *  of a given element into the positional and/or size attributes of that element. If successful,
 *  the 'transform' attribute is either removed or simplified to a scale() matrix.
 *  Currently, only 'image' and 'rect' elements are considered.
 *  @param[in] elem element to check
 *  @param[in] matrix matrix representing the 'transform' attribute of the element
 *  @return true on success */
bool TransformSimplifier::incorporateTransform (XMLElement *elem, const Matrix &matrix) {
	if ((elem->name() == "image" || elem->name() == "rect") && matrix.get(0, 1) == 0 && matrix.get(1, 0) == 0) {
		double tx = matrix.get(0, 2);
		double ty = matrix.get(1, 2);
		double sx = matrix.get(0, 0);
		double sy = matrix.get(1, 1);
		double x=0, y=0;

		if (const char *xstr = elem->getAttributeValue("x"))
			x = strtod(xstr, nullptr);
		if (const char *ystr = elem->getAttributeValue("y"))
			y = strtod(ystr, nullptr);
		// width and height attributes must not become negative. Hence, only apply the scaling
		// values if they are non-negative. Otherwise, keep a scaling matrix. Also retain scaling
		// transformations in image elements to avoid the need of attribute 'preseveAspectRatio'.
		if (sx < 0 || sy < 0 || elem->name() == "image") {
			x += (sx == 0 ? 0 : tx/sx);
			y += (sy == 0 ? 0 : ty/sy);
			elem->addAttribute("transform", "scale("+XMLString(sx)+","+XMLString(sy)+")");
		}
		else {
			x = x*sx + tx;
			y = y*sy + ty;
			if (const char *wstr = elem->getAttributeValue("width"))
				elem->addAttribute("width", sx*strtod(wstr, nullptr));
			if (const char *hstr = elem->getAttributeValue("height"))
				elem->addAttribute("height", sy*strtod(hstr, nullptr));
			elem->removeAttribute("transform");
		}
		elem->addAttribute("x", x);  // update x attribute
		elem->addAttribute("y", y);  // update y attribute
		return true;
	}
	return false;
}


static string translate_cmd (double dx, double dy) {
	string ret;
	XMLString dxstr(dx), dystr(dy);
	if (dxstr != "0" || dystr != "0") {
		ret = "translate("+dxstr;
		if (dystr != "0")
			ret += " "+dystr;
		ret += ')';
	}
	return ret;
}


static string scale_cmd (double sx, double sy) {
	string ret;
	XMLString sxstr(sx), systr(sy);
	if (sxstr != "1" || systr != "1") {
		ret = "scale("+sxstr;
		if (systr != sxstr)
			ret += " "+systr;
		ret += ')';
	}
	return ret;
}


static string rotate_cmd (double rad) {
	string ret;
	XMLString degstr(math::rad2deg(fmod(rad, math::TWO_PI)));
	if (degstr != "0")
		ret = "rotate("+degstr+")";
	return ret;
}


static string skewx_cmd (double rad) {
	string ret;
	XMLString degstr(math::rad2deg(fmod(rad, math::PI)));
	if (degstr != "0")
		ret = "skewX("+degstr+")";
	return ret;
}


static string skewy_cmd (double rad) {
	string ret;
	XMLString degstr(math::rad2deg(fmod(rad, math::PI)));
	if (degstr != "0")
		ret = "skewY("+degstr+")";
	return ret;
}


static bool not_equal (double x, double y) {
	return abs(x-y) >= 1e-6;
}


/** Decomposes a transformation matrix into a sequence of basic SVG transformations, i.e.
 *  translation, rotation, scaling, and skewing. The algorithm (QR-based decomposition)
 *  is taken from http://frederic-wang.fr/decomposition-of-2d-transform-matrices.html.
 *  @param[in] matrix matrix to decompose
 *  @return string containing the SVG transformation commands */
string TransformSimplifier::decompose (const Matrix &matrix) {
	// transformation matrix [a b c d e f] according to
	// https://www.w3.org/TR/SVG11/coords.html#EstablishingANewUserSpace
	double a = matrix.get(0, 0);
	double b = matrix.get(1, 0);
	double c = matrix.get(0, 1);
	double d = matrix.get(1, 1);
	double e = matrix.get(0, 2);
	double f = matrix.get(1, 2);
	string ret = translate_cmd(e, f);
	double delta = a*d - b*c;
	if (not_equal(a, 0) || not_equal(b, 0)) {
		double r = sqrt(a*a + b*b);
		ret += rotate_cmd(b > 0 ? acos(a/r) : -acos(a/r));
		ret += scale_cmd(r, delta/r);
		ret += skewx_cmd(atan((a*c + b*d)/(r*r)));
	}
	else if (not_equal(c, 0) || not_equal(d, 0)) {
		double s = sqrt(c*c + d*d);
		ret += rotate_cmd(math::HALF_PI - (d > 0 ? acos(-c/s) : -acos(c/s)));
		ret += scale_cmd(delta/s, s);
		ret += skewy_cmd(atan((a*c + b*d)/(s*s)));
	}
	else
		ret += scale_cmd(0, 0);
	return ret;
}