/**
 * Project HtmlToLatex from SourceForge
 * https://sourceforge.net/projects/htmltolatex/
 *
 * original Author : Michal Kebrt
 * (contains Word-to-LaTeX (Word-to-XML) convertor, mkrss (RSS reader)
 * and a couple of other programs)
 * original licence is GNU/GPL (unknown version)
 * last modified date of the original project : 2012-09-19
 *
 * adaptation : FaVdB (c) 2023
 *
 */
package storybook.exim.doc.LATEX;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map.Entry;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import storybook.exim.doc.LATEX.LatexConverter.LK;
import storybook.exim.doc.LATEX.css.CSS;
import storybook.exim.doc.LATEX.css.CSSParser;
import storybook.exim.doc.LATEX.css.CSSParserHandler;

/**
 * Program configuration. All of the configuration from the XML configuration file is stored in this
 * class.
 */
public class LatexConf {

	private HashMap<String, ElemConfigItem> _elements;
	private HashMap<String, String> _chars;
	private HashMap<Integer, String> _charsNum;
	private HashMap<String, CSS> _styles;
	private HashMap<String, CSSItem> _stylesConf;
	private LatexConverter.LK _linksConversion;
	private boolean _makeCmdsFromCSS = false;

	/**
	 * Prefix of new LaTeX commands generated by the program.
	 */
	private String _commandsPrefix = "";

	/**
	 * Loads all the configuration.
	 *
	 * @param cssFile
	 * @throws java.lang.Exception
	 */
	public LatexConf(String cssFile) throws Exception {
		_elements = new HashMap<>(100);
		_chars = new HashMap<>(250);
		_charsNum = new HashMap<>(250);
		_styles = new HashMap<>(20);
		_stylesConf = new HashMap<>(50);
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			Document document = builder.parse(getClass().getResourceAsStream("config.xml"));
			Element root = document.getDocumentElement();
			loadElementsConfiguration(root);
			loadLinksConfiguration(root);
			loadCharsConfiguration(root);
			loadStylesConfiguration(root);
			if (cssFile != null && !cssFile.isEmpty()) {
				loadStyleSheet(new File(cssFile));
			}
		} catch (IOException | ParserConfigurationException | SAXException e) {
			throw new Exception("Can't load configuration.\n" + e.getMessage());
		}
	}

	public LatexConf(Latex programInput) throws Exception {
		_elements = new HashMap<>(100);
		_chars = new HashMap<>(250);
		_charsNum = new HashMap<>(250);
		_styles = new HashMap<>(20);
		_stylesConf = new HashMap<>(50);
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			Document document;
			if (programInput != null && programInput.configExists()) {
				document = builder.parse(new File(programInput.getConfigFile()));
			} else {
				document = builder.parse(getClass().getResourceAsStream("config.xml"));
			}
			Element root = document.getDocumentElement();
			loadElementsConfiguration(root);
			loadLinksConfiguration(root);
			loadCharsConfiguration(root);
			loadStylesConfiguration(root);
			if (programInput != null && !programInput.getCssFile().isEmpty()) {
				loadStyleSheet(new File(programInput.getCssFile()));
			}
		} catch (IOException | ParserConfigurationException | SAXException e) {
			throw new Exception("Can't load configuration.\n" + e.getMessage());
		}
	}

	/**
	 * Loads mapping between HTML elements and LaTeX commands.
	 *
	 * @param root root element of the XML configuration file
	 */
	private void loadElementsConfiguration(Element root) {
		// get elements' configuration
		NodeList nl = root.getElementsByTagName("element");
		for (int i = 0; i < nl.getLength(); i++) {
			Element e = (Element) nl.item(i);
			String elementName = e.getAttribute("name");
			String leaveText = e.getAttribute("leaveText");
			String ignoreContent = e.getAttribute("ignoreContent");
			String ignoreStyles = e.getAttribute("ignoreStyle");

			NodeList nl2 = e.getElementsByTagName("start");
			String elementStart = nl2.item(0).getTextContent();
			nl2 = e.getElementsByTagName("end");
			String elementEnd = nl2.item(0).getTextContent();

			_elements.put(elementName, new ElemConfigItem(replaceSpecialStrings(elementStart),
			   replaceSpecialStrings(elementEnd),
			   leaveText,
			   ignoreContent,
			   ignoreStyles));
		}
	}

	/**
	 * Loads mapping between HTML named entities (ie. &amp;lt;) and LaTeX commands. Mappings between
	 * HTML decimal decimal entities and LaTeX commands are also loaded.
	 *
	 * @param root root element of the XML configuration file
	 */
	private void loadCharsConfiguration(Element root) {
		// get chars conversion configuration
		NodeList nl = root.getElementsByTagName("char");
		for (int i = 0; i < nl.getLength(); i++) {
			Element e = (Element) nl.item(i);
			String charName = e.getAttribute("name");
			String convertTo = e.getAttribute("convertTo");
			String charNum = e.getAttribute("num");
			try {
				if (!charNum.equals("")) {
					Integer charNumInt = Integer.valueOf(charNum);
					_charsNum.put(charNumInt, replaceSpecialStrings(convertTo));
				}
			} catch (NumberFormatException ex) {
				System.err.println("Error in configuration.\n" + ex.toString());
			}

			_chars.put(charName, replaceSpecialStrings(convertTo));
		}

		// get chars conversion configuration
		nl = root.getElementsByTagName("charNum");
		for (int i = 0; i < nl.getLength(); i++) {
			Element e = (Element) nl.item(i);
			String convertTo = e.getAttribute("convertTo");
			String charNum = e.getAttribute("num");
			try {
				if (!charNum.equals("")) {
					Integer charNumInt = Integer.valueOf(charNum);
					_charsNum.put(charNumInt, replaceSpecialStrings(convertTo));
				}
			} catch (NumberFormatException ex) {
				System.err.println("Error in configuration.\n" + ex.toString());
			}
		}
	}

	/**
	 * Loads {@link LinksConversion options} for converting hyperlinks.
	 *
	 * @param root root element of the XML configuration file
	 */
	private void loadLinksConfiguration(Element root) {
		// get links conversion configuration
		NodeList nl = root.getElementsByTagName("links");
		Element links = (Element) nl.item(0);
		String type = links.getAttribute("conversion");

		switch (type) {
			case "footnotes":
				_linksConversion = LK.FOOTNOTES;
				break;
			case "biblio":
				_linksConversion = LK.BIBLIO;
				break;
			case "hypertex":
				_linksConversion = LK.HYPERTEX;
				break;
			default:
				_linksConversion = LK.IGNORE;
				break;
		}
	}

	/**
	 * Loads mappings between CSS properties and LaTeX commands.
	 *
	 * @param root root element of the XML configuration file
	 */
	private void loadStylesConfiguration(Element root) {
		NodeList nl = root.getElementsByTagName("cssStyles");
		if (nl.getLength() == 1) {
			Element elem = (Element) nl.item(0);
			String makeCmds = elem.getAttribute("makeCommands");
			_commandsPrefix = elem.getAttribute("commandsPrefix");
			if (makeCmds.equals("yes")) {
				_makeCmdsFromCSS = true;
			}
		}
		nl = root.getElementsByTagName("property");
		for (int i = 0; i < nl.getLength(); i++) {
			Element e = (Element) nl.item(i);
			String propertyName = e.getAttribute("name");

			NodeList nl2 = e.getElementsByTagName("value");
			for (int j = 0; j < nl2.getLength(); j++) {
				Element el = (Element) nl2.item(j);
				String propertyValue = el.getAttribute("name");
				String start = el.getAttribute("start");
				String end = el.getAttribute("end");
				_stylesConf.put(propertyName + "-" + propertyValue,
				   new CSSItem(replaceSpecialStrings(start),
					  replaceSpecialStrings(end)));
			}
		}
	}

	/**
	 * Loads user style sheet.
	 *
	 * @param f CSS file
	 */
	private void loadStyleSheet(File f) {
		CSSParser parser = new CSSParser();
		parser.parse(f, new CSSParserHandler(this));
		for (Entry<String, CSS> entry : _styles.entrySet()) {
			CSS style = (CSS) entry.getValue();
			style.makeLaTeXCommands(this);
		}
	}

	/**
	 * Replaces special @-strings with appropriate strings (ie. &quot;@NL&quot; with
	 * &quot;\n&quot;).
	 *
	 * @return string without special @-strings
	 * @param str input string
	 */
	private String replaceSpecialStrings(String str) {
		str = str.replace("@NL", "\n").replace("@TAB", "\t").replace("@QUOT", "'").replace("@DOUBLEQUOT", "\"").replace("@AMP", "&").replace("@LT", "<")
		   .replace("@GT", ">");

		return str;
	}

	/**
	 * Returns element's configuration.
	 *
	 * @param name element's name
	 * @return element's configuration
	 * @throws java.lang.Exception
	 */
	public ElemConfigItem getElement(String name) throws Exception {
		ElemConfigItem item;
		if ((item = _elements.get(name)) != null) {
			return item;
		}

		throw new Exception("Can't find specified config item " + name);
	}

	/**
	 * Returns the way of converting hyperlinks.
	 *
	 * @return the way of converting hyperlinks
	 */
	public LatexConverter.LK getLinksConversionType() {
		return _linksConversion;
	}

	/**
	 * Returns LaTeX command for the specified entity.
	 *
	 * @param charName entity name
	 * @return LaTeX command for the specified entity
	 * @throws java.lang.Exception
	 */
	public String getChar(String charName) throws Exception {
		String convertTo;
		if ((convertTo = _chars.get(charName)) != null) {
			return convertTo;
		}

		throw new Exception("Can't find specified config item " + charName);
	}

	/**
	 * Returns LaTeX command for the specified entity.
	 *
	 * @param charNum entity number
	 * @return LaTeX command for the specified entity
	 * @throws java.lang.Exception
	 */
	public String getChar(Integer charNum) throws Exception {
		String convertTo;
		if ((convertTo = _charsNum.get(charNum)) != null) {
			return convertTo;
		}

		throw new Exception("Can't find specified config item " + charNum.toString());
	}

	/**
	 * Returns style defined in the user stylesheet.
	 *
	 * @param styleName style name
	 * @return style defined in the user stylesheet
	 */
	public CSS getStyle(String styleName) {
		return _styles.get(styleName);
	}

	/**
	 * Finds style for element with specified <code>class</code> attribute
	 *
	 * @param className element's <code>class</code> attribute
	 * @param elementName element name
	 * @return CSS style
	 */
	public CSS findStyleClass(String className, String elementName) {
		CSS style;
		if ((style = _styles.get(elementName + "." + className)) != null) {
			return style;
		} else if ((style = _styles.get("." + className)) != null) {
			return style;
		} else {
			return null;
		}
	}

	/**
	 * Finds style for element with specified <code>id</code> attribute
	 *
	 * @param elementId element's <code>id</code> attribute
	 * @param elementName element name
	 * @return CSS style
	 */
	public CSS findStyleId(String elementId, String elementName) {
		CSS style;
		if ((style = _styles.get(elementName + "#" + elementId)) != null) {
			return style;
		} else if ((style = _styles.get("#" + elementId)) != null) {
			return style;
		} else {
			return null;
		}
	}

	/**
	 * Finds style for element.
	 *
	 * @param elementName element name
	 * @return CSS style
	 */
	public CSS findStyle(String elementName) {
		CSS style;
		if ((style = _styles.get(elementName)) != null) {
			return style;
		} else {
			return null;
		}
	}

	/**
	 * Returns CSS property configuration.
	 *
	 * @param property property and value name (&lt;propertyName&gt;-&lt;valueName&gt;)
	 * @return CSS property configuration
	 * @throws java.lang.Exception
	 */
	public CSSItem getPropertyConf(String property) throws Exception {
		CSSItem ret;
		if ((ret = _stylesConf.get(property)) != null) {
			return ret;
		}

		throw new Exception("Can't find specified config item " + property);
	}

	/**
	 * Adds user style to the configuration.
	 *
	 * @param name style name
	 * @param style CSS style
	 */
	public void addStyle(String name, CSS style) {
		_styles.put(name, style);
	}

	/**
	 * Makes new LaTeX commands from the CSS styles.
	 *
	 * @return string containing new commands definitions
	 */
	public String makeCmdsFromCSS() {
		String ret = "\n% commands generated by html2latex from oStorybook";
		for (Entry<String, CSS> entry : _styles.entrySet()) {
			String styleName = (String) entry.getKey();
			CSS style = (CSS) entry.getValue();
			ret += "\n\\newcommand{" + getCmdStyleName(styleName) + "}[1]{ " + style.getStart() + "#1" + style.getEnd() + " }";
		}
		return ret + "\n";
	}

	/**
	 * style name without special chars
	 *
	 * @param styleName style name
	 * @return style name without special chars (ie. &quot;#&quot;) - suitable fo creating new LaTeX
	 * command
	 */
	public String getCmdStyleName(String styleName) {
		return "\\" + _commandsPrefix
		   + styleName.replaceAll("\\W", "").replaceAll("\\d", "").replace("_", "");
	}

	/**
	 * Returns true when new LaTeX commands are to be made from CSS styles.
	 *
	 * @return true when new LaTeX commands are to be made from CSS styles
	 */
	public boolean getMakeCmdsFromCSS() {
		return _makeCmdsFromCSS;
	}

	/**
	 * Class representing configuration of each HTML element.
	 */
	public class ElemConfigItem {

		private final String _start;
		private final String _end;
		private boolean _leaveText = false;
		private boolean _ignoreContent = false;
		private boolean _ignoreStyles = false;

		/**
		 * Cstr.
		 *
		 * @param start mapping between start tag and LaTeX command
		 * @param end mapping between end tag and LaTeX command
		 * @param leaveText &quot;yes&quot; if the element's content mustn't be touched
		 * @param ignoreContent &quot;yes&quot; if the element's content will be ignored
		 * @param ignoreStyles &quot;yes&quot; if the element's CSS styles will be ignored
		 */
		public ElemConfigItem(String start, String end,
		   String leaveText, String ignoreContent, String ignoreStyles) {
			_start = start;
			_end = end;
			if (leaveText.equals("yes")) {
				_leaveText = true;
			}
			if (ignoreContent.equals("yes")) {
				_ignoreContent = true;
			}
			if (ignoreStyles.equals("yes")) {
				_ignoreStyles = true;
			}
		}

		/**
		 * Returns mapping between start tag and LaTeX command.
		 *
		 * @return mapping between start tag and LaTeX command
		 */
		public String getStart() {
			return _start;
		}

		/**
		 * Returns mapping between end tag and LaTeX command.
		 *
		 * @return mapping between end tag and LaTeX command
		 */
		public String getEnd() {
			return _end;
		}

		/**
		 * Returns leaveText property.
		 *
		 * @return true if the element's content mustn't be touched
		 */
		public boolean leaveText() {
			return _leaveText;
		}

		/**
		 * Returns ignoreContent property.
		 *
		 * @return true if the element's content is ignored
		 */
		public boolean ignoreContent() {
			return _ignoreContent;
		}

		/**
		 * Returns ignoreStyles property.
		 *
		 * @return true if the element's CSS styles are ignored
		 */
		public boolean ignoreStyles() {
			return _ignoreStyles;
		}
	}

	/**
	 * Class representing configuration of each CSS property defined in the configuration file.
	 */
	public class CSSItem {

		private final String _start;
		private final String _end;

		/**
		 * Property config item.
		 *
		 * @param start start command
		 * @param end end command
		 */
		public CSSItem(String start, String end) {
			_start = start;
			_end = end;
		}

		/**
		 * Returns start command.
		 *
		 * @return start command
		 */
		public String getStart() {
			return _start;
		}

		/**
		 * Returns end command.
		 *
		 * @return end command
		 */
		public String getEnd() {
			return _end;
		}
	}
}
